Add personas and model Cards (0.5.1): tone presets, stem.assistent.json, wanted queue.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+576
-23
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API).
|
||||
* v0.5.0: Krea knowledge packs, aspect/vary/lock_seed patches, slash commands, chips.
|
||||
* v0.5.3: live inventory+cards in context, taste memory, post-download cards, wanted YAML fix.
|
||||
*/
|
||||
(function () {
|
||||
const LS_BASE = 'swarm_assistent_base_url';
|
||||
const LS_MODEL = 'swarm_assistent_model';
|
||||
const LS_PACK = 'swarm_assistent_pack';
|
||||
const LS_PERSONA = 'swarm_assistent_persona';
|
||||
const LS_VIEW = 'swarm_assistent_view';
|
||||
const LS_AUTO_VISION = 'swarm_assistent_auto_vision';
|
||||
const LS_AUTO_APPLY = 'swarm_assistent_auto_apply';
|
||||
const LS_AUTO_GENERATE = 'swarm_assistent_auto_generate';
|
||||
@@ -13,6 +15,7 @@
|
||||
const LS_AUTO_DOWNLOAD = 'swarm_assistent_auto_download';
|
||||
const LS_PANE_WIDTH = 'swarm_assistent_pane_width';
|
||||
const LS_WELCOMED = 'swarm_assistent_welcomed';
|
||||
const LS_TASTE = 'swarm_assistent_taste';
|
||||
const TAB_BUTTON_ID = 'maintab_assistent';
|
||||
const GEN_ID = 'generate';
|
||||
const MAX_REF_SLOTS = 4;
|
||||
@@ -41,6 +44,9 @@
|
||||
inpaint_edit: 'inpaint_edit',
|
||||
describe: 'describe_ref',
|
||||
describe_ref: 'describe_ref',
|
||||
card: 'catalog_card',
|
||||
catalog: 'catalog_card',
|
||||
catalog_card: 'catalog_card',
|
||||
};
|
||||
|
||||
const WELCOME_HTML = `
|
||||
@@ -65,6 +71,7 @@
|
||||
/vary — новый seed, тот же промпт
|
||||
/pack write|critique|compose|params|inpaint|describe
|
||||
/civitai <query> — поиск LoRA (Confirm в чате)
|
||||
/inventory — rescan моделей + обновить список LoRA
|
||||
|
||||
Чипсы над полем ввода делают то же для aspect / seed / vary.`;
|
||||
|
||||
@@ -77,6 +84,8 @@
|
||||
preferredModel: null,
|
||||
dragDepth: 0,
|
||||
inventory: { loras: [], checkpoints: [], wildcards: [], has_civitai_key: false },
|
||||
inventoryFetchedAt: 0,
|
||||
taste: { styles: [], likes: [], avoid: [], notes: '', updated: 0 },
|
||||
streamEl: null,
|
||||
critiqueHopUsed: false,
|
||||
visionHopUsed: false,
|
||||
@@ -88,6 +97,11 @@
|
||||
selectedSlotId: 'ref1',
|
||||
refSeq: 1,
|
||||
packUserTouched: false,
|
||||
view: 'chat',
|
||||
personas: [],
|
||||
modelCards: {},
|
||||
cardsSelection: null,
|
||||
cardsBusy: false,
|
||||
};
|
||||
|
||||
function $(id) {
|
||||
@@ -1100,14 +1114,18 @@
|
||||
batch: parseInt(val('input_images') || val('input_batchsize') || '0', 10) || null,
|
||||
prompt_image_count: countPromptImages(),
|
||||
selected_loras: [],
|
||||
available_loras: (inv.loras || []).slice(0, 100),
|
||||
available_loras: slimInventoryLoras(inv.loras || [], 100),
|
||||
available_checkpoints: slimInventoryCheckpoints(inv.checkpoints || [], 40),
|
||||
wildcards: (inv.wildcards || []).map((w) => w.name || w).slice(0, 60),
|
||||
inventory_at: inv.inventory_at || null,
|
||||
has_vision_image: attachableSlots().length > 0,
|
||||
image_slots: slotCatalog(),
|
||||
attached_slot_ids: attachableSlots().map((s) => s.id),
|
||||
has_civitai_key: !!inv.has_civitai_key,
|
||||
auto_apply: !!$('sa_auto_apply')?.checked,
|
||||
auto_generate: !!$('sa_auto_generate')?.checked,
|
||||
persona: $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral',
|
||||
model_cards: [],
|
||||
...initCtx,
|
||||
};
|
||||
|
||||
@@ -1131,6 +1149,23 @@
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
// Recommendation cards for current checkpoint + selected LoRAs only.
|
||||
const cardKeys = [];
|
||||
if (ctx.checkpoint?.name) {
|
||||
cardKeys.push({ kind: 'checkpoint', name: ctx.checkpoint.name });
|
||||
}
|
||||
for (const l of ctx.selected_loras || []) {
|
||||
if (l?.name) {
|
||||
cardKeys.push({ kind: 'lora', name: l.name });
|
||||
}
|
||||
}
|
||||
for (const k of cardKeys) {
|
||||
const cached = state.modelCards[`${k.kind}:${k.name}`];
|
||||
if (cached) {
|
||||
ctx.model_cards.push(cached);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback if inventory empty
|
||||
if (!ctx.available_loras.length) {
|
||||
try {
|
||||
@@ -1162,6 +1197,58 @@
|
||||
return ctx;
|
||||
}
|
||||
|
||||
function slimInventoryLoras(list, limit) {
|
||||
const selected = new Set();
|
||||
try {
|
||||
if (typeof loraHelper !== 'undefined' && loraHelper && Array.isArray(loraHelper.selected)) {
|
||||
for (const l of loraHelper.selected) {
|
||||
selected.add(String(l.name || l || '').toLowerCase());
|
||||
}
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
function slimInventoryCheckpoints(list, limit) {
|
||||
return (list || []).slice(0, limit).map((c) => {
|
||||
const out = {
|
||||
name: c.name,
|
||||
title: c.title || c.name,
|
||||
architecture: c.architecture || null,
|
||||
compat_class: c.compat_class || null,
|
||||
has_card: !!c.has_card,
|
||||
krea_likely: !!c.krea_likely,
|
||||
};
|
||||
if (c.blurb) {
|
||||
out.blurb = c.blurb;
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
function isPatchObject(obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return false;
|
||||
@@ -1205,10 +1292,50 @@
|
||||
obj.movement != null ||
|
||||
obj.clear_prompt_images != null ||
|
||||
obj.slot_to_prompt_image != null ||
|
||||
obj.pack != null
|
||||
obj.pack != null ||
|
||||
obj.triggers != null ||
|
||||
obj.when != null ||
|
||||
obj.prompt_hint != null ||
|
||||
obj.notes != null
|
||||
);
|
||||
}
|
||||
|
||||
function isCardObject(obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
(obj.kind || obj.triggers || obj.when || obj.prompt_hint || obj.notes) &&
|
||||
(obj.name || obj.triggers || obj.when)
|
||||
);
|
||||
}
|
||||
|
||||
function extractCardJson(text) {
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||||
let match;
|
||||
let last = null;
|
||||
while ((match = re.exec(text)) !== null) {
|
||||
try {
|
||||
const obj = JSON.parse(match[1].trim());
|
||||
if (isCardObject(obj)) {
|
||||
last = obj;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
if (last) {
|
||||
return last;
|
||||
}
|
||||
try {
|
||||
const obj = JSON.parse(text.trim());
|
||||
return isCardObject(obj) ? obj : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function extractPatch(text) {
|
||||
if (!text) {
|
||||
return { prose: text || '', patch: null };
|
||||
@@ -1845,7 +1972,7 @@
|
||||
$('sa_input').value = `LoRA "${payload.name}" is now installed. Enable it with its triggers and improve the prompt.`;
|
||||
}
|
||||
sendChat({ fromDownload: true });
|
||||
});
|
||||
}, { rescan: true });
|
||||
} else {
|
||||
setStatus(msg || 'Download failed');
|
||||
if (btn) {
|
||||
@@ -1866,7 +1993,8 @@
|
||||
}
|
||||
if (data.success || data.overall_percent >= 1 || data.current_percent >= 1) {
|
||||
if (data.success || data.overall_percent >= 0.99) {
|
||||
onDone(true);
|
||||
// Swarm docs: download does not always refresh model list — force both.
|
||||
triggerSwarmModelRefresh(() => onDone(true));
|
||||
} else if (data.current_percent != null) {
|
||||
setStatus(`Download ${(data.current_percent * 100).toFixed(0)}%`);
|
||||
}
|
||||
@@ -2051,6 +2179,8 @@
|
||||
const base = localStorage.getItem(LS_BASE);
|
||||
const model = localStorage.getItem(LS_MODEL);
|
||||
const pack = localStorage.getItem(LS_PACK);
|
||||
const persona = localStorage.getItem(LS_PERSONA);
|
||||
const view = localStorage.getItem(LS_VIEW);
|
||||
const auto = localStorage.getItem(LS_AUTO_VISION);
|
||||
const autoApply = localStorage.getItem(LS_AUTO_APPLY);
|
||||
const autoGen = localStorage.getItem(LS_AUTO_GENERATE);
|
||||
@@ -2063,6 +2193,9 @@
|
||||
if (pack && $('sa_pack')) {
|
||||
$('sa_pack').value = pack;
|
||||
}
|
||||
if (persona && $('sa_persona')) {
|
||||
$('sa_persona').value = persona;
|
||||
}
|
||||
if (auto != null && $('sa_auto_vision')) {
|
||||
$('sa_auto_vision').checked = auto === '1';
|
||||
}
|
||||
@@ -2084,12 +2217,17 @@
|
||||
if (paneW) {
|
||||
document.documentElement.style.setProperty('--sa-image-width', paneW);
|
||||
}
|
||||
if (view === 'cards' || view === 'chat') {
|
||||
state.view = view;
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
localStorage.setItem(LS_BASE, $('sa_base_url')?.value || '');
|
||||
localStorage.setItem(LS_MODEL, $('sa_model')?.value || '');
|
||||
localStorage.setItem(LS_PACK, $('sa_pack')?.value || 'write_prompt');
|
||||
localStorage.setItem(LS_PERSONA, $('sa_persona')?.value || 'neutral');
|
||||
localStorage.setItem(LS_VIEW, state.view || 'chat');
|
||||
localStorage.setItem(LS_AUTO_VISION, $('sa_auto_vision')?.checked ? '1' : '0');
|
||||
localStorage.setItem(LS_AUTO_APPLY, $('sa_auto_apply')?.checked ? '1' : '0');
|
||||
localStorage.setItem(LS_AUTO_GENERATE, $('sa_auto_generate')?.checked ? '1' : '0');
|
||||
@@ -2163,7 +2301,367 @@
|
||||
);
|
||||
}
|
||||
|
||||
function refreshInventory(done) {
|
||||
function refreshInventory(done, opts = {}) {
|
||||
if (typeof genericRequest !== 'function') {
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const rescan = !!opts.rescan;
|
||||
genericRequest(
|
||||
'AssistentListInventory',
|
||||
{ rescan },
|
||||
(data) => {
|
||||
state.inventory = {
|
||||
loras: data.loras || [],
|
||||
checkpoints: data.checkpoints || [],
|
||||
wildcards: data.wildcards || [],
|
||||
has_civitai_key: !!data.has_civitai_key,
|
||||
inventory_at: data.inventory_at || Math.floor(Date.now() / 1000),
|
||||
rescanned: !!data.rescanned,
|
||||
};
|
||||
state.inventoryFetchedAt = Date.now();
|
||||
const n = state.inventory.loras.length;
|
||||
const ck = state.inventory.checkpoints.length;
|
||||
setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts${rescan ? ' (rescanned)' : ''}`);
|
||||
prefetchActiveModelCards();
|
||||
if (state.view === 'cards') {
|
||||
renderCardsList();
|
||||
}
|
||||
if (done) {
|
||||
done(state.inventory);
|
||||
}
|
||||
},
|
||||
0,
|
||||
(err) => {
|
||||
console.warn('Assistent inventory', err);
|
||||
if (done) {
|
||||
done(null);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function refreshInventoryAsync(opts = {}) {
|
||||
return new Promise((resolve) => refreshInventory(resolve, opts));
|
||||
}
|
||||
|
||||
function setCardStatus(msg) {
|
||||
const el = $('sa_card_status');
|
||||
if (el) {
|
||||
el.textContent = msg || '';
|
||||
}
|
||||
}
|
||||
|
||||
function setView(view) {
|
||||
state.view = view === 'cards' ? 'cards' : 'chat';
|
||||
const chat = $('sa_view_chat');
|
||||
const cards = $('sa_view_cards');
|
||||
if (chat) {
|
||||
chat.hidden = state.view !== 'chat';
|
||||
}
|
||||
if (cards) {
|
||||
cards.hidden = state.view !== 'cards';
|
||||
}
|
||||
$('sa_tab_chat')?.classList.toggle('sa-subtab-active', state.view === 'chat');
|
||||
$('sa_tab_cards')?.classList.toggle('sa-subtab-active', state.view === 'cards');
|
||||
saveSettings();
|
||||
if (state.view === 'cards') {
|
||||
renderCardsList();
|
||||
}
|
||||
}
|
||||
|
||||
function refreshPersonas() {
|
||||
if (typeof genericRequest !== 'function') {
|
||||
return;
|
||||
}
|
||||
genericRequest(
|
||||
'AssistentListPersonas',
|
||||
{},
|
||||
(data) => {
|
||||
const list = data.personas || [];
|
||||
state.personas = list;
|
||||
const sel = $('sa_persona');
|
||||
if (!sel) {
|
||||
return;
|
||||
}
|
||||
const prefer = localStorage.getItem(LS_PERSONA) || data.default || 'neutral';
|
||||
sel.innerHTML = '';
|
||||
for (const p of list) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = p.id;
|
||||
opt.textContent = p.title || p.id;
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
if ([...sel.options].some((o) => o.value === prefer)) {
|
||||
sel.value = prefer;
|
||||
} else if (data.default) {
|
||||
sel.value = data.default;
|
||||
}
|
||||
},
|
||||
0,
|
||||
(err) => console.warn('Assistent personas', err),
|
||||
);
|
||||
}
|
||||
|
||||
function prefetchCard(kind, name) {
|
||||
return new Promise((resolve) => {
|
||||
if (!kind || !name || typeof genericRequest !== 'function') {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
const key = `${kind}:${name}`;
|
||||
genericRequest(
|
||||
'AssistentGetCard',
|
||||
{ kind, name },
|
||||
(data) => {
|
||||
if (data?.card) {
|
||||
state.modelCards[key] = data.card;
|
||||
}
|
||||
resolve(data?.card || null);
|
||||
},
|
||||
0,
|
||||
() => resolve(null),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function prefetchActiveModelCards() {
|
||||
const keys = [];
|
||||
try {
|
||||
const ck = resolveCurrentCheckpoint();
|
||||
if (ck?.name) {
|
||||
keys.push({ kind: 'checkpoint', name: 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
await Promise.all(keys.map((k) => prefetchCard(k.kind, k.name)));
|
||||
}
|
||||
|
||||
function cardsCatalog() {
|
||||
const kind = $('sa_cards_kind')?.value || 'all';
|
||||
const inv = state.inventory || {};
|
||||
const rows = [];
|
||||
if (kind === 'all' || kind === 'checkpoint') {
|
||||
for (const c of inv.checkpoints || []) {
|
||||
rows.push({ kind: 'checkpoint', name: c.name, title: c.title || c.name, has_card: !!c.has_card });
|
||||
}
|
||||
}
|
||||
if (kind === 'all' || kind === 'lora') {
|
||||
for (const l of inv.loras || []) {
|
||||
rows.push({ kind: 'lora', name: l.name, title: l.title || l.name, has_card: !!l.has_card, trigger: l.trigger_phrase });
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function renderCardsList() {
|
||||
const root = $('sa_cards_list');
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
root.innerHTML = '';
|
||||
const rows = cardsCatalog();
|
||||
if (!rows.length) {
|
||||
root.innerHTML = '<div class="sa-chat-empty-hint">Inventory empty — Refresh.</div>';
|
||||
return;
|
||||
}
|
||||
for (const row of rows) {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'sa-card-row';
|
||||
if (state.cardsSelection && state.cardsSelection.kind === row.kind && state.cardsSelection.name === row.name) {
|
||||
btn.classList.add('sa-selected');
|
||||
}
|
||||
btn.innerHTML = `<div class="sa-card-row-kind">${row.kind}</div><div>${escapeHtml(row.title || row.name)}</div><div class="sa-card-row-meta">${row.has_card ? 'card ✓' : 'no card'}${row.trigger ? ' · ' + escapeHtml(String(row.trigger).slice(0, 40)) : ''}</div>`;
|
||||
btn.addEventListener('click', () => selectCardModel(row));
|
||||
root.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function selectCardModel(row) {
|
||||
state.cardsSelection = row;
|
||||
renderCardsList();
|
||||
if ($('sa_card_title')) {
|
||||
$('sa_card_title').textContent = row.title || row.name;
|
||||
}
|
||||
const badge = $('sa_card_badge');
|
||||
if (badge) {
|
||||
badge.hidden = false;
|
||||
badge.textContent = row.has_card ? 'has card' : 'missing card';
|
||||
}
|
||||
setCardStatus('Loading…');
|
||||
genericRequest(
|
||||
'AssistentGetCardMeta',
|
||||
{ kind: row.kind, name: row.name },
|
||||
async (data) => {
|
||||
const card = data.card || {
|
||||
kind: row.kind,
|
||||
name: row.name,
|
||||
triggers: data.trigger_phrase ? [data.trigger_phrase] : [],
|
||||
weight: row.kind === 'lora' ? 0.8 : 1,
|
||||
when: '',
|
||||
avoid: '',
|
||||
prompt_hint: '',
|
||||
notes: '',
|
||||
civitai_url: '',
|
||||
version_id: data.version_id || null,
|
||||
};
|
||||
if ($('sa_card_json')) {
|
||||
$('sa_card_json').value = JSON.stringify(card, null, 2);
|
||||
}
|
||||
state.modelCards[`${row.kind}:${row.name}`] = card;
|
||||
// Attach 1–2 Civitai examples to board as Ref vision.
|
||||
const urls = (data.example_urls || []).slice(0, 2);
|
||||
for (const url of urls) {
|
||||
try {
|
||||
await addRefFromUrl(url);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
setCardStatus(data.has_card ? 'Loaded card' : 'No card yet — Generate or edit JSON');
|
||||
},
|
||||
0,
|
||||
(err) => setCardStatus(String(err || 'Load failed')),
|
||||
);
|
||||
}
|
||||
|
||||
async function addRefFromUrl(url) {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
addRefSlot({ src: url, select: false });
|
||||
}
|
||||
|
||||
function readCardDraft() {
|
||||
const raw = $('sa_card_json')?.value || '';
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
setCardStatus('Invalid JSON');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveCurrentCard({ enqueue } = {}) {
|
||||
const sel = state.cardsSelection;
|
||||
if (!sel) {
|
||||
setCardStatus('Select a model');
|
||||
return;
|
||||
}
|
||||
const card = readCardDraft();
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
card.kind = card.kind || sel.kind;
|
||||
card.name = card.name || sel.name;
|
||||
setCardStatus('Saving…');
|
||||
genericRequest(
|
||||
'AssistentSaveCard',
|
||||
{ kind: sel.kind, name: sel.name, card, enqueue_wanted: !!enqueue },
|
||||
(data) => {
|
||||
if (data.error) {
|
||||
setCardStatus(data.error);
|
||||
return;
|
||||
}
|
||||
state.modelCards[`${sel.kind}:${sel.name}`] = card;
|
||||
setCardStatus(data.installed ? `Saved ${data.path}` : `Draft + wanted → ${data.path}`);
|
||||
refreshInventory();
|
||||
},
|
||||
0,
|
||||
(err) => setCardStatus(String(err || 'Save failed')),
|
||||
);
|
||||
}
|
||||
|
||||
function enqueueWantedOnly() {
|
||||
const sel = state.cardsSelection;
|
||||
const card = readCardDraft() || {};
|
||||
if (!sel && !card.civitai_url) {
|
||||
setCardStatus('Need model or civitai_url');
|
||||
return;
|
||||
}
|
||||
genericRequest(
|
||||
'AssistentEnqueueWanted',
|
||||
{
|
||||
kind: (card.kind || sel?.kind || 'lora'),
|
||||
url: card.civitai_url || '',
|
||||
version_id: card.version_id || 0,
|
||||
title: card.name || sel?.name || '',
|
||||
card,
|
||||
},
|
||||
(data) => {
|
||||
setCardStatus(data.already ? 'Already in wanted queue' : `Wanted → ${data.path}`);
|
||||
},
|
||||
0,
|
||||
(err) => setCardStatus(String(err || 'Enqueue failed')),
|
||||
);
|
||||
}
|
||||
|
||||
async function generateCardWithAssistent() {
|
||||
const sel = state.cardsSelection;
|
||||
if (!sel) {
|
||||
setCardStatus('Select a model');
|
||||
return;
|
||||
}
|
||||
if (state.busy) {
|
||||
setCardStatus('Chat busy');
|
||||
return;
|
||||
}
|
||||
setPackValue('catalog_card', { flash: true });
|
||||
setView('chat');
|
||||
const meta = await new Promise((resolve) => {
|
||||
genericRequest(
|
||||
'AssistentGetCardMeta',
|
||||
{ kind: sel.kind, name: sel.name },
|
||||
(data) => resolve(data),
|
||||
0,
|
||||
() => resolve(null),
|
||||
);
|
||||
});
|
||||
const forced = `Write a recommendation card for this ${sel.kind}: ${sel.name}. Use metadata/triggers only; output one JSON card.`;
|
||||
await sendChat({
|
||||
forcedUserText: forced,
|
||||
skipSlash: true,
|
||||
skipAutoPack: true,
|
||||
fromCards: true,
|
||||
cardTarget: {
|
||||
kind: sel.kind,
|
||||
name: sel.name,
|
||||
meta,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function inventoryIsStale(maxAgeMs = 20000) {
|
||||
if (!state.inventoryFetchedAt) {
|
||||
return true;
|
||||
}
|
||||
return (Date.now() - state.inventoryFetchedAt) > maxAgeMs;
|
||||
}
|
||||
|
||||
async function ensureFreshInventory({ forceRescan } = {}) {
|
||||
const rescan = forceRescan || inventoryIsStale(20000);
|
||||
await refreshInventoryAsync({ rescan });
|
||||
}
|
||||
|
||||
function triggerSwarmModelRefresh(done) {
|
||||
if (typeof genericRequest !== 'function') {
|
||||
if (done) {
|
||||
done();
|
||||
@@ -2171,23 +2669,15 @@
|
||||
return;
|
||||
}
|
||||
genericRequest(
|
||||
'AssistentListInventory',
|
||||
{},
|
||||
(data) => {
|
||||
state.inventory = {
|
||||
loras: data.loras || [],
|
||||
checkpoints: data.checkpoints || [],
|
||||
wildcards: data.wildcards || [],
|
||||
has_civitai_key: !!data.has_civitai_key,
|
||||
};
|
||||
setStatus(`Inventory: ${state.inventory.loras.length} LoRAs, ${state.inventory.wildcards.length} wildcards`);
|
||||
'TriggerRefresh',
|
||||
{ strong: true },
|
||||
() => {
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
},
|
||||
0,
|
||||
(err) => {
|
||||
console.warn('Assistent inventory', err);
|
||||
() => {
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
@@ -2196,7 +2686,16 @@
|
||||
}
|
||||
|
||||
async function handleReplySideEffects(reply, civitaiResults, opts = {}) {
|
||||
const { fromAutoCritique, fromVisionHop } = opts;
|
||||
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');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const { patch } = extractPatch(reply);
|
||||
if (Array.isArray(patch?.actions) && patch.actions.map(String).includes('interrupt')) {
|
||||
doInterruptNow();
|
||||
@@ -2389,6 +2888,17 @@
|
||||
await applyQuickPatch({ vary: true, seed: -1, actions: ['generate'] }, 'Vary (new seed)');
|
||||
return true;
|
||||
}
|
||||
if (cmd === 'inventory' || cmd === 'inv') {
|
||||
setStatus('Rescanning models…');
|
||||
triggerSwarmModelRefresh(async () => {
|
||||
await refreshInventoryAsync({ rescan: true });
|
||||
const n = state.inventory?.loras?.length || 0;
|
||||
const ck = state.inventory?.checkpoints?.length || 0;
|
||||
appendSystemNote(`Inventory refreshed: ${n} LoRAs, ${ck} checkpoints.`);
|
||||
setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts (rescanned)`);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (cmd === 'pack') {
|
||||
if (!setPackValue(arg, { flash: true, user: true })) {
|
||||
setStatus('Pack: write|critique|compose|params|inpaint|describe');
|
||||
@@ -2483,14 +2993,20 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts.skipAutoPack && !opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop) {
|
||||
if (!opts.skipAutoPack && !opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.fromCards) {
|
||||
const guessed = autoSelectPack(text);
|
||||
if (guessed) {
|
||||
setPackValue(guessed, { flash: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Cards mode must not be overridden by auto-pack; keep catalog_card.
|
||||
if (opts.fromCards || state.view === 'cards') {
|
||||
setPackValue('catalog_card', { flash: false });
|
||||
}
|
||||
|
||||
const pack = $('sa_pack')?.value || 'write_prompt';
|
||||
const persona = $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral';
|
||||
const model = $('sa_model')?.value;
|
||||
if (!model) {
|
||||
setStatus('Pick an Ollama model in ⚙');
|
||||
@@ -2498,6 +3014,11 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Always pull latest LoRA/checkpoint list before the LLM sees context
|
||||
// (rescans disk when inventory is older than ~20s or after downloads).
|
||||
setStatus('Refreshing inventory…');
|
||||
await ensureFreshInventory({ forceRescan: !!opts.fromDownload });
|
||||
|
||||
if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop) {
|
||||
state.critiqueHopUsed = false;
|
||||
state.visionHopUsed = false;
|
||||
@@ -2534,6 +3055,18 @@
|
||||
const context = collectLiveContext();
|
||||
context.has_vision_image = !!images;
|
||||
context.attached_slot_ids = visionSlots.map((s) => s.id);
|
||||
context.persona = persona;
|
||||
if (opts.cardTarget) {
|
||||
context.card_target = opts.cardTarget;
|
||||
}
|
||||
if (opts.fromCards || pack === 'catalog_card') {
|
||||
context.auto_apply = false;
|
||||
context.auto_generate = false;
|
||||
}
|
||||
await prefetchActiveModelCards();
|
||||
// Refresh cards into context after prefetch
|
||||
const refreshed = collectLiveContext();
|
||||
context.model_cards = refreshed.model_cards;
|
||||
const messages = state.history.slice(-12).map((m) => ({ role: m.role, content: m.content }));
|
||||
if (images && messages.length) {
|
||||
messages[messages.length - 1].images = images;
|
||||
@@ -2550,6 +3083,7 @@
|
||||
baseUrl,
|
||||
model,
|
||||
pack,
|
||||
persona,
|
||||
includeBase: true,
|
||||
messages,
|
||||
context_json: JSON.stringify(context),
|
||||
@@ -2557,6 +3091,7 @@
|
||||
messages,
|
||||
context_json: JSON.stringify(context),
|
||||
pack,
|
||||
persona,
|
||||
base_url: baseUrl,
|
||||
model,
|
||||
},
|
||||
@@ -2815,6 +3350,7 @@
|
||||
}
|
||||
window.__swarmAssistentWired = true;
|
||||
loadSettings();
|
||||
setView(state.view || 'chat');
|
||||
updateGate();
|
||||
ensureBoard();
|
||||
renderBoard();
|
||||
@@ -2824,11 +3360,26 @@
|
||||
}
|
||||
maybeWelcome();
|
||||
refreshModels();
|
||||
refreshPersonas();
|
||||
refreshInventory();
|
||||
wireDropZone();
|
||||
wireSplitter();
|
||||
registerSendButton();
|
||||
|
||||
$('sa_tab_chat')?.addEventListener('click', () => setView('chat'));
|
||||
$('sa_tab_cards')?.addEventListener('click', () => setView('cards'));
|
||||
$('sa_persona')?.addEventListener('change', saveSettings);
|
||||
$('sa_cards_kind')?.addEventListener('change', renderCardsList);
|
||||
$('sa_btn_cards_refresh')?.addEventListener('click', () => refreshInventory(() => renderCardsList(), { rescan: true }));
|
||||
$('sa_btn_card_meta')?.addEventListener('click', () => {
|
||||
if (state.cardsSelection) {
|
||||
selectCardModel(state.cardsSelection);
|
||||
}
|
||||
});
|
||||
$('sa_btn_card_generate')?.addEventListener('click', () => generateCardWithAssistent());
|
||||
$('sa_btn_card_save')?.addEventListener('click', () => saveCurrentCard());
|
||||
$('sa_btn_card_wanted')?.addEventListener('click', () => enqueueWantedOnly());
|
||||
|
||||
$('sa_btn_settings')?.addEventListener('click', () => {
|
||||
const s = $('sa_settings');
|
||||
if (s) {
|
||||
@@ -2839,7 +3390,7 @@
|
||||
saveSettings();
|
||||
refreshModels();
|
||||
});
|
||||
$('sa_btn_refresh_inventory')?.addEventListener('click', () => refreshInventory());
|
||||
$('sa_btn_refresh_inventory')?.addEventListener('click', () => refreshInventory(null, { rescan: true }));
|
||||
$('sa_btn_add_ref')?.addEventListener('click', () => addRefSlot({ select: true }));
|
||||
$('sa_btn_use_current')?.addEventListener('click', () => snapshotGenerateToRef());
|
||||
$('sa_btn_as_init')?.addEventListener('click', async () => {
|
||||
@@ -2951,9 +3502,11 @@
|
||||
setInterval(syncGenerateSlot, 700);
|
||||
setInterval(() => {
|
||||
if (!state.busy) {
|
||||
refreshInventory();
|
||||
const tabOn = !!document.getElementById(TAB_BUTTON_ID)?.classList.contains('tab-button-selected')
|
||||
|| !!document.getElementById('swarm_assistent_root')?.offsetParent;
|
||||
refreshInventory(null, { rescan: inventoryIsStale(tabOn ? 45000 : 120000) });
|
||||
}
|
||||
}, 120000);
|
||||
}, 30000);
|
||||
|
||||
window.swarmAssistent = {
|
||||
setImageFromSrc,
|
||||
|
||||
Reference in New Issue
Block a user