/**
* Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API).
* v0.7.0: Config presets, persona folders, exact KV + vector memory, chat|memory model roles.
*/
(function () {
const LS_BASE = 'swarm_assistent_base_url';
const LS_MODEL = 'swarm_assistent_model';
const LS_EMBED = 'swarm_assistent_embed_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 LS_HISTORY = 'swarm_assistent_history';
const LS_BOARD_TAB = 'swarm_assistent_board_tab';
const TAB_BUTTON_ID = 'maintab_assistent';
const GEN_ID = 'generate';
const MAX_REF_SLOTS = 4;
const CONTEXT_PROMPT_MAX = 2000;
let 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],
};
let 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',
};
let WELCOME_HTML = `
Assistent · Krea 2
- Generate слева — живой просмотр. В чат сам не уходит.
- Refs — референсы на отдельной вкладке: drop / paste / Снимок gen.
- Галочка vision на окне — отправить кадр модели.
- Чипсы aspect / seed / Vary / Turbo·RAW. В чате:
/help.
- Кнопки патча только у последнего предложения.
Напиши, что сгенерировать — или кинь референс и попроси правку.`;
let 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|card
/civitai — поиск LoRA (Confirm в чате)
/inventory — rescan моделей + обновить список LoRA
Чипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.`;
let SLASH_COMMANDS = [
{ cmd: '/help', hint: 'список команд' },
{ cmd: '/gen', hint: 'Generate сейчас' },
{ cmd: '/look ', hint: 'generate|refN' },
{ cmd: '/init', hint: 'как Init' },
{ cmd: '/mask', hint: 'как Mask' },
{ cmd: '/clear', hint: 'сброс Init/Mask' },
{ cmd: '/interrupt', hint: 'стоп' },
{ cmd: '/aspect ', hint: '16:9' },
{ cmd: '/seed ', hint: 'lock|random' },
{ cmd: '/vary', hint: 'новый seed' },
{ cmd: '/pack ', hint: 'write|critique|…' },
{ cmd: '/civitai ', hint: 'запрос LoRA' },
{ cmd: '/inventory', hint: 'rescan моделей' },
];
const state = {
history: [],
packsLoaded: false,
config: null,
exact: null,
sessionExact: {},
lastUserParamIntent: false,
enabledSkills: [],
kreaProfiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 }, raw: { steps: 28, cfg: 4.5 } },
preferredEmbed: null,
busy: false,
generating: false,
chatEpoch: 0,
waitImageTimer: null,
lastImageDataUrl: null,
preferredModel: null,
dragDepth: 0,
inventory: { loras: [], checkpoints: [], wildcards: [], has_civitai_key: false },
inventoryFetchedAt: 0,
taste: { styles: [], likes: [], avoid: [], notes: '', updated: 0 },
tasteSaveTimer: null,
streamEl: null,
streamMeta: null,
critiqueHopUsed: false,
visionHopUsed: false,
busyPhase: 'idle',
busyStarted: 0,
gotDelta: false,
busyTimer: null,
slots: [],
selectedSlotId: 'ref1',
refSeq: 1,
packUserTouched: false,
view: 'chat',
boardTab: 'generate',
personas: [],
modelCards: {},
cardsSelection: null,
cardsBusy: false,
pendingPersonaNote: null,
slashIndex: 0,
};
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 Krea 2 models only. Current: ${escapeHtml(
seen,
)} — pick a checkpoint with architecture krea-2.`
: 'Swarm Assistent is for Krea 2 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, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
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 isEmptyParamField(raw, { treatZeroEmpty = false } = {}) {
if (raw == null) {
return true;
}
const s = String(raw).trim();
if (s === '') {
return true;
}
if (treatZeroEmpty && (s === '0' || Number(s) === 0)) {
return true;
}
return false;
}
function userTextMentionsParams(text) {
return /\b(steps?|cfg|seed|sigma|размер|aspect|ширин|высот|resolution|batch|турбо|turbo|raw)\b/i.test(String(text || ''));
}
function applyAspectTableFrom(obj) {
if (!obj || typeof obj !== 'object') {
return false;
}
const next = {};
for (const [k, v] of Object.entries(obj)) {
if (Array.isArray(v) && v.length >= 2) {
next[k] = [Number(v[0]), Number(v[1])];
}
}
if (!Object.keys(next).length) {
return false;
}
ASPECT_TABLE = next;
return true;
}
function resolveExactBundle() {
const exact = state.exact || state.config?.exact || {};
const profiles = exact.profiles || state.kreaProfiles || {};
return { exact, profiles };
}
function detectKreaProfileName() {
try {
const model = resolveCurrentCheckpoint();
const blob = `${model?.name || ''} ${model?.title || ''}`.toLowerCase();
const hasRaw = /\braw\b/.test(blob);
const hasTurbo = /\bturbo\b/.test(blob);
return hasRaw && !hasTurbo ? 'raw' : 'turbo';
} catch (e) {
return (state.exact?.generation?.profile) || 'turbo';
}
}
function mergedGenerationDefaults(profileName) {
const { exact, profiles } = resolveExactBundle();
const gen = exact.generation && typeof exact.generation === 'object' ? { ...exact.generation } : {};
const profile = profileName || gen.profile || detectKreaProfileName();
const fromProfile = profiles[profile] && typeof profiles[profile] === 'object' ? { ...profiles[profile] } : {};
const session = state.sessionExact && typeof state.sessionExact === 'object' ? { ...state.sessionExact } : {};
// Profile (turbo/raw) overrides generation defaults; session overrides both.
return { ...gen, ...fromProfile, profile, ...session };
}
function exactDefaultFor(key, profileName) {
const { exact, profiles } = resolveExactBundle();
const profile = profileName || exact.generation?.profile || detectKreaProfileName();
const fromProfile = profiles[profile]?.[key];
if (fromProfile != null) {
return fromProfile;
}
return exact.generation?.[key];
}
function rememberSessionExact(partial) {
if (!partial || typeof partial !== 'object') {
return;
}
const keys = ['steps', 'cfg', 'sigma_shift', 'aspect', 'width', 'height', 'images', 'batch', 'seed', 'sampler', 'scheduler'];
for (const k of keys) {
if (partial[k] != null) {
state.sessionExact[k] = partial[k];
}
}
if (partial.images == null && partial.batch != null) {
state.sessionExact.images = partial.batch;
}
}
function fillEmptyParamsFromExact() {
const defaults = mergedGenerationDefaults();
if (isEmptyParamField(val('input_steps'), { treatZeroEmpty: true }) && defaults.steps != null) {
setVal('input_steps', String(defaults.steps));
}
const cfgRaw = val('input_cfgscale') || val('input_cfg');
if (isEmptyParamField(cfgRaw, { treatZeroEmpty: true }) && defaults.cfg != null) {
if (document.getElementById('input_cfgscale')) {
setVal('input_cfgscale', String(defaults.cfg));
} else if (document.getElementById('input_cfg')) {
setVal('input_cfg', String(defaults.cfg));
}
}
if (isEmptyParamField(val('input_sigmashift')) && defaults.sigma_shift != null) {
setVal('input_sigmashift', String(defaults.sigma_shift));
}
const wEmpty = isEmptyParamField(val('input_width'), { treatZeroEmpty: true });
const hEmpty = isEmptyParamField(val('input_height'), { treatZeroEmpty: true });
if ((wEmpty || hEmpty) && defaults.aspect) {
const size = sizeFromAspect(defaults.aspect);
if (size) {
if (wEmpty) {
setVal('input_width', String(size[0]));
}
if (hEmpty) {
setVal('input_height', String(size[1]));
}
}
} else {
if (wEmpty && defaults.width != null) {
setVal('input_width', String(defaults.width));
}
if (hEmpty && defaults.height != null) {
setVal('input_height', String(defaults.height));
}
}
const batchId = document.getElementById('input_images') ? 'input_images' : (document.getElementById('input_batchsize') ? 'input_batchsize' : null);
if (batchId && isEmptyParamField(val(batchId), { treatZeroEmpty: true })) {
const batch = defaults.images != null ? defaults.images : defaults.batch;
if (batch != null) {
setVal(batchId, String(batch));
}
}
}
function shouldSkipSessionRollback(key, patchValue) {
if (state.lastUserParamIntent) {
return false;
}
if (state.sessionExact[key] == null) {
return false;
}
const sessionVal = state.sessionExact[key];
if (String(sessionVal) === String(patchValue)) {
return false;
}
const exactVal = exactDefaultFor(key);
if (exactVal == null) {
return false;
}
// Model trying to restore file exact while session override differs — keep session.
return String(patchValue) === String(exactVal);
}
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('Нет текущего кадра Generate');
return false;
}
const empty = refSlots().find((s) => !s.src);
let ok = false;
if (empty) {
ok = setSlotSrc(empty.id, src, { note: `Снимок → ${empty.label}` });
} else {
const created = addRefSlot({ src, select: true });
if (created?.src) {
setStatus(`Снимок → ${created.label}`);
flashImagePane(created.id);
ok = true;
} else {
const last = refSlots()[refSlots().length - 1];
if (last) {
ok = setSlotSrc(last.id, src, { note: `Снимок → ${last.label} (замена)` });
}
}
}
if (ok) {
setBoardTab('refs');
}
return ok;
}
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 [
['Применить всё', 'all'],
['Промпт', 'prompt'],
['LoRAs', 'loras'],
['Параметры', '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 = 'Применить + Generate';
genBtn.addEventListener('click', async () => {
if (isGenerateUnavailable()) {
return;
}
await applyPatch(patch, 'all');
await runGenerateFromPatch({ ...patch, actions: ['generate'] }, { force: true });
});
actions.appendChild(genBtn);
host.appendChild(actions);
syncPatchActionAvailability();
}
function renderBoard() {
const board = $('sa_board');
if (!board) {
return;
}
ensureBoard();
const tab = state.boardTab === 'refs' ? 'refs' : 'generate';
const refs = refSlots();
board.classList.toggle('sa-board-many', tab === 'refs' && (refs.some((s) => s.src) || refs.length > 1));
board.classList.toggle('sa-board-gen-only', tab === 'generate');
board.innerHTML = '';
const toShow = tab === 'generate'
? state.slots.filter((s) => s.type === 'generate')
: state.slots.filter((s) => s.type !== 'generate');
for (const slot of toShow) {
board.appendChild(buildSlotEl(slot));
}
if (tab === 'refs' && refs.length < MAX_REF_SLOTS) {
const add = document.createElement('div');
add.className = 'sa-add-cell';
add.textContent = '+ Ref';
add.title = 'Добавить окно референса';
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);
}
syncBoardChrome();
syncGenerateBusy();
syncLastImageAlias();
}
function syncBoardChrome() {
const tab = state.boardTab === 'refs' ? 'refs' : 'generate';
$('sa_board_tab_gen')?.classList.toggle('sa-board-tab-active', tab === 'generate');
$('sa_board_tab_refs')?.classList.toggle('sa-board-tab-active', tab === 'refs');
$('sa_board_tab_gen')?.setAttribute('aria-selected', tab === 'generate' ? 'true' : 'false');
$('sa_board_tab_refs')?.setAttribute('aria-selected', tab === 'refs' ? 'true' : 'false');
const addBtn = $('sa_btn_add_ref');
if (addBtn) {
addBtn.hidden = tab !== 'refs';
}
const maskBtn = $('sa_btn_as_mask');
const clearSlotBtn = $('sa_btn_clear_image');
if (maskBtn) {
maskBtn.hidden = tab !== 'refs';
}
if (clearSlotBtn) {
clearSlotBtn.hidden = tab !== 'refs';
}
const badge = $('sa_refs_badge');
if (badge) {
const refs = refSlots();
const withImg = refs.filter((s) => s.src).length;
const withVision = refs.filter((s) => s.src && s.attach).length;
if (withImg || withVision) {
badge.hidden = false;
badge.textContent = withVision ? `${withImg} · vision ${withVision}` : String(withImg);
} else {
badge.hidden = true;
}
}
}
function setBoardTab(tab, { persist = true } = {}) {
state.boardTab = tab === 'refs' ? 'refs' : 'generate';
if (persist) {
try {
localStorage.setItem(LS_BOARD_TAB, state.boardTab);
} catch (e) { /* ignore */ }
}
renderBoard();
}
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'
? 'Generate
Живой просмотр текущей генерации
'
: 'Reference
Drop · paste · Снимок gen
';
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 = '';
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);
setBoardTab('refs');
} 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 persistHistory() {
try {
const slim = (state.history || [])
.filter((m) => m && (m.role === 'user' || m.role === 'assistant') && !m.systemish)
.slice(-24)
.map((m) => ({
role: m.role,
content: String(m.content || '').slice(0, 4000),
persona: m.persona || undefined,
pack: m.pack || undefined,
}));
localStorage.setItem(LS_HISTORY, JSON.stringify(slim));
} catch (e) { /* ignore */ }
}
function restoreHistory() {
try {
const raw = localStorage.getItem(LS_HISTORY);
if (!raw) {
return;
}
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed) || !parsed.length) {
return;
}
state.history = parsed
.filter((m) => m && (m.role === 'user' || m.role === 'assistant') && m.content)
.slice(-24)
.map((m) => ({
role: m.role,
content: String(m.content),
persona: m.persona,
pack: m.pack,
}));
const box = $('sa_messages');
if (!box || !state.history.length) {
return;
}
hideChatEmpty();
for (const m of state.history) {
if (m.role === 'user') {
appendMessage('user', m.content);
} else {
appendMessage('assistant', m.content, null, null, {
persona: m.persona ? { id: m.persona, title: m.persona } : null,
pack: m.pack,
});
}
}
} catch (e) { /* ignore */ }
}
function clearPersistedHistory() {
try {
localStorage.removeItem(LS_HISTORY);
} catch (e) { /* ignore */ }
}
function hideSlashMenu() {
const menu = $('sa_slash_menu');
if (menu) {
menu.hidden = true;
menu.innerHTML = '';
}
state.slashIndex = 0;
}
function slashMatches(text) {
const t = String(text || '');
if (!t.startsWith('/')) {
return [];
}
const q = t.toLowerCase();
return SLASH_COMMANDS.filter((c) => c.cmd.toLowerCase().startsWith(q) || q === '/' || c.cmd.toLowerCase().includes(q.slice(1)));
}
function renderSlashMenu(items) {
const menu = $('sa_slash_menu');
if (!menu) {
return;
}
if (!items.length) {
hideSlashMenu();
return;
}
menu.hidden = false;
menu.innerHTML = '';
state.slashIndex = Math.max(0, Math.min(state.slashIndex, items.length - 1));
items.forEach((item, i) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'sa-slash-item' + (i === state.slashIndex ? ' sa-slash-active' : '');
btn.setAttribute('role', 'option');
btn.innerHTML = `${escapeHtml(item.cmd.trim())} — ${escapeHtml(item.hint)}`;
btn.addEventListener('mousedown', (e) => {
e.preventDefault();
applySlashPick(item);
});
menu.appendChild(btn);
});
}
function applySlashPick(item) {
const input = $('sa_input');
if (!input || !item) {
return;
}
input.value = item.cmd;
hideSlashMenu();
input.focus();
const pos = input.value.length;
input.setSelectionRange(pos, pos);
}
function updateSlashMenuFromInput() {
const text = $('sa_input')?.value || '';
if (!text.startsWith('/') || text.includes('\n') || /\s/.test(text.trim().slice(1)) && !text.endsWith(' ')) {
// show while typing command token only
const token = text.split(/\s/)[0] || '';
if (!token.startsWith('/') || (text.includes(' ') && !SLASH_COMMANDS.some((c) => c.cmd.startsWith(token)))) {
if (!(token.startsWith('/') && !text.includes(' '))) {
hideSlashMenu();
return;
}
}
}
const token = (text.split(/\s/)[0] || '');
if (!token.startsWith('/') || text.indexOf(' ') > 0) {
hideSlashMenu();
return;
}
renderSlashMenu(slashMatches(token));
}
function onPersonaChanged() {
const id = $('sa_persona')?.value || 'neutral';
state.sessionExact = {};
state.lastUserParamIntent = false;
saveSettings();
loadConfig(id, (data) => {
const title = data?.personas?.find((p) => p.id === id)?.title
|| (state.personas || []).find((p) => p.id === id)?.title
|| id;
if (data?.personas) {
state.personas = data.personas;
}
appendSystemNote(`Тон → ${title}`);
state.pendingPersonaNote = `Persona is now ${id} (${title}). Adopt this voice from now on.`;
if (data?.assistant?.default_pack && $('sa_pack') && !state.packUserTouched) {
const packId = data.assistant.default_pack;
if ([...($('sa_pack').options || [])].some((o) => o.value === packId)) {
$('sa_pack').value = packId;
}
}
fillEmptyParamsFromExact();
});
}
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(/)/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 slimPromptForContext(raw) {
let s = String(raw || '');
s = s.replace(/data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=]+/g, '[image omitted]');
s = s.replace(/]*>[\s\S]*?<\/image>/gi, '[image omitted]');
s = s.replace(/
]*>/gi, '[image omitted]');
if (s.length > CONTEXT_PROMPT_MAX) {
s = s.slice(0, CONTEXT_PROMPT_MAX) + '…';
}
return s;
}
function collectLiveContext() {
const inv = state.inventory || {};
const initCtx = readInitContext();
const ctx = {
architecture_ok: isKreaSelected(),
checkpoint: null,
prompt: slimPromptForContext(val('alt_prompt_textbox') || val('input_prompt') || ''),
negative: slimPromptForContext(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: [],
taste_profile: summarizeTaste(),
...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: checkpoint + selected LoRAs + other has_card entries (capped).
const cardKeys = [];
const seenCard = new Set();
const addKey = (kind, name) => {
if (!kind || !name) {
return;
}
const key = `${kind}:${name}`;
if (seenCard.has(key)) {
return;
}
seenCard.add(key);
cardKeys.push({ kind, name });
};
if (ctx.checkpoint?.name) {
addKey('checkpoint', ctx.checkpoint.name);
}
for (const l of ctx.selected_loras || []) {
if (l?.name) {
addKey('lora', l.name);
}
}
for (const l of inv.loras || []) {
if (l?.has_card && l?.name) {
addKey('lora', l.name);
}
if (cardKeys.length >= 14) {
break;
}
}
for (const c of inv.checkpoints || []) {
if (c?.has_card && c?.name) {
addKey('checkpoint', c.name);
}
if (cardKeys.length >= 16) {
break;
}
}
for (const k of cardKeys) {
const cached = state.modelCards[`${k.kind}:${k.name}`];
if (cached) {
ctx.model_cards.push(slimCardForContext(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 */ }
}
try {
const model = ctx.checkpoint || {};
const blob = `${model.name || ''} ${model.title || ''}`.toLowerCase();
const hasRaw = /\braw\b/.test(blob);
const hasTurbo = /\bturbo\b/.test(blob);
const profile = hasRaw && !hasTurbo ? 'raw' : 'turbo';
ctx.krea_profile = profile;
const defaults = mergedGenerationDefaults(profile);
ctx.recommended_params = {
steps: defaults.steps ?? 8,
cfg: defaults.cfg ?? 1,
sigma_shift: defaults.sigma_shift ?? 1.15,
};
if (defaults.aspect) {
ctx.recommended_params.aspect = defaults.aspect;
}
} catch (e) {
ctx.krea_profile = 'turbo';
const defaults = mergedGenerationDefaults('turbo');
ctx.recommended_params = {
steps: defaults.steps ?? 8,
cfg: defaults.cfg ?? 1,
sigma_shift: defaults.sigma_shift ?? 1.15,
};
}
ctx.exact = state.exact || state.config?.exact || null;
ctx.session_exact = state.sessionExact && Object.keys(state.sessionExact).length
? { ...state.sessionExact }
: {};
return ctx;
}
function slimCardForContext(card) {
if (!card || typeof card !== 'object') {
return null;
}
const out = {
kind: card.kind || null,
name: card.name || null,
triggers: Array.isArray(card.triggers) ? card.triggers.slice(0, 8) : undefined,
weight: card.weight != null ? card.weight : undefined,
when: card.when ? String(card.when).slice(0, 160) : undefined,
avoid: card.avoid ? String(card.avoid).slice(0, 120) : undefined,
prompt_hint: card.prompt_hint ? String(card.prompt_hint).slice(0, 160) : undefined,
notes: card.notes ? String(card.notes).slice(0, 200) : undefined,
};
const clean = {};
for (const [k, v] of Object.entries(out)) {
if (v != null && v !== '') {
clean[k] = v;
}
}
return clean;
}
function summarizeTaste() {
const t = state.taste || {};
if (!(t.styles?.length || t.likes?.length || t.avoid?.length || t.notes)) {
return null;
}
return {
styles: (t.styles || []).slice(0, 8),
likes: (t.likes || []).slice(0, 10),
avoid: (t.avoid || []).slice(0, 8),
notes: t.notes ? String(t.notes).slice(0, 240) : undefined,
};
}
function slimInventoryLoras(list, limit) {
const selected = new Set();
try {
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 softCap = Math.min(limit || 100, 80);
const rows = (list || []).map((l) => {
const sel = selected.has(String(l.name || '').toLowerCase());
const hasCard = !!l.has_card;
const krea = !!l.krea_likely;
const blurb = l.blurb || l.usage_hint || null;
return {
name: l.name,
title: l.title || l.name,
trigger_phrase: l.trigger_phrase || null,
triggers: Array.isArray(l.triggers) ? l.triggers.slice(0, 8) : undefined,
architecture: l.architecture || null,
compat_class: l.compat_class || null,
has_card: hasCard,
krea_likely: krea,
blurb,
default_weight: l.default_weight || undefined,
tags: Array.isArray(l.tags) ? l.tags.slice(0, 6) : undefined,
_score: (sel ? 1000 : 0) + (hasCard ? 200 : 0) + (krea ? 50 : 0) + (blurb ? 10 : 0),
};
});
rows.sort((a, b) => b._score - a._score || String(a.name).localeCompare(String(b.name)));
// Full detail for top tier; name+trigger only for the rest within softCap.
const fullDetail = 36;
return rows.slice(0, softCap).map((row, idx) => {
const out = { name: row.name, title: row.title };
if (row.trigger_phrase) {
out.trigger_phrase = row.trigger_phrase;
}
if (row.triggers) {
out.triggers = row.triggers;
}
if (row.krea_likely) {
out.krea_likely = true;
}
if (row.has_card) {
out.has_card = true;
}
const rich = idx < fullDetail || row._score >= 200;
if (rich) {
if (row.architecture) {
out.architecture = row.architecture;
}
if (row.compat_class) {
out.compat_class = row.compat_class;
}
if (row.blurb) {
out.blurb = row.blurb;
}
if (row.default_weight) {
out.default_weight = row.default_weight;
}
if (row.tags) {
out.tags = row.tags;
}
}
return out;
});
}
function slimInventoryCheckpoints(list, limit) {
const rows = (list || []).slice();
rows.sort((a, b) => ((b.krea_likely ? 1 : 0) - (a.krea_likely ? 1 : 0)) || ((b.has_card ? 1 : 0) - (a.has_card ? 1 : 0)) || String(a.name).localeCompare(String(b.name)));
return rows.slice(0, limit || 40).map((c) => {
const out = {
name: c.name,
title: c.title || c.name,
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 loadTaste() {
try {
const raw = localStorage.getItem(LS_TASTE);
if (!raw) {
return;
}
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
state.taste = {
styles: Array.isArray(parsed.styles) ? parsed.styles.slice(0, 12) : [],
likes: Array.isArray(parsed.likes) ? parsed.likes.slice(0, 16) : [],
avoid: Array.isArray(parsed.avoid) ? parsed.avoid.slice(0, 12) : [],
notes: String(parsed.notes || '').slice(0, 400),
updated: parsed.updated || 0,
};
}
} catch (e) { /* ignore */ }
}
function saveTaste() {
try {
localStorage.setItem(LS_TASTE, JSON.stringify(state.taste || {}));
} catch (e) { /* ignore */ }
saveTasteToServerDebounced();
}
function pushUnique(arr, value, max) {
const v = String(value || '').trim();
if (!v || v.length < 2) {
return;
}
const lower = v.toLowerCase();
const next = (arr || []).filter((x) => String(x).toLowerCase() !== lower);
next.unshift(v.slice(0, 80));
return next.slice(0, max);
}
function updateTasteFromPatch(patch, userText) {
if (!patch) {
return;
}
const taste = state.taste || { styles: [], likes: [], avoid: [], notes: '' };
if (Array.isArray(patch.loras)) {
for (const l of patch.loras) {
const name = l?.name || l;
if (name) {
taste.likes = pushUnique(taste.likes, name, 16);
}
}
}
const aspect = patch.aspect || null;
if (aspect) {
taste.styles = pushUnique(taste.styles, `aspect ${aspect}`, 12);
}
if (patch.creativity) {
taste.styles = pushUnique(taste.styles, `creativity:${patch.creativity}`, 12);
}
const ut = String(userText || '').toLowerCase();
if (/фото|photo|photoreal|реализм|film grain/.test(ut)) {
taste.styles = pushUnique(taste.styles, 'photoreal / film', 12);
}
if (/аниме|anime|illustration|иллюстр/.test(ut)) {
taste.styles = pushUnique(taste.styles, 'illustration / anime', 12);
}
if (/без\s+3d|не\s+3d|no\s+3d|не\s+render/.test(ut)) {
taste.avoid = pushUnique(taste.avoid, '3D render look', 12);
}
taste.updated = Date.now();
state.taste = taste;
saveTaste();
}
function isPatchObject(obj) {
if (!obj || typeof obj !== 'object') {
return false;
}
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
);
}
function isCardObject(obj) {
if (!obj || typeof obj !== 'object') {
return false;
}
// Prefer card shape over gen patch when both could match.
const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint);
const genish = !!(obj.prompt != null || obj.loras || obj.actions || obj.width || obj.height
|| obj.steps || obj.cfg || obj.aspect || obj.seed != null);
if (cardish && !genish && (obj.name || obj.triggers || obj.when)) {
return true;
}
return !!(obj.kind && obj.name && (obj.triggers || obj.when || obj.prompt_hint || obj.notes != null));
}
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(/]*>[\s\S]*?<\/image>/gi, '')
.replace(/]*\/?>/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 (userTextMentionsParams(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 defaults = mergedGenerationDefaults();
const capture = state.lastUserParamIntent;
const aspectSize = sizeFromAspect(patch.aspect);
if (patch.aspect != null && !shouldSkipSessionRollback('aspect', patch.aspect)) {
if (aspectSize) {
setVal('input_width', String(aspectSize[0]));
setVal('input_height', String(aspectSize[1]));
}
if (capture) {
rememberSessionExact({ aspect: patch.aspect });
}
} else if (patch.aspect == null && isEmptyParamField(val('input_width'), { treatZeroEmpty: true })
&& isEmptyParamField(val('input_height'), { treatZeroEmpty: true }) && defaults.aspect) {
const fill = sizeFromAspect(defaults.aspect);
if (fill) {
setVal('input_width', String(fill[0]));
setVal('input_height', String(fill[1]));
}
} else {
if (patch.width != null && !shouldSkipSessionRollback('width', patch.width)) {
setVal('input_width', String(patch.width));
if (capture) {
rememberSessionExact({ width: patch.width });
}
} else if (patch.width == null && isEmptyParamField(val('input_width'), { treatZeroEmpty: true }) && defaults.width != null) {
setVal('input_width', String(defaults.width));
}
if (patch.height != null && !shouldSkipSessionRollback('height', patch.height)) {
setVal('input_height', String(patch.height));
if (capture) {
rememberSessionExact({ height: patch.height });
}
} else if (patch.height == null && isEmptyParamField(val('input_height'), { treatZeroEmpty: true }) && defaults.height != null) {
setVal('input_height', String(defaults.height));
}
}
if (patch.steps != null && !shouldSkipSessionRollback('steps', patch.steps)) {
setVal('input_steps', String(patch.steps));
if (capture) {
rememberSessionExact({ steps: patch.steps });
}
} else if (patch.steps == null && isEmptyParamField(val('input_steps'), { treatZeroEmpty: true }) && defaults.steps != null) {
setVal('input_steps', String(defaults.steps));
}
if (patch.cfg != null && !shouldSkipSessionRollback('cfg', patch.cfg)) {
if (document.getElementById('input_cfgscale')) {
setVal('input_cfgscale', String(patch.cfg));
} else {
setVal('input_cfg', String(patch.cfg));
}
if (capture) {
rememberSessionExact({ cfg: patch.cfg });
}
} else if (patch.cfg == null) {
const cfgRaw = val('input_cfgscale') || val('input_cfg');
if (isEmptyParamField(cfgRaw, { treatZeroEmpty: true }) && defaults.cfg != null) {
if (document.getElementById('input_cfgscale')) {
setVal('input_cfgscale', String(defaults.cfg));
} else if (document.getElementById('input_cfg')) {
setVal('input_cfg', String(defaults.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 && !shouldSkipSessionRollback('seed', patch.seed)) {
setVal('input_seed', String(patch.seed));
if (capture) {
rememberSessionExact({ seed: patch.seed });
}
}
if (patch.sigma_shift != null && !shouldSkipSessionRollback('sigma_shift', patch.sigma_shift)) {
setVal('input_sigmashift', String(patch.sigma_shift));
if (capture) {
rememberSessionExact({ sigma_shift: patch.sigma_shift });
}
} else if (patch.sigma_shift == null && isEmptyParamField(val('input_sigmashift')) && defaults.sigma_shift != null) {
setVal('input_sigmashift', String(defaults.sigma_shift));
}
if (patch.sampler != null) {
if (document.getElementById('input_sampler')) {
setVal('input_sampler', String(patch.sampler));
}
if (capture) {
rememberSessionExact({ sampler: patch.sampler });
}
}
if (patch.scheduler != null && document.getElementById('input_scheduler')) {
setVal('input_scheduler', String(patch.scheduler));
if (capture) {
rememberSessionExact({ scheduler: patch.scheduler });
}
}
const batch = patch.images != null ? patch.images : patch.batch;
if (batch != null && !shouldSkipSessionRollback('images', batch)) {
if (document.getElementById('input_images')) {
setVal('input_images', String(batch));
} else if (document.getElementById('input_batchsize')) {
setVal('input_batchsize', String(batch));
}
if (capture) {
rememberSessionExact({ images: batch });
}
} else if (batch == null) {
const batchId = document.getElementById('input_images') ? 'input_images' : (document.getElementById('input_batchsize') ? 'input_batchsize' : null);
const defBatch = defaults.images != null ? defaults.images : defaults.batch;
if (batchId && isEmptyParamField(val(batchId), { treatZeroEmpty: true }) && defBatch != null) {
setVal(batchId, String(defBatch));
}
}
}
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 cancelWaitForNewImage() {
if (state.waitImageTimer) {
clearInterval(state.waitImageTimer);
state.waitImageTimer = null;
}
}
function bumpChatEpoch() {
state.chatEpoch = (state.chatEpoch || 0) + 1;
return state.chatEpoch;
}
function doInterruptNow() {
bumpChatEpoch();
cancelWaitForNewImage();
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) {
cancelWaitForNewImage();
const epoch = state.chatEpoch;
return new Promise((resolve) => {
const start = Date.now();
state.waitImageTimer = setInterval(() => {
if (epoch !== state.chatEpoch) {
cancelWaitForNewImage();
resolve(null);
return;
}
const src = findCurrentGenerateSrc();
if (src && src !== prevSrc && !looksLikeModelPreview(src)) {
cancelWaitForNewImage();
resolve(src);
} else if (Date.now() - start > timeoutMs) {
cancelWaitForNewImage();
resolve(null);
}
}, 400);
});
}
async function runGenerateFromPatch(patch, opts = {}) {
const force = !!opts.force;
if ((!force && !$('sa_auto_generate')?.checked) || !patchHasGenTrigger(patch)) {
return null;
}
const prev = findCurrentGenerateSrc();
setStatus('Генерация…');
startBusyUi('generating');
state.generating = true;
setInterruptVisible(true);
const ok = triggerGenerate();
if (!ok) {
state.generating = false;
setInterruptVisible(state.busy);
setStatus('Не удалось запустить Generate');
return null;
}
const src = await waitForNewImage(prev);
state.generating = false;
setInterruptVisible(state.busy);
if (!state.busy) {
stopBusyUi(src ? 'Generate готов' : 'Generate завершён (новое изображение не найдено)');
}
if (src) {
const gen = generateSlot();
if (gen) {
gen.src = src;
renderBoard();
}
setStatus('Generate готов');
return src;
}
if (state.busy) {
setStatus('Generate завершён (новое изображение не найдено)');
}
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 currentPersonaInfo() {
const id = ($('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral').trim() || 'neutral';
const known = (state.personas || []).find((p) => p && p.id === id);
return {
id,
title: (known && known.title) || ({
neutral: 'Нейтральный',
lewd: 'Пошляк',
aggressive: 'Агрессивный',
}[id] || id),
};
}
function mountAssistantMeta(div, meta = {}) {
if (!div || div.querySelector('.sa-msg-meta')) {
return;
}
const persona = meta.persona || currentPersonaInfo();
const pack = meta.pack || $('sa_pack')?.value || '';
div.dataset.persona = persona.id || 'neutral';
if (pack) {
div.dataset.pack = pack;
}
const row = document.createElement('div');
row.className = 'sa-msg-meta';
const chip = document.createElement('span');
chip.className = `sa-persona-mark sa-persona-${persona.id || 'neutral'}`;
chip.textContent = persona.title || persona.id;
chip.title = `Характер: ${persona.title || persona.id}${pack ? ` · режим ${pack}` : ''}`;
row.appendChild(chip);
if (pack && pack !== 'write_prompt') {
const packEl = document.createElement('span');
packEl.className = 'sa-pack-mark';
packEl.textContent = pack.replace(/_/g, ' ');
packEl.title = `Режим: ${pack}`;
row.appendChild(packEl);
}
div.insertBefore(row, div.firstChild);
}
function setAssistantBody(div, text) {
if (!div) {
return;
}
let body = div.querySelector('.sa-msg-body');
if (!body) {
body = document.createElement('div');
body.className = 'sa-msg-body';
div.appendChild(body);
}
body.textContent = text || '';
}
function appendMessage(role, text, patch, civitaiResults, meta) {
const box = $('sa_messages');
if (!box) {
return null;
}
hideChatEmpty();
const div = document.createElement('div');
div.className = `sa-msg ${role}`;
if (role === 'assistant') {
mountAssistantMeta(div, meta);
}
const { prose, patch: extracted } = role === 'assistant' ? extractPatch(text) : { prose: text, patch: null };
const finalPatch = patch || extracted;
if (role === 'assistant') {
setAssistantBody(div, prose || text || '');
} else {
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(meta) {
const box = $('sa_messages');
if (!box) {
return null;
}
hideChatEmpty();
const div = document.createElement('div');
div.className = 'sa-msg assistant sa-streaming sa-typing';
mountAssistantMeta(div, meta);
const body = document.createElement('div');
body.className = 'sa-msg-body';
body.innerHTML = 'Waiting for the model…';
div.appendChild(body);
box.appendChild(div);
box.scrollTop = box.scrollHeight;
state.streamEl = div;
state.streamMeta = meta || null;
return div;
}
function appendStreamDelta(delta) {
if (!state.streamEl) {
beginStreamMessage(state.streamMeta || undefined);
}
if (state.streamEl) {
if (state.streamEl.classList.contains('sa-typing')) {
state.streamEl.classList.remove('sa-typing');
setAssistantBody(state.streamEl, '');
}
state.gotDelta = true;
if (state.busyPhase !== 'refining') {
setBusyPhase('streaming');
}
let body = state.streamEl.querySelector('.sa-msg-body');
if (!body) {
setAssistantBody(state.streamEl, '');
body = state.streamEl.querySelector('.sa-msg-body');
}
body.textContent += delta;
const box = $('sa_messages');
if (box) {
box.scrollTop = box.scrollHeight;
}
}
}
function finalizeStreamMessage(fullReply, civitaiResults) {
const el = state.streamEl;
const meta = state.streamMeta;
state.streamEl = null;
state.streamMeta = null;
if (!el) {
appendMessage('assistant', fullReply, null, civitaiResults, meta || undefined);
return;
}
el.classList.remove('sa-streaming', 'sa-typing');
mountAssistantMeta(el, meta || undefined);
const card = extractCardJson(fullReply);
const { prose, patch } = extractPatch(fullReply);
setAssistantBody(el, prose || fullReply || '');
el.querySelectorAll('.sa-patch, .sa-civitai-list').forEach((n) => n.remove());
if (patch && !isCardObject(patch) && !(card && !patch.prompt && !patch.actions && !patch.loras)) {
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);
} else if (card) {
const wrap = document.createElement('div');
wrap.className = 'sa-patch sa-card-json-preview';
const pre = document.createElement('pre');
pre.textContent = JSON.stringify(card, null, 2);
wrap.appendChild(pre);
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(async () => {
await maybeWriteCardAfterDownload({
kind: 'lora',
name: payload.name,
civitai: card,
});
}, { 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');
}
}
async function maybeWriteCardAfterDownload({ kind, name, civitai }) {
const display = name || civitai?.file_name || civitai?.name || 'model';
appendSystemNote(`Downloaded ${display}. Writing a recommendation card…`);
setPackValue('catalog_card', { flash: true });
const meta = {
triggers: civitai?.triggers || [],
base_model: civitai?.base_model,
civitai_url: civitai?.url || civitai?.civitai_url,
version_id: civitai?.version_id || civitai?.modelVersionId,
name: display,
};
if ($('sa_input')) {
$('sa_input').value = '';
}
await sendChat({
forcedUserText: `LoRA "${display}" is now installed. Write a recommendation card (JSON) using its triggers/metadata. Then briefly suggest how to enable it in the next generate.`,
skipSlash: true,
skipAutoPack: true,
fromDownload: true,
fromCards: true,
cardTarget: { kind: kind || 'lora', name: display, meta },
});
}
function wantsAutoVision() {
return !!$('sa_auto_vision')?.checked;
}
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;
}
const embed = localStorage.getItem(LS_EMBED);
if (embed) {
state.preferredEmbed = embed;
}
if (paneW) {
document.documentElement.style.setProperty('--sa-image-width', paneW);
}
if (view === 'cards' || view === 'chat') {
state.view = view;
}
const boardTab = localStorage.getItem(LS_BOARD_TAB);
if (boardTab === 'refs' || boardTab === 'generate') {
state.boardTab = boardTab;
}
}
function saveSettings() {
localStorage.setItem(LS_BASE, $('sa_base_url')?.value || '');
localStorage.setItem(LS_MODEL, $('sa_model')?.value || '');
localStorage.setItem(LS_EMBED, $('sa_embed_model')?.value || state.preferredEmbed || '');
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');
persistServerSettings();
}
function persistServerSettings() {
if (typeof genericRequest !== 'function') {
return;
}
const skills = {};
document.querySelectorAll('#sa_skills_box input[data-skill]')?.forEach((el) => {
skills[el.getAttribute('data-skill')] = !!el.checked;
});
const persona = $('sa_persona')?.value || 'neutral';
const settings = {
embed_model: $('sa_embed_model')?.value || state.preferredEmbed || '',
base_url: $('sa_base_url')?.value || '',
[persona]: { skills },
};
genericRequest('AssistentSaveSettings', { settings }, () => {}, 0, () => {});
}
function applyConfigPayload(data, { applyDefaults = false } = {}) {
if (!data || data.error) {
return;
}
state.config = data;
if (data.exact && typeof data.exact === 'object') {
state.exact = data.exact;
}
const aspectSource = data.exact?.aspect_table || data.model?.aspect_table;
if (aspectSource && typeof aspectSource === 'object') {
applyAspectTableFrom(aspectSource);
}
const profileSource = data.exact?.profiles || data.model?.profiles;
if (profileSource && typeof profileSource === 'object') {
state.kreaProfiles = profileSource;
}
if (data.ui?.pack_aliases) {
PACK_ALIASES = { ...PACK_ALIASES, ...data.ui.pack_aliases };
}
if (data.ui?.welcome_html) {
WELCOME_HTML = data.ui.welcome_html;
}
if (data.ui?.help_text) {
HELP_TEXT = data.ui.help_text;
}
if (Array.isArray(data.ui?.slash) && data.ui.slash.length) {
SLASH_COMMANDS = data.ui.slash.map((s) => ({
cmd: s.cmd || '',
hint: s.hint || '',
action: s.action || '',
}));
}
state.enabledSkills = Array.isArray(data.enabled_skills) ? data.enabled_skills.slice() : [];
if (Array.isArray(data.personas)) {
state.personas = data.personas;
}
renderPersonaOptions(data.personas || [], data.persona || data.default_persona);
renderPackOptions(data.packs || [], applyDefaults ? data.assistant?.default_pack : null);
renderChips(data.ui?.chips || []);
renderSkillChecks(data.skills || [], state.enabledSkills);
if (applyDefaults && data.assistant?.default_pack && $('sa_pack') && !localStorage.getItem(LS_PACK)) {
$('sa_pack').value = data.assistant.default_pack;
}
if (data.assistant?.embed_model && !state.preferredEmbed) {
state.preferredEmbed = data.assistant.embed_model;
}
if (applyDefaults || data.exact) {
fillEmptyParamsFromExact();
}
}
function renderPersonaOptions(personas, selected) {
const sel = $('sa_persona');
if (!sel) {
return;
}
const cur = selected || sel.value || localStorage.getItem(LS_PERSONA) || 'neutral';
sel.innerHTML = '';
for (const p of personas) {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.title || p.id;
if (p.accent) {
opt.dataset.accent = p.accent;
}
sel.appendChild(opt);
}
if ([...sel.options].some((o) => o.value === cur)) {
sel.value = cur;
}
}
function renderPackOptions(packs, preferred) {
const sel = $('sa_pack');
if (!sel) {
return;
}
const cur = preferred || sel.value || localStorage.getItem(LS_PACK) || 'write_prompt';
sel.innerHTML = '';
const list = (packs || []).slice().sort((a, b) => (a.order || 100) - (b.order || 100));
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 === cur)) {
sel.value = cur;
}
}
function renderChips(chips) {
const box = $('sa_chips');
if (!box || !Array.isArray(chips) || !chips.length) {
return;
}
box.innerHTML = '';
for (const c of chips) {
if (c.sep) {
const sep = document.createElement('span');
sep.className = 'sa-chip-sep';
sep.setAttribute('aria-hidden', 'true');
box.appendChild(sep);
continue;
}
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'sa-chip';
btn.textContent = c.label || c.value || '';
if (c.title) {
btn.title = c.title;
}
const action = c.action || '';
const value = c.value ?? '';
if (action === 'aspect') {
btn.setAttribute('data-aspect', value);
} else if (action === 'seed') {
btn.setAttribute('data-seed', value);
} else if (action === 'vary') {
btn.setAttribute('data-vary', value || '1');
} else if (action === 'krea_profile') {
btn.setAttribute('data-krea-profile', value);
}
box.appendChild(btn);
}
}
function renderSkillChecks(skills, enabled) {
const box = $('sa_skills_box');
if (!box) {
return;
}
const on = new Set(enabled || []);
box.innerHTML = '';
for (const s of skills || []) {
const label = document.createElement('label');
label.className = 'sa-check';
const input = document.createElement('input');
input.type = 'checkbox';
input.setAttribute('data-skill', s.id);
input.checked = on.has(s.id) || (!enabled?.length && !!s.default);
input.addEventListener('change', () => {
state.enabledSkills = [...document.querySelectorAll('#sa_skills_box input[data-skill]:checked')].map((el) => el.getAttribute('data-skill'));
saveSettings();
});
label.appendChild(input);
label.appendChild(document.createTextNode(` ${s.title || s.id}`));
box.appendChild(label);
}
state.enabledSkills = [...document.querySelectorAll('#sa_skills_box input[data-skill]:checked')].map((el) => el.getAttribute('data-skill'));
}
function loadConfig(persona, done) {
if (typeof genericRequest !== 'function') {
done?.(null);
return;
}
genericRequest(
'AssistentGetConfig',
{ persona: persona || $('sa_persona')?.value || 'neutral' },
(data) => {
applyConfigPayload(data, { applyDefaults: true });
done?.(data);
},
0,
() => done?.(null),
);
}
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 setEmbedModelOptions(models) {
const sel = $('sa_embed_model');
if (!sel) {
return;
}
const names = (models || []).map((n) => String(n || '').trim()).filter(Boolean);
sel.innerHTML = '';
if (!names.length) {
const opt = document.createElement('option');
opt.value = state.preferredEmbed || 'nomic-embed-text';
opt.textContent = opt.value + ' (ожидается pull)';
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.preferredEmbed || localStorage.getItem(LS_EMBED) || state.config?.assistant?.embed_model;
if (prefer && names.includes(prefer)) {
sel.value = prefer;
} else if (prefer && !names.includes(prefer)) {
const opt = document.createElement('option');
opt.value = prefer;
opt.textContent = prefer;
sel.appendChild(opt);
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 || [];
const memoryModels = data.memory_models || [];
setModelOptions(models);
setEmbedModelOptions(memoryModels);
const prefer = state.preferredModel || localStorage.getItem(LS_MODEL);
if (prefer && models.includes(prefer) && $('sa_model')) {
$('sa_model').value = prefer;
}
setStatus(models.length ? `${models.length} chat · ${memoryModels.length} memory` : '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() {
loadConfig($('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral', (data) => {
if (data?.personas) {
state.personas = data.personas;
}
});
}
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 = [];
const seen = new Set();
const add = (kind, name) => {
if (!kind || !name) {
return;
}
const key = `${kind}:${name}`;
if (seen.has(key)) {
return;
}
seen.add(key);
keys.push({ kind, name });
};
try {
const ck = resolveCurrentCheckpoint();
if (ck?.name) {
add('checkpoint', ck.name);
}
} catch (e) { /* ignore */ }
try {
if (typeof loraHelper !== 'undefined' && Array.isArray(loraHelper?.selected)) {
for (const l of loraHelper.selected) {
add('lora', l?.name || l);
}
}
} catch (e) { /* ignore */ }
for (const l of state.inventory?.loras || []) {
if (l?.has_card) {
add('lora', l.name);
}
if (keys.length >= 14) {
break;
}
}
for (const c of state.inventory?.checkpoints || []) {
if (c?.has_card) {
add('checkpoint', c.name);
}
if (keys.length >= 16) {
break;
}
}
await Promise.all(keys.map((k) => prefetchCard(k.kind, k.name)));
}
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,
hash: c.hash || '',
preview_url: c.preview_url || null,
has_sidecar: !!c.has_sidecar,
});
}
}
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,
hash: l.hash || '',
preview_url: l.preview_url || null,
has_sidecar: !!l.has_sidecar,
});
}
}
return rows;
}
function renderCardsList() {
const root = $('sa_cards_list');
if (!root) {
return;
}
root.innerHTML = '';
const rows = cardsCatalog();
if (!rows.length) {
root.innerHTML = 'Inventory пуст — Обновить.
';
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');
}
const thumb = row.preview_url
? `
`
: '';
const metaBits = [];
metaBits.push(row.has_card ? 'card ✓' : 'нет card');
if (row.has_sidecar) {
metaBits.push('sidecar');
}
if (row.trigger) {
metaBits.push(String(row.trigger).slice(0, 40));
}
btn.innerHTML = `${thumb}${escapeHtml(row.kind)}
${escapeHtml(row.title || row.name)}
${escapeHtml(metaBits.join(' · '))}
`;
btn.addEventListener('click', () => selectCardModel(row));
root.appendChild(btn);
}
}
function wireCardForm() {
const sync = () => {
if ($('sa_card_show_json')?.checked) {
syncCardJsonFromForm();
}
};
['sa_card_triggers', 'sa_card_weight', 'sa_card_when', 'sa_card_avoid', 'sa_card_hint', 'sa_card_notes', 'sa_card_url']
.forEach((id) => $(id)?.addEventListener('change', sync));
$('sa_card_show_json')?.addEventListener('change', () => {
const on = !!$('sa_card_show_json')?.checked;
const ta = $('sa_card_json');
if (ta) {
ta.hidden = !on;
if (on) {
syncCardJsonFromForm();
}
}
});
$('sa_card_json')?.addEventListener('change', () => {
if ($('sa_card_show_json')?.checked) {
applyCardToForm(readCardDraft() || {});
}
});
}
function applyCardToForm(card) {
card = card || {};
const triggers = Array.isArray(card.triggers) ? card.triggers.join(', ') : (card.triggers || '');
if ($('sa_card_triggers')) {
$('sa_card_triggers').value = triggers;
}
if ($('sa_card_weight')) {
$('sa_card_weight').value = card.weight != null ? card.weight : (state.cardsSelection?.kind === 'lora' ? 0.8 : 1);
}
if ($('sa_card_when')) {
$('sa_card_when').value = card.when || '';
}
if ($('sa_card_avoid')) {
$('sa_card_avoid').value = card.avoid || '';
}
if ($('sa_card_hint')) {
$('sa_card_hint').value = card.prompt_hint || '';
}
if ($('sa_card_notes')) {
$('sa_card_notes').value = card.notes || '';
}
if ($('sa_card_url')) {
$('sa_card_url').value = card.civitai_url || '';
}
if ($('sa_card_json')) {
$('sa_card_json').value = JSON.stringify(card, null, 2);
}
}
function syncCardJsonFromForm() {
const sel = state.cardsSelection || {};
let base = {};
try {
base = JSON.parse($('sa_card_json')?.value || '{}');
} catch (e) {
base = {};
}
const triggers = String($('sa_card_triggers')?.value || '')
.split(/[,;]/)
.map((s) => s.trim())
.filter(Boolean);
const card = {
...base,
kind: sel.kind || base.kind || 'lora',
name: sel.name || base.name || '',
triggers,
weight: parseFloat($('sa_card_weight')?.value || '0.8') || 0.8,
when: $('sa_card_when')?.value || '',
avoid: $('sa_card_avoid')?.value || '',
prompt_hint: $('sa_card_hint')?.value || '',
notes: $('sa_card_notes')?.value || '',
civitai_url: $('sa_card_url')?.value || '',
version_id: base.version_id != null ? base.version_id : null,
};
if ($('sa_card_json')) {
$('sa_card_json').value = JSON.stringify(card, null, 2);
}
return card;
}
function renderCardPreviews(urls) {
const root = $('sa_card_previews');
if (!root) {
return;
}
const list = (urls || []).filter(Boolean).slice(0, 6);
root.innerHTML = '';
if (!list.length) {
root.hidden = true;
return;
}
root.hidden = false;
for (const url of list) {
const img = document.createElement('img');
img.className = 'sa-card-thumb';
img.src = url;
img.alt = 'preview';
img.title = 'Клик — на вкладку Refs';
img.addEventListener('click', () => {
setBoardTab('refs');
addRefFromUrl(url);
setStatus('Превью → Refs');
});
root.appendChild(img);
}
}
function mergeCivitaiIntoCard(card, data) {
const out = { ...(card || {}) };
const civ = data?.civitai;
if (!out.triggers?.length && data?.trigger_phrase) {
out.triggers = [data.trigger_phrase];
}
if (civ) {
const trained = civ.trainedWords || civ.trained_words;
if ((!out.triggers || !out.triggers.length) && Array.isArray(trained) && trained.length) {
out.triggers = trained.slice(0, 12);
}
if (!out.civitai_url) {
const mid = civ.modelId || civ.model?.id || civ.model?.modelId;
const vid = civ.id || data.version_id;
if (mid && vid) {
out.civitai_url = `https://civitai.red/models/${mid}?modelVersionId=${vid}`;
} else if (vid) {
out.civitai_url = `https://civitai.red/models/0?modelVersionId=${vid}`;
}
}
if (out.version_id == null && (civ.id || data.version_id)) {
out.version_id = civ.id || data.version_id;
}
if (!out.notes && civ.description) {
out.notes = String(civ.description).replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 400);
}
}
if (out.version_id == null && data?.version_id) {
out.version_id = data.version_id;
}
return out;
}
function formatMetaStatus(data) {
if (!data) {
return 'Нет ответа';
}
if (data.error) {
return String(data.error);
}
const parts = [];
if (data.has_sidecar) {
parts.push(`Сидикарь ✓ · version ${data.version_id || '?'}`);
} else if (data.fetched) {
parts.push(`Civitai ✓ · version ${data.version_id || '?'}`);
} else {
parts.push('Сидикаря нет');
}
const n = (data.example_urls || data.preview_urls || []).length;
if (n) {
parts.push(`${n} кадр${n === 1 ? '' : 'а'}`);
}
if (data.has_card) {
parts.push('карточка Assistent ✓');
} else {
parts.push('карточки Assistent нет');
}
if (data.fetch_error) {
parts.push(String(data.fetch_error));
}
return parts.join(' · ');
}
function applyCardMetaResponse(row, data, { preserveUser } = {}) {
let 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 (preserveUser) {
const current = syncCardJsonFromForm();
card = {
...mergeCivitaiIntoCard(card, data),
when: current.when || card.when || '',
avoid: current.avoid || card.avoid || '',
prompt_hint: current.prompt_hint || card.prompt_hint || '',
notes: current.notes || card.notes || '',
};
} else {
card = mergeCivitaiIntoCard(card, data);
}
applyCardToForm(card);
state.modelCards[`${row.kind}:${row.name}`] = card;
const urls = [
...(data.preview_urls || []),
...(data.example_urls || []),
];
renderCardPreviews(urls);
const badge = $('sa_card_badge');
if (badge) {
badge.hidden = false;
badge.textContent = data.has_card ? 'card ✓' : (data.has_sidecar || data.fetched ? 'meta ✓' : 'нет меты');
}
setCardStatus(formatMetaStatus(data));
}
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 = '…';
}
setCardStatus('Читаю локальную мету…');
genericRequest(
'AssistentGetCardMeta',
{ kind: row.kind, name: row.name, fetch: false },
(data) => applyCardMetaResponse(row, data || {}),
0,
(err) => setCardStatus(String(err || 'Ошибка загрузки')),
);
}
function fetchCardMetaLive() {
const row = state.cardsSelection;
if (!row) {
setCardStatus('Выбери модель');
return;
}
setCardStatus('Сидикаря нет · ищу по SHA…');
genericRequest(
'AssistentGetCardMeta',
{ kind: row.kind, name: row.name, fetch: true },
(data) => applyCardMetaResponse(row, data || {}, { preserveUser: true }),
0,
(err) => setCardStatus(String(err || 'Civitai: ошибка запроса')),
);
}
async function addRefFromUrl(url) {
if (!url) {
return;
}
addRefSlot({ src: url, select: false });
}
function readCardDraft() {
const fromForm = syncCardJsonFromForm();
if ($('sa_card_show_json')?.checked) {
const raw = $('sa_card_json')?.value || '';
try {
return JSON.parse(raw);
} catch (e) {
setCardStatus('Невалидный JSON');
return null;
}
}
return fromForm;
}
function saveCurrentCard({ enqueue } = {}) {
const sel = state.cardsSelection;
if (!sel) {
setCardStatus('Выбери модель');
return;
}
const card = readCardDraft();
if (!card) {
return;
}
card.kind = card.kind || sel.kind;
card.name = card.name || sel.name;
setCardStatus('Сохраняю…');
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
? `Карточка Assistent сохранена · ${data.path}`
: `Черновик + wanted · ${data.path}`);
refreshInventory(() => renderCardsList());
},
0,
(err) => setCardStatus(String(err || 'Ошибка сохранения')),
);
}
function enqueueWantedOnly() {
const sel = state.cardsSelection;
const card = readCardDraft() || {};
if (!sel && !card.civitai_url) {
setCardStatus('Нужна модель или 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 ? 'Уже в wanted' : `Wanted → ${data.path}`);
},
0,
(err) => setCardStatus(String(err || 'Ошибка enqueue')),
);
}
function shortLoraName(name) {
const s = String(name || '');
const base = s.split(/[/\\]/).pop() || s;
return base.replace(/\.safetensors$/i, '').slice(0, 28);
}
function renderLoraChips() {
const root = $('sa_lora_chips');
if (!root) {
return;
}
root.innerHTML = '';
let selected = [];
try {
if (typeof loraHelper !== 'undefined' && Array.isArray(loraHelper?.selected)) {
selected = loraHelper.selected.map((l) => ({
name: l.name || l,
weight: (loraHelper.loraWeightPref && loraHelper.loraWeightPref[l.name || l]) || 1,
}));
}
} catch (e) { /* ignore */ }
for (const l of selected) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'sa-lora-chip';
btn.title = `${l.name} ×${l.weight} — клик снять`;
btn.textContent = `${shortLoraName(l.name)} ${Number(l.weight).toFixed(2)}`;
btn.addEventListener('click', () => {
try {
if (typeof loraHelper !== 'undefined' && typeof loraHelper.removeLora === 'function') {
loraHelper.removeLora(l.name);
} else if (loraHelper?.selected) {
loraHelper.selected = loraHelper.selected.filter((x) => (x.name || x) !== l.name);
if (typeof loraHelper.rebuildUI === 'function') {
loraHelper.rebuildUI();
}
}
} catch (e) { /* ignore */ }
renderLoraChips();
});
root.appendChild(btn);
}
const add = document.createElement('button');
add.type = 'button';
add.className = 'sa-lora-chip sa-lora-add';
add.textContent = '+ LoRA';
add.title = 'Добавить из inventory';
add.addEventListener('click', (e) => {
e.stopPropagation();
openLoraPicker(add);
});
root.appendChild(add);
}
function openLoraPicker(anchor) {
document.querySelectorAll('.sa-lora-picker').forEach((n) => n.remove());
const picker = document.createElement('div');
picker.className = 'sa-lora-picker';
const inv = (state.inventory?.loras || []).slice().sort((a, b) => (b.krea_likely ? 1 : 0) - (a.krea_likely ? 1 : 0));
const filter = document.createElement('input');
filter.type = 'search';
filter.placeholder = 'Фильтр LoRA…';
filter.style.cssText = 'width:100%;box-sizing:border-box;margin-bottom:0.25rem;padding:0.3rem;';
picker.appendChild(filter);
const list = document.createElement('div');
picker.appendChild(list);
const draw = () => {
list.innerHTML = '';
const q = filter.value.trim().toLowerCase();
let n = 0;
for (const l of inv) {
const name = l.name || '';
if (q && !String(name).toLowerCase().includes(q) && !String(l.title || '').toLowerCase().includes(q)) {
continue;
}
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = `${shortLoraName(name)}${l.krea_likely ? ' · krea' : ''}`;
btn.title = name;
btn.addEventListener('click', async () => {
await applyPatch({
loras: [
...((() => {
try {
return (loraHelper?.selected || []).map((x) => ({
name: x.name || x,
weight: (loraHelper.loraWeightPref && loraHelper.loraWeightPref[x.name || x]) || 1,
}));
} catch (e) {
return [];
}
})()),
{ name, weight: l.default_weight ? parseFloat(l.default_weight) : 0.8, triggers: l.triggers || (l.trigger_phrase ? [l.trigger_phrase] : []) },
],
}, 'loras');
picker.remove();
renderLoraChips();
});
list.appendChild(btn);
if (++n >= 40) {
break;
}
}
if (!n) {
list.innerHTML = 'Нет LoRA
';
}
};
filter.addEventListener('input', draw);
draw();
const composer = $('sa_composer') || document.body;
composer.style.position = composer.style.position || 'relative';
composer.appendChild(picker);
const onDoc = (ev) => {
if (!picker.contains(ev.target) && ev.target !== anchor) {
picker.remove();
document.removeEventListener('mousedown', onDoc);
}
};
setTimeout(() => document.addEventListener('mousedown', onDoc), 0);
filter.focus();
}
function loadTasteFromServer() {
if (typeof genericRequest !== 'function') {
return;
}
genericRequest(
'AssistentGetTaste',
{},
(data) => {
const remote = data?.taste;
if (!remote || typeof remote !== 'object') {
return;
}
const remoteUpdated = remote.updated || 0;
const localUpdated = state.taste?.updated || 0;
if (remoteUpdated >= localUpdated) {
state.taste = {
styles: Array.isArray(remote.styles) ? remote.styles.slice(0, 12) : [],
likes: Array.isArray(remote.likes) ? remote.likes.slice(0, 16) : [],
avoid: Array.isArray(remote.avoid) ? remote.avoid.slice(0, 12) : [],
notes: String(remote.notes || '').slice(0, 400),
updated: remoteUpdated || Date.now(),
};
saveTasteLocalOnly();
}
},
0,
() => {},
);
}
function saveTasteLocalOnly() {
try {
localStorage.setItem(LS_TASTE, JSON.stringify(state.taste || {}));
} catch (e) { /* ignore */ }
}
function saveTasteToServerDebounced() {
if (state.tasteSaveTimer) {
clearTimeout(state.tasteSaveTimer);
}
state.tasteSaveTimer = setTimeout(() => {
if (typeof genericRequest !== 'function') {
return;
}
genericRequest(
'AssistentSaveTaste',
{ taste: state.taste || {} },
() => {},
0,
() => {},
);
}, 800);
}
async function generateCardWithAssistent() {
const sel = state.cardsSelection;
if (!sel) {
setCardStatus('Выбери модель');
return;
}
if (state.busy) {
setCardStatus('Чат занят');
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) {
if ($('sa_card_json')) {
$('sa_card_json').value = JSON.stringify(card, null, 2);
}
if (opts.fromDownload || opts.cardTarget) {
const kind = card.kind || opts.cardTarget?.kind || 'lora';
const name = card.name || opts.cardTarget?.name;
if (name && typeof genericRequest === 'function') {
genericRequest(
'AssistentSaveCard',
{ kind, name, card, enqueue_wanted: false },
(data) => {
if (data?.path) {
state.modelCards[`${kind}:${name}`] = card;
setCardStatus(data.installed ? `Card saved → ${data.path}` : `Card draft → ${data.path}`);
setStatus(`Card saved for ${name}`);
}
},
0,
() => setCardStatus('Card draft ready — Save manually'),
);
}
} else {
setView('cards');
setCardStatus('Draft from Assistent — review & Save');
}
}
return;
}
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');
updateTasteFromPatch(patch, opts.userText || '');
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'];
}
const prevIntent = state.lastUserParamIntent;
state.lastUserParamIntent = true;
await applyPatch(withActions, 'all');
state.lastUserParamIntent = prevIntent;
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(`Неизвестный слот: ${arg || GEN_ID}`);
return true;
}
if (slot.type !== 'generate') {
setBoardTab('refs');
} else {
setBoardTab('generate');
}
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 ');
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) {
state.lastUserParamIntent = userTextMentionsParams(text);
}
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('Выбери модель Krea 2');
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('Выбери модель Ollama в ⚙');
refreshModels();
return;
}
const chatEpoch = bumpChatEpoch();
state.busy = true;
setInterruptVisible(true);
startBusyUi('thinking');
saveSettings();
// Always pull latest LoRA/checkpoint list before the LLM sees context
// (rescans disk when inventory is older than ~20s or after downloads).
setStatus('Обновляю inventory…');
try {
await ensureFreshInventory({ forceRescan: !!opts.fromDownload });
await prefetchActiveModelCards();
} catch (e) {
console.warn('Assistent inventory refresh', e);
}
if (chatEpoch !== state.chatEpoch) {
return;
}
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('Кодирую изображение…');
images = [];
for (const slot of visionSlots) {
if (chatEpoch !== state.chatEpoch) {
return;
}
const b64 = await imageToBase64ForOllama(slot.src);
if (b64) {
images.push(b64);
}
}
if (!images.length) {
images = null;
}
}
if (chatEpoch !== state.chatEpoch) {
return;
}
const msgMeta = {
persona: currentPersonaInfo(),
pack,
};
state.history.push({ role: 'user', content: text });
if (state.pendingPersonaNote) {
state.history.push({ role: 'user', content: state.pendingPersonaNote, systemish: true });
state.pendingPersonaNote = null;
}
appendMessage('user', text);
$('sa_input').value = '';
persistHistory();
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();
if (chatEpoch !== state.chatEpoch) {
return;
}
// 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;
}
startBusyUi('thinking');
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),
skills: state.enabledSkills || [],
embed_model: $('sa_embed_model')?.value || state.preferredEmbed || '',
raw: {
messages,
context_json: JSON.stringify(context),
pack,
persona,
base_url: baseUrl,
model,
skills: state.enabledSkills || [],
embed_model: $('sa_embed_model')?.value || state.preferredEmbed || '',
},
};
const finishOk = async (reply, civitaiResults) => {
if (chatEpoch !== state.chatEpoch) {
return;
}
state.busy = false;
setInterruptVisible(state.generating);
const prose = extractPatch(reply).prose || reply;
state.history.push({ role: 'assistant', content: prose, persona, pack });
persistHistory();
stopBusyUi('Готово');
await handleReplySideEffects(reply, civitaiResults, {
...opts,
userText: text,
attachedSlotIds: visionSlots.map((s) => s.id),
});
};
const finishErr = (msg) => {
if (chatEpoch !== state.chatEpoch) {
return;
}
state.busy = false;
setInterruptVisible(state.generating);
stopBusyUi(msg);
if (state.streamEl) {
state.streamEl.classList.remove('sa-streaming', 'sa-typing');
state.streamEl.classList.add('error');
setAssistantBody(state.streamEl, msg);
state.streamEl = null;
state.streamMeta = null;
} else {
appendMessage('error', msg);
}
};
if (typeof makeWSRequest === 'function') {
beginStreamMessage(msgMeta);
makeWSRequest(
'AssistentChatWS',
payload,
(data) => {
if (chatEpoch !== state.chatEpoch) {
return;
}
if (data.phase === 'waiting_ollama') {
setBusyPhase('loading');
const label = state.streamEl?.querySelector('.sa-typing-label');
if (label) {
label.textContent = `Загружаю ${modelShort(model)} в GPU…`;
}
return;
}
if (data.error) {
finishErr(String(data.error));
return;
}
if (data.clear_stream) {
if (state.streamEl) {
state.streamEl.classList.add('sa-typing');
const body = state.streamEl.querySelector('.sa-msg-body') || state.streamEl;
body.innerHTML = 'Уточняю…';
}
setBusyPhase('refining');
return;
}
if (data.delta) {
appendStreamDelta(data.delta);
return;
}
if (data.done || data.reply != null) {
const reply = data.reply || (state.streamEl?.querySelector('.sa-msg-body')?.textContent) || '';
const civitai = data.civitai_results || [];
finalizeStreamMessage(reply, civitai);
finishOk(reply, civitai);
}
},
0,
(err) => {
if (chatEpoch !== state.chatEpoch) {
return;
}
// Fallback to HTTP AssistentChat
console.warn('AssistentChatWS failed, falling back', err);
if (state.streamEl) {
state.streamEl.remove();
state.streamEl = null;
state.streamMeta = null;
}
genericRequest(
'AssistentChat',
payload,
(data) => {
if (chatEpoch !== state.chatEpoch) {
return;
}
if (data.error) {
finishErr(String(data.error));
return;
}
const reply = data.reply || '';
appendMessage('assistant', reply, null, data.civitai_results || [], msgMeta);
finishOk(reply, data.civitai_results || []);
},
0,
(err2) => finishErr(String(err2 || err || 'Chat failed')),
);
},
);
return;
}
genericRequest(
'AssistentChat',
payload,
(data) => {
if (chatEpoch !== state.chatEpoch) {
return;
}
if (data.error) {
finishErr(String(data.error));
return;
}
const reply = data.reply || '';
appendMessage('assistant', reply, null, data.civitai_results || [], msgMeta);
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();
loadTaste();
loadTasteFromServer();
setView(state.view || 'chat');
updateGate();
ensureBoard();
setBoardTab(state.boardTab || 'generate', { persist: false });
syncGenerateSlot();
if (wantsAutoVision()) {
refreshImagePreview();
}
restoreHistory();
maybeWelcome();
loadConfig(localStorage.getItem(LS_PERSONA) || 'neutral', () => {
refreshModels();
refreshInventory(() => {
renderCardsList();
renderLoraChips();
});
});
wireDropZone();
wireSplitter();
registerSendButton();
wireSlashInput();
wireCardForm();
$('sa_tab_chat')?.addEventListener('click', () => setView('chat'));
$('sa_tab_cards')?.addEventListener('click', () => setView('cards'));
$('sa_board_tab_gen')?.addEventListener('click', () => setBoardTab('generate'));
$('sa_board_tab_refs')?.addEventListener('click', () => setBoardTab('refs'));
$('sa_persona')?.addEventListener('change', onPersonaChanged);
$('sa_cards_kind')?.addEventListener('change', renderCardsList);
$('sa_btn_cards_refresh')?.addEventListener('click', () => refreshInventory(() => renderCardsList(), { rescan: true }));
$('sa_btn_card_meta')?.addEventListener('click', () => fetchCardMetaLive());
$('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(() => {
renderCardsList();
renderLoraChips();
}, { rescan: true }));
$('sa_btn_add_ref')?.addEventListener('click', () => {
setBoardTab('refs');
addRefSlot({ select: true });
});
$('sa_btn_use_current')?.addEventListener('click', () => snapshotGenerateToRef());
$('sa_btn_as_init')?.addEventListener('click', async () => {
const src = selectedSrc() || findCurrentGenerateSrc();
if (!src) {
setStatus('Нет изображения для 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('Выбери окно с маской');
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('Прервано');
});
$('sa_btn_clear')?.addEventListener('click', () => {
state.history = [];
state.critiqueHopUsed = false;
state.visionHopUsed = false;
state.packUserTouched = false;
state.pendingPersonaNote = null;
state.sessionExact = {};
state.lastUserParamIntent = false;
clearPersistedHistory();
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 = 'Совместная работа с Krea 2
Напиши промпт, кинь refs, чипсы aspect или /help.
';
box.appendChild(empty);
}
stopBusyUi('');
setStatus('');
});
$('sa_base_url')?.addEventListener('change', saveSettings);
$('sa_model')?.addEventListener('change', saveSettings);
$('sa_embed_model')?.addEventListener('change', () => {
state.preferredEmbed = $('sa_embed_model')?.value || '';
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');
const profile = btn.getAttribute('data-krea-profile');
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');
} else if (profile === 'turbo') {
const p = state.kreaProfiles?.turbo || mergedGenerationDefaults('turbo');
await applyQuickPatch({ steps: p.steps ?? 8, cfg: p.cfg ?? 1, sigma_shift: p.sigma_shift ?? 1.15, actions: ['generate'] }, 'Turbo');
} else if (profile === 'raw') {
const p = state.kreaProfiles?.raw || mergedGenerationDefaults('raw');
await applyQuickPatch({ steps: p.steps ?? 28, cfg: p.cfg ?? 4.5, sigma_shift: p.sigma_shift, actions: ['generate'] }, 'RAW');
}
renderLoraChips();
});
$('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(renderLoraChips, 4000);
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: 'Изображение отправлено в Assistent', preferSelected: false });
setBoardTab('refs');
},
isKreaSelected,
resolveCurrentCheckpoint,
refreshInventory,
applyPatch,
triggerGenerate,
setInitFromSrc,
setMaskFromSrc,
clearInitAndMask,
slotById,
renderBoard,
setBoardTab,
};
}
function wireSlashInput() {
const input = $('sa_input');
if (!input || input.dataset.saSlashWired) {
return;
}
input.dataset.saSlashWired = '1';
input.addEventListener('input', () => updateSlashMenuFromInput());
input.addEventListener('keydown', (e) => {
const menu = $('sa_slash_menu');
const open = menu && !menu.hidden;
if (open) {
const items = slashMatches((input.value.split(/\s/)[0] || ''));
if (e.key === 'ArrowDown') {
e.preventDefault();
state.slashIndex = Math.min(items.length - 1, (state.slashIndex || 0) + 1);
renderSlashMenu(items);
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
state.slashIndex = Math.max(0, (state.slashIndex || 0) - 1);
renderSlashMenu(items);
return;
}
if (e.key === 'Tab' || (e.key === 'Enter' && !e.shiftKey)) {
const pick = items[state.slashIndex || 0];
if (pick && input.value.trim() === (input.value.split(/\s/)[0] || '')) {
e.preventDefault();
applySlashPick(pick);
return;
}
}
if (e.key === 'Escape') {
hideSlashMenu();
return;
}
}
if (e.key === 'Enter' && !e.shiftKey && !e.altKey) {
e.preventDefault();
hideSlashMenu();
sendChat();
}
});
input.addEventListener('blur', () => setTimeout(hideSlashMenu, 150));
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', wire);
} else {
wire();
}
})();