Bump Assistent to 0.5.3: live inventory, cards, taste, wanted YAML.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-21 21:28:30 +03:00
co-authored by Cursor
parent 0b429542b5
commit ca98ab577a
4 changed files with 296 additions and 42 deletions
+287 -39
View File
@@ -1126,6 +1126,7 @@
auto_generate: !!$('sa_auto_generate')?.checked,
persona: $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral',
model_cards: [],
taste_profile: summarizeTaste(),
...initCtx,
};
@@ -1149,20 +1150,48 @@
}
} catch (e) { /* ignore */ }
// Recommendation cards for current checkpoint + selected LoRAs only.
// Recommendation cards: checkpoint + selected LoRAs + other has_card entries (capped).
const cardKeys = [];
const seenCard = new Set();
const addKey = (kind, name) => {
if (!kind || !name) {
return;
}
const key = `${kind}:${name}`;
if (seenCard.has(key)) {
return;
}
seenCard.add(key);
cardKeys.push({ kind, name });
};
if (ctx.checkpoint?.name) {
cardKeys.push({ kind: 'checkpoint', name: ctx.checkpoint.name });
addKey('checkpoint', ctx.checkpoint.name);
}
for (const l of ctx.selected_loras || []) {
if (l?.name) {
cardKeys.push({ kind: 'lora', name: l.name });
addKey('lora', l.name);
}
}
for (const l of inv.loras || []) {
if (l?.has_card && l?.name) {
addKey('lora', l.name);
}
if (cardKeys.length >= 14) {
break;
}
}
for (const c of inv.checkpoints || []) {
if (c?.has_card && c?.name) {
addKey('checkpoint', c.name);
}
if (cardKeys.length >= 16) {
break;
}
}
for (const k of cardKeys) {
const cached = state.modelCards[`${k.kind}:${k.name}`];
if (cached) {
ctx.model_cards.push(cached);
ctx.model_cards.push(slimCardForContext(cached));
}
}
@@ -1197,6 +1226,42 @@
return ctx;
}
function slimCardForContext(card) {
if (!card || typeof card !== 'object') {
return null;
}
const out = {
kind: card.kind || null,
name: card.name || null,
triggers: Array.isArray(card.triggers) ? card.triggers.slice(0, 8) : undefined,
weight: card.weight != null ? card.weight : undefined,
when: card.when ? String(card.when).slice(0, 160) : undefined,
avoid: card.avoid ? String(card.avoid).slice(0, 120) : undefined,
prompt_hint: card.prompt_hint ? String(card.prompt_hint).slice(0, 160) : undefined,
notes: card.notes ? String(card.notes).slice(0, 200) : undefined,
};
const clean = {};
for (const [k, v] of Object.entries(out)) {
if (v != null && v !== '') {
clean[k] = v;
}
}
return clean;
}
function summarizeTaste() {
const t = state.taste || {};
if (!(t.styles?.length || t.likes?.length || t.avoid?.length || t.notes)) {
return null;
}
return {
styles: (t.styles || []).slice(0, 8),
likes: (t.likes || []).slice(0, 10),
avoid: (t.avoid || []).slice(0, 8),
notes: t.notes ? String(t.notes).slice(0, 240) : undefined,
};
}
function slimInventoryLoras(list, limit) {
const selected = new Set();
try {
@@ -1206,26 +1271,60 @@
}
}
} catch (e) { /* ignore */ }
const rows = (list || []).map((l) => ({
name: l.name,
title: l.title || l.name,
trigger_phrase: l.trigger_phrase || null,
triggers: Array.isArray(l.triggers) ? l.triggers.slice(0, 8) : undefined,
architecture: l.architecture || null,
compat_class: l.compat_class || null,
has_card: !!l.has_card,
krea_likely: !!l.krea_likely,
blurb: l.blurb || l.usage_hint || null,
default_weight: l.default_weight || undefined,
tags: Array.isArray(l.tags) ? l.tags.slice(0, 6) : undefined,
_sel: selected.has(String(l.name || '').toLowerCase()),
}));
rows.sort((a, b) => (b._sel - a._sel) || (b.krea_likely - a.krea_likely) || String(a.name).localeCompare(String(b.name)));
return rows.slice(0, limit).map(({ _sel, ...rest }) => {
const out = {};
for (const [k, v] of Object.entries(rest)) {
if (v != null && v !== '' && !(Array.isArray(v) && !v.length)) {
out[k] = v;
const softCap = Math.min(limit || 100, 80);
const rows = (list || []).map((l) => {
const sel = selected.has(String(l.name || '').toLowerCase());
const hasCard = !!l.has_card;
const krea = !!l.krea_likely;
const blurb = l.blurb || l.usage_hint || null;
return {
name: l.name,
title: l.title || l.name,
trigger_phrase: l.trigger_phrase || null,
triggers: Array.isArray(l.triggers) ? l.triggers.slice(0, 8) : undefined,
architecture: l.architecture || null,
compat_class: l.compat_class || null,
has_card: hasCard,
krea_likely: krea,
blurb,
default_weight: l.default_weight || undefined,
tags: Array.isArray(l.tags) ? l.tags.slice(0, 6) : undefined,
_score: (sel ? 1000 : 0) + (hasCard ? 200 : 0) + (krea ? 50 : 0) + (blurb ? 10 : 0),
};
});
rows.sort((a, b) => b._score - a._score || String(a.name).localeCompare(String(b.name)));
// Full detail for top tier; name+trigger only for the rest within softCap.
const fullDetail = 36;
return rows.slice(0, softCap).map((row, idx) => {
const out = { name: row.name, title: row.title };
if (row.trigger_phrase) {
out.trigger_phrase = row.trigger_phrase;
}
if (row.triggers) {
out.triggers = row.triggers;
}
if (row.krea_likely) {
out.krea_likely = true;
}
if (row.has_card) {
out.has_card = true;
}
const rich = idx < fullDetail || row._score >= 200;
if (rich) {
if (row.architecture) {
out.architecture = row.architecture;
}
if (row.compat_class) {
out.compat_class = row.compat_class;
}
if (row.blurb) {
out.blurb = row.blurb;
}
if (row.default_weight) {
out.default_weight = row.default_weight;
}
if (row.tags) {
out.tags = row.tags;
}
}
return out;
@@ -1233,7 +1332,9 @@
}
function slimInventoryCheckpoints(list, limit) {
return (list || []).slice(0, limit).map((c) => {
const rows = (list || []).slice();
rows.sort((a, b) => ((b.krea_likely ? 1 : 0) - (a.krea_likely ? 1 : 0)) || ((b.has_card ? 1 : 0) - (a.has_card ? 1 : 0)) || String(a.name).localeCompare(String(b.name)));
return rows.slice(0, limit || 40).map((c) => {
const out = {
name: c.name,
title: c.title || c.name,
@@ -1249,6 +1350,77 @@
});
}
function loadTaste() {
try {
const raw = localStorage.getItem(LS_TASTE);
if (!raw) {
return;
}
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
state.taste = {
styles: Array.isArray(parsed.styles) ? parsed.styles.slice(0, 12) : [],
likes: Array.isArray(parsed.likes) ? parsed.likes.slice(0, 16) : [],
avoid: Array.isArray(parsed.avoid) ? parsed.avoid.slice(0, 12) : [],
notes: String(parsed.notes || '').slice(0, 400),
updated: parsed.updated || 0,
};
}
} catch (e) { /* ignore */ }
}
function saveTaste() {
try {
localStorage.setItem(LS_TASTE, JSON.stringify(state.taste || {}));
} catch (e) { /* ignore */ }
}
function pushUnique(arr, value, max) {
const v = String(value || '').trim();
if (!v || v.length < 2) {
return;
}
const lower = v.toLowerCase();
const next = (arr || []).filter((x) => String(x).toLowerCase() !== lower);
next.unshift(v.slice(0, 80));
return next.slice(0, max);
}
function updateTasteFromPatch(patch, userText) {
if (!patch) {
return;
}
const taste = state.taste || { styles: [], likes: [], avoid: [], notes: '' };
if (Array.isArray(patch.loras)) {
for (const l of patch.loras) {
const name = l?.name || l;
if (name) {
taste.likes = pushUnique(taste.likes, name, 16);
}
}
}
const aspect = patch.aspect || null;
if (aspect) {
taste.styles = pushUnique(taste.styles, `aspect ${aspect}`, 12);
}
if (patch.creativity) {
taste.styles = pushUnique(taste.styles, `creativity:${patch.creativity}`, 12);
}
const ut = String(userText || '').toLowerCase();
if (/фото|photo|photoreal|реализм|film grain/.test(ut)) {
taste.styles = pushUnique(taste.styles, 'photoreal / film', 12);
}
if (/аниме|anime|illustration|иллюстр/.test(ut)) {
taste.styles = pushUnique(taste.styles, 'illustration / anime', 12);
}
if (/без\s+3d|не\s+3d|no\s+3d|не\s+render/.test(ut)) {
taste.avoid = pushUnique(taste.avoid, '3D render look', 12);
}
taste.updated = Date.now();
state.taste = taste;
saveTaste();
}
function isPatchObject(obj) {
if (!obj || typeof obj !== 'object') {
return false;
@@ -1967,11 +2139,12 @@
if (btn) {
btn.textContent = 'Downloaded';
}
refreshInventory(() => {
if ($('sa_input')) {
$('sa_input').value = `LoRA "${payload.name}" is now installed. Enable it with its triggers and improve the prompt.`;
}
sendChat({ fromDownload: true });
refreshInventory(async () => {
await maybeWriteCardAfterDownload({
kind: 'lora',
name: payload.name,
civitai: card,
});
}, { rescan: true });
} else {
setStatus(msg || 'Download failed');
@@ -2008,6 +2181,30 @@
}
}
async function maybeWriteCardAfterDownload({ kind, name, civitai }) {
const display = name || civitai?.file_name || civitai?.name || 'model';
appendSystemNote(`Downloaded ${display}. Writing a recommendation card…`);
setPackValue('catalog_card', { flash: true });
const meta = {
triggers: civitai?.triggers || [],
base_model: civitai?.base_model,
civitai_url: civitai?.url || civitai?.civitai_url,
version_id: civitai?.version_id || civitai?.modelVersionId,
name: display,
};
if ($('sa_input')) {
$('sa_input').value = '';
}
await sendChat({
forcedUserText: `LoRA "${display}" is now installed. Write a recommendation card (JSON) using its triggers/metadata. Then briefly suggest how to enable it in the next generate.`,
skipSlash: true,
skipAutoPack: true,
fromDownload: true,
fromCards: true,
cardTarget: { kind: kind || 'lora', name: display, meta },
});
}
function wantsAutoVision() {
return !!$('sa_auto_vision')?.checked;
}
@@ -2429,22 +2626,47 @@
async function prefetchActiveModelCards() {
const keys = [];
const seen = new Set();
const add = (kind, name) => {
if (!kind || !name) {
return;
}
const key = `${kind}:${name}`;
if (seen.has(key)) {
return;
}
seen.add(key);
keys.push({ kind, name });
};
try {
const ck = resolveCurrentCheckpoint();
if (ck?.name) {
keys.push({ kind: 'checkpoint', name: ck.name });
add('checkpoint', ck.name);
}
} catch (e) { /* ignore */ }
try {
if (typeof loraHelper !== 'undefined' && Array.isArray(loraHelper?.selected)) {
for (const l of loraHelper.selected) {
const name = l?.name || l;
if (name) {
keys.push({ kind: 'lora', name });
}
add('lora', l?.name || l);
}
}
} catch (e) { /* ignore */ }
for (const l of state.inventory?.loras || []) {
if (l?.has_card) {
add('lora', l.name);
}
if (keys.length >= 14) {
break;
}
}
for (const c of state.inventory?.checkpoints || []) {
if (c?.has_card) {
add('checkpoint', c.name);
}
if (keys.length >= 16) {
break;
}
}
await Promise.all(keys.map((k) => prefetchCard(k.kind, k.name)));
}
@@ -2689,10 +2911,32 @@
const { fromAutoCritique, fromVisionHop, fromCards } = opts;
if (fromCards) {
const card = extractCardJson(reply);
if (card && $('sa_card_json')) {
$('sa_card_json').value = JSON.stringify(card, null, 2);
setView('cards');
setCardStatus('Draft from Assistent — review & Save');
if (card) {
if ($('sa_card_json')) {
$('sa_card_json').value = JSON.stringify(card, null, 2);
}
if (opts.fromDownload || opts.cardTarget) {
const kind = card.kind || opts.cardTarget?.kind || 'lora';
const name = card.name || opts.cardTarget?.name;
if (name && typeof genericRequest === 'function') {
genericRequest(
'AssistentSaveCard',
{ kind, name, card, enqueue_wanted: false },
(data) => {
if (data?.path) {
state.modelCards[`${kind}:${name}`] = card;
setCardStatus(data.installed ? `Card saved → ${data.path}` : `Card draft → ${data.path}`);
setStatus(`Card saved for ${name}`);
}
},
0,
() => setCardStatus('Card draft ready — Save manually'),
);
}
} else {
setView('cards');
setCardStatus('Draft from Assistent — review & Save');
}
}
return;
}
@@ -2715,6 +2959,7 @@
}
if (patch && $('sa_auto_apply')?.checked) {
await applyPatch(patch, 'all');
updateTasteFromPatch(patch, opts.userText || '');
if (!fromAutoCritique) {
const src = await runGenerateFromPatch(patch);
if (src) {
@@ -3018,6 +3263,7 @@
// (rescans disk when inventory is older than ~20s or after downloads).
setStatus('Refreshing inventory…');
await ensureFreshInventory({ forceRescan: !!opts.fromDownload });
await prefetchActiveModelCards();
if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop) {
state.critiqueHopUsed = false;
@@ -3104,6 +3350,7 @@
stopBusyUi('Done');
await handleReplySideEffects(reply, civitaiResults, {
...opts,
userText: text,
attachedSlotIds: visionSlots.map((s) => s.id),
});
};
@@ -3350,6 +3597,7 @@
}
window.__swarmAssistentWired = true;
loadSettings();
loadTaste();
setView(state.view || 'chat');
updateGate();
ensureBoard();