/** * Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API). * v0.8.0: Split assets — SA.request (assistent.api.js) and SA.*Patch (assistent.patch.js). */ (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_CHATS = 'swarm_assistent_chats_v1'; const LS_BOARD_TAB = 'swarm_assistent_board_tab'; const MAX_CHATS = 40; const MAX_CHAT_MSGS = 24; const TAB_BUTTON_ID = 'maintab_assistent'; const GEN_ID = 'generate'; let MAX_REF_SLOTS = 4; let CONTEXT_PROMPT_MAX = 2000; let HISTORY_KEEP_TURNS = 4; 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
Напиши, что сгенерировать — или кинь референс и попроси правку.`; let HELP_TEXT = `Slash-команды (без LLM): /help — этот список /new — новый чат (текущий сохранится в Историю) /history — открыть список чатов /debug — сводка UI/Exact (без LLM) /debug ask — то же + короткий ответ модели /why — сразу /debug ask /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: '/new', hint: 'новый чат' }, { cmd: '/history', hint: 'история чатов' }, { cmd: '/debug', hint: 'сводка · ask = с LLM' }, { cmd: '/why', hint: 'debug + пояснение LLM' }, { 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, lastPatch: null, pendingSilentGen: 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, chats: [], activeChatId: null, restoringChat: false, chatsPanelOpen: false, chatsQuery: '', chatsSearchHits: null, slashIndex: 0, llmParked: false, memoryRows: [], wanted: { count: 0, items: [] }, wantedKeys: new Set(), ollamaHealth: 'unknown', }; /** Disk persistence module (assistent.persist.js) — absent means localStorage only. */ function diskPersist() { return (window.SA && window.SA.persist) || null; } 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…', parking: 'Освобождаю VRAM (park LLM)…', applying: 'Applying patch…', silent_gen: 'Применяю патч → Generate…', 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 userAsksGenerate(text) { const t = String(text || ''); if (!t.trim()) { return false; } if (/^(gen|generate|go|рисуй|давай|ещё|еще)\s*[!.…]*$/i.test(t.trim())) { return true; } return /\b(сгенерируй|сгенерировать|генерируй|generate|нарисуй|перегенерируй|перерисуй|сделай\s+(картинк|изображен|фото)|run\s+generat|\/gen)\b/i.test(t); } function rememberLastPatch(patch) { if (patch && typeof patch === 'object' && !isCardObject(patch)) { state.lastPatch = patch; syncBuildGenButton(); } } function syncBuildGenButton() { const btn = $('sa_btn_build_gen'); if (!btn) { return; } if (state.lastPatch) { const keys = Object.keys(state.lastPatch).filter((k) => state.lastPatch[k] != null).slice(0, 6); btn.title = `Есть патч Assistent (${keys.join(', ') || '…'}) → Apply + Generate`; btn.classList.add('sa-has-patch'); } else { btn.title = 'Нет патча — Generate с текущим промптом SwarmUI'; btn.classList.remove('sa-has-patch'); } } function syncModeBadge() { const badge = $('sa_mode_badge'); const pack = $('sa_pack')?.value || 'write_prompt'; if (!badge) { return; } const shortMap = { write_prompt: 'write', critique_image: 'critique', compose_scene: 'compose', fix_params: 'params', inpaint_edit: 'inpaint', describe_ref: 'describe', catalog_card: 'card', }; const short = shortMap[pack] || pack.replace(/_/g, ' ').slice(0, 12); badge.textContent = short; badge.dataset.pack = pack; badge.title = `Режим: ${pack}`; badge.classList.toggle('sa-mode-hot', pack === 'critique_image' || pack === 'inpaint_edit'); } function syncLiveParamsBar() { const el = $('sa_live_params'); if (!el) { return; } const w = parseInt(val('input_width') || '0', 10) || null; const h = parseInt(val('input_height') || '0', 10) || null; const aspect = guessAspectFromSize(w, h) || '—'; const steps = val('input_steps') || '—'; const cfg = val('input_cfgscale') || val('input_cfg') || '—'; const seed = val('input_seed') || '—'; const profile = detectKreaProfileName(); el.textContent = `${aspect} · ${w || '?'}×${h || '?'} · steps ${steps} · cfg ${cfg} · ${profile} · seed ${seed}`; } 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 (state.restoringChat || !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.restoringChat) { return false; } 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(); setTimeout(() => $('sa_input')?.focus(), 50); return true; } const pane = document.getElementById('assistent'); if (pane && typeof bootstrap !== 'undefined' && bootstrap.Tab) { try { bootstrap.Tab.getOrCreateInstance(tab || pane).show(); } catch (e) { /* ignore */ } } setTimeout(() => $('sa_input')?.focus(), 50); return !!tab; } function historyMessageLimit() { const turns = Math.max(1, Number(HISTORY_KEEP_TURNS) || 4); return turns * 2; } 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, allowPreview = 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; } if (cleaned && !allowPreview && looksLikeModelPreview(cleaned)) { setStatus('Пропуск превью модели (нужна реальная генерация)'); 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() { // Never promote checkpoint/LoRA card previews into Refs. const src = generateSlot()?.src && !looksLikeModelPreview(generateSlot().src) ? generateSlot().src : findCurrentGenerateSrc({ allowPreview: false }); 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 mountPatchBlock(host, patch, { silent = false } = {}) { if (!host || !patch) { return; } rememberLastPatch(patch); const wrap = document.createElement('div'); wrap.className = 'sa-patch' + (silent ? ' sa-patch-auto' : ''); const details = document.createElement('details'); details.className = 'sa-patch-details'; const summary = document.createElement('summary'); const keys = Object.keys(patch).filter((k) => patch[k] != null && k !== 'notes' && k !== 'actions'); summary.textContent = silent ? `Патч применён · ${keys.slice(0, 6).join(', ') || 'generate'}` : `JSON патч · ${keys.slice(0, 8).join(', ') || '…'}`; const pre = document.createElement('pre'); pre.textContent = JSON.stringify(patch, null, 2); details.appendChild(summary); details.appendChild(pre); wrap.appendChild(details); mountPatchActions(wrap, patch, { silent }); host.appendChild(wrap); } function mountPatchActions(parent, patch, { silent = false } = {}) { if (!parent || !patch) { return; } rememberLastPatch(patch); retireStalePatchActions(); const wrap = parent.classList.contains('sa-patch') ? parent : null; const host = wrap || parent; if (silent) { const note = document.createElement('div'); note.className = 'sa-patch-actions sa-patch-silent sa-patch-current'; const willGen = Array.isArray(patch.actions) && patch.actions.map(String).includes('generate') || !!state.pendingSilentGen; note.textContent = willGen ? 'Применено автоматически · Generate…' : 'Применено автоматически'; host.appendChild(note); return; } 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; } startBusyUi('silent_gen'); await applyPatch(patch, 'all'); await runGenerateFromPatch({ ...patch, actions: ['generate'] }, { force: true }); }); actions.appendChild(genBtn); host.appendChild(actions); syncPatchActionAvailability(); } async function buildCurrentAndGenerate() { if (state.busy || state.generating) { setStatus('Занято — подожди или нажми Стоп'); return; } if (isGenerateUnavailable()) { setStatus('Generate недоступен — дождись SwarmUI'); return; } const patch = state.lastPatch; if (patch) { startBusyUi('silent_gen'); setStatus('Собираю патч → Generate…'); await applyPatch(patch, 'all'); syncLiveParamsBar(); await runGenerateFromPatch({ ...patch, actions: ['generate'] }, { force: true }); return; } startBusyUi('generating'); setStatus('Generate с текущим промптом…'); await runGenerateFromPatch({ actions: ['generate'] }, { force: true }); } 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 slot = generateSlot(); if (!slot) { return; } // Drop checkpoint/LoRA card previews that slipped into the Generate pane. if (scrubPreviewFromGenerateSlot()) { renderBoard(); } const src = findCurrentGenerateSrc(); 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'); } else if (!src && !slot.src) { const empty = document.querySelector('.sa-slot-gen .sa-image-empty'); const frame = document.querySelector('.sa-slot-gen'); const img = document.querySelector('.sa-slot-gen img'); if (img) { img.remove(); } if (empty) { empty.hidden = false; } frame?.classList.remove('sa-has-image', 'sa-attached'); } 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 chatUid() { return `c_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; } function slimHistoryMessages(list) { return (list || []) .filter((m) => m && (m.role === 'user' || m.role === 'assistant') && !m.systemish) .slice(-MAX_CHAT_MSGS) .map((m) => ({ role: m.role, content: String(m.content || '').slice(0, 4000), persona: m.persona || undefined, pack: m.pack || undefined, })); } function titleFromMessages(messages) { const u = (messages || []).find((m) => m.role === 'user' && m.content); const t = String(u?.content || '').replace(/\s+/g, ' ').trim(); return t ? t.slice(0, 52) : 'Новый чат'; } function snapshotChatParams() { let loras = []; try { if (typeof loraHelper !== 'undefined' && loraHelper && Array.isArray(loraHelper.selected)) { loras = loraHelper.selected.map((l) => ({ name: l.name || l, weight: (loraHelper.loraWeightPref && loraHelper.loraWeightPref[l.name || l]) || 1, })); } } catch (e) { /* ignore */ } let lastPatch = null; try { lastPatch = state.lastPatch ? JSON.parse(JSON.stringify(state.lastPatch)) : null; } catch (e) { lastPatch = null; } let sessionExact = {}; try { sessionExact = state.sessionExact && typeof state.sessionExact === 'object' ? JSON.parse(JSON.stringify(state.sessionExact)) : {}; } catch (e) { sessionExact = {}; } return { prompt: val('alt_prompt_textbox') || val('input_prompt') || '', negative: val('input_negativeprompt') || val('alt_negativeprompt_textbox') || '', width: parseInt(val('input_width') || '0', 10) || null, height: parseInt(val('input_height') || '0', 10) || null, steps: parseInt(val('input_steps') || '0', 10) || null, cfg: parseFloat(val('input_cfgscale') || val('input_cfg') || '') || null, sigma_shift: parseFloat(val('input_sigmashift') || '') || null, seed: val('input_seed') || null, sampler: val('input_sampler') || null, scheduler: val('input_scheduler') || null, batch: parseInt(val('input_images') || val('input_batchsize') || '0', 10) || null, loras, persona: $('sa_persona')?.value || 'neutral', pack: $('sa_pack')?.value || 'write_prompt', sessionExact, lastPatch, }; } async function restoreChatParams(params) { state.restoringChat = true; try { // Always reset chat-scoped state so previous chat cannot leak. state.sessionExact = {}; state.lastPatch = null; state.lastUserParamIntent = false; if (!params || typeof params !== 'object') { syncBuildGenButton(); syncLiveParamsBar(); syncModeBadge(); return { restored: false }; } const promptBox = document.getElementById('alt_prompt_textbox') || document.getElementById('input_prompt'); if (promptBox) { promptBox.value = params.prompt != null ? String(params.prompt) : ''; promptBox.dispatchEvent(new Event('input', { bubbles: true })); promptBox.dispatchEvent(new Event('change', { bubbles: true })); } setVal('input_negativeprompt', params.negative != null ? String(params.negative) : ''); // Force-write numerics when present so prior chat values cannot stick. if (params.width != null) { setVal('input_width', String(params.width)); } if (params.height != null) { setVal('input_height', String(params.height)); } if (params.steps != null) { setVal('input_steps', String(params.steps)); } if (params.cfg != null) { if (document.getElementById('input_cfgscale')) { setVal('input_cfgscale', String(params.cfg)); } else { setVal('input_cfg', String(params.cfg)); } } if (params.sigma_shift != null) { setVal('input_sigmashift', String(params.sigma_shift)); } if (params.seed != null && params.seed !== '') { setVal('input_seed', String(params.seed)); } if (params.sampler) { setVal('input_sampler', String(params.sampler)); } if (params.scheduler) { setVal('input_scheduler', String(params.scheduler)); } if (params.batch != null) { if (document.getElementById('input_images')) { setVal('input_images', String(params.batch)); } else if (document.getElementById('input_batchsize')) { setVal('input_batchsize', String(params.batch)); } } const loras = Array.isArray(params.loras) ? params.loras : []; await applyPatch({ loras }, 'loras'); if (params.pack) { setPackValue(params.pack, { flash: false }); } if (params.persona) { await applyPersonaForChat(params.persona, { quiet: true }); } state.sessionExact = params.sessionExact && typeof params.sessionExact === 'object' ? { ...params.sessionExact } : {}; state.lastPatch = params.lastPatch || null; syncBuildGenButton(); syncLiveParamsBar(); syncModeBadge(); renderLoraChips(); syncChipHighlight(); return { restored: true }; } finally { state.restoringChat = false; } } function applyPersonaForChat(personaId, { quiet = false } = {}) { const id = String(personaId || 'neutral').trim() || 'neutral'; return new Promise((resolve) => { const sel = $('sa_persona'); if (sel && [...sel.options].some((o) => o.value === id)) { sel.value = id; } if (!quiet) { onPersonaChanged(); resolve(); return; } // Quiet: reload persona config/exact without chat spam or wiping sessionExact. saveSettings(); if (typeof genericRequest !== 'function') { resolve(); return; } const packKeep = $('sa_pack')?.value; genericRequest( 'AssistentGetConfig', { persona: id }, (data) => { applyConfigPayload(data, { applyDefaults: false }); if (sel && [...sel.options].some((o) => o.value === id)) { sel.value = id; } if (packKeep) { setPackValue(packKeep, { flash: false }); } resolve(); }, 0, () => resolve(), ); }); } function persistChatsStore() { try { const chats = (state.chats || []) .filter((c) => c && c.id && (c.id === state.activeChatId || (c.messages || []).length > 0)) .slice() .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)) .slice(0, MAX_CHATS) .map((c) => ({ id: c.id, title: c.title || 'Новый чат', createdAt: c.createdAt || Date.now(), updatedAt: c.updatedAt || Date.now(), messages: slimHistoryMessages(c.messages || []), params: c.params || null, })); state.chats = chats; localStorage.setItem(LS_CHATS, JSON.stringify({ version: 1, chats })); saveActiveChatToDisk(); } catch (e) { console.warn('Assistent: persist chats failed', e); try { // Quota fallback: keep fewer / shorter chats. const slim = (state.chats || []) .filter((c) => c && c.id && (c.id === state.activeChatId || (c.messages || []).length > 0)) .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)) .slice(0, 12) .map((c) => ({ ...c, messages: slimHistoryMessages(c.messages).slice(-historyMessageLimit()).map((m) => ({ ...m, content: String(m.content || '').slice(0, 1500), })), })); state.chats = slim; localStorage.setItem(LS_CHATS, JSON.stringify({ version: 1, chats: slim })); saveActiveChatToDisk(); } catch (e2) { console.warn('Assistent: chats quota fallback failed', e2); } } } /** Mirrors the active chat onto the data volume (debounced inside SA.persist). */ function saveActiveChatToDisk() { const persist = diskPersist(); if (!persist || !state.activeChatId) { return; } const chat = findChat(state.activeChatId); if (!chat || !(chat.messages || []).length) { return; } persist.saveChat(chat); } function loadChatsStore() { state.chats = []; try { const raw = localStorage.getItem(LS_CHATS); if (raw) { const parsed = JSON.parse(raw); if (Array.isArray(parsed?.chats)) { state.chats = parsed.chats.filter((c) => c && c.id); } } } catch (e) { /* ignore */ } migrateLegacyHistoryIntoChats(); } /** Disk wins over localStorage — chats follow the data volume, not the browser. */ async function loadChatsFromDisk() { const persist = diskPersist(); if (!persist) { return; } let chats = null; try { chats = await persist.loadChats(); } catch (e) { console.warn('Assistent: disk chats failed', e); return; } if (!Array.isArray(chats) || !chats.length) { return; } state.chats = chats.filter((c) => c && c.id).slice(0, MAX_CHATS); try { localStorage.setItem(LS_CHATS, JSON.stringify({ version: 1, chats: state.chats })); } catch (e) { /* quota — disk is the source of truth anyway */ } } function migrateLegacyHistoryIntoChats() { try { const raw = localStorage.getItem(LS_HISTORY); if (!raw) { return; } const parsed = JSON.parse(raw); if (!Array.isArray(parsed) || !parsed.length) { localStorage.removeItem(LS_HISTORY); return; } const messages = slimHistoryMessages(parsed); if (!messages.length) { localStorage.removeItem(LS_HISTORY); return; } const already = state.chats.some((c) => (c.messages || []).length === messages.length && (c.messages[0]?.content || '') === (messages[0]?.content || '')); if (!already) { state.chats.unshift({ id: chatUid(), title: titleFromMessages(messages), createdAt: Date.now() - 1000, updatedAt: Date.now() - 1000, messages, params: null, }); persistChatsStore(); } localStorage.removeItem(LS_HISTORY); } catch (e) { try { localStorage.removeItem(LS_HISTORY); } catch (e2) { /* ignore */ } } } function findChat(id) { return (state.chats || []).find((c) => c.id === id) || null; } function saveActiveChatToStore({ dropEmpty = false } = {}) { if (!state.activeChatId || state.restoringChat) { return; } const chat = findChat(state.activeChatId); if (!chat) { return; } chat.messages = slimHistoryMessages(state.history); chat.params = snapshotChatParams(); chat.updatedAt = Date.now(); chat.title = titleFromMessages(chat.messages); if (dropEmpty && !chat.messages.length) { state.chats = state.chats.filter((c) => c.id !== chat.id); if (state.activeChatId === chat.id) { state.activeChatId = null; } } persistChatsStore(); } function resetMessagesUi(emptyHint) { const box = $('sa_messages'); if (!box) { return; } box.innerHTML = ''; const empty = document.createElement('div'); empty.className = 'sa-chat-empty'; empty.id = 'sa_chat_empty'; empty.innerHTML = emptyHint || '
Новый чат
Параметры Generate остаются как сейчас.
+ — ещё один чат · История — вернуться к прошлому (с его параметрами).
'; box.appendChild(empty); } function renderHistoryIntoUi(messages) { const box = $('sa_messages'); if (!box) { return; } box.innerHTML = ''; const list = slimHistoryMessages(messages); if (!list.length) { resetMessagesUi(); return; } for (const m of list) { if (m.role === 'user') { appendMessage('user', m.content, null, null, { historical: true }); } else { appendMessage('assistant', m.content, null, null, { persona: m.persona ? { id: m.persona, title: m.persona } : null, pack: m.pack, historical: true, }); } } } function updateSessionLabel() { const el = $('sa_session_label'); if (!el) { return; } const chat = findChat(state.activeChatId); el.textContent = chat?.title || 'Новый чат'; el.title = (chat?.title || 'Новый чат') + ' — клик: История'; } function savedChatsCount() { return (state.chats || []).filter((c) => (c.messages || []).length > 0).length; } function syncHistoryBadge() { const btn = $('sa_btn_chats'); if (!btn) { return; } const n = savedChatsCount(); btn.textContent = n > 0 ? `История (${n})` : 'История'; btn.title = n > 0 ? `Сохранённых чатов: ${n}. Переключение восстанавливает параметры.` : 'История чатов (пока пусто)'; } function formatChatWhen(ts) { if (!ts) { return ''; } try { return new Date(ts).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } catch (e) { return ''; } } function chatMatchesQuery(chat, q) { if (!q) { return true; } const title = String(chat?.title || '').toLowerCase(); if (title.includes(q)) { return true; } const msgs = chat?.messages || []; for (const m of msgs) { if (String(m?.content || '').toLowerCase().includes(q)) { return true; } } return false; } function renderChatsList() { const root = $('sa_chats_list'); if (!root) { return; } root.innerHTML = ''; syncHistoryBadge(); const q = (state.chatsQuery || '').trim().toLowerCase(); let chats = (state.chats || []) .slice() .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)) .filter((c) => (c.messages || []).length > 0 || (c.messages_count || 0) > 0); if (q) { const local = chats.filter((c) => chatMatchesQuery(c, q)); const seen = new Set(local.map((c) => c.id)); const extra = (state.chatsSearchHits || []).filter((h) => h && h.id && !seen.has(h.id)); chats = local.concat(extra); } if (!chats.length) { root.innerHTML = q ? '
Ничего не нашлось.
' : '
Пока пусто. Напиши что-нибудь в чат — он появится здесь. Переключение вернёт и сообщения, и параметры Generate.
'; return; } for (const c of chats) { const row = document.createElement('div'); row.className = 'sa-chat-row' + (c.id === state.activeChatId ? ' sa-chat-row-active' : ''); row.dataset.id = c.id; const n = (c.messages || []).length || Number(c.messages_count) || 0; const bits = []; if (c.params?.width && c.params?.height) { bits.push(`${c.params.width}×${c.params.height}`); } if (c.params?.steps != null) { bits.push(`steps ${c.params.steps}`); } if (c.params?.cfg != null) { bits.push(`cfg ${c.params.cfg}`); } if (Array.isArray(c.params?.loras) && c.params.loras.length) { bits.push(`LoRA ${c.params.loras.length}`); } const noParams = !c.params ? ' · без снимка params' : ''; row.innerHTML = ``; root.appendChild(row); } } function setChatsPanelOpen(open) { state.chatsPanelOpen = !!open; const panel = $('sa_chats_panel'); const btn = $('sa_btn_chats'); if (panel) { panel.hidden = !state.chatsPanelOpen; } btn?.setAttribute('aria-expanded', state.chatsPanelOpen ? 'true' : 'false'); if (state.chatsPanelOpen) { saveActiveChatToStore(); const search = $('sa_chats_search'); if (search) { search.value = state.chatsQuery || ''; search.focus(); } renderChatsList(); } } async function startNewChat({ saveCurrent = true, force = false } = {}) { if (!force && (state.busy || state.generating)) { setStatus('Занято — дождись конца ответа или Стоп'); return; } if (force) { // Drop in-flight reply so it cannot land in the new chat. abortInFlightWork({ status: '' }); } setChatsPanelOpen(false); if (saveCurrent) { saveActiveChatToStore({ dropEmpty: true }); } state.sessionExact = {}; state.lastUserParamIntent = false; state.pendingSilentGen = false; state.lastPatch = null; const chat = { id: chatUid(), title: 'Новый чат', createdAt: Date.now(), updatedAt: Date.now(), messages: [], params: snapshotChatParams(), }; state.chats.unshift(chat); state.activeChatId = chat.id; state.history = []; state.critiqueHopUsed = false; state.visionHopUsed = false; state.packUserTouched = false; state.pendingPersonaNote = null; if (state.streamEl) { try { state.streamEl.remove(); } catch (e) { /* ignore */ } state.streamEl = null; } syncBuildGenButton(); resetMessagesUi(); persistChatsStore(); updateSessionLabel(); syncHistoryBadge(); renderChatsList(); setStatus('Новый чат — параметры Generate как сейчас'); maybeWelcome(); } async function switchToChat(id) { if (!id || id === state.activeChatId) { setChatsPanelOpen(false); return; } if (state.busy || state.generating) { setStatus('Занято — нельзя сменить чат сейчас'); return; } saveActiveChatToStore({ dropEmpty: true }); let chat = findChat(id); if (!chat || !(chat.messages || []).length) { try { const full = await diskPersist()?.getChat?.(id); if (full) { const idx = (state.chats || []).findIndex((c) => c.id === id); if (idx >= 0) { state.chats[idx] = full; } else { state.chats.unshift(full); } chat = full; } } catch (e) { console.warn('Assistent: getChat failed', id, e); } } if (!chat) { setStatus('Чат не найден'); return; } state.activeChatId = chat.id; state.history = slimHistoryMessages(chat.messages); state.critiqueHopUsed = false; state.visionHopUsed = false; state.packUserTouched = false; state.pendingPersonaNote = null; state.pendingSilentGen = false; state.lastUserParamIntent = false; if (state.streamEl) { try { state.streamEl.remove(); } catch (e) { /* ignore */ } state.streamEl = null; } renderHistoryIntoUi(state.history); const result = await restoreChatParams(chat.params); updateSessionLabel(); syncHistoryBadge(); renderChatsList(); setChatsPanelOpen(false); setView('chat'); if (result?.restored) { setStatus(`Чат «${chat.title}» · параметры восстановлены`); } else { setStatus(`Чат «${chat.title}» · снимок параметров отсутствует — Generate не менялся`); } } function deleteChat(id) { if (!id) { return; } const wasActive = id === state.activeChatId; state.chats = state.chats.filter((c) => c.id !== id); if (wasActive) { state.activeChatId = null; } diskPersist()?.deleteChat(id)?.catch?.((e) => console.warn('Assistent: disk delete failed', e)); persistChatsStore(); syncHistoryBadge(); if (wasActive) { startNewChat({ saveCurrent: false, force: true }); } else { renderChatsList(); } } async function initChatSessions() { loadChatsStore(); await loadChatsFromDisk(); // Always open a fresh chat on startup; past chats stay in History. startNewChat({ saveCurrent: false, force: true }); syncHistoryBadge(); renderChatsList(); } function persistHistory() { if (state.restoringChat) { return; } if (!state.activeChatId) { const chat = { id: chatUid(), title: 'Новый чат', createdAt: Date.now(), updatedAt: Date.now(), messages: [], params: snapshotChatParams(), }; state.chats.unshift(chat); state.activeChatId = chat.id; } const chat = findChat(state.activeChatId); if (!chat) { return; } chat.messages = slimHistoryMessages(state.history); chat.params = snapshotChatParams(); chat.updatedAt = Date.now(); chat.title = titleFromMessages(chat.messages); persistChatsStore(); updateSessionLabel(); syncHistoryBadge(); } function restoreHistory() { // Replaced by initChatSessions — kept as no-op for safety. } function clearPersistedHistory() { if (state.activeChatId) { const chat = findChat(state.activeChatId); if (chat) { chat.messages = []; chat.title = 'Новый чат'; chat.params = snapshotChatParams(); chat.updatedAt = Date.now(); } persistChatsStore(); } updateSessionLabel(); renderChatsList(); } function clearChatHistory() { abortInFlightWork({ status: '' }); state.history = []; state.critiqueHopUsed = false; state.visionHopUsed = false; state.packUserTouched = false; state.pendingPersonaNote = null; state.sessionExact = {}; state.lastUserParamIntent = false; state.pendingSilentGen = false; state.lastPatch = null; syncBuildGenButton(); clearPersistedHistory(); resetMessagesUi('
Чат очищен
Сообщения сброшены. Параметры Generate на месте. + — новый чат в Историю, История — прошлые диалоги.
'); setStatus('Чат очищен'); updateSessionLabel(); syncHistoryBadge(); } 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(); renderPersonaControls(data?.controls || {}, data?.control_values || data?.exact?.controls || {}); syncPersonaDeleteButton(data?.persona_source || data?.personas?.find((p) => p.id === id)?.source); }); } 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.session_exact = state.sessionExact && Object.keys(state.sessionExact).length ? { ...state.sessionExact } : {}; // Exact KV is already in the system prompt — do not duplicate the full blob into live context. 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(); } // Fallback key list — only used if assistent.patch.js failed to load. const FALLBACK_PATCH_KEYS = [ 'prompt', 'loras', 'width', 'height', 'steps', 'cfg', 'seed', 'sigma_shift', 'sampler', 'actions', 'search_query', 'civitai_query', 'init_creativity', 'denoise', 'look_at', 'vision_from', 'vision_slots', 'aspect', 'batch', 'vary', 'lock_seed', 'pack', ]; function isPatchObject(obj) { if (window.SA && typeof SA.isPatchObject === 'function') { return SA.isPatchObject(obj); } if (!obj || typeof obj !== 'object') { return false; } return FALLBACK_PATCH_KEYS.some((k) => obj[k] !== undefined && obj[k] !== 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 (window.SA && typeof SA.extractPatch === 'function') { return SA.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) { try { const obj = JSON.parse(match[1].trim()); 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 clearPatchBlocksOnly() { document.querySelectorAll('#sa_messages .sa-patch, #sa_messages .sa-patch-stale, #sa_messages .sa-civitai-list').forEach((el) => el.remove()); state.lastPatch = null; syncBuildGenButton(); setStatus('Патчи убраны из чата'); } function toggleMoreMenu(menuId, btnId) { const menu = $(menuId); const btn = $(btnId); if (!menu) { return; } const open = menu.hidden; document.querySelectorAll('.sa-more-menu').forEach((m) => { m.hidden = true; }); document.querySelectorAll('#sa_btn_board_more, #sa_btn_clear_more').forEach((b) => b.setAttribute('aria-expanded', 'false')); if (open) { menu.hidden = false; btn?.setAttribute('aria-expanded', 'true'); } } function closeAllMoreMenus() { document.querySelectorAll('.sa-more-menu').forEach((m) => { m.hidden = true; }); document.querySelectorAll('#sa_btn_board_more, #sa_btn_clear_more').forEach((b) => b.setAttribute('aria-expanded', 'false')); } 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); } syncModeBadge(); return true; } function autoSelectPack(text) { if (state.packUserTouched) { return null; } const t = String(text || '').toLowerCase(); if (!t.trim()) { return null; } if (/\b(поправь|исправь|перепиши|улучши|fix\s+it|make\s+it\s+better|rewrite)\b/i.test(t)) { return 'write_prompt'; } 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)'); } } // Persona Exact controls (model or user patch). Ignore persona_delete. if (patch.controls && typeof patch.controls === 'object' && !Array.isArray(patch.controls)) { const schema = state.config?.controls || {}; const next = { ...(state.config?.control_values || state.exact?.controls || {}) }; for (const [k, v] of Object.entries(patch.controls)) { if (schema[k]) { next[k] = v; } } savePersonaControls(next); renderPersonaControls(schema, next); } const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : []; const wantSwitch = acts.includes('persona_switch') || (patch.persona && typeof patch.persona === 'string') || patch._persona_cloned || patch._persona_written; if (wantSwitch) { const newId = String(patch.persona || patch._persona_cloned || patch._persona_written || '').trim(); if (newId && AssistentConfigSafeIdClient(newId)) { await refreshPersonasAndSwitch(newId); } else if (acts.includes('persona_clone') || acts.includes('persona_write') || patch.persona_clone) { await refreshPersonasAndSwitch(null); } } syncChipHighlight(); syncLiveParamsBar(); syncBuildGenButton(); if (state.activeChatId && !state.restoringChat) { const chat = findChat(state.activeChatId); if (chat) { chat.params = snapshotChatParams(); chat.updatedAt = Date.now(); persistChatsStore(); } } if (!state.restoringChat) { setStatus(patch._persona_error ? `Persona: ${patch._persona_error}` : 'Applied patch'); } } function AssistentConfigSafeIdClient(id) { return /^[A-Za-z0-9][A-Za-z0-9_\-]{0,63}$/.test(String(id || '')); } async function refreshPersonasAndSwitch(preferId) { await new Promise((resolve) => { genericRequest( 'AssistentListPersonas', {}, async (data) => { if (Array.isArray(data?.personas)) { state.personas = data.personas.map((p) => ({ id: p.id, title: p.title, accent: p.accent, source: p.source, })); renderPersonaOptions(state.personas, preferId || $('sa_persona')?.value); } if (preferId && $('sa_persona')) { if ([...$('sa_persona').options].some((o) => o.value === preferId)) { $('sa_persona').value = preferId; await applyPersonaForChat(preferId, { quiet: true }); } } else { loadConfig($('sa_persona')?.value, () => resolve()); return; } resolve(); }, 0, () => resolve(), ); }); } 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; } /** Unloads the chat model from VRAM so Krea 2 gets the whole GPU. Never touches the embed model. */ function parkLlm() { return new Promise((resolve) => { const model = $('sa_model')?.value; if (!model || state.llmParked || typeof genericRequest !== 'function') { resolve(false); return; } const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434'; let settled = false; const finish = (ok) => { if (settled) { return; } settled = true; if (ok) { state.llmParked = true; } resolve(!!ok); }; setTimeout(() => finish(false), 8000); genericRequest('AssistentParkLlm', { baseUrl, model }, () => finish(true), 0, () => finish(false)); }); } /** Fire-and-forget re-load of the chat model once the user is back in the chat. */ function warmLlm() { const model = $('sa_model')?.value; if (!model || !state.llmParked || typeof genericRequest !== 'function') { return; } state.llmParked = false; const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434'; genericRequest('AssistentWarmLlm', { baseUrl, model }, () => {}, 0, () => {}); } function cancelWaitForNewImage() { if (state.waitImageTimer) { clearInterval(state.waitImageTimer); state.waitImageTimer = null; } } function bumpChatEpoch() { state.chatEpoch = (state.chatEpoch || 0) + 1; return state.chatEpoch; } function clearInFlightUi({ status } = {}) { state.busy = false; state.generating = false; state.pendingSilentGen = false; if (state.streamEl) { try { state.streamEl.remove(); } catch (e) { /* ignore */ } state.streamEl = null; state.streamMeta = null; } setInterruptVisible(false); syncGenerateBusy(); syncPatchActionAvailability(); if (status != null) { stopBusyUi(status); } else { stopBusyUi(''); } } /** Invalidate in-flight Assistent WS/wait; optionally also interrupt Swarm Generate. */ function abortInFlightWork({ status, interruptSwarm = false } = {}) { bumpChatEpoch(); cancelWaitForNewImage(); if (interruptSwarm) { try { if (typeof doInterrupt === 'function') { doInterrupt(false); } else if (typeof genericRequest === 'function') { genericRequest('InterruptAll', { other_sessions: false }, () => {}, 0, () => {}); } } catch (e) { /* ignore */ } } clearInFlightUi({ status: status != null ? status : '' }); } 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(); startBusyUi('parking'); setStatus('Освобождаю VRAM…'); await parkLlm(); 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 завершён (новое изображение не найдено)'); } // Auto-critique loads the model itself on the next request — don't pay for it twice. const paneVisible = !!document.getElementById('swarm_assistent_root')?.offsetParent; if (state.view === 'chat' && paneVisible && !$('sa_auto_critique')?.checked) { warmLlm(); } if (src) { const gen = generateSlot(); if (gen) { gen.src = src; renderBoard(); } setStatus('Generate готов'); return src; } if (state.busy) { setStatus('Generate завершён (новое изображение не найдено)'); } return null; } /** Resolves the freshest real Generate frame — never a model preview. */ async function resolveFinishedGenerateSrc(hint, { settleMs = 20000 } = {}) { scrubPreviewFromGenerateSlot(); let src = hint && !looksLikeModelPreview(hint) ? hint : null; if (!src) { src = findCurrentGenerateSrc(); } // Batch still running: the last frame is not the final one yet. if (isGenerateUnavailable()) { const settled = await waitForNewImage(src, settleMs); if (settled) { src = settled; } } return src && !looksLikeModelPreview(src) ? src : null; } async function maybeAutoCritique(imageSrc) { if (!$('sa_auto_critique')?.checked || state.critiqueHopUsed) { return; } const src = await resolveFinishedGenerateSrc(imageSrc); if (!src) { setStatus('Авто-критика пропущена — нет готового кадра Generate'); return; } state.critiqueHopUsed = true; setPackValue('critique_image', { flash: true }); 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; gen.src = src; renderBoard(); } setStatus('Auto-critique…'); await sendChat({ fromAutoCritique: true, forceSlotIds: [GEN_ID] }); } /** Board action: attach the finished Generate frame and ask for a verdict. */ async function askLookAtResult() { if (state.busy || state.generating) { setStatus('Занято — дождись конца ответа или Стоп'); return; } if (!updateGate()) { setStatus('Выбери модель Krea 2'); return; } const src = await resolveFinishedGenerateSrc(generateSlot()?.src, { settleMs: 8000 }); if (!src) { setStatus('Нет готового кадра Generate — сначала сгенерируй'); return; } const gen = generateSlot(); if (gen) { gen.src = src; gen.attach = true; } setBoardTab('generate'); renderBoard(); setView('chat'); setPackValue('critique_image', { flash: true }); if ($('sa_input')) { $('sa_input').value = 'Посмотри результат: что получилось, что сломалось, и как поправить промпт и параметры для следующего кадра.'; } await sendChat({ forceSlotIds: [GEN_ID], skipAutoPack: true }); } 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 || ''; } // Historical replay: show prose only — no Apply strip (lastPatch comes from chat.params). if (finalPatch && !(meta && meta.historical)) { const silent = !!(meta && meta.silentPatch); mountPatchBlock(div, finalPatch, { silent }); } 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 silent = !!(meta && meta.silentPatch) || !!state.pendingSilentGen; mountPatchBlock(el, patch, { silent }); } 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 = 'Уже установлена'; actions.appendChild(note); } else if (r.download_url) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'basic-button sa-primary'; btn.textContent = 'Подтвердить скачивание'; btn.addEventListener('click', () => downloadCivitaiLoRA(r, btn)); actions.appendChild(btn); } else { const note = document.createElement('span'); note.textContent = 'Нет 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 = 'Скачиваю…'; } setStatus(`Скачиваю ${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(`Скачано ${payload.name}`); if (btn) { btn.textContent = 'Скачано'; } refreshInventory(async () => { await maybeWriteCardAfterDownload({ kind: 'lora', name: payload.name, civitai: card, }); }, { rescan: true }); } else { setStatus(msg || 'Ошибка скачивания'); if (btn) { btn.disabled = false; btn.textContent = 'Подтвердить скачивание'; } appendMessage('error', msg || 'Ошибка скачивания'); } }; 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; } // Only SwarmUI checkpoint/LoRA card routes — not bare "/models/" (matches Civitai page URLs). return s.includes('.preview.') || s.includes('placeholder') || s.includes('/viewspecial/') || s.includes('viewspecial/') || s.includes('/view/models/') || /\/view\/models\//.test(s) || /[?&](?:path|file)=[^&]*\.preview\./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.getAttribute?.('data-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; } // Strip cache-busters for classification, keep original for display when accepted. if (!allowPreview && looksLikeModelPreview(src)) { return null; } return src; } function scrubPreviewFromGenerateSlot() { const slot = generateSlot(); if (!slot?.src) { return false; } if (!looksLikeModelPreview(slot.src)) { return false; } slot.src = null; slot.attach = false; syncLastImageAlias(); return true; } function refreshImagePreview() { scrubPreviewFromGenerateSlot(); if (wantsAutoVision()) { const gen = generateSlot(); if (gen) { const src = findCurrentGenerateSrc(); if (src) { gen.attach = true; gen.src = src; } else { gen.attach = false; } 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 collectUiState() { return { pack: $('sa_pack')?.value || 'write_prompt', persona: $('sa_persona')?.value || 'neutral', auto_vision: !!$('sa_auto_vision')?.checked, auto_apply: !!$('sa_auto_apply')?.checked, auto_generate: !!$('sa_auto_generate')?.checked, auto_critique: !!$('sa_auto_critique')?.checked, auto_download: !!$('sa_auto_download')?.checked, pane_width: localStorage.getItem(LS_PANE_WIDTH) || '', embed_model: $('sa_embed_model')?.value || state.preferredEmbed || '', base_url: $('sa_base_url')?.value || '', model: $('sa_model')?.value || '', view: state.view || 'chat', board_tab: state.boardTab || 'generate', }; } /** * Fills fields the browser has never seen from sqlite ui_state, so a fresh * browser on the same volume inherits the previous session. Existing localStorage wins. * auto_download is only ever restored when it is off — the danger flag stays opt-in. */ async function applyDiskUiState() { const persist = diskPersist(); if (!persist) { return; } let ui = null; try { ui = await persist.loadUiState(); } catch (e) { return; } if (!ui || typeof ui !== 'object') { return; } const fill = (lsKey, value, apply) => { if (value == null || value === '' || localStorage.getItem(lsKey) != null) { return; } localStorage.setItem(lsKey, String(value)); apply?.(String(value)); }; fill(LS_BASE, ui.base_url, (v) => { if ($('sa_base_url')) { $('sa_base_url').value = v; } }); fill(LS_MODEL, ui.model, (v) => { state.preferredModel = v; }); fill(LS_EMBED, ui.embed_model, (v) => { state.preferredEmbed = v; }); fill(LS_PACK, ui.pack, (v) => { if ($('sa_pack')) { $('sa_pack').value = v; } }); fill(LS_PERSONA, ui.persona, (v) => { if ($('sa_persona')) { $('sa_persona').value = v; } }); fill(LS_PANE_WIDTH, ui.pane_width, (v) => document.documentElement.style.setProperty('--sa-image-width', v)); if (ui.view === 'cards' || ui.view === 'chat') { fill(LS_VIEW, ui.view, (v) => { state.view = v; }); } if (ui.board_tab === 'refs' || ui.board_tab === 'generate') { fill(LS_BOARD_TAB, ui.board_tab, (v) => { state.boardTab = v; }); } for (const [key, lsKey, id] of [ ['auto_vision', LS_AUTO_VISION, 'sa_auto_vision'], ['auto_apply', LS_AUTO_APPLY, 'sa_auto_apply'], ['auto_generate', LS_AUTO_GENERATE, 'sa_auto_generate'], ['auto_critique', LS_AUTO_CRITIQUE, 'sa_auto_critique'], ['auto_download', LS_AUTO_DOWNLOAD, 'sa_auto_download'], ]) { if (ui[key] == null || localStorage.getItem(lsKey) != null) { continue; } const on = ui[key] === true || ui[key] === '1' || ui[key] === 1; if (on && key === 'auto_download') { continue; } localStorage.setItem(lsKey, on ? '1' : '0'); const el = $(id); if (el) { el.checked = on; } } } function saveUiStateToDisk() { diskPersist()?.saveUiState(collectUiState()); } 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(); saveUiStateToDisk(); } 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; } const asst = data.assistant || {}; if (asst.history_keep_turns != null) { HISTORY_KEEP_TURNS = Math.max(1, Number(asst.history_keep_turns) || 4); } if (asst.max_ref_slots != null) { MAX_REF_SLOTS = Math.max(1, Number(asst.max_ref_slots) || 4); } if (asst.context_prompt_max != null) { CONTEXT_PROMPT_MAX = Math.max(200, Number(asst.context_prompt_max) || 2000); } if (applyDefaults || data.exact) { fillEmptyParamsFromExact(); } renderPersonaControls(data.controls || {}, data.control_values || data.exact?.controls || {}); syncPersonaDeleteButton(data.persona_source || data.personas?.find((p) => p.id === (data.persona || $('sa_persona')?.value))?.source); } function syncPersonaDeleteButton(source) { const btn = $('sa_persona_delete'); if (!btn) { return; } const src = String(source || ''); const canDelete = src === 'overlay' || src === 'overlay+bundled'; btn.hidden = !canDelete; btn.disabled = !canDelete; } let controlSaveTimer = null; function renderPersonaControls(schema, values) { const box = $('sa_persona_controls'); if (!box) { return; } box.innerHTML = ''; const keys = schema && typeof schema === 'object' ? Object.keys(schema) : []; if (!keys.length) { box.hidden = true; return; } box.hidden = false; for (const id of keys) { const def = schema[id]; if (!def || typeof def !== 'object') { continue; } if (String(def.type || 'slider').toLowerCase() !== 'slider') { continue; } const min = Number(def.min ?? -1); const max = Number(def.max ?? 1); const step = Number(def.step ?? 0.05); const defVal = Number(def.default ?? 0); let cur = values && values[id] != null ? Number(values[id]) : defVal; if (Number.isNaN(cur)) { cur = defVal; } const row = document.createElement('div'); row.className = 'sa-control-row'; row.title = def.hint || id; const lab = document.createElement('label'); lab.textContent = def.label || id; const input = document.createElement('input'); input.type = 'range'; input.min = String(min); input.max = String(max); input.step = String(step); input.value = String(cur); input.dataset.controlId = id; const valEl = document.createElement('span'); valEl.className = 'sa-control-val'; valEl.textContent = cur.toFixed(2); const onInput = () => { const v = Number(input.value); valEl.textContent = v.toFixed(2); if (controlSaveTimer) { clearTimeout(controlSaveTimer); } controlSaveTimer = setTimeout(() => savePersonaControls({ [id]: v }), 350); }; input.addEventListener('input', onInput); input.addEventListener('change', onInput); row.appendChild(lab); row.appendChild(input); row.appendChild(valEl); box.appendChild(row); } } function savePersonaControls(partial) { const persona = $('sa_persona')?.value || 'neutral'; if (typeof genericRequest !== 'function') { return; } genericRequest( 'AssistentSaveControls', { persona, controls: partial || {} }, (data) => { if (data?.error) { setStatus(data.error); return; } if (data?.control_values && state.config) { state.config.control_values = data.control_values; if (state.exact) { state.exact.controls = data.control_values; } } if (data?.controls) { renderPersonaControls(data.controls, data.control_values || {}); } }, 0, () => setStatus('controls save failed'), ); } async function deleteCurrentOverlayPersona() { const id = $('sa_persona')?.value; if (!id) { return; } const meta = (state.personas || []).find((p) => p.id === id); const title = meta?.title || id; const src = meta?.source || state.config?.persona_source || ''; if (src !== 'overlay' && src !== 'overlay+bundled') { setStatus('Bundled personas cannot be deleted'); return; } if (!window.confirm(`Удалить «${title}»?\nПоставка (bundled) не трогается.`)) { return; } await new Promise((resolve) => { genericRequest( 'AssistentDeletePersona', { persona: id }, async (data) => { if (data?.error) { setStatus(data.error); resolve(); return; } const next = data?.default_persona || 'neutral'; if (Array.isArray(data?.personas)) { state.personas = data.personas; } renderPersonaOptions(state.personas || [], next); if ($('sa_persona')) { $('sa_persona').value = next; } await applyPersonaForChat(next, { quiet: false }); setStatus(`Удалено: ${id}`); resolve(); }, 0, () => { setStatus('delete failed'); resolve(); }, ); }); } 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; } const meta = (personas || []).find((p) => p.id === sel.value); syncPersonaDeleteButton(meta?.source || state.config?.persona_source); } 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)'); if (models.length) { setOllamaHealth('ok', `Ollama · ${models.length}`, `Чат-моделей: ${models.length}, память: ${memoryModels.length}`); } else { setOllamaHealth('warn', 'Ollama · 0 моделей', 'Нет чат-моделей — сделай ollama pull'); } saveSettings(); }, 0, (err) => { const msg = String(err || 'Ollama unreachable'); setStatus(msg); setModelOptions([], { error: msg }); setOllamaHealth('down', 'Ollama ✕', 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 memoryKindFilter() { return $('sa_mem_kind')?.value || 'all'; } function renderMemoryKinds(kinds) { const sel = $('sa_mem_kind'); if (!sel) { return; } const cur = sel.value || 'all'; sel.innerHTML = ''; const all = document.createElement('option'); all.value = 'all'; all.textContent = 'Все типы'; sel.appendChild(all); for (const kind of kinds || []) { const opt = document.createElement('option'); opt.value = kind; opt.textContent = kind; sel.appendChild(opt); } if ([...sel.options].some((o) => o.value === cur)) { sel.value = cur; } } function renderMemoryList() { const root = $('sa_mem_list'); if (!root) { return; } const filter = memoryKindFilter(); const rows = (state.memoryRows || []).filter((m) => filter === 'all' || m.kind === filter); root.innerHTML = ''; if (!rows.length) { root.innerHTML = '
Память пуста — она наполняется из карточек, seed и патчей memory_upsert.
'; return; } for (const row of rows) { const el = document.createElement('div'); el.className = 'sa-mem-row'; const bundled = row.source === 'bundled'; const when = row.updated ? formatChatWhen(row.updated * 1000) : ''; const scope = row.scope === 'personal' ? `персона ${row.persona || '—'}` : 'общая'; el.innerHTML = `
${escapeHtml(row.kind || 'note')}${escapeHtml(row.key || '')}
${escapeHtml(clipDebug(row.text, 220))}
${escapeHtml([scope, row.source || 'user', when].filter(Boolean).join(' · '))}
`; const forget = document.createElement('button'); forget.type = 'button'; forget.className = 'basic-button sa-mem-forget'; forget.textContent = '×'; if (bundled) { forget.disabled = true; forget.title = 'Bundled — вернётся при reseed, правь Config/_base/memory-seed/'; } else { forget.title = 'Забыть'; forget.addEventListener('click', () => forgetMemory(row)); } el.appendChild(forget); root.appendChild(el); } } function refreshMemoryList() { if (typeof genericRequest !== 'function') { return; } const list = $('sa_mem_list'); if (list && !state.memoryRows.length) { list.innerHTML = '
Читаю память…
'; } genericRequest( 'AssistentListMemory', { limit: 200 }, (data) => { state.memoryRows = Array.isArray(data?.memories) ? data.memories : []; renderMemoryKinds(data?.kinds || []); renderMemoryList(); const foot = $('sa_mem_total'); if (foot) { foot.textContent = `Всего: ${data?.total ?? state.memoryRows.length} · ${data?.embed_model || '—'}`; } }, 0, (err) => { if (list) { list.innerHTML = `
Память недоступна: ${escapeHtml(String(err || 'ошибка'))}
`; } }, ); } function forgetMemory(row) { if (!row?.kind || !row?.key || typeof genericRequest !== 'function') { return; } genericRequest( 'AssistentForgetMemory', { kind: row.kind, key: row.key, source: row.source || '', scope: row.scope || '', persona: row.scope === 'personal' ? (row.persona || '') : '', }, () => { state.memoryRows = (state.memoryRows || []).filter((m) => !(m.kind === row.kind && m.key === row.key && m.source === row.source && m.persona === row.persona)); renderMemoryList(); setStatus(`Забыто: ${row.kind}/${row.key}`); }, 0, (err) => setStatus(String(err || 'Не удалось забыть')), ); } function modelKeyLeaf(name) { return String(name || '') .replace(/\\/g, '/') .split('/') .pop() .replace(/\.(safetensors|ckpt|pt|pth|gguf|bin)$/i, '') .trim() .toLowerCase(); } function refreshWantedQueue() { if (typeof genericRequest !== 'function') { return; } genericRequest( 'AssistentListWanted', {}, (data) => { const items = Array.isArray(data?.items) ? data.items : []; state.wanted = { count: data?.count ?? items.length, items }; const keys = new Set(); for (const item of items) { const leaf = modelKeyLeaf(item?.title); if (leaf) { keys.add(leaf); } if (item?.version_id) { keys.add(`v${item.version_id}`); } } state.wantedKeys = keys; const el = $('sa_mem_wanted'); if (el) { el.textContent = state.wanted.count ? `Очередь wanted: ${state.wanted.count} (скачается на следующем up)` : 'Очередь wanted: пусто'; el.title = items.slice(0, 12).map((i) => `${i.kind}: ${i.title || i.url}`).join('\n'); } if (state.view === 'cards') { renderCardsList(); } }, 0, () => { const el = $('sa_mem_wanted'); if (el) { el.textContent = 'Очередь wanted: —'; } }, ); } function isWantedModel(row) { const keys = state.wantedKeys; if (!keys || !keys.size) { return false; } for (const candidate of [row?.name, row?.title]) { const leaf = modelKeyLeaf(candidate); if (leaf && keys.has(leaf)) { return true; } } return false; } function setOllamaHealth(level, text, title) { state.ollamaHealth = level; const el = $('sa_ollama_health'); if (!el) { return; } el.hidden = false; el.textContent = text; el.title = title || text; el.classList.remove('sa-health-ok', 'sa-health-warn', 'sa-health-down'); el.classList.add(`sa-health-${level}`); } function probeOllamaHealth() { if (typeof genericRequest !== 'function') { return; } const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434'; genericRequest( 'AssistentListModels', { baseUrl }, (data) => { if (data?.error) { setOllamaHealth('down', 'Ollama ✕', String(data.error)); return; } const chat = (data.models || []).length; const mem = (data.memory_models || []).length; if (!chat) { setOllamaHealth('warn', 'Ollama · 0 моделей', 'Нет чат-моделей — сделай ollama pull'); return; } setOllamaHealth('ok', `Ollama · ${chat}`, `Чат-моделей: ${chat}, память: ${mem} · ${baseUrl}`); }, 0, (err) => setOllamaHealth('down', 'Ollama ✕', `Нет связи: ${String(err || '')} · ${baseUrl}`), ); } 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(); } else if (state.llmParked && !state.generating) { // Back in the chat — bring the model home before the user hits Send. warmLlm(); } } 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 (isWantedModel(row)) { metaBits.push('⏳ wanted'); btn.classList.add('sa-card-row-wanted'); } 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', (e) => { if (e.target?.closest?.('[data-chat]')) { e.preventDefault(); e.stopPropagation(); sendCardToChat(row); return; } selectCardModel(row); }); root.appendChild(btn); } } function sendCardToChat(row) { if (!row?.name) { return; } selectCardModel(row); setView('chat'); const kind = row.kind === 'checkpoint' ? 'checkpoint' : 'LoRA'; const triggers = row.trigger ? ` Triggers: ${row.trigger}.` : ''; if ($('sa_input')) { $('sa_input').value = `Используй ${kind} «${row.name}».${triggers} Учти карточку/triggers и предложи патч.`; $('sa_input').focus(); } setStatus(`В чат → ${row.name}`); } 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()); if (enqueue || !data.installed) { refreshWantedQueue(); } }, 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}`); refreshWantedQueue(); }, 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; // sqlite taste is the source of truth; localStorage only wins when it is strictly newer. const localEmpty = !localUpdated && !(state.taste?.styles?.length || state.taste?.likes?.length || state.taste?.avoid?.length); if (localEmpty || 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, fromDebug } = 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 (patch) { rememberLastPatch(patch); } 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 (fromDebug) { // Q&A only — never apply patches / generate from a debug explanation turn. state.pendingSilentGen = false; return; } const wantsGen = !!(opts.userWantsGenerate || state.pendingSilentGen || (Array.isArray(patch?.actions) && patch.actions.map(String).includes('generate'))); const doApply = !!(patch && (wantsGen || $('sa_auto_apply')?.checked)); if (doApply) { if (wantsGen) { startBusyUi('silent_gen'); } else { setBusyPhase('applying'); } await applyPatch(patch, 'all'); syncLiveParamsBar(); updateTasteFromPatch(patch, opts.userText || ''); if (!fromAutoCritique && (wantsGen || $('sa_auto_generate')?.checked)) { const src = await runGenerateFromPatch( { ...patch, actions: Array.isArray(patch.actions) ? patch.actions : ['generate'] }, { force: wantsGen }, ); if (src) { await maybeAutoCritique(src); } } else if (!state.generating) { stopBusyUi(wantsGen ? 'Применено' : ''); } } state.pendingSilentGen = false; } 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; } function clipDebug(s, max) { const t = String(s || '').replace(/\s+/g, ' ').trim(); if (!t) { return '—'; } return t.length > max ? `${t.slice(0, max)}…` : t; } function formatDebugLoras(list) { if (!Array.isArray(list) || !list.length) { return 'нет'; } return list.slice(0, 8).map((l) => { const name = l?.name || l; const w = l?.weight != null ? `@${l.weight}` : ''; return `${name}${w}`; }).join(', '); } function buildDebugSummary() { const persona = $('sa_persona')?.value || 'neutral'; const pack = $('sa_pack')?.value || 'write_prompt'; const chatModel = $('sa_model')?.value || '—'; const embed = $('sa_embed_model')?.value || state.preferredEmbed || '—'; const profile = detectKreaProfileName(); const defaults = mergedGenerationDefaults(profile); const session = state.sessionExact || {}; const exactGen = state.exact?.generation || state.config?.exact?.generation || {}; const ctx = (() => { try { return collectLiveContext(); } catch (e) { return {}; } })(); const aspect = guessAspectFromSize(ctx.width, ctx.height) || defaults.aspect || '—'; const why = []; if (Object.keys(session).length) { why.push(`session_exact перекрывает Exact: ${Object.keys(session).join(', ')}`); } else { why.push('session_exact пуст — params из Exact + профиль чекпоинта'); } why.push(`профиль чекпоинта: ${profile} (имя/title → turbo|raw)`); if (persona === 'cinema' || state.exact?.generation?.aspect) { why.push(`persona/exact aspect: ${state.exact?.generation?.aspect || exactGen.aspect || '—'}`); } if (state.lastPatch) { const keys = Object.keys(state.lastPatch).filter((k) => state.lastPatch[k] != null && k !== 'notes'); why.push(`последний патч задал: ${keys.slice(0, 12).join(', ')}`); } else { why.push('последнего патча Assistent ещё нет'); } why.push('приоритет: user → session_exact → exact(+persona) → live UI → memory_hits (shared+personal)'); const lines = [ '### Debug Assistent', `persona=${persona} · pack=${pack}`, `chat=${chatModel} · embed=${embed}`, `skills=${(state.enabledSkills || []).join(',') || '—'}`, `auto: apply=${!!$('sa_auto_apply')?.checked} gen=${!!$('sa_auto_generate')?.checked} vision=${!!$('sa_auto_vision')?.checked} critique=${!!$('sa_auto_critique')?.checked}`, '', 'Live SwarmUI:', ` ckpt=${ctx.checkpoint?.name || '—'} · krea_profile=${ctx.krea_profile || profile}`, ` ${ctx.width || '?'}×${ctx.height || '?'} (${aspect}) · steps=${ctx.steps ?? '—'} · cfg=${ctx.cfg ?? '—'} · sigma=${ctx.sigma_shift ?? '—'} · seed=${ctx.seed ?? '—'} · batch=${ctx.batch ?? '—'}`, ` loras: ${formatDebugLoras(ctx.selected_loras || ctx.enabled_loras)}`, ` prompt: ${clipDebug(ctx.prompt, 220)}`, ` negative: ${clipDebug(ctx.negative, 120)}`, ` init=${!!ctx.has_init_image} mask=${!!ctx.has_mask_image} prompt_images=${ctx.prompt_image_count || 0}`, '', 'Exact defaults (merged):', ` generation=${JSON.stringify(exactGen)}`, ` effective=${JSON.stringify({ steps: defaults.steps, cfg: defaults.cfg, sigma_shift: defaults.sigma_shift, aspect: defaults.aspect, images: defaults.images, profile: defaults.profile, })}`, ` session_exact=${Object.keys(session).length ? JSON.stringify(session) : '{}'}`, '', 'Почему так:', ...why.map((w) => ` · ${w}`), ]; if (state.lastPatch) { lines.push('', `last_patch: ${clipDebug(JSON.stringify(state.lastPatch), 360)}`); } return lines.join('\n'); } 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 === 'new' || cmd === 'newchat') { await startNewChat({ saveCurrent: true }); return true; } if (cmd === 'history' || cmd === 'chats' || cmd === 'sessions') { setChatsPanelOpen(true); setStatus('/history'); return true; } if (cmd === 'debug' || cmd === 'dbg' || cmd === 'why') { const dump = buildDebugSummary(); appendSystemNote(dump); const argL = String(arg || '').toLowerCase().trim(); const wantLlm = cmd === 'why' || /^(ask|llm|explain|поясни|почему|модель)(\s|$)/i.test(argL); if (wantLlm) { setStatus('/debug ask…'); await sendChat({ forcedUserText: 'Отладка Assistent. Ниже факты UI (уже собраны клиентом). ' + 'Кратко своими словами (5–10 строк, язык пользователя): какие промпт/params сейчас, ' + 'что из Exact vs session_exact vs live, что сделал последний патч и почему так логично. ' + 'Без JSON patch, без generate, без look_at.\n\n' + dump, skipSlash: true, skipAutoPack: true, fromDebug: true, }); } else { setStatus('/debug'); } 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(); clearInFlightUi({ status: 'Прервано' }); 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; } if (cmd === 'persona') { const sub = (parts[1] || 'new').toLowerCase(); const rest = parts.slice(2).join(' ').trim(); setPackValue('author_persona', { flash: true, user: true }); if (sub === 'save') { await sendChat({ skipAutoPack: true, forcedUserText: 'Сохрани согласованный черновик личности сейчас (persona_clone / persona_write). Не удаляй личности.', }); return true; } const fromId = sub === 'clone' && rest ? rest.split(/\s+/)[0] : ($('sa_persona')?.value || 'neutral'); await sendChat({ skipAutoPack: true, forcedUserText: `Начни интервью author_persona: клон с источника «${fromId}». ` + 'Спрашивай по полкам группами. Не пиши на диск, пока мало ответов. Не удаляй личности.', }); 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; } scrubPreviewFromGenerateSlot(); const have = ids.map((id) => slotById(id)).filter((s) => s && s.src && !looksLikeModelPreview(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: нет реального кадра (превью модели пропущено)'); 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 || state.generating) && !opts.fromVisionHop && !opts.fromAutoCritique) { return; } const rawInput = ($('sa_input')?.value || '').trim(); const text = (opts.forcedUserText || rawInput).trim(); if (!text) { return; } if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.fromDebug) { state.lastUserParamIntent = userTextMentionsParams(text); state.pendingSilentGen = userAsksGenerate(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; // Ollama reloads the model for this request (keep_alive 15m), so it is no longer parked. state.llmParked = false; 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 && !looksLikeModelPreview(generateSlot().src) && !wantedIds.includes(GEN_ID)) { wantedIds.push(GEN_ID); } const visionSlots = wantedIds.map((id) => slotById(id)).filter((s) => s && s.src && !looksLikeModelPreview(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, silentPatch: !!state.pendingSilentGen, }; 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(-historyMessageLimit()).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 || '', }; const finishOk = async (reply, civitaiResults) => { if (chatEpoch !== state.chatEpoch) { return; } const prose = extractPatch(reply).prose || reply; state.history.push({ role: 'assistant', content: prose, persona, pack }); persistHistory(); setBusyPhase(state.pendingSilentGen || userAsksGenerate(text) ? 'silent_gen' : 'thinking'); try { await handleReplySideEffects(reply, civitaiResults, { ...opts, userText: text, userWantsGenerate: !!state.pendingSilentGen || userAsksGenerate(text), attachedSlotIds: visionSlots.map((s) => s.id), }); } finally { if (chatEpoch !== state.chatEpoch) { return; } state.busy = false; setInterruptVisible(state.generating); if (!state.generating) { stopBusyUi('Готово'); } } }; 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 = ''; saveUiStateToDisk(); }); } 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, ); } /** Disk state first (chats + UI prefs), then config / models / inventory. */ async function bootstrapPersisted() { try { await applyDiskUiState(); } catch (e) { console.warn('Assistent: ui-state restore failed', e); } try { await initChatSessions(); } catch (e) { console.warn('Assistent: chat sessions failed', e); } loadConfig(localStorage.getItem(LS_PERSONA) || 'neutral', () => { refreshModels(); refreshInventory(() => { renderCardsList(); renderLoraChips(); }); }); probeOllamaHealth(); refreshWantedQueue(); } 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(); } bootstrapPersisted(); wireDropZone(); wireSplitter(); registerSendButton(); wireSlashInput(); wireCardForm(); $('sa_btn_new_chat')?.addEventListener('click', () => startNewChat({ saveCurrent: true })); $('sa_btn_chats')?.addEventListener('click', (e) => { e.stopPropagation(); setChatsPanelOpen(!state.chatsPanelOpen); }); $('sa_session_label')?.addEventListener('click', (e) => { e.stopPropagation(); setChatsPanelOpen(!state.chatsPanelOpen); }); $('sa_session_label')?.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setChatsPanelOpen(!state.chatsPanelOpen); } }); $('sa_chats_panel')?.addEventListener('click', (e) => e.stopPropagation()); $('sa_chats_list')?.addEventListener('click', (e) => { const row = e.target.closest('.sa-chat-row'); if (!row) { return; } const id = row.dataset.id; if (e.target.closest('[data-del]')) { e.preventDefault(); if (window.confirm('Удалить этот чат из истории?')) { deleteChat(id); } return; } if (e.target.closest('[data-open]')) { switchToChat(id); } }); let chatsSearchTimer = null; $('sa_chats_search')?.addEventListener('input', () => { const q = ($('sa_chats_search')?.value || '').trim(); state.chatsQuery = q; if (!q) { state.chatsSearchHits = null; renderChatsList(); return; } renderChatsList(); clearTimeout(chatsSearchTimer); chatsSearchTimer = setTimeout(async () => { try { const hits = await diskPersist()?.searchChats?.(q); if ((state.chatsQuery || '') !== q) { return; } state.chatsSearchHits = Array.isArray(hits) ? hits : []; renderChatsList(); } catch (e) { /* ignore */ } }, 220); }); $('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_persona_delete')?.addEventListener('click', () => deleteCurrentOverlayPersona()); $('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; if (!s.hidden) { refreshMemoryList(); refreshWantedQueue(); } } }); $('sa_btn_mem_refresh')?.addEventListener('click', () => { refreshMemoryList(); refreshWantedQueue(); }); $('sa_mem_kind')?.addEventListener('change', renderMemoryList); $('sa_btn_look_result')?.addEventListener('click', () => askLookAtResult()); $('sa_ollama_health')?.addEventListener('click', () => probeOllamaHealth()); document.addEventListener('keydown', (e) => { if (e.key !== 'Escape') { return; } let closed = false; const settings = $('sa_settings'); if (settings && !settings.hidden) { settings.hidden = true; closed = true; } if (state.chatsPanelOpen) { setChatsPanelOpen(false); closed = true; } const slash = $('sa_slash_menu'); if (slash && !slash.hidden) { slash.hidden = true; closed = true; } closeAllMoreMenus(); if (closed) { e.preventDefault(); } }); // Focus composer when Assistent tab becomes visible document.getElementById(TAB_BUTTON_ID)?.addEventListener('click', () => { setTimeout(() => $('sa_input')?.focus(), 80); }); $('sa_btn_refresh_models')?.addEventListener('click', () => { saveSettings(); refreshModels(); probeOllamaHealth(); }); $('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 () => { closeAllMoreMenus(); const src = selectedSrc() || findCurrentGenerateSrc(); if (!src) { setStatus('Нет изображения для Init'); return; } await setInitFromSrc(src); const pack = $('sa_pack'); if (pack && pack.value === 'write_prompt') { setPackValue('inpaint_edit', { flash: true }); } }); $('sa_btn_as_mask')?.addEventListener('click', async () => { closeAllMoreMenus(); const src = selectedSrc(); if (!src) { setStatus('Выбери окно с маской'); return; } await setMaskFromSrc(src); setPackValue('inpaint_edit', { flash: true }); }); $('sa_btn_clear_init')?.addEventListener('click', () => { clearInitAndMask(); closeAllMoreMenus(); }); $('sa_btn_clear_image')?.addEventListener('click', () => clearSlot(state.selectedSlotId)); $('sa_btn_board_more')?.addEventListener('click', (e) => { e.stopPropagation(); toggleMoreMenu('sa_board_more_menu', 'sa_btn_board_more'); }); $('sa_btn_send')?.addEventListener('click', () => sendChat()); $('sa_btn_build_gen')?.addEventListener('click', () => buildCurrentAndGenerate()); $('sa_btn_interrupt')?.addEventListener('click', () => { doInterruptNow(); clearInFlightUi({ status: 'Прервано' }); }); $('sa_btn_clear')?.addEventListener('click', () => { if (window.confirm('Очистить весь чат Assistent?')) { clearChatHistory(); } }); $('sa_btn_clear_more')?.addEventListener('click', (e) => { e.stopPropagation(); toggleMoreMenu('sa_clear_more_menu', 'sa_btn_clear_more'); }); $('sa_btn_clear_confirm')?.addEventListener('click', () => { closeAllMoreMenus(); if (window.confirm('Очистить весь чат Assistent?')) { clearChatHistory(); } }); $('sa_btn_clear_patches')?.addEventListener('click', () => { closeAllMoreMenus(); clearPatchBlocksOnly(); }); $('sa_btn_card_to_chat')?.addEventListener('click', () => { if (state.cardsSelection) { sendCardToChat(state.cardsSelection); } else { setCardStatus('Сначала выбери модель в списке'); } }); document.addEventListener('click', () => { if (state.chatsPanelOpen) { setChatsPanelOpen(false); } closeAllMoreMenus(); }); $('sa_board_more_menu')?.addEventListener('click', (e) => e.stopPropagation()); $('sa_clear_more_menu')?.addEventListener('click', (e) => e.stopPropagation()); $('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(); syncModeBadge(); }); $('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); syncLiveParamsBar(); setInterval(syncLiveParamsBar, 1200); syncModeBadge(); syncBuildGenButton(); setInterval(updateGate, 2000); setInterval(syncGenerateSlot, 700); setInterval(() => { if (!state.busy && !state.generating) { probeOllamaHealth(); } }, 45000); setInterval(() => { if (!state.busy && !state.generating) { refreshWantedQueue(); } }, 120000); window.addEventListener('beforeunload', () => { try { saveActiveChatToStore({ dropEmpty: true }); const chat = findChat(state.activeChatId); if (chat && (chat.messages || []).length) { diskPersist()?.saveChat(chat, { immediate: true }); } diskPersist()?.saveUiState(collectUiState(), { immediate: true }); } catch (e) { /* ignore */ } }); 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(); } })();