Files
swarm-assistent/Assets/assistent.js
T

3537 lines
130 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API).
* 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';
const LS_AUTO_CRITIQUE = 'swarm_assistent_auto_critique';
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;
const ASPECT_TABLE = {
'1:1': [1024, 1024],
'4:3': [1184, 896],
'3:2': [1248, 832],
'16:9': [1376, 768],
'2.35:1': [1568, 672],
'4:5': [928, 1152],
'2:3': [832, 1248],
'9:16': [768, 1376],
};
const PACK_ALIASES = {
write: 'write_prompt',
write_prompt: 'write_prompt',
critique: 'critique_image',
critique_image: 'critique_image',
compose: 'compose_scene',
compose_scene: 'compose_scene',
params: 'fix_params',
fix_params: 'fix_params',
inpaint: 'inpaint_edit',
inpaint_edit: 'inpaint_edit',
describe: 'describe_ref',
describe_ref: 'describe_ref',
card: 'catalog_card',
catalog: 'catalog_card',
catalog_card: 'catalog_card',
};
const WELCOME_HTML = `
<div class="sa-welcome-title">Assistent · Krea 2</div>
<ul>
<li><strong>Generate</strong> слева — живой просмотр текущей генерации. В чат сам не уходит.</li>
<li><strong>Ref</strong> — референсы: drop / paste / Snapshot gen / Send to Assistent.</li>
<li>Глаз на окне — отправить это изображение мне в vision.</li>
<li>Чипсы aspect / seed / Vary — быстрые патчи. В чате: <code>/help</code>.</li>
<li>Кнопки патча только у последнего предложения. Пока идёт генерация, Apply + Generate крутит спиннер.</li>
</ul>
Напиши, что сгенерировать — или кинь референс и попроси правку.`;
const HELP_TEXT = `Slash-команды (без LLM):
/help — этот список
/gen — Generate сейчас
/look generate|refN — прикрепить окно к vision
/init /mask /clear — Init / Mask / Clear Init
/interrupt — остановить генерацию
/aspect 16:9 — размер из таблицы 1K
/seed lock|random — зафиксировать или рандомизировать seed
/vary — новый seed, тот же промпт
/pack write|critique|compose|params|inpaint|describe
/civitai <query> — поиск LoRA (Confirm в чате)
/inventory — rescan моделей + обновить список LoRA
Чипсы над полем ввода делают то же для aspect / seed / vary.`;
const state = {
history: [],
packsLoaded: false,
busy: false,
generating: false,
lastImageDataUrl: null,
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,
busyPhase: 'idle',
busyStarted: 0,
gotDelta: false,
busyTimer: null,
slots: [],
selectedSlotId: 'ref1',
refSeq: 1,
packUserTouched: false,
view: 'chat',
personas: [],
modelCards: {},
cardsSelection: null,
cardsBusy: false,
};
function $(id) {
return document.getElementById(id);
}
function modelShort(name) {
const s = String(name || '');
const slash = s.lastIndexOf('/');
return (slash >= 0 ? s.slice(slash + 1) : s) || 'model';
}
function fmtElapsed(ms) {
const s = Math.max(0, Math.floor(ms / 1000));
if (s < 60) {
return `${s}s`;
}
return `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`;
}
function hideChatEmpty() {
const empty = $('sa_chat_empty');
if (empty) {
empty.hidden = true;
}
}
function showChatEmptyIfIdle() {
const box = $('sa_messages');
const empty = $('sa_chat_empty');
if (!box || !empty) {
return;
}
const hasMsg = [...box.children].some((el) => el.id !== 'sa_chat_empty');
empty.hidden = hasMsg;
}
function setBusyPhase(phase) {
state.busyPhase = phase || 'thinking';
tickBusyUi();
syncPatchActionAvailability();
syncGenerateBusy();
}
function tickBusyUi() {
if (state.busyPhase === 'idle') {
return;
}
const elapsed = Date.now() - (state.busyStarted || Date.now());
if (!state.gotDelta && (state.busyPhase === 'thinking' || state.busyPhase === 'waiting') && elapsed > 1600) {
state.busyPhase = 'loading';
}
const model = modelShort($('sa_model')?.value);
const labels = {
encoding: 'Encoding image…',
waiting: 'Waiting for Ollama…',
loading: `Loading ${model} into GPU… first load can take a minute`,
thinking: 'Thinking…',
streaming: 'Writing…',
generating: 'Generating image…',
applying: 'Applying patch…',
refining: 'Civitai search done — refining…',
};
const text = labels[state.busyPhase] || 'Working…';
const barText = $('sa_livebar_text');
if (barText) {
barText.textContent = text;
}
const elapsedEl = $('sa_elapsed');
if (elapsedEl) {
elapsedEl.textContent = fmtElapsed(elapsed);
}
const status = $('sa_status');
if (status) {
status.textContent = text;
status.classList.add('sa-status-busy');
}
}
function startBusyUi(phase) {
state.busyStarted = Date.now();
state.gotDelta = false;
state.busyPhase = phase || 'thinking';
$('swarm_assistent_root')?.classList.add('sa-is-busy');
$('sa_composer')?.classList.add('sa-composer-busy');
const send = $('sa_btn_send');
if (send) {
send.disabled = true;
}
const input = $('sa_input');
if (input) {
input.classList.add('sa-input-busy');
}
const bar = $('sa_livebar');
if (bar) {
bar.hidden = false;
}
const dot = $('sa_live_dot');
if (dot) {
dot.hidden = false;
}
tickBusyUi();
syncPatchActionAvailability();
syncGenerateBusy();
if (state.busyTimer) {
clearInterval(state.busyTimer);
}
state.busyTimer = setInterval(tickBusyUi, 400);
}
function stopBusyUi(finalStatus) {
if (state.busyTimer) {
clearInterval(state.busyTimer);
state.busyTimer = null;
}
const elapsed = Date.now() - (state.busyStarted || Date.now());
state.busyPhase = 'idle';
$('swarm_assistent_root')?.classList.remove('sa-is-busy');
$('sa_composer')?.classList.remove('sa-composer-busy');
const send = $('sa_btn_send');
if (send) {
send.disabled = false;
}
const input = $('sa_input');
if (input) {
input.classList.remove('sa-input-busy');
}
const bar = $('sa_livebar');
if (bar) {
bar.hidden = true;
}
const dot = $('sa_live_dot');
if (dot) {
dot.hidden = true;
}
const status = $('sa_status');
if (status) {
status.classList.remove('sa-status-busy');
}
if (finalStatus != null) {
const suffix = elapsed >= 1000 ? ` · ${fmtElapsed(elapsed)}` : '';
setStatus(finalStatus + suffix);
}
syncPatchActionAvailability();
syncGenerateBusy();
}
function setStatus(text) {
const el = $('sa_status');
if (el) {
el.textContent = text || '';
}
}
function setInterruptVisible(on) {
const btn = $('sa_btn_interrupt');
if (btn) {
btn.hidden = !on;
btn.classList.toggle('sa-interrupt-active', !!on);
}
}
function looksLikeKrea(text) {
const s = String(text || '');
return /krea\s*2|krea2|krea-2/i.test(s) || /krea/i.test(s);
}
function resolveCurrentCheckpoint() {
const out = {
name: null,
architecture: null,
compat_class: null,
title: null,
class: null,
source: null,
};
try {
if (typeof currentModelHelper !== 'undefined' && currentModelHelper) {
out.name = currentModelHelper.curModel || null;
out.architecture = currentModelHelper.curArch || null;
out.compat_class = currentModelHelper.curCompatClass || null;
out.source = 'currentModelHelper';
}
} catch (e) { /* ignore */ }
try {
if (typeof getCurrentModel === 'function') {
const model = getCurrentModel();
if (model) {
out.name = out.name || model.name || null;
out.title = model.title || null;
out.architecture = out.architecture || model.architecture || null;
out.class = model.class || null;
out.compat_class = out.compat_class || model.compat_class || null;
out.source = out.source || 'getCurrentModel';
}
}
} catch (e) { /* ignore */ }
try {
const sel =
document.getElementById('current_model') ||
document.getElementById('input_model');
if (sel) {
const opt = sel.selectedOptions && sel.selectedOptions[0];
const hint = [
sel.value,
opt && opt.text,
opt && opt.dataset && opt.dataset.cleanname,
]
.filter(Boolean)
.join(' ');
if (!out.name && sel.value) {
out.name = sel.value;
out.source = out.source || 'dropdown';
}
if (hint && !out.architecture) {
out.title = out.title || hint;
}
}
} catch (e) { /* ignore */ }
return out;
}
function isKreaSelected() {
try {
const m = resolveCurrentCheckpoint();
const blob = [
m.architecture,
m.compat_class,
m.title,
m.name,
m.class,
].join(' ');
return looksLikeKrea(blob);
} catch (e) {
return false;
}
}
function updateGate() {
const ok = isKreaSelected();
const gate = $('sa_gate');
const layout = $('sa_layout');
if (gate) {
gate.hidden = ok;
if (!ok) {
const m = resolveCurrentCheckpoint();
const seen = [m.architecture, m.compat_class, m.name]
.filter(Boolean)
.join(' · ');
const p = gate.querySelector('p');
if (p) {
p.innerHTML = seen
? `Swarm Assistent is for <strong>Krea 2</strong> models only. Current: <code>${escapeHtml(
seen,
)}</code> — pick a checkpoint with architecture <code>krea-2</code>.`
: 'Swarm Assistent is for <strong>Krea 2</strong> models only. Select a Krea 2 checkpoint on Generate to enable the chat.';
}
}
}
if (layout) {
layout.classList.toggle('sa-disabled', !ok);
}
return ok;
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function val(id) {
const el = document.getElementById(id);
return el ? el.value : '';
}
function setVal(id, value) {
const el = document.getElementById(id);
if (!el) {
return;
}
el.value = value;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
function openAssistentTab() {
const tab = document.getElementById(TAB_BUTTON_ID);
if (tab) {
tab.click();
return true;
}
const pane = document.getElementById('assistent');
if (pane && typeof bootstrap !== 'undefined' && bootstrap.Tab) {
try {
bootstrap.Tab.getOrCreateInstance(tab || pane).show();
} catch (e) { /* ignore */ }
}
return !!tab;
}
function flashImagePane(slotId) {
const el = document.querySelector(`.sa-slot[data-id="${slotId || state.selectedSlotId}"]`);
if (!el) {
return;
}
el.classList.remove('sa-flash');
void el.offsetWidth;
el.classList.add('sa-flash');
}
function ensureBoard() {
if (state.slots.length) {
return;
}
state.slots = [
{ id: GEN_ID, type: 'generate', label: 'Generate', src: null, attach: false },
{ id: 'ref1', type: 'ref', label: 'Ref 1', src: null, attach: true },
];
state.refSeq = 1;
state.selectedSlotId = 'ref1';
}
function slotById(id) {
ensureBoard();
const key = normalizeSlotId(id);
return state.slots.find((s) => s.id === key) || null;
}
function generateSlot() {
return slotById(GEN_ID);
}
function refSlots() {
ensureBoard();
return state.slots.filter((s) => s.type === 'ref');
}
function normalizeSlotId(id) {
const raw = String(id || '').trim().toLowerCase();
if (!raw) {
return '';
}
if (raw === 'gen' || raw === 'current' || raw === 'live' || raw === 'generation') {
return GEN_ID;
}
if (raw === 'selected' || raw === 'sel') {
return state.selectedSlotId;
}
const m = raw.match(/^ref\s*[_-]?\s*(\d+)$/);
if (m) {
return `ref${m[1]}`;
}
return raw;
}
function selectedSlot() {
return slotById(state.selectedSlotId) || generateSlot();
}
function selectedSrc() {
return selectedSlot()?.src || null;
}
function syncLastImageAlias() {
const attached = attachableSlots();
state.lastImageDataUrl = (attached[0] || selectedSlot() || generateSlot())?.src || null;
}
function attachableSlots() {
ensureBoard();
return state.slots.filter((s) => s.attach && s.src);
}
function setSlotSrc(id, src, { select = true, attach = null, note = null, switchTab = false } = {}) {
const slot = slotById(id);
if (!slot) {
return false;
}
const cleaned = src ? String(src).trim().split(/\s+/)[0] : null;
if (cleaned && cleaned.startsWith('#')) {
return false;
}
slot.src = cleaned || null;
if (attach != null) {
slot.attach = !!attach;
} else if (slot.type === 'ref' && slot.src) {
slot.attach = true;
}
if (select) {
state.selectedSlotId = slot.id;
}
syncLastImageAlias();
renderBoard();
flashImagePane(slot.id);
if (switchTab) {
openAssistentTab();
}
if (note) {
setStatus(note);
}
return true;
}
function addRefSlot({ src = null, select = true } = {}) {
ensureBoard();
if (refSlots().length >= MAX_REF_SLOTS) {
setStatus(`Max ${MAX_REF_SLOTS} reference windows`);
const empty = refSlots().find((s) => !s.src);
if (empty && src) {
return setSlotSrc(empty.id, src, { select, note: `Loaded into ${empty.label}` });
}
return empty || null;
}
state.refSeq += 1;
const id = `ref${state.refSeq}`;
const slot = {
id,
type: 'ref',
label: `Ref ${state.refSeq}`,
src: src || null,
attach: !!src,
};
state.slots.push(slot);
if (select) {
state.selectedSlotId = id;
}
renderBoard();
return slot;
}
function clearSlot(id, { silent = false } = {}) {
const slot = slotById(id);
if (!slot) {
return;
}
if (slot.type === 'generate') {
if (!silent) {
setStatus('Generate window is live — use Snapshot gen to copy it');
}
return;
}
slot.src = null;
slot.attach = true;
syncLastImageAlias();
renderBoard();
if (!silent) {
setStatus(`${slot.label} cleared`);
}
}
function snapshotGenerateToRef() {
const src = generateSlot()?.src || findCurrentGenerateSrc({ allowPreview: true });
if (!src) {
setStatus('No current Generate image');
return false;
}
const empty = refSlots().find((s) => !s.src);
if (empty) {
return setSlotSrc(empty.id, src, { note: `Snapshot → ${empty.label}` });
}
const created = addRefSlot({ src, select: true });
if (created?.src) {
setStatus(`Snapshot → ${created.label}`);
flashImagePane(created.id);
return true;
}
const last = refSlots()[refSlots().length - 1];
if (last) {
return setSlotSrc(last.id, src, { note: `Snapshot → ${last.label} (replaced)` });
}
return false;
}
function putImageOnBoard(src, { note = null, switchTab = false, preferSelected = true } = {}) {
if (!src) {
return false;
}
ensureBoard();
const sel = selectedSlot();
if (preferSelected && sel && sel.type === 'ref') {
return setSlotSrc(sel.id, src, { note: note || `Loaded into ${sel.label}`, switchTab });
}
const empty = refSlots().find((s) => !s.src);
if (empty) {
return setSlotSrc(empty.id, src, { note: note || `Loaded into ${empty.label}`, switchTab });
}
const created = addRefSlot({ src, select: true });
if (created) {
if (switchTab) {
openAssistentTab();
}
if (note) {
setStatus(note);
}
return true;
}
return false;
}
function setImageFromSrc(src, opts = {}) {
return putImageOnBoard(src, opts);
}
function clearVisionImage(opts) {
clearSlot(state.selectedSlotId, opts);
}
function slotCatalog() {
ensureBoard();
return state.slots.map((s) => ({
id: s.id,
type: s.type,
label: s.label,
has_image: !!s.src,
attach: !!s.attach,
selected: s.id === state.selectedSlotId,
}));
}
function lookAtIdsFromPatch(patch) {
if (!patch) {
return [];
}
const raw = patch.look_at || patch.vision_from || patch.vision_slots;
const list = Array.isArray(raw) ? raw : (raw ? [raw] : []);
if (Array.isArray(patch.actions)) {
for (const a of patch.actions.map(String)) {
const m = a.match(/^look_at[_:]?(generate|ref\d+|selected)$/i);
if (m) {
list.push(m[1]);
}
}
}
return [...new Set(list.map(normalizeSlotId).filter(Boolean))];
}
function resolveSlotSrc(id) {
if (!id) {
return selectedSrc() || generateSlot()?.src || findCurrentGenerateSrc();
}
const slot = slotById(id);
if (slot?.src) {
return slot.src;
}
if (normalizeSlotId(id) === GEN_ID) {
return findCurrentGenerateSrc();
}
return null;
}
function isGenerateUnavailable() {
if (state.generating || state.busy) {
return true;
}
try {
if (typeof mainGenHandler !== 'undefined' && mainGenHandler) {
if (mainGenHandler.isGenerating === true || mainGenHandler.running === true) {
return true;
}
}
} catch (e) { /* ignore */ }
const interrupt = document.getElementById('interrupt_button')
|| document.getElementById('alt_interrupt_button');
if (interrupt && !interrupt.hidden && interrupt.offsetParent !== null) {
return true;
}
const genBtn = document.getElementById('generate_button') || document.getElementById('alt_generate_button');
if (genBtn && (genBtn.disabled || /interrupt/i.test(genBtn.textContent || ''))) {
return true;
}
return false;
}
function syncGenerateBusy() {
const overlay = document.querySelector('.sa-slot-gen .sa-slot-busy');
if (overlay) {
overlay.hidden = !state.generating && state.busyPhase !== 'generating';
}
}
function syncPatchActionAvailability() {
const bar = document.querySelector('.sa-patch-actions.sa-patch-current');
if (!bar) {
return;
}
const locked = isGenerateUnavailable();
bar.querySelectorAll('.sa-btn-gen').forEach((btn) => {
btn.disabled = locked;
let spin = btn.querySelector('.sa-spinner');
if (locked) {
if (!spin) {
spin = document.createElement('span');
spin.className = 'sa-spinner sa-spinner-btn';
spin.setAttribute('aria-hidden', 'true');
btn.prepend(spin);
}
} else if (spin) {
spin.remove();
}
});
}
function retireStalePatchActions() {
document.querySelectorAll('.sa-patch-actions').forEach((el) => {
const note = document.createElement('div');
note.className = 'sa-patch-stale';
note.textContent = 'Superseded — use the latest proposal';
el.replaceWith(note);
});
}
function mountPatchActions(parent, patch) {
if (!parent || !patch) {
return;
}
retireStalePatchActions();
const wrap = parent.classList.contains('sa-patch') ? parent : null;
const host = wrap || parent;
const actions = document.createElement('div');
actions.className = 'sa-patch-actions sa-patch-current';
for (const [label, which] of [
['Apply all', 'all'],
['Prompt', 'prompt'],
['LoRAs', 'loras'],
['Params', 'params'],
]) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'basic-button';
btn.textContent = label;
btn.addEventListener('click', () => applyPatch(patch, which));
actions.appendChild(btn);
}
const genBtn = document.createElement('button');
genBtn.type = 'button';
genBtn.className = 'basic-button sa-btn-gen';
genBtn.textContent = 'Apply + Generate';
genBtn.addEventListener('click', async () => {
if (isGenerateUnavailable()) {
return;
}
await applyPatch(patch, 'all');
await runGenerateFromPatch({ ...patch, actions: ['generate'] });
});
actions.appendChild(genBtn);
host.appendChild(actions);
syncPatchActionAvailability();
}
function renderBoard() {
const board = $('sa_board');
if (!board) {
return;
}
ensureBoard();
const refs = refSlots();
board.classList.toggle('sa-board-many', refs.some((s) => s.src) || refs.length > 1);
board.innerHTML = '';
for (const slot of state.slots) {
board.appendChild(buildSlotEl(slot));
}
if (refs.length < MAX_REF_SLOTS) {
const add = document.createElement('div');
add.className = 'sa-add-cell';
add.textContent = '+ Ref';
add.title = 'Add a reference window';
add.addEventListener('click', (e) => {
e.stopPropagation();
addRefSlot({ select: true });
});
add.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
});
add.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
const created = addRefSlot({ select: true });
if (created) {
state.selectedSlotId = created.id;
await handleDropDataTransfer(e.dataTransfer, created.id);
}
});
board.appendChild(add);
}
syncGenerateBusy();
syncLastImageAlias();
}
function buildSlotEl(slot) {
const el = document.createElement('div');
el.className = `sa-slot${slot.type === 'generate' ? ' sa-slot-gen' : ''}`;
el.dataset.id = slot.id;
if (slot.src) {
el.classList.add('sa-has-image');
}
if (slot.id === state.selectedSlotId) {
el.classList.add('sa-selected');
}
const bar = document.createElement('div');
bar.className = 'sa-slot-bar';
const chip = document.createElement('span');
chip.className = `sa-slot-chip${slot.type === 'generate' ? ' sa-live' : ''}`;
chip.textContent = slot.type === 'generate' ? 'Generate' : slot.label;
bar.appendChild(chip);
const attachLab = document.createElement('label');
attachLab.className = 'sa-slot-attach';
attachLab.title = 'Attach this window to the next chat (vision)';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = !!slot.attach;
cb.addEventListener('click', (e) => e.stopPropagation());
cb.addEventListener('change', (e) => {
e.stopPropagation();
slot.attach = cb.checked;
syncLastImageAlias();
});
attachLab.appendChild(cb);
attachLab.appendChild(document.createTextNode(' vision'));
bar.appendChild(attachLab);
el.appendChild(bar);
if (slot.src) {
const img = document.createElement('img');
img.alt = slot.label;
img.src = slot.src;
el.appendChild(img);
} else {
const empty = document.createElement('div');
empty.className = 'sa-image-empty';
empty.innerHTML = slot.type === 'generate'
? '<div class="sa-empty-title">Generate</div><div class="sa-empty-hint">Live view of the current generation</div>'
: '<div class="sa-empty-title">Reference</div><div class="sa-empty-hint">Drop · paste · Snapshot gen</div>';
el.appendChild(empty);
}
const busy = document.createElement('div');
busy.className = 'sa-slot-busy';
busy.hidden = !(slot.type === 'generate' && (state.generating || state.busyPhase === 'generating'));
busy.innerHTML = '<span class="sa-spinner" aria-hidden="true"></span>';
el.appendChild(busy);
el.addEventListener('click', () => {
state.selectedSlotId = slot.id;
renderBoard();
});
el.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
el.classList.add('sa-dragover');
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'copy';
}
});
el.addEventListener('dragleave', () => el.classList.remove('sa-dragover'));
el.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
el.classList.remove('sa-dragover');
const targetId = slot.type === 'generate' ? null : slot.id;
if (slot.type === 'generate') {
const created = addRefSlot({ select: true });
await handleDropDataTransfer(e.dataTransfer, created?.id);
} else {
await handleDropDataTransfer(e.dataTransfer, targetId);
}
});
return el;
}
function syncGenerateSlot() {
const src = findCurrentGenerateSrc();
const slot = generateSlot();
if (!slot) {
return;
}
if (src && src !== slot.src) {
slot.src = src;
const img = document.querySelector('.sa-slot-gen img');
const empty = document.querySelector('.sa-slot-gen .sa-image-empty');
const frame = document.querySelector('.sa-slot-gen');
if (img) {
img.src = src;
} else if (frame) {
renderBoard();
return;
}
if (empty) {
empty.hidden = true;
}
frame?.classList.add('sa-has-image');
}
syncGenerateBusy();
syncPatchActionAvailability();
}
function maybeWelcome() {
if (localStorage.getItem(LS_WELCOMED) === '1') {
return;
}
if (!$('sa_messages')) {
return;
}
localStorage.setItem(LS_WELCOMED, '1');
const box = $('sa_messages');
hideChatEmpty();
const div = document.createElement('div');
div.className = 'sa-msg assistant sa-welcome';
div.innerHTML = WELCOME_HTML;
box.appendChild(div);
box.scrollTop = box.scrollHeight;
}
function countPromptImages() {
try {
const box = document.getElementById('alt_prompt_textbox') || document.getElementById('input_prompt');
if (!box) {
return 0;
}
const text = box.value || '';
const matches = text.match(/<image(?:\s|\/|>)/gi) || text.match(/data:image\//gi);
return matches ? matches.length : 0;
} catch (e) {
return 0;
}
}
function triggerChangeForEl(el) {
if (!el) {
return;
}
if (typeof triggerChangeFor === 'function') {
triggerChangeFor(el);
return;
}
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
function openInitImageGroup() {
try {
const initEl = document.getElementById('input_initimage');
if (initEl && typeof toggleGroupOpen === 'function') {
toggleGroupOpen(initEl, true);
}
} catch (e) { /* ignore */ }
const toggler = document.getElementById('input_group_content_initimage_toggle');
if (toggler) {
toggler.checked = true;
triggerChangeForEl(toggler);
}
}
function hasFileParam(id) {
const el = document.getElementById(id);
return !!(el && el.files && el.files.length > 0);
}
function clearFileParam(id) {
const el = document.getElementById(id);
if (!el) {
return false;
}
try {
el.value = '';
if (el.files && typeof DataTransfer !== 'undefined') {
el.files = new DataTransfer().files;
}
} catch (e) { /* ignore */ }
triggerChangeForEl(el);
return true;
}
function guessImageMime(src) {
const s = String(src || '');
if (s.startsWith('data:image/')) {
const m = s.match(/^data:(image\/[a-z0-9.+-]+)/i);
return (m && m[1]) || 'image/png';
}
const path = s.split('?')[0];
const ext = path.substring(path.lastIndexOf('.') + 1).toLowerCase();
if (ext === 'jpg' || ext === 'jpeg') {
return 'image/jpeg';
}
if (ext === 'webp') {
return 'image/webp';
}
if (ext === 'gif') {
return 'image/gif';
}
return 'image/png';
}
async function srcToBlob(src) {
if (!src) {
return null;
}
const cleaned = String(src).trim().split(/\s+/)[0];
if (cleaned.startsWith('data:') || cleaned.startsWith('/') || cleaned.startsWith('View/') || cleaned.startsWith('http')) {
try {
const resp = await fetch(cleaned);
return await resp.blob();
} catch (e) {
console.warn('Assistent: fetch blob failed', e);
}
}
return await new Promise((resolve) => {
const tmpImg = new Image();
tmpImg.crossOrigin = 'Anonymous';
tmpImg.onload = () => {
try {
const canvas = document.createElement('canvas');
canvas.width = tmpImg.naturalWidth;
canvas.height = tmpImg.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(tmpImg, 0, 0);
canvas.toBlob((blob) => resolve(blob), 'image/png');
} catch (e) {
resolve(null);
}
};
tmpImg.onerror = () => resolve(null);
tmpImg.src = cleaned;
});
}
async function setFileParamFromSrc(paramId, src, { filename = 'assistent.png' } = {}) {
const el = document.getElementById(paramId);
if (!el) {
setStatus(`Missing ${paramId} on Generate tab`);
return false;
}
const blob = await srcToBlob(src);
if (!blob) {
setStatus('Could not load image for init/mask');
return false;
}
const mime = blob.type || guessImageMime(src);
const file = new File([blob], filename, { type: mime });
const container = new DataTransfer();
container.items.add(file);
el.files = container.files;
triggerChangeForEl(el);
openInitImageGroup();
return true;
}
async function setInitFromSrc(src) {
const ok = await setFileParamFromSrc('input_initimage', src, { filename: 'assistent_init.png' });
if (ok) {
setStatus('Init Image set');
}
return ok;
}
async function setMaskFromSrc(src) {
const ok = await setFileParamFromSrc('input_maskimage', src, { filename: 'assistent_mask.png' });
if (ok) {
setStatus('Mask Image set (white = edit)');
}
return ok;
}
function clearInitAndMask() {
clearFileParam('input_initimage');
clearFileParam('input_maskimage');
const toggler = document.getElementById('input_group_content_initimage_toggle');
if (toggler) {
toggler.checked = false;
triggerChangeForEl(toggler);
}
setStatus('Init Image + Mask cleared');
}
function readInitContext() {
const creativityRaw = val('input_initimagecreativity');
const creativity = creativityRaw === '' ? null : parseFloat(creativityRaw);
return {
has_init_image: hasFileParam('input_initimage'),
has_mask_image: hasFileParam('input_maskimage'),
init_creativity: Number.isFinite(creativity) ? creativity : null,
mask_blur: parseFloat(val('input_maskblur') || '') || null,
mask_grow: parseInt(val('input_maskgrow') || val('input_maskshrinkgrow') || '', 10) || null,
init_group_on: !!(document.getElementById('input_group_content_initimage_toggle')?.checked),
};
}
function collectLiveContext() {
const inv = state.inventory || {};
const initCtx = readInitContext();
const ctx = {
architecture_ok: isKreaSelected(),
checkpoint: null,
prompt: val('alt_prompt_textbox') || val('input_prompt') || '',
negative: val('input_negativeprompt') || val('alt_negativeprompt_textbox') || '',
width: parseInt(val('input_width') || '0', 10) || null,
height: parseInt(val('input_height') || '0', 10) || null,
steps: parseInt(val('input_steps') || '0', 10) || null,
cfg: parseFloat(val('input_cfgscale') || val('input_cfg') || '') || null,
sigma_shift: parseFloat(val('input_sigmashift') || '') || null,
seed: val('input_seed') || null,
sampler: val('input_sampler') || val('input_samplerate') || null,
scheduler: val('input_scheduler') || null,
batch: parseInt(val('input_images') || val('input_batchsize') || '0', 10) || null,
prompt_image_count: countPromptImages(),
selected_loras: [],
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,
};
try {
const model = resolveCurrentCheckpoint();
if (model.name || model.architecture) {
ctx.checkpoint = {
name: model.name || model.title || null,
architecture: model.architecture || model.compat_class || model.class || null,
title: model.title || null,
};
}
} catch (e) { /* ignore */ }
try {
if (typeof loraHelper !== 'undefined' && loraHelper && Array.isArray(loraHelper.selected)) {
ctx.selected_loras = loraHelper.selected.map((l) => ({
name: l.name || l,
weight: (loraHelper.loraWeightPref && loraHelper.loraWeightPref[l.name || l]) || 1,
}));
}
} 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 {
const models = (typeof allModels !== 'undefined' && allModels) || (typeof model_list !== 'undefined' && model_list) || [];
const list = Array.isArray(models) ? models : Object.values(models || {});
for (const m of list) {
if (!m) {
continue;
}
const folder = `${m.folder || m.path || ''}`;
const isLora = /lora/i.test(m.category || m.type || '') || (m.name && String(m.name).toLowerCase().includes('lora'));
const inLoraFolder = /lora/i.test(folder);
if (!(isLora || inLoraFolder)) {
continue;
}
ctx.available_loras.push({
name: m.name || m.title,
title: m.title || m.name,
trigger_phrase: m.trigger_phrase || m.trigger || (m.metadata && (m.metadata.trigger_phrase || m.metadata.trigger)) || null,
architecture: m.architecture || null,
});
}
if (ctx.available_loras.length > 80) {
ctx.available_loras = ctx.available_loras.slice(0, 80);
}
} catch (e) { /* ignore */ }
}
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;
}
return (
obj.prompt != null ||
obj.loras ||
obj.width ||
obj.height ||
obj.steps ||
obj.cfg ||
obj.seed != null ||
obj.sigma_shift != null ||
obj.sampler ||
obj.actions ||
obj.search_query ||
obj.civitai_query ||
obj.use_init_image != null ||
obj.clear_init_image != null ||
obj.init_creativity != null ||
obj.denoise != null ||
obj.use_mask_image != null ||
obj.clear_mask_image != null ||
obj.mask_blur != null ||
obj.mask_grow != null ||
obj.look_at != null ||
obj.vision_from != null ||
obj.vision_slots != null ||
obj.slot_to_init != null ||
obj.slot_to_mask != null ||
obj.snapshot_generate != null ||
obj.select_slot != null ||
obj.aspect != null ||
obj.images != null ||
obj.batch != null ||
obj.vary != null ||
obj.lock_seed != null ||
obj.creativity != null ||
obj.intensity != null ||
obj.complexity != null ||
obj.movement != null ||
obj.clear_prompt_images != null ||
obj.slot_to_prompt_image != 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 };
}
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
let match;
let lastPatch = null;
let prose = text;
while ((match = re.exec(text)) !== null) {
const raw = match[1].trim();
try {
const obj = JSON.parse(raw);
if (isPatchObject(obj)) {
lastPatch = obj;
prose = (text.slice(0, match.index) + text.slice(match.index + match[0].length)).trim();
}
} catch (e) { /* not json */ }
}
return { prose, patch: lastPatch };
}
function normalizeAspect(raw) {
if (raw == null) {
return null;
}
let s = String(raw).trim().toLowerCase().replace(/\s+/g, '');
if (!s) {
return null;
}
if (s === 'square') {
s = '1:1';
} else if (s === 'portrait' || s === 'vert') {
s = '2:3';
} else if (s === 'landscape' || s === 'horiz') {
s = '16:9';
} else if (s === 'cinematic' || s === 'ultrawide') {
s = '2.35:1';
}
return ASPECT_TABLE[s] ? s : null;
}
function sizeFromAspect(aspect) {
const key = normalizeAspect(aspect);
return key ? ASPECT_TABLE[key] : null;
}
function guessAspectFromSize(w, h) {
const width = parseInt(w, 10);
const height = parseInt(h, 10);
if (!width || !height) {
return null;
}
let best = null;
let bestDist = Infinity;
for (const [key, [aw, ah]] of Object.entries(ASPECT_TABLE)) {
const dist = Math.abs(width / height - aw / ah) + Math.abs(width - aw) / 4000 + Math.abs(height - ah) / 4000;
if (dist < bestDist) {
bestDist = dist;
best = key;
}
}
return bestDist < 0.12 ? best : null;
}
function clearPromptImagesInBox() {
const box = document.getElementById('alt_prompt_textbox') || document.getElementById('input_prompt');
if (!box) {
return false;
}
const before = box.value || '';
const next = before
.replace(/<image\b[^>]*>[\s\S]*?<\/image>/gi, '')
.replace(/<image\b[^>]*\/?>/gi, '')
.replace(/data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=]+/g, '')
.replace(/\n{3,}/g, '\n\n')
.trim();
if (next === before.trim()) {
return false;
}
box.value = next;
box.dispatchEvent(new Event('input', { bubbles: true }));
box.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}
function setPackValue(packName, { flash, user } = {}) {
const pack = $('sa_pack');
if (!pack || !packName) {
return false;
}
const resolved = PACK_ALIASES[String(packName).trim()] || String(packName).trim();
if (![...pack.options].some((o) => o.value === resolved)) {
return false;
}
if (pack.value !== resolved) {
pack.value = resolved;
saveSettings();
}
if (user) {
state.packUserTouched = true;
}
if (flash) {
pack.classList.add('sa-pack-flash');
setTimeout(() => pack.classList.remove('sa-pack-flash'), 900);
}
return true;
}
function autoSelectPack(text) {
if (state.packUserTouched) {
return null;
}
const t = String(text || '').toLowerCase();
if (!t.trim()) {
return null;
}
if (/\b(опиши\s+реф|опиши\s+изображ|prompt\s+from\s+image|describe\s+(this|the|ref|image)|reverse\s*prompt)\b/i.test(t)
|| /опиши\s+(этот|эту|картинк|референс)/i.test(t)) {
return 'describe_ref';
}
if (/\b(critique|что\s+не\s+так|посмотри|смотри|разбери|критик)\b/i.test(t)) {
return 'critique_image';
}
if (/\b(inpaint|замажь|закрась|руки|лицо|маск|mask|img2img|init\s*image)\b/i.test(t)) {
return 'inpaint_edit';
}
if (/\b(steps?|cfg|seed|sigma|размер|aspect|ширин|высот|resolution|batch)\b/i.test(t)) {
return 'fix_params';
}
if (/\b(сцен|moodboard|атмосфер|compose|scene|мизансцен)\b/i.test(t)) {
return 'compose_scene';
}
return 'write_prompt';
}
function patchHasGenTrigger(patch) {
if (!patch) {
return false;
}
if (Array.isArray(patch.actions) && patch.actions.map(String).includes('generate')) {
return true;
}
return (
patch.prompt != null ||
patch.loras ||
patch.width != null ||
patch.height != null ||
patch.aspect != null ||
patch.steps != null ||
patch.cfg != null ||
patch.seed != null ||
patch.sigma_shift != null ||
patch.images != null ||
patch.batch != null ||
patch.vary === true ||
patch.use_init_image ||
patch.clear_init_image ||
patch.init_creativity != null ||
patch.denoise != null ||
patch.use_mask_image ||
patch.clear_mask_image ||
patch.clear_prompt_images
);
}
async function applyPatch(patch, which) {
if (!patch) {
return;
}
const doPrompt = !which || which === 'all' || which === 'prompt';
const doLoras = !which || which === 'all' || which === 'loras';
const doParams = !which || which === 'all' || which === 'size' || which === 'params';
const doInit = !which || which === 'all' || which === 'params' || which === 'init';
if (patch.pack) {
setPackValue(patch.pack, { flash: true });
}
if (doPrompt && patch.clear_prompt_images) {
clearPromptImagesInBox();
}
if (doPrompt && patch.prompt != null) {
const box = document.getElementById('alt_prompt_textbox') || document.getElementById('input_prompt');
if (box) {
box.value = patch.prompt;
box.dispatchEvent(new Event('input', { bubbles: true }));
box.dispatchEvent(new Event('change', { bubbles: true }));
}
if (patch.negative != null) {
setVal('input_negativeprompt', patch.negative);
}
if (Array.isArray(patch.loras)) {
for (const l of patch.loras) {
const triggers = l.triggers || (l.trigger_phrase ? [l.trigger_phrase] : []);
for (const t of triggers) {
if (t && box && box.value && !box.value.includes(t)) {
box.value = `${box.value.trim()}, ${t}`;
box.dispatchEvent(new Event('input', { bubbles: true }));
}
}
}
}
}
if (doLoras && Array.isArray(patch.loras) && typeof loraHelper !== 'undefined' && loraHelper) {
try {
if (typeof loraHelper.clearLoras === 'function') {
loraHelper.clearLoras();
}
} catch (e) { /* ignore */ }
for (const l of patch.loras) {
const name = l.name;
if (!name) {
continue;
}
try {
if (typeof loraHelper.selectLora === 'function') {
loraHelper.selectLora(name);
}
if (loraHelper.loraWeightPref && l.weight != null) {
loraHelper.loraWeightPref[name] = l.weight;
}
} catch (e) {
console.warn('Assistent: selectLora failed', name, e);
}
}
try {
if (typeof loraHelper.rebuildUI === 'function') {
loraHelper.rebuildUI();
}
} catch (e) { /* ignore */ }
}
if (doParams) {
const aspectSize = sizeFromAspect(patch.aspect);
if (aspectSize) {
setVal('input_width', String(aspectSize[0]));
setVal('input_height', String(aspectSize[1]));
} else {
if (patch.width != null) {
setVal('input_width', String(patch.width));
}
if (patch.height != null) {
setVal('input_height', String(patch.height));
}
}
if (patch.steps != null) {
setVal('input_steps', String(patch.steps));
}
if (patch.cfg != null) {
if (document.getElementById('input_cfgscale')) {
setVal('input_cfgscale', String(patch.cfg));
} else {
setVal('input_cfg', String(patch.cfg));
}
}
if (patch.vary === true) {
setVal('input_seed', '-1');
} else if (patch.lock_seed === true) {
const cur = val('input_seed');
if (cur && String(cur) !== '-1') {
setVal('input_seed', cur);
}
} else if (patch.seed != null) {
setVal('input_seed', String(patch.seed));
}
if (patch.sigma_shift != null) {
setVal('input_sigmashift', String(patch.sigma_shift));
}
if (patch.sampler != null) {
if (document.getElementById('input_sampler')) {
setVal('input_sampler', String(patch.sampler));
}
}
if (patch.scheduler != null && document.getElementById('input_scheduler')) {
setVal('input_scheduler', String(patch.scheduler));
}
const batch = patch.images != null ? patch.images : patch.batch;
if (batch != null) {
if (document.getElementById('input_images')) {
setVal('input_images', String(batch));
} else if (document.getElementById('input_batchsize')) {
setVal('input_batchsize', String(batch));
}
}
}
if (doInit) {
const creativity = patch.init_creativity != null ? patch.init_creativity : patch.denoise;
if (creativity != null && document.getElementById('input_initimagecreativity')) {
setVal('input_initimagecreativity', String(creativity));
openInitImageGroup();
}
if (patch.mask_blur != null && document.getElementById('input_maskblur')) {
setVal('input_maskblur', String(patch.mask_blur));
}
if (patch.mask_grow != null) {
if (document.getElementById('input_maskgrow')) {
setVal('input_maskgrow', String(patch.mask_grow));
} else if (document.getElementById('input_maskshrinkgrow')) {
setVal('input_maskshrinkgrow', String(patch.mask_grow));
}
}
if (patch.clear_init_image || patch.clear_mask_image) {
if (patch.clear_init_image) {
clearFileParam('input_initimage');
}
if (patch.clear_mask_image) {
clearFileParam('input_maskimage');
}
if (patch.clear_init_image && patch.clear_mask_image) {
const toggler = document.getElementById('input_group_content_initimage_toggle');
if (toggler) {
toggler.checked = false;
triggerChangeForEl(toggler);
}
}
}
if (patch.select_slot) {
const id = normalizeSlotId(patch.select_slot);
if (slotById(id)) {
state.selectedSlotId = id;
renderBoard();
}
}
if (patch.snapshot_generate) {
snapshotGenerateToRef();
}
const initId = patch.slot_to_init || (patch.use_init_image || (Array.isArray(patch.actions) && patch.actions.map(String).includes('use_init')) ? state.selectedSlotId : null);
const maskId = patch.slot_to_mask || null;
const src = resolveSlotSrc(patch.slot_to_init) || selectedSrc() || findCurrentGenerateSrc();
const wantInit = patch.use_init_image === true
|| !!patch.slot_to_init
|| (Array.isArray(patch.actions) && patch.actions.map(String).includes('use_init'));
const wantMask = patch.use_mask_image === true
|| !!patch.slot_to_mask
|| (Array.isArray(patch.actions) && patch.actions.map(String).includes('use_mask'));
if (wantInit) {
const initSrc = resolveSlotSrc(initId) || src;
if (initSrc) {
await setInitFromSrc(initSrc);
} else {
setStatus('No image for Init — drop a ref or wait for Generate');
}
}
if (wantMask) {
const maskSrc = resolveSlotSrc(maskId) || src;
if (maskSrc) {
await setMaskFromSrc(maskSrc);
} else {
setStatus('No image for Mask — drop a mask (white=edit) first');
}
}
if (patch.slot_to_prompt_image) {
setStatus('Prompt Images: drop the ref into the Swarm prompt box (no auto helper yet)');
}
}
syncChipHighlight();
setStatus('Applied patch');
}
function triggerGenerate() {
try {
if (typeof mainGenHandler !== 'undefined' && mainGenHandler && typeof mainGenHandler.doGenerate === 'function') {
mainGenHandler.doGenerate();
return true;
}
} catch (e) {
console.warn('Assistent: doGenerate failed', e);
}
const btn =
document.getElementById('generate_button') ||
document.getElementById('alt_generate_button') ||
document.querySelector('button.generate-button') ||
document.querySelector('#generate_button, button[id*="generate"]');
if (btn) {
btn.click();
return true;
}
return false;
}
function doInterruptNow() {
try {
if (typeof doInterrupt === 'function') {
doInterrupt(false);
return;
}
} catch (e) { /* ignore */ }
if (typeof genericRequest === 'function') {
genericRequest('InterruptAll', { other_sessions: false }, () => {}, 0, () => {});
}
}
function waitForNewImage(prevSrc, timeoutMs = 180000) {
return new Promise((resolve) => {
const start = Date.now();
const timer = setInterval(() => {
const src = findCurrentGenerateSrc();
if (src && src !== prevSrc && !looksLikeModelPreview(src)) {
clearInterval(timer);
resolve(src);
} else if (Date.now() - start > timeoutMs) {
clearInterval(timer);
resolve(null);
}
}, 400);
});
}
async function runGenerateFromPatch(patch) {
if (!$('sa_auto_generate')?.checked || !patchHasGenTrigger(patch)) {
return null;
}
const prev = findCurrentGenerateSrc();
setStatus('Generating…');
startBusyUi('generating');
state.generating = true;
setInterruptVisible(true);
const ok = triggerGenerate();
if (!ok) {
state.generating = false;
setInterruptVisible(state.busy);
setStatus('Could not start Generate (UI hook missing)');
return null;
}
const src = await waitForNewImage(prev);
state.generating = false;
setInterruptVisible(state.busy);
if (!state.busy) {
stopBusyUi(src ? 'Generate done' : 'Generate finished (no new image detected)');
}
if (src) {
const gen = generateSlot();
if (gen) {
gen.src = src;
renderBoard();
}
setStatus('Generate done');
return src;
}
if (state.busy) {
setStatus('Generate finished (no new image detected)');
}
return null;
}
async function maybeAutoCritique(imageSrc) {
if (!$('sa_auto_critique')?.checked || state.critiqueHopUsed || !imageSrc) {
return;
}
state.critiqueHopUsed = true;
const pack = $('sa_pack');
if (pack) {
pack.value = 'critique_image';
saveSettings();
}
if ($('sa_input')) {
$('sa_input').value = 'Critique this result and improve the prompt for the next generation.';
}
const gen = generateSlot();
if (gen) {
gen.attach = true;
if (imageSrc) {
gen.src = imageSrc;
}
renderBoard();
}
setStatus('Auto-critique…');
await sendChat({ fromAutoCritique: true, forceSlotIds: [GEN_ID] });
}
function appendMessage(role, text, patch, civitaiResults) {
const box = $('sa_messages');
if (!box) {
return null;
}
hideChatEmpty();
const div = document.createElement('div');
div.className = `sa-msg ${role}`;
const { prose, patch: extracted } = role === 'assistant' ? extractPatch(text) : { prose: text, patch: null };
const finalPatch = patch || extracted;
div.textContent = prose || text || '';
if (finalPatch) {
const wrap = document.createElement('div');
wrap.className = 'sa-patch';
const pre = document.createElement('pre');
pre.textContent = JSON.stringify(finalPatch, null, 2);
wrap.appendChild(pre);
mountPatchActions(wrap, finalPatch);
div.appendChild(wrap);
}
if (civitaiResults && civitaiResults.length) {
div.appendChild(buildCivitaiCards(civitaiResults));
}
box.appendChild(div);
box.scrollTop = box.scrollHeight;
return div;
}
function beginStreamMessage() {
const box = $('sa_messages');
if (!box) {
return null;
}
hideChatEmpty();
const div = document.createElement('div');
div.className = 'sa-msg assistant sa-streaming sa-typing';
div.innerHTML = '<span class="sa-dots" aria-hidden="true"><i></i><i></i><i></i></span><span class="sa-typing-label">Waiting for the model…</span>';
box.appendChild(div);
box.scrollTop = box.scrollHeight;
state.streamEl = div;
return div;
}
function appendStreamDelta(delta) {
if (!state.streamEl) {
beginStreamMessage();
}
if (state.streamEl) {
if (state.streamEl.classList.contains('sa-typing')) {
state.streamEl.classList.remove('sa-typing');
state.streamEl.textContent = '';
}
state.gotDelta = true;
if (state.busyPhase !== 'refining') {
setBusyPhase('streaming');
}
state.streamEl.textContent += delta;
const box = $('sa_messages');
if (box) {
box.scrollTop = box.scrollHeight;
}
}
}
function finalizeStreamMessage(fullReply, civitaiResults) {
const el = state.streamEl;
state.streamEl = null;
if (!el) {
appendMessage('assistant', fullReply, null, civitaiResults);
return;
}
el.classList.remove('sa-streaming', 'sa-typing');
const { prose, patch } = extractPatch(fullReply);
el.textContent = prose || fullReply || '';
if (patch) {
const wrap = document.createElement('div');
wrap.className = 'sa-patch';
const pre = document.createElement('pre');
pre.textContent = JSON.stringify(patch, null, 2);
wrap.appendChild(pre);
mountPatchActions(wrap, patch);
el.appendChild(wrap);
}
if (civitaiResults && civitaiResults.length) {
el.appendChild(buildCivitaiCards(civitaiResults));
}
const box = $('sa_messages');
if (box) {
box.scrollTop = box.scrollHeight;
}
}
function buildCivitaiCards(results) {
const list = document.createElement('div');
list.className = 'sa-civitai-list';
for (const r of results) {
const card = document.createElement('div');
card.className = 'sa-civitai-card' + (r.already_installed ? ' sa-installed' : '');
const title = document.createElement('div');
title.className = 'sa-civitai-title';
title.textContent = r.name || r.file_name || 'LoRA';
card.appendChild(title);
const meta = document.createElement('div');
meta.className = 'sa-civitai-meta';
const bits = [
r.base_model || '?',
r.krea_likely ? 'Krea?' : null,
r.already_installed ? 'already installed' : null,
(r.triggers || []).slice(0, 3).join(', ') || null,
].filter(Boolean);
meta.textContent = bits.join(' · ');
card.appendChild(meta);
const actions = document.createElement('div');
actions.className = 'sa-civitai-actions';
if (r.already_installed) {
const note = document.createElement('span');
note.textContent = 'Installed';
actions.appendChild(note);
} else if (r.download_url) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'basic-button sa-primary';
btn.textContent = 'Confirm download';
btn.addEventListener('click', () => downloadCivitaiLoRA(r, btn));
actions.appendChild(btn);
} else {
const note = document.createElement('span');
note.textContent = 'No download URL';
actions.appendChild(note);
}
card.appendChild(actions);
list.appendChild(card);
}
return list;
}
function downloadCivitaiLoRA(card, btn) {
if (!card.download_url) {
return;
}
if (btn) {
btn.disabled = true;
btn.textContent = 'Downloading…';
}
setStatus(`Downloading ${card.file_name || card.name}…`);
setInterruptVisible(true);
const payload = {
url: card.download_url,
type: 'LoRA',
name: card.file_name || card.name || 'lora',
};
const onDone = (ok, msg) => {
setInterruptVisible(state.busy || state.generating);
if (ok) {
setStatus(`Downloaded ${payload.name}`);
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 });
}, { rescan: true });
} else {
setStatus(msg || 'Download failed');
if (btn) {
btn.disabled = false;
btn.textContent = 'Confirm download';
}
appendMessage('error', msg || 'Download failed');
}
};
if (typeof makeWSRequest === 'function') {
makeWSRequest(
'DoModelDownloadWS',
payload,
(data) => {
if (data.error) {
onDone(false, String(data.error));
return;
}
if (data.success || data.overall_percent >= 1 || data.current_percent >= 1) {
if (data.success || data.overall_percent >= 0.99) {
// 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)}%`);
}
}
},
0,
(err) => onDone(false, String(err || 'Download failed')),
);
} else {
onDone(false, 'makeWSRequest unavailable');
}
}
function wantsAutoVision() {
return !!$('sa_auto_vision')?.checked;
}
function looksLikeModelPreview(src) {
const s = String(src || '').toLowerCase();
if (!s) {
return false;
}
return s.includes('.preview.')
|| s.includes('placeholder')
|| /\/models\//i.test(s);
}
function findCurrentGenerateSrc({ allowPreview = false } = {}) {
let src = null;
try {
const cur = document.getElementById('current_image_img')
|| document.querySelector('#current_image img')
|| document.querySelector('.current-image img')
|| document.querySelector('#current_image_batch img');
if (cur) {
src = cur.dataset?.src || cur.src || null;
}
} catch (e) { /* ignore */ }
if (!src) {
try {
if (typeof currentMetadataMap !== 'undefined' && currentMetadataMap && currentMetadataMap.image) {
src = currentMetadataMap.image;
}
} catch (e) { /* ignore */ }
}
if (!src) {
return null;
}
if (!allowPreview && looksLikeModelPreview(src)) {
return null;
}
return src;
}
function refreshImagePreview() {
if (wantsAutoVision()) {
const gen = generateSlot();
if (gen) {
gen.attach = true;
const src = findCurrentGenerateSrc();
if (src) {
gen.src = src;
}
renderBoard();
}
}
syncGenerateSlot();
}
function fileToDataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result || ''));
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
async function acceptImageFile(file, slotId) {
if (!file || !String(file.type || '').startsWith('image/')) {
setStatus('Not an image file');
return false;
}
const dataUrl = await fileToDataUrl(file);
if (slotId) {
return setSlotSrc(slotId, dataUrl, { note: `Loaded ${file.name || 'image'}` });
}
return putImageOnBoard(dataUrl, { note: `Loaded ${file.name || 'image'}` });
}
async function handleDropDataTransfer(dt, slotId) {
if (!dt) {
return false;
}
if (dt.files && dt.files.length) {
for (const file of dt.files) {
if (String(file.type || '').startsWith('image/')) {
return acceptImageFile(file, slotId);
}
}
}
const uri = (dt.getData('text/uri-list') || dt.getData('text/plain') || '').trim();
if (uri) {
const first = uri.split('\n').map((l) => l.trim()).find((l) => l && !l.startsWith('#'));
if (first) {
if (slotId) {
return setSlotSrc(slotId, first, { note: 'Image from drag' });
}
return putImageOnBoard(first, { note: 'Image from drag' });
}
}
const html = dt.getData('text/html') || '';
const m = html.match(/src=["']([^"']+)["']/i);
if (m && m[1]) {
if (slotId) {
return setSlotSrc(slotId, m[1], { note: 'Image from drag' });
}
return putImageOnBoard(m[1], { note: 'Image from drag' });
}
return false;
}
async function imageToBase64ForOllama(src, maxEdge = 1024) {
if (!src) {
return null;
}
const dataUrl = await srcToDataUrl(src);
if (!dataUrl) {
return null;
}
try {
const img = await new Promise((resolve, reject) => {
const el = new Image();
el.onload = () => resolve(el);
el.onerror = reject;
el.src = dataUrl;
});
const w = img.naturalWidth || img.width || 0;
const h = img.naturalHeight || img.height || 0;
const edge = Math.max(w, h);
const canvas = document.createElement('canvas');
if (!edge || edge <= maxEdge) {
canvas.width = Math.max(w, 1);
canvas.height = Math.max(h, 1);
canvas.getContext('2d').drawImage(img, 0, 0);
} else {
const scale = maxEdge / edge;
canvas.width = Math.max(1, Math.round(w * scale));
canvas.height = Math.max(1, Math.round(h * scale));
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
}
const jpeg = canvas.toDataURL('image/jpeg', 0.85);
const i = jpeg.indexOf(',');
return i >= 0 ? jpeg.slice(i + 1) : null;
} catch (e) {
console.warn('Assistent: vision resize failed', e);
const i = dataUrl.indexOf(',');
return i >= 0 ? dataUrl.slice(i + 1) : null;
}
}
async function srcToDataUrl(src) {
if (src.startsWith('data:')) {
return src;
}
try {
const resp = await fetch(src);
const blob = await resp.blob();
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result || ''));
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (e) {
console.warn('Assistent: vision fetch failed', e);
return null;
}
}
function loadSettings() {
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);
const autoCrit = localStorage.getItem(LS_AUTO_CRITIQUE);
const autoDl = localStorage.getItem(LS_AUTO_DOWNLOAD);
const paneW = localStorage.getItem(LS_PANE_WIDTH);
if (base && $('sa_base_url')) {
$('sa_base_url').value = base;
}
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';
}
if ($('sa_auto_apply')) {
$('sa_auto_apply').checked = autoApply == null ? true : autoApply === '1';
}
if ($('sa_auto_generate')) {
$('sa_auto_generate').checked = autoGen == null ? true : autoGen === '1';
}
if ($('sa_auto_critique') && autoCrit != null) {
$('sa_auto_critique').checked = autoCrit === '1';
}
if ($('sa_auto_download') && autoDl != null) {
$('sa_auto_download').checked = autoDl === '1';
}
if (model) {
state.preferredModel = model;
}
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');
localStorage.setItem(LS_AUTO_CRITIQUE, $('sa_auto_critique')?.checked ? '1' : '0');
localStorage.setItem(LS_AUTO_DOWNLOAD, $('sa_auto_download')?.checked ? '1' : '0');
}
function setModelOptions(models, { error } = {}) {
const sel = $('sa_model');
if (!sel) {
return;
}
const names = (models || []).map((n) => String(n || '').trim()).filter(Boolean);
sel.innerHTML = '';
if (error) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = `⚠ ${String(error).replace(/\s+/g, ' ').slice(0, 90)}`;
sel.appendChild(opt);
sel.disabled = true;
return;
}
sel.disabled = false;
if (!names.length) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'No Ollama models — pull / Refresh';
sel.appendChild(opt);
return;
}
for (const name of names) {
const opt = document.createElement('option');
opt.value = name;
opt.textContent = name;
sel.appendChild(opt);
}
const prefer = state.preferredModel || localStorage.getItem(LS_MODEL);
if (prefer && names.includes(prefer)) {
sel.value = prefer;
}
}
function refreshModels() {
const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434';
setStatus('Loading models…');
if (typeof genericRequest !== 'function') {
setStatus('SwarmUI API not ready');
setModelOptions([], { error: 'SwarmUI API not ready' });
return;
}
genericRequest(
'AssistentListModels',
{ baseUrl },
(data) => {
const models = data.models || [];
setModelOptions(models);
const prefer = state.preferredModel || localStorage.getItem(LS_MODEL);
if (prefer && models.includes(prefer) && $('sa_model')) {
$('sa_model').value = prefer;
}
setStatus(models.length ? `${models.length} models` : 'No Ollama models (gpu-rent: ollama pull)');
saveSettings();
},
0,
(err) => {
const msg = String(err || 'Ollama unreachable');
setStatus(msg);
setModelOptions([], { error: msg });
appendMessage('error', msg);
},
);
}
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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 12 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();
}
return;
}
genericRequest(
'TriggerRefresh',
{ strong: true },
() => {
if (done) {
done();
}
},
0,
() => {
if (done) {
done();
}
},
);
}
async function handleReplySideEffects(reply, civitaiResults, 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();
}
if (civitaiResults && civitaiResults.length && $('sa_auto_download')?.checked) {
const pick = civitaiResults.find((r) => !r.already_installed && r.download_url && r.krea_likely)
|| civitaiResults.find((r) => !r.already_installed && r.download_url);
if (pick) {
downloadCivitaiLoRA(pick, null);
}
}
if (patch && !fromVisionHop && !fromAutoCritique) {
const hopped = await maybeVisionHop(patch, opts.attachedSlotIds || []);
if (hopped) {
return;
}
}
if (patch && $('sa_auto_apply')?.checked) {
await applyPatch(patch, 'all');
if (!fromAutoCritique) {
const src = await runGenerateFromPatch(patch);
if (src) {
await maybeAutoCritique(src);
}
}
}
}
async function applyQuickPatch(patch, note) {
const withActions = { ...patch };
if (!Array.isArray(withActions.actions) && patchHasGenTrigger(withActions)) {
withActions.actions = ['generate'];
}
await applyPatch(withActions, 'all');
setStatus(note || 'Applied');
if ($('sa_auto_generate')?.checked) {
await runGenerateFromPatch(withActions);
}
syncChipHighlight();
}
function syncChipHighlight() {
const bar = $('sa_chips');
if (!bar) {
return;
}
const cur = guessAspectFromSize(val('input_width'), val('input_height'));
const seed = val('input_seed');
bar.querySelectorAll('[data-aspect]').forEach((btn) => {
btn.classList.toggle('sa-chip-active', btn.getAttribute('data-aspect') === cur);
});
bar.querySelectorAll('[data-seed]').forEach((btn) => {
const mode = btn.getAttribute('data-seed');
const active = (mode === 'lock' && seed && seed !== '-1') || (mode === 'random' && (!seed || seed === '-1'));
btn.classList.toggle('sa-chip-active', active);
});
}
function appendSystemNote(text) {
const box = $('sa_messages');
if (!box) {
return;
}
hideChatEmpty();
const div = document.createElement('div');
div.className = 'sa-msg assistant sa-system-note';
div.textContent = text;
box.appendChild(div);
box.scrollTop = box.scrollHeight;
}
async function handleSlashCommand(raw) {
const text = String(raw || '').trim();
if (!text.startsWith('/')) {
return false;
}
const parts = text.slice(1).split(/\s+/);
const cmd = (parts[0] || '').toLowerCase();
const arg = parts.slice(1).join(' ').trim();
if (cmd === 'help' || cmd === '?') {
appendSystemNote(HELP_TEXT);
setStatus('/help');
return true;
}
if (cmd === 'gen' || cmd === 'generate') {
const prev = findCurrentGenerateSrc();
startBusyUi('generating');
state.generating = true;
setInterruptVisible(true);
if (!triggerGenerate()) {
state.generating = false;
stopBusyUi('Could not start Generate');
return true;
}
const src = await waitForNewImage(prev);
state.generating = false;
setInterruptVisible(state.busy);
if (src) {
const gen = generateSlot();
if (gen) {
gen.src = src;
renderBoard();
}
stopBusyUi('Generate done');
} else {
stopBusyUi('Generate finished');
}
return true;
}
if (cmd === 'look') {
const id = normalizeSlotId(arg || GEN_ID) || GEN_ID;
const slot = slotById(id);
if (!slot) {
setStatus(`Unknown slot: ${arg || GEN_ID}`);
return true;
}
if (!slot.src && id === GEN_ID) {
const src = findCurrentGenerateSrc();
if (src) {
slot.src = src;
}
}
if (!slot.src) {
setStatus(`Slot ${id} is empty`);
return true;
}
slot.attach = true;
renderBoard();
if ($('sa_input')) {
$('sa_input').value = `Look at ${id} and describe what you see.`;
}
setPackValue('critique_image', { flash: true });
await sendChat({ forceSlotIds: [id], skipAutoPack: true });
return true;
}
if (cmd === 'init') {
const src = selectedSrc() || findCurrentGenerateSrc();
if (!src) {
setStatus('No image for Init');
return true;
}
await setInitFromSrc(src);
setPackValue('inpaint_edit', { flash: true });
return true;
}
if (cmd === 'mask') {
const src = selectedSrc();
if (!src) {
setStatus('Select a window with a mask image');
return true;
}
await setMaskFromSrc(src);
setPackValue('inpaint_edit', { flash: true });
return true;
}
if (cmd === 'clear') {
clearInitAndMask();
return true;
}
if (cmd === 'interrupt' || cmd === 'stop') {
doInterruptNow();
state.busy = false;
state.generating = false;
setInterruptVisible(false);
syncGenerateBusy();
syncPatchActionAvailability();
stopBusyUi('Interrupted');
return true;
}
if (cmd === 'aspect') {
const key = normalizeAspect(arg);
if (!key) {
setStatus(`Unknown aspect. Try: ${Object.keys(ASPECT_TABLE).join(', ')}`);
return true;
}
await applyQuickPatch({ aspect: key, actions: ['generate'] }, `Aspect ${key}`);
return true;
}
if (cmd === 'seed') {
const mode = (arg || 'random').toLowerCase();
if (mode === 'lock' || mode === 'keep') {
await applyQuickPatch({ lock_seed: true }, 'Seed locked');
} else {
await applyQuickPatch({ seed: -1, vary: true, actions: ['generate'] }, 'Seed random');
}
return true;
}
if (cmd === 'vary') {
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');
} else {
setStatus(`Pack → ${$('sa_pack')?.value}`);
}
return true;
}
if (cmd === 'civitai') {
if (!arg) {
setStatus('/civitai <query>');
return true;
}
if ($('sa_input')) {
$('sa_input').value = `Find a Krea 2 LoRA for: ${arg}`;
}
setPackValue('write_prompt', { flash: true });
await sendChat({
skipAutoPack: true,
forcedUserText: `Search Civitai for Krea-compatible LoRA: ${arg}. Prefer actions search_civitai.`,
});
return true;
}
appendSystemNote(`Unknown command /${cmd}.\n\n${HELP_TEXT}`);
setStatus(`Unknown /${cmd}`);
return true;
}
async function maybeVisionHop(patch, attachedSlotIds) {
const ids = lookAtIdsFromPatch(patch);
if (!ids.length || state.visionHopUsed) {
return false;
}
const have = ids.map((id) => slotById(id)).filter((s) => s && s.src);
if (!have.length) {
const genSrc = findCurrentGenerateSrc();
if (ids.includes(GEN_ID) && genSrc) {
const gen = generateSlot();
if (gen) {
gen.src = genSrc;
have.push(gen);
}
}
}
if (!have.length) {
setStatus('look_at: those windows are empty');
return false;
}
const already = new Set(attachedSlotIds || []);
const need = have.filter((s) => !already.has(s.id));
if (!need.length) {
return false;
}
state.visionHopUsed = true;
for (const s of need) {
s.attach = true;
}
renderBoard();
if ($('sa_input')) {
$('sa_input').value = `Look at board slots: ${need.map((s) => s.id).join(', ')}. Continue using these images.`;
}
setStatus(`Vision hop ← ${need.map((s) => s.label).join(', ')}`);
await sendChat({ fromVisionHop: true, forceSlotIds: need.map((s) => s.id) });
return true;
}
async function sendChat(opts = {}) {
if (state.busy && !opts.fromVisionHop && !opts.fromAutoCritique) {
return;
}
const rawInput = ($('sa_input')?.value || '').trim();
const text = (opts.forcedUserText || rawInput).trim();
if (!text) {
return;
}
if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.skipSlash) {
if (rawInput.startsWith('/')) {
if ($('sa_input')) {
$('sa_input').value = '';
}
const handled = await handleSlashCommand(rawInput);
if (handled) {
return;
}
}
}
if (!updateGate()) {
setStatus('Select a Krea 2 model');
return;
}
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 ⚙');
refreshModels();
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;
}
let wantedIds = (opts.forceSlotIds || []).map(normalizeSlotId).filter(Boolean);
if (!wantedIds.length) {
wantedIds = attachableSlots().map((s) => s.id);
}
if (wantsAutoVision() && generateSlot()?.src && !wantedIds.includes(GEN_ID)) {
wantedIds.push(GEN_ID);
}
const visionSlots = wantedIds.map((id) => slotById(id)).filter((s) => s && s.src);
let images = null;
if (visionSlots.length) {
startBusyUi('encoding');
setStatus('Encoding image…');
images = [];
for (const slot of visionSlots) {
const b64 = await imageToBase64ForOllama(slot.src);
if (b64) {
images.push(b64);
}
}
if (!images.length) {
images = null;
}
}
state.history.push({ role: 'user', content: text });
appendMessage('user', text);
$('sa_input').value = '';
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;
}
state.busy = true;
setInterruptVisible(true);
startBusyUi('thinking');
saveSettings();
const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434';
// Flat fields: SwarmUI JObject params receive the whole request body.
const payload = {
baseUrl,
model,
pack,
persona,
includeBase: true,
messages,
context_json: JSON.stringify(context),
raw: {
messages,
context_json: JSON.stringify(context),
pack,
persona,
base_url: baseUrl,
model,
},
};
const finishOk = async (reply, civitaiResults) => {
state.busy = false;
setInterruptVisible(state.generating);
state.history.push({ role: 'assistant', content: reply });
stopBusyUi('Done');
await handleReplySideEffects(reply, civitaiResults, {
...opts,
attachedSlotIds: visionSlots.map((s) => s.id),
});
};
const finishErr = (msg) => {
state.busy = false;
setInterruptVisible(state.generating);
stopBusyUi(msg);
if (state.streamEl) {
state.streamEl.classList.remove('sa-streaming', 'sa-typing');
state.streamEl.classList.add('error');
state.streamEl.textContent = msg;
state.streamEl = null;
} else {
appendMessage('error', msg);
}
};
if (typeof makeWSRequest === 'function') {
beginStreamMessage();
makeWSRequest(
'AssistentChatWS',
payload,
(data) => {
if (data.phase === 'waiting_ollama') {
setBusyPhase('loading');
const label = state.streamEl?.querySelector('.sa-typing-label');
if (label) {
label.textContent = `Loading ${modelShort(model)} into GPU…`;
}
return;
}
if (data.error) {
finishErr(String(data.error));
return;
}
if (data.clear_stream) {
if (state.streamEl) {
state.streamEl.classList.add('sa-typing');
state.streamEl.innerHTML = '<span class="sa-dots" aria-hidden="true"><i></i><i></i><i></i></span><span class="sa-typing-label">Refining…</span>';
}
setBusyPhase('refining');
return;
}
if (data.delta) {
appendStreamDelta(data.delta);
return;
}
if (data.done || data.reply != null) {
const reply = data.reply || (state.streamEl && state.streamEl.textContent) || '';
const civitai = data.civitai_results || [];
finalizeStreamMessage(reply, civitai);
finishOk(reply, civitai);
}
},
0,
(err) => {
// Fallback to HTTP AssistentChat
console.warn('AssistentChatWS failed, falling back', err);
if (state.streamEl) {
state.streamEl.remove();
state.streamEl = null;
}
genericRequest(
'AssistentChat',
payload,
(data) => {
if (data.error) {
finishErr(String(data.error));
return;
}
const reply = data.reply || '';
appendMessage('assistant', reply, null, data.civitai_results || []);
finishOk(reply, data.civitai_results || []);
},
0,
(err2) => finishErr(String(err2 || err || 'Chat failed')),
);
},
);
return;
}
genericRequest(
'AssistentChat',
payload,
(data) => {
if (data.error) {
finishErr(String(data.error));
return;
}
const reply = data.reply || '';
appendMessage('assistant', reply, null, data.civitai_results || []);
finishOk(reply, data.civitai_results || []);
},
0,
(err) => finishErr(String(err || 'Chat failed')),
);
}
function wireDropZone() {
const board = $('sa_board');
const layout = $('sa_layout');
layout?.addEventListener('dragover', (e) => {
if (e.dataTransfer?.types?.includes('Files') || e.dataTransfer?.types?.includes('text/uri-list')) {
e.preventDefault();
}
});
layout?.addEventListener('drop', async (e) => {
if (!e.dataTransfer) {
return;
}
if (e.target && e.target.closest && e.target.closest('.sa-slot, .sa-add-cell')) {
return;
}
e.preventDefault();
await handleDropDataTransfer(e.dataTransfer);
});
board?.addEventListener('keydown', (e) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
if (e.target && (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT')) {
return;
}
e.preventDefault();
clearSlot(state.selectedSlotId);
}
});
document.addEventListener('paste', async (e) => {
const pane = document.getElementById('assistent');
if (!pane || !pane.classList.contains('active')) {
return;
}
if (e.target && (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT')) {
const items = e.clipboardData?.items;
let hasImage = false;
if (items) {
for (const item of items) {
if (item.type.startsWith('image/')) {
hasImage = true;
break;
}
}
}
if (!hasImage) {
return;
}
}
const items = e.clipboardData?.items;
if (!items) {
return;
}
for (const item of items) {
if (item.type.startsWith('image/')) {
e.preventDefault();
const file = item.getAsFile();
if (file) {
const sel = selectedSlot();
await acceptImageFile(file, sel && sel.type === 'ref' ? sel.id : null);
}
return;
}
}
});
}
function wireSplitter() {
const splitter = $('sa_splitter');
const layout = $('sa_layout');
const pane = $('sa_image_pane');
if (!splitter || !layout || !pane) {
return;
}
let dragging = false;
splitter.addEventListener('mousedown', (e) => {
e.preventDefault();
dragging = true;
splitter.classList.add('sa-dragging');
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
});
window.addEventListener('mousemove', (e) => {
if (!dragging) {
return;
}
const rect = layout.getBoundingClientRect();
const x = e.clientX - rect.left;
const pct = Math.min(56, Math.max(22, (x / rect.width) * 100));
const value = `${pct}%`;
document.documentElement.style.setProperty('--sa-image-width', value);
localStorage.setItem(LS_PANE_WIDTH, value);
});
window.addEventListener('mouseup', () => {
if (!dragging) {
return;
}
dragging = false;
splitter.classList.remove('sa-dragging');
document.body.style.cursor = '';
document.body.style.userSelect = '';
});
}
function registerSendButton() {
if (typeof registerMediaButton !== 'function') {
setTimeout(registerSendButton, 500);
return;
}
if (window.__swarmAssistentMediaRegistered) {
return;
}
window.__swarmAssistentMediaRegistered = true;
registerMediaButton(
'Send to Assistent',
(src) => {
putImageOnBoard(src, {
switchTab: true,
note: 'Image sent to Assistent',
preferSelected: false,
});
const pack = $('sa_pack');
if (pack && pack.value === 'write_prompt') {
pack.value = 'critique_image';
saveSettings();
}
},
'Open Assistent with this image (vision / critique / prompt help)',
['image'],
true,
true,
);
}
function wire() {
if (!$('swarm_assistent_root')) {
return;
}
if (typeof genericRequest !== 'function') {
setTimeout(wire, 300);
return;
}
if (window.__swarmAssistentWired) {
return;
}
window.__swarmAssistentWired = true;
loadSettings();
setView(state.view || 'chat');
updateGate();
ensureBoard();
renderBoard();
syncGenerateSlot();
if (wantsAutoVision()) {
refreshImagePreview();
}
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) {
s.hidden = !s.hidden;
}
});
$('sa_btn_refresh_models')?.addEventListener('click', () => {
saveSettings();
refreshModels();
});
$('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 () => {
const src = selectedSrc() || findCurrentGenerateSrc();
if (!src) {
setStatus('No selected/Generate image for Init');
return;
}
await setInitFromSrc(src);
const pack = $('sa_pack');
if (pack && pack.value === 'write_prompt') {
pack.value = 'inpaint_edit';
saveSettings();
}
});
$('sa_btn_as_mask')?.addEventListener('click', async () => {
const src = selectedSrc();
if (!src) {
setStatus('Select a window with an image for Mask');
return;
}
await setMaskFromSrc(src);
const pack = $('sa_pack');
if (pack) {
pack.value = 'inpaint_edit';
saveSettings();
}
});
$('sa_btn_clear_init')?.addEventListener('click', clearInitAndMask);
$('sa_btn_clear_image')?.addEventListener('click', () => clearSlot(state.selectedSlotId));
$('sa_btn_send')?.addEventListener('click', () => sendChat());
$('sa_btn_interrupt')?.addEventListener('click', () => {
doInterruptNow();
state.busy = false;
state.generating = false;
setInterruptVisible(false);
syncGenerateBusy();
syncPatchActionAvailability();
if (state.streamEl?.classList.contains('sa-typing')) {
state.streamEl.remove();
state.streamEl = null;
}
stopBusyUi('Interrupted');
});
$('sa_btn_clear')?.addEventListener('click', () => {
state.history = [];
state.critiqueHopUsed = false;
state.visionHopUsed = false;
state.packUserTouched = false;
const box = $('sa_messages');
if (box) {
box.innerHTML = '';
const empty = document.createElement('div');
empty.className = 'sa-chat-empty';
empty.id = 'sa_chat_empty';
empty.innerHTML = '<div class="sa-chat-empty-title">Collaborative Krea 2</div><div class="sa-chat-empty-hint">Write a prompt, drop refs, use aspect chips, or type <code>/help</code>.</div>';
box.appendChild(empty);
}
stopBusyUi('');
setStatus('');
});
$('sa_input')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey && !e.altKey) {
e.preventDefault();
sendChat();
}
});
$('sa_base_url')?.addEventListener('change', saveSettings);
$('sa_model')?.addEventListener('change', saveSettings);
$('sa_pack')?.addEventListener('change', () => {
state.packUserTouched = true;
saveSettings();
});
$('sa_chips')?.addEventListener('click', async (e) => {
const btn = e.target.closest('.sa-chip');
if (!btn || state.busy || state.generating) {
return;
}
const aspect = btn.getAttribute('data-aspect');
const seed = btn.getAttribute('data-seed');
const vary = btn.getAttribute('data-vary');
if (aspect) {
await applyQuickPatch({ aspect, actions: ['generate'] }, `Aspect ${aspect}`);
} else if (seed === 'lock') {
await applyQuickPatch({ lock_seed: true }, 'Seed locked');
} else if (seed === 'random') {
await applyQuickPatch({ seed: -1, actions: ['generate'] }, 'Seed random');
} else if (vary) {
await applyQuickPatch({ vary: true, seed: -1, actions: ['generate'] }, 'Vary');
}
});
$('sa_auto_vision')?.addEventListener('change', () => {
saveSettings();
const gen = generateSlot();
if (gen) {
gen.attach = wantsAutoVision();
renderBoard();
}
});
$('sa_auto_apply')?.addEventListener('change', saveSettings);
$('sa_auto_generate')?.addEventListener('change', saveSettings);
$('sa_auto_critique')?.addEventListener('change', saveSettings);
$('sa_auto_download')?.addEventListener('change', saveSettings);
syncChipHighlight();
setInterval(syncChipHighlight, 2500);
setInterval(updateGate, 2000);
setInterval(syncGenerateSlot, 700);
setInterval(() => {
if (!state.busy) {
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) });
}
}, 30000);
window.swarmAssistent = {
setImageFromSrc,
putImageOnBoard,
clearVisionImage,
snapshotGenerateToRef,
openAssistentTab,
sendToAssistent: (src) => putImageOnBoard(src, { switchTab: true, note: 'Image sent to Assistent', preferSelected: false }),
isKreaSelected,
resolveCurrentCheckpoint,
refreshInventory,
applyPatch,
triggerGenerate,
setInitFromSrc,
setMaskFromSrc,
clearInitAndMask,
slotById,
renderBoard,
};
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', wire);
} else {
wire();
}
})();