Bump Assistent to 0.5.3: live inventory, cards, taste, wanted YAML.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+287
-39
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user