/** * Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API). * v0.7.2: Slim Exact in prompt; no Exact blob in live ctx; tighter preview filter; critique requires vision+JSON. */ (function () { const LS_BASE = 'swarm_assistent_base_url'; const LS_MODEL = 'swarm_assistent_model'; const LS_EMBED = 'swarm_assistent_embed_model'; const LS_PACK = 'swarm_assistent_pack'; const LS_PERSONA = 'swarm_assistent_persona'; const LS_VIEW = 'swarm_assistent_view'; const LS_AUTO_VISION = 'swarm_assistent_auto_vision'; const LS_AUTO_APPLY = 'swarm_assistent_auto_apply'; const LS_AUTO_GENERATE = 'swarm_assistent_auto_generate'; const LS_AUTO_CRITIQUE = 'swarm_assistent_auto_critique'; const LS_AUTO_DOWNLOAD = 'swarm_assistent_auto_download'; const LS_PANE_WIDTH = 'swarm_assistent_pane_width'; const LS_WELCOMED = 'swarm_assistent_welcomed'; const LS_TASTE = 'swarm_assistent_taste'; const LS_HISTORY = 'swarm_assistent_history'; const LS_BOARD_TAB = 'swarm_assistent_board_tab'; const TAB_BUTTON_ID = 'maintab_assistent'; const GEN_ID = 'generate'; const MAX_REF_SLOTS = 4; const CONTEXT_PROMPT_MAX = 2000; let ASPECT_TABLE = { '1:1': [1024, 1024], '4:3': [1184, 896], '3:2': [1248, 832], '16:9': [1376, 768], '2.35:1': [1568, 672], '4:5': [928, 1152], '2:3': [832, 1248], '9:16': [768, 1376], }; let PACK_ALIASES = { write: 'write_prompt', write_prompt: 'write_prompt', critique: 'critique_image', critique_image: 'critique_image', compose: 'compose_scene', compose_scene: 'compose_scene', params: 'fix_params', fix_params: 'fix_params', inpaint: 'inpaint_edit', inpaint_edit: 'inpaint_edit', describe: 'describe_ref', describe_ref: 'describe_ref', card: 'catalog_card', catalog: 'catalog_card', catalog_card: 'catalog_card', }; let WELCOME_HTML = `
Assistent · Krea 2
Напиши, что сгенерировать — или кинь референс и попроси правку.`; let HELP_TEXT = `Slash-команды (без LLM): /help — этот список /gen — Generate сейчас /look generate|refN — прикрепить окно к vision /init /mask /clear — Init / Mask / Clear Init /interrupt — остановить генерацию /aspect 16:9 — размер из таблицы 1K /seed lock|random — зафиксировать или рандомизировать seed /vary — новый seed, тот же промпт /pack write|critique|compose|params|inpaint|describe|card /civitai — поиск LoRA (Confirm в чате) /inventory — rescan моделей + обновить список LoRA Чипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.`; let SLASH_COMMANDS = [ { cmd: '/help', hint: 'список команд' }, { cmd: '/gen', hint: 'Generate сейчас' }, { cmd: '/look ', hint: 'generate|refN' }, { cmd: '/init', hint: 'как Init' }, { cmd: '/mask', hint: 'как Mask' }, { cmd: '/clear', hint: 'сброс Init/Mask' }, { cmd: '/interrupt', hint: 'стоп' }, { cmd: '/aspect ', hint: '16:9' }, { cmd: '/seed ', hint: 'lock|random' }, { cmd: '/vary', hint: 'новый seed' }, { cmd: '/pack ', hint: 'write|critique|…' }, { cmd: '/civitai ', hint: 'запрос LoRA' }, { cmd: '/inventory', hint: 'rescan моделей' }, ]; const state = { history: [], packsLoaded: false, config: null, exact: null, sessionExact: {}, lastUserParamIntent: false, enabledSkills: [], kreaProfiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 }, raw: { steps: 28, cfg: 4.5 } }, preferredEmbed: null, busy: false, generating: false, chatEpoch: 0, waitImageTimer: null, lastImageDataUrl: null, preferredModel: null, dragDepth: 0, inventory: { loras: [], checkpoints: [], wildcards: [], has_civitai_key: false }, inventoryFetchedAt: 0, taste: { styles: [], likes: [], avoid: [], notes: '', updated: 0 }, tasteSaveTimer: null, streamEl: null, streamMeta: null, critiqueHopUsed: false, visionHopUsed: false, busyPhase: 'idle', busyStarted: 0, gotDelta: false, busyTimer: null, slots: [], selectedSlotId: 'ref1', refSeq: 1, packUserTouched: false, view: 'chat', boardTab: 'generate', personas: [], modelCards: {}, cardsSelection: null, cardsBusy: false, pendingPersonaNote: null, slashIndex: 0, }; function $(id) { return document.getElementById(id); } function modelShort(name) { const s = String(name || ''); const slash = s.lastIndexOf('/'); return (slash >= 0 ? s.slice(slash + 1) : s) || 'model'; } function fmtElapsed(ms) { const s = Math.max(0, Math.floor(ms / 1000)); if (s < 60) { return `${s}s`; } return `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`; } function hideChatEmpty() { const empty = $('sa_chat_empty'); if (empty) { empty.hidden = true; } } function showChatEmptyIfIdle() { const box = $('sa_messages'); const empty = $('sa_chat_empty'); if (!box || !empty) { return; } const hasMsg = [...box.children].some((el) => el.id !== 'sa_chat_empty'); empty.hidden = hasMsg; } function setBusyPhase(phase) { state.busyPhase = phase || 'thinking'; tickBusyUi(); syncPatchActionAvailability(); syncGenerateBusy(); } function tickBusyUi() { if (state.busyPhase === 'idle') { return; } const elapsed = Date.now() - (state.busyStarted || Date.now()); if (!state.gotDelta && (state.busyPhase === 'thinking' || state.busyPhase === 'waiting') && elapsed > 1600) { state.busyPhase = 'loading'; } const model = modelShort($('sa_model')?.value); const labels = { encoding: 'Encoding image…', waiting: 'Waiting for Ollama…', loading: `Loading ${model} into GPU… first load can take a minute`, thinking: 'Thinking…', streaming: 'Writing…', generating: 'Generating image…', applying: 'Applying patch…', refining: 'Civitai search done — refining…', }; const text = labels[state.busyPhase] || 'Working…'; const barText = $('sa_livebar_text'); if (barText) { barText.textContent = text; } const elapsedEl = $('sa_elapsed'); if (elapsedEl) { elapsedEl.textContent = fmtElapsed(elapsed); } const status = $('sa_status'); if (status) { status.textContent = text; status.classList.add('sa-status-busy'); } } function startBusyUi(phase) { state.busyStarted = Date.now(); state.gotDelta = false; state.busyPhase = phase || 'thinking'; $('swarm_assistent_root')?.classList.add('sa-is-busy'); $('sa_composer')?.classList.add('sa-composer-busy'); const send = $('sa_btn_send'); if (send) { send.disabled = true; } const input = $('sa_input'); if (input) { input.classList.add('sa-input-busy'); } const bar = $('sa_livebar'); if (bar) { bar.hidden = false; } const dot = $('sa_live_dot'); if (dot) { dot.hidden = false; } tickBusyUi(); syncPatchActionAvailability(); syncGenerateBusy(); if (state.busyTimer) { clearInterval(state.busyTimer); } state.busyTimer = setInterval(tickBusyUi, 400); } function stopBusyUi(finalStatus) { if (state.busyTimer) { clearInterval(state.busyTimer); state.busyTimer = null; } const elapsed = Date.now() - (state.busyStarted || Date.now()); state.busyPhase = 'idle'; $('swarm_assistent_root')?.classList.remove('sa-is-busy'); $('sa_composer')?.classList.remove('sa-composer-busy'); const send = $('sa_btn_send'); if (send) { send.disabled = false; } const input = $('sa_input'); if (input) { input.classList.remove('sa-input-busy'); } const bar = $('sa_livebar'); if (bar) { bar.hidden = true; } const dot = $('sa_live_dot'); if (dot) { dot.hidden = true; } const status = $('sa_status'); if (status) { status.classList.remove('sa-status-busy'); } if (finalStatus != null) { const suffix = elapsed >= 1000 ? ` · ${fmtElapsed(elapsed)}` : ''; setStatus(finalStatus + suffix); } syncPatchActionAvailability(); syncGenerateBusy(); } function setStatus(text) { const el = $('sa_status'); if (el) { el.textContent = text || ''; } } function setInterruptVisible(on) { const btn = $('sa_btn_interrupt'); if (btn) { btn.hidden = !on; btn.classList.toggle('sa-interrupt-active', !!on); } } function looksLikeKrea(text) { const s = String(text || ''); return /krea\s*2|krea2|krea-2/i.test(s) || /krea/i.test(s); } function resolveCurrentCheckpoint() { const out = { name: null, architecture: null, compat_class: null, title: null, class: null, source: null, }; try { if (typeof currentModelHelper !== 'undefined' && currentModelHelper) { out.name = currentModelHelper.curModel || null; out.architecture = currentModelHelper.curArch || null; out.compat_class = currentModelHelper.curCompatClass || null; out.source = 'currentModelHelper'; } } catch (e) { /* ignore */ } try { if (typeof getCurrentModel === 'function') { const model = getCurrentModel(); if (model) { out.name = out.name || model.name || null; out.title = model.title || null; out.architecture = out.architecture || model.architecture || null; out.class = model.class || null; out.compat_class = out.compat_class || model.compat_class || null; out.source = out.source || 'getCurrentModel'; } } } catch (e) { /* ignore */ } try { const sel = document.getElementById('current_model') || document.getElementById('input_model'); if (sel) { const opt = sel.selectedOptions && sel.selectedOptions[0]; const hint = [ sel.value, opt && opt.text, opt && opt.dataset && opt.dataset.cleanname, ] .filter(Boolean) .join(' '); if (!out.name && sel.value) { out.name = sel.value; out.source = out.source || 'dropdown'; } if (hint && !out.architecture) { out.title = out.title || hint; } } } catch (e) { /* ignore */ } return out; } function isKreaSelected() { try { const m = resolveCurrentCheckpoint(); const blob = [ m.architecture, m.compat_class, m.title, m.name, m.class, ].join(' '); return looksLikeKrea(blob); } catch (e) { return false; } } function updateGate() { const ok = isKreaSelected(); const gate = $('sa_gate'); const layout = $('sa_layout'); if (gate) { gate.hidden = ok; if (!ok) { const m = resolveCurrentCheckpoint(); const seen = [m.architecture, m.compat_class, m.name] .filter(Boolean) .join(' · '); const p = gate.querySelector('p'); if (p) { p.innerHTML = seen ? `Swarm Assistent is for Krea 2 models only. Current: ${escapeHtml( seen, )} — pick a checkpoint with architecture krea-2.` : 'Swarm Assistent is for Krea 2 models only. Select a Krea 2 checkpoint on Generate to enable the chat.'; } } } if (layout) { layout.classList.toggle('sa-disabled', !ok); } return ok; } function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function val(id) { const el = document.getElementById(id); return el ? el.value : ''; } function setVal(id, value) { const el = document.getElementById(id); if (!el) { return; } el.value = value; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } function isEmptyParamField(raw, { treatZeroEmpty = false } = {}) { if (raw == null) { return true; } const s = String(raw).trim(); if (s === '') { return true; } if (treatZeroEmpty && (s === '0' || Number(s) === 0)) { return true; } return false; } function userTextMentionsParams(text) { return /\b(steps?|cfg|seed|sigma|размер|aspect|ширин|высот|resolution|batch|турбо|turbo|raw)\b/i.test(String(text || '')); } function applyAspectTableFrom(obj) { if (!obj || typeof obj !== 'object') { return false; } const next = {}; for (const [k, v] of Object.entries(obj)) { if (Array.isArray(v) && v.length >= 2) { next[k] = [Number(v[0]), Number(v[1])]; } } if (!Object.keys(next).length) { return false; } ASPECT_TABLE = next; return true; } function resolveExactBundle() { const exact = state.exact || state.config?.exact || {}; const profiles = exact.profiles || state.kreaProfiles || {}; return { exact, profiles }; } function detectKreaProfileName() { try { const model = resolveCurrentCheckpoint(); const blob = `${model?.name || ''} ${model?.title || ''}`.toLowerCase(); const hasRaw = /\braw\b/.test(blob); const hasTurbo = /\bturbo\b/.test(blob); return hasRaw && !hasTurbo ? 'raw' : 'turbo'; } catch (e) { return (state.exact?.generation?.profile) || 'turbo'; } } function mergedGenerationDefaults(profileName) { const { exact, profiles } = resolveExactBundle(); const gen = exact.generation && typeof exact.generation === 'object' ? { ...exact.generation } : {}; const profile = profileName || gen.profile || detectKreaProfileName(); const fromProfile = profiles[profile] && typeof profiles[profile] === 'object' ? { ...profiles[profile] } : {}; const session = state.sessionExact && typeof state.sessionExact === 'object' ? { ...state.sessionExact } : {}; // Profile (turbo/raw) overrides generation defaults; session overrides both. return { ...gen, ...fromProfile, profile, ...session }; } function exactDefaultFor(key, profileName) { const { exact, profiles } = resolveExactBundle(); const profile = profileName || exact.generation?.profile || detectKreaProfileName(); const fromProfile = profiles[profile]?.[key]; if (fromProfile != null) { return fromProfile; } return exact.generation?.[key]; } function rememberSessionExact(partial) { if (!partial || typeof partial !== 'object') { return; } const keys = ['steps', 'cfg', 'sigma_shift', 'aspect', 'width', 'height', 'images', 'batch', 'seed', 'sampler', 'scheduler']; for (const k of keys) { if (partial[k] != null) { state.sessionExact[k] = partial[k]; } } if (partial.images == null && partial.batch != null) { state.sessionExact.images = partial.batch; } } function fillEmptyParamsFromExact() { const defaults = mergedGenerationDefaults(); if (isEmptyParamField(val('input_steps'), { treatZeroEmpty: true }) && defaults.steps != null) { setVal('input_steps', String(defaults.steps)); } const cfgRaw = val('input_cfgscale') || val('input_cfg'); if (isEmptyParamField(cfgRaw, { treatZeroEmpty: true }) && defaults.cfg != null) { if (document.getElementById('input_cfgscale')) { setVal('input_cfgscale', String(defaults.cfg)); } else if (document.getElementById('input_cfg')) { setVal('input_cfg', String(defaults.cfg)); } } if (isEmptyParamField(val('input_sigmashift')) && defaults.sigma_shift != null) { setVal('input_sigmashift', String(defaults.sigma_shift)); } const wEmpty = isEmptyParamField(val('input_width'), { treatZeroEmpty: true }); const hEmpty = isEmptyParamField(val('input_height'), { treatZeroEmpty: true }); if ((wEmpty || hEmpty) && defaults.aspect) { const size = sizeFromAspect(defaults.aspect); if (size) { if (wEmpty) { setVal('input_width', String(size[0])); } if (hEmpty) { setVal('input_height', String(size[1])); } } } else { if (wEmpty && defaults.width != null) { setVal('input_width', String(defaults.width)); } if (hEmpty && defaults.height != null) { setVal('input_height', String(defaults.height)); } } const batchId = document.getElementById('input_images') ? 'input_images' : (document.getElementById('input_batchsize') ? 'input_batchsize' : null); if (batchId && isEmptyParamField(val(batchId), { treatZeroEmpty: true })) { const batch = defaults.images != null ? defaults.images : defaults.batch; if (batch != null) { setVal(batchId, String(batch)); } } } function shouldSkipSessionRollback(key, patchValue) { if (state.lastUserParamIntent) { return false; } if (state.sessionExact[key] == null) { return false; } const sessionVal = state.sessionExact[key]; if (String(sessionVal) === String(patchValue)) { return false; } const exactVal = exactDefaultFor(key); if (exactVal == null) { return false; } // Model trying to restore file exact while session override differs — keep session. return String(patchValue) === String(exactVal); } function openAssistentTab() { const tab = document.getElementById(TAB_BUTTON_ID); if (tab) { tab.click(); return true; } const pane = document.getElementById('assistent'); if (pane && typeof bootstrap !== 'undefined' && bootstrap.Tab) { try { bootstrap.Tab.getOrCreateInstance(tab || pane).show(); } catch (e) { /* ignore */ } } return !!tab; } function flashImagePane(slotId) { const el = document.querySelector(`.sa-slot[data-id="${slotId || state.selectedSlotId}"]`); if (!el) { return; } el.classList.remove('sa-flash'); void el.offsetWidth; el.classList.add('sa-flash'); } function ensureBoard() { if (state.slots.length) { return; } state.slots = [ { id: GEN_ID, type: 'generate', label: 'Generate', src: null, attach: false }, { id: 'ref1', type: 'ref', label: 'Ref 1', src: null, attach: true }, ]; state.refSeq = 1; state.selectedSlotId = 'ref1'; } function slotById(id) { ensureBoard(); const key = normalizeSlotId(id); return state.slots.find((s) => s.id === key) || null; } function generateSlot() { return slotById(GEN_ID); } function refSlots() { ensureBoard(); return state.slots.filter((s) => s.type === 'ref'); } function normalizeSlotId(id) { const raw = String(id || '').trim().toLowerCase(); if (!raw) { return ''; } if (raw === 'gen' || raw === 'current' || raw === 'live' || raw === 'generation') { return GEN_ID; } if (raw === 'selected' || raw === 'sel') { return state.selectedSlotId; } const m = raw.match(/^ref\s*[_-]?\s*(\d+)$/); if (m) { return `ref${m[1]}`; } return raw; } function selectedSlot() { return slotById(state.selectedSlotId) || generateSlot(); } function selectedSrc() { return selectedSlot()?.src || null; } function syncLastImageAlias() { const attached = attachableSlots(); state.lastImageDataUrl = (attached[0] || selectedSlot() || generateSlot())?.src || null; } function attachableSlots() { ensureBoard(); return state.slots.filter((s) => s.attach && s.src); } function setSlotSrc(id, src, { select = true, attach = null, note = null, switchTab = false, 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 mountPatchActions(parent, patch) { if (!parent || !patch) { return; } retireStalePatchActions(); const wrap = parent.classList.contains('sa-patch') ? parent : null; const host = wrap || parent; const actions = document.createElement('div'); actions.className = 'sa-patch-actions sa-patch-current'; for (const [label, which] of [ ['Применить всё', 'all'], ['Промпт', 'prompt'], ['LoRAs', 'loras'], ['Параметры', 'params'], ]) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'basic-button'; btn.textContent = label; btn.addEventListener('click', () => applyPatch(patch, which)); actions.appendChild(btn); } const genBtn = document.createElement('button'); genBtn.type = 'button'; genBtn.className = 'basic-button sa-btn-gen'; genBtn.textContent = 'Применить + Generate'; genBtn.addEventListener('click', async () => { if (isGenerateUnavailable()) { return; } await applyPatch(patch, 'all'); await runGenerateFromPatch({ ...patch, actions: ['generate'] }, { force: true }); }); actions.appendChild(genBtn); host.appendChild(actions); syncPatchActionAvailability(); } function renderBoard() { const board = $('sa_board'); if (!board) { return; } ensureBoard(); const tab = state.boardTab === 'refs' ? 'refs' : 'generate'; const refs = refSlots(); board.classList.toggle('sa-board-many', tab === 'refs' && (refs.some((s) => s.src) || refs.length > 1)); board.classList.toggle('sa-board-gen-only', tab === 'generate'); board.innerHTML = ''; const toShow = tab === 'generate' ? state.slots.filter((s) => s.type === 'generate') : state.slots.filter((s) => s.type !== 'generate'); for (const slot of toShow) { board.appendChild(buildSlotEl(slot)); } if (tab === 'refs' && refs.length < MAX_REF_SLOTS) { const add = document.createElement('div'); add.className = 'sa-add-cell'; add.textContent = '+ Ref'; add.title = 'Добавить окно референса'; add.addEventListener('click', (e) => { e.stopPropagation(); addRefSlot({ select: true }); }); add.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); }); add.addEventListener('drop', async (e) => { e.preventDefault(); e.stopPropagation(); const created = addRefSlot({ select: true }); if (created) { state.selectedSlotId = created.id; await handleDropDataTransfer(e.dataTransfer, created.id); } }); board.appendChild(add); } syncBoardChrome(); syncGenerateBusy(); syncLastImageAlias(); } function syncBoardChrome() { const tab = state.boardTab === 'refs' ? 'refs' : 'generate'; $('sa_board_tab_gen')?.classList.toggle('sa-board-tab-active', tab === 'generate'); $('sa_board_tab_refs')?.classList.toggle('sa-board-tab-active', tab === 'refs'); $('sa_board_tab_gen')?.setAttribute('aria-selected', tab === 'generate' ? 'true' : 'false'); $('sa_board_tab_refs')?.setAttribute('aria-selected', tab === 'refs' ? 'true' : 'false'); const addBtn = $('sa_btn_add_ref'); if (addBtn) { addBtn.hidden = tab !== 'refs'; } const maskBtn = $('sa_btn_as_mask'); const clearSlotBtn = $('sa_btn_clear_image'); if (maskBtn) { maskBtn.hidden = tab !== 'refs'; } if (clearSlotBtn) { clearSlotBtn.hidden = tab !== 'refs'; } const badge = $('sa_refs_badge'); if (badge) { const refs = refSlots(); const withImg = refs.filter((s) => s.src).length; const withVision = refs.filter((s) => s.src && s.attach).length; if (withImg || withVision) { badge.hidden = false; badge.textContent = withVision ? `${withImg} · vision ${withVision}` : String(withImg); } else { badge.hidden = true; } } } function setBoardTab(tab, { persist = true } = {}) { state.boardTab = tab === 'refs' ? 'refs' : 'generate'; if (persist) { try { localStorage.setItem(LS_BOARD_TAB, state.boardTab); } catch (e) { /* ignore */ } } renderBoard(); } function buildSlotEl(slot) { const el = document.createElement('div'); el.className = `sa-slot${slot.type === 'generate' ? ' sa-slot-gen' : ''}`; el.dataset.id = slot.id; if (slot.src) { el.classList.add('sa-has-image'); } if (slot.id === state.selectedSlotId) { el.classList.add('sa-selected'); } const bar = document.createElement('div'); bar.className = 'sa-slot-bar'; const chip = document.createElement('span'); chip.className = `sa-slot-chip${slot.type === 'generate' ? ' sa-live' : ''}`; chip.textContent = slot.type === 'generate' ? 'Generate' : slot.label; bar.appendChild(chip); const attachLab = document.createElement('label'); attachLab.className = 'sa-slot-attach'; attachLab.title = 'Attach this window to the next chat (vision)'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = !!slot.attach; cb.addEventListener('click', (e) => e.stopPropagation()); cb.addEventListener('change', (e) => { e.stopPropagation(); slot.attach = cb.checked; syncLastImageAlias(); }); attachLab.appendChild(cb); attachLab.appendChild(document.createTextNode(' vision')); bar.appendChild(attachLab); el.appendChild(bar); if (slot.src) { const img = document.createElement('img'); img.alt = slot.label; img.src = slot.src; el.appendChild(img); } else { const empty = document.createElement('div'); empty.className = 'sa-image-empty'; empty.innerHTML = slot.type === 'generate' ? '
Generate
Живой просмотр текущей генерации
' : '
Reference
Drop · paste · Снимок gen
'; el.appendChild(empty); } const busy = document.createElement('div'); busy.className = 'sa-slot-busy'; busy.hidden = !(slot.type === 'generate' && (state.generating || state.busyPhase === 'generating')); busy.innerHTML = ''; el.appendChild(busy); el.addEventListener('click', () => { state.selectedSlotId = slot.id; renderBoard(); }); el.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); el.classList.add('sa-dragover'); if (e.dataTransfer) { e.dataTransfer.dropEffect = 'copy'; } }); el.addEventListener('dragleave', () => el.classList.remove('sa-dragover')); el.addEventListener('drop', async (e) => { e.preventDefault(); e.stopPropagation(); el.classList.remove('sa-dragover'); const targetId = slot.type === 'generate' ? null : slot.id; if (slot.type === 'generate') { const created = addRefSlot({ select: true }); await handleDropDataTransfer(e.dataTransfer, created?.id); setBoardTab('refs'); } else { await handleDropDataTransfer(e.dataTransfer, targetId); } }); return el; } function syncGenerateSlot() { const 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 persistHistory() { try { const slim = (state.history || []) .filter((m) => m && (m.role === 'user' || m.role === 'assistant') && !m.systemish) .slice(-24) .map((m) => ({ role: m.role, content: String(m.content || '').slice(0, 4000), persona: m.persona || undefined, pack: m.pack || undefined, })); localStorage.setItem(LS_HISTORY, JSON.stringify(slim)); } catch (e) { /* ignore */ } } function restoreHistory() { try { const raw = localStorage.getItem(LS_HISTORY); if (!raw) { return; } const parsed = JSON.parse(raw); if (!Array.isArray(parsed) || !parsed.length) { return; } state.history = parsed .filter((m) => m && (m.role === 'user' || m.role === 'assistant') && m.content) .slice(-24) .map((m) => ({ role: m.role, content: String(m.content), persona: m.persona, pack: m.pack, })); const box = $('sa_messages'); if (!box || !state.history.length) { return; } hideChatEmpty(); for (const m of state.history) { if (m.role === 'user') { appendMessage('user', m.content); } else { appendMessage('assistant', m.content, null, null, { persona: m.persona ? { id: m.persona, title: m.persona } : null, pack: m.pack, }); } } } catch (e) { /* ignore */ } } function clearPersistedHistory() { try { localStorage.removeItem(LS_HISTORY); } catch (e) { /* ignore */ } } function hideSlashMenu() { const menu = $('sa_slash_menu'); if (menu) { menu.hidden = true; menu.innerHTML = ''; } state.slashIndex = 0; } function slashMatches(text) { const t = String(text || ''); if (!t.startsWith('/')) { return []; } const q = t.toLowerCase(); return SLASH_COMMANDS.filter((c) => c.cmd.toLowerCase().startsWith(q) || q === '/' || c.cmd.toLowerCase().includes(q.slice(1))); } function renderSlashMenu(items) { const menu = $('sa_slash_menu'); if (!menu) { return; } if (!items.length) { hideSlashMenu(); return; } menu.hidden = false; menu.innerHTML = ''; state.slashIndex = Math.max(0, Math.min(state.slashIndex, items.length - 1)); items.forEach((item, i) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'sa-slash-item' + (i === state.slashIndex ? ' sa-slash-active' : ''); btn.setAttribute('role', 'option'); btn.innerHTML = `${escapeHtml(item.cmd.trim())} — ${escapeHtml(item.hint)}`; btn.addEventListener('mousedown', (e) => { e.preventDefault(); applySlashPick(item); }); menu.appendChild(btn); }); } function applySlashPick(item) { const input = $('sa_input'); if (!input || !item) { return; } input.value = item.cmd; hideSlashMenu(); input.focus(); const pos = input.value.length; input.setSelectionRange(pos, pos); } function updateSlashMenuFromInput() { const text = $('sa_input')?.value || ''; if (!text.startsWith('/') || text.includes('\n') || /\s/.test(text.trim().slice(1)) && !text.endsWith(' ')) { // show while typing command token only const token = text.split(/\s/)[0] || ''; if (!token.startsWith('/') || (text.includes(' ') && !SLASH_COMMANDS.some((c) => c.cmd.startsWith(token)))) { if (!(token.startsWith('/') && !text.includes(' '))) { hideSlashMenu(); return; } } } const token = (text.split(/\s/)[0] || ''); if (!token.startsWith('/') || text.indexOf(' ') > 0) { hideSlashMenu(); return; } renderSlashMenu(slashMatches(token)); } function onPersonaChanged() { const id = $('sa_persona')?.value || 'neutral'; state.sessionExact = {}; state.lastUserParamIntent = false; saveSettings(); loadConfig(id, (data) => { const title = data?.personas?.find((p) => p.id === id)?.title || (state.personas || []).find((p) => p.id === id)?.title || id; if (data?.personas) { state.personas = data.personas; } appendSystemNote(`Тон → ${title}`); state.pendingPersonaNote = `Persona is now ${id} (${title}). Adopt this voice from now on.`; if (data?.assistant?.default_pack && $('sa_pack') && !state.packUserTouched) { const packId = data.assistant.default_pack; if ([...($('sa_pack').options || [])].some((o) => o.value === packId)) { $('sa_pack').value = packId; } } fillEmptyParamsFromExact(); }); } function countPromptImages() { try { const box = document.getElementById('alt_prompt_textbox') || document.getElementById('input_prompt'); if (!box) { return 0; } const text = box.value || ''; const matches = text.match(/)/gi) || text.match(/data:image\//gi); return matches ? matches.length : 0; } catch (e) { return 0; } } function triggerChangeForEl(el) { if (!el) { return; } if (typeof triggerChangeFor === 'function') { triggerChangeFor(el); return; } el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } function openInitImageGroup() { try { const initEl = document.getElementById('input_initimage'); if (initEl && typeof toggleGroupOpen === 'function') { toggleGroupOpen(initEl, true); } } catch (e) { /* ignore */ } const toggler = document.getElementById('input_group_content_initimage_toggle'); if (toggler) { toggler.checked = true; triggerChangeForEl(toggler); } } function hasFileParam(id) { const el = document.getElementById(id); return !!(el && el.files && el.files.length > 0); } function clearFileParam(id) { const el = document.getElementById(id); if (!el) { return false; } try { el.value = ''; if (el.files && typeof DataTransfer !== 'undefined') { el.files = new DataTransfer().files; } } catch (e) { /* ignore */ } triggerChangeForEl(el); return true; } function guessImageMime(src) { const s = String(src || ''); if (s.startsWith('data:image/')) { const m = s.match(/^data:(image\/[a-z0-9.+-]+)/i); return (m && m[1]) || 'image/png'; } const path = s.split('?')[0]; const ext = path.substring(path.lastIndexOf('.') + 1).toLowerCase(); if (ext === 'jpg' || ext === 'jpeg') { return 'image/jpeg'; } if (ext === 'webp') { return 'image/webp'; } if (ext === 'gif') { return 'image/gif'; } return 'image/png'; } async function srcToBlob(src) { if (!src) { return null; } const cleaned = String(src).trim().split(/\s+/)[0]; if (cleaned.startsWith('data:') || cleaned.startsWith('/') || cleaned.startsWith('View/') || cleaned.startsWith('http')) { try { const resp = await fetch(cleaned); return await resp.blob(); } catch (e) { console.warn('Assistent: fetch blob failed', e); } } return await new Promise((resolve) => { const tmpImg = new Image(); tmpImg.crossOrigin = 'Anonymous'; tmpImg.onload = () => { try { const canvas = document.createElement('canvas'); canvas.width = tmpImg.naturalWidth; canvas.height = tmpImg.naturalHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(tmpImg, 0, 0); canvas.toBlob((blob) => resolve(blob), 'image/png'); } catch (e) { resolve(null); } }; tmpImg.onerror = () => resolve(null); tmpImg.src = cleaned; }); } async function setFileParamFromSrc(paramId, src, { filename = 'assistent.png' } = {}) { const el = document.getElementById(paramId); if (!el) { setStatus(`Missing ${paramId} on Generate tab`); return false; } const blob = await srcToBlob(src); if (!blob) { setStatus('Could not load image for init/mask'); return false; } const mime = blob.type || guessImageMime(src); const file = new File([blob], filename, { type: mime }); const container = new DataTransfer(); container.items.add(file); el.files = container.files; triggerChangeForEl(el); openInitImageGroup(); return true; } async function setInitFromSrc(src) { const ok = await setFileParamFromSrc('input_initimage', src, { filename: 'assistent_init.png' }); if (ok) { setStatus('Init Image set'); } return ok; } async function setMaskFromSrc(src) { const ok = await setFileParamFromSrc('input_maskimage', src, { filename: 'assistent_mask.png' }); if (ok) { setStatus('Mask Image set (white = edit)'); } return ok; } function clearInitAndMask() { clearFileParam('input_initimage'); clearFileParam('input_maskimage'); const toggler = document.getElementById('input_group_content_initimage_toggle'); if (toggler) { toggler.checked = false; triggerChangeForEl(toggler); } setStatus('Init Image + Mask cleared'); } function readInitContext() { const creativityRaw = val('input_initimagecreativity'); const creativity = creativityRaw === '' ? null : parseFloat(creativityRaw); return { has_init_image: hasFileParam('input_initimage'), has_mask_image: hasFileParam('input_maskimage'), init_creativity: Number.isFinite(creativity) ? creativity : null, mask_blur: parseFloat(val('input_maskblur') || '') || null, mask_grow: parseInt(val('input_maskgrow') || val('input_maskshrinkgrow') || '', 10) || null, init_group_on: !!(document.getElementById('input_group_content_initimage_toggle')?.checked), }; } function slimPromptForContext(raw) { let s = String(raw || ''); s = s.replace(/data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=]+/g, '[image omitted]'); s = s.replace(/]*>[\s\S]*?<\/image>/gi, '[image omitted]'); s = s.replace(/]*>/gi, '[image omitted]'); if (s.length > CONTEXT_PROMPT_MAX) { s = s.slice(0, CONTEXT_PROMPT_MAX) + '…'; } return s; } function collectLiveContext() { const inv = state.inventory || {}; const initCtx = readInitContext(); const ctx = { architecture_ok: isKreaSelected(), checkpoint: null, prompt: slimPromptForContext(val('alt_prompt_textbox') || val('input_prompt') || ''), negative: slimPromptForContext(val('input_negativeprompt') || val('alt_negativeprompt_textbox') || ''), width: parseInt(val('input_width') || '0', 10) || null, height: parseInt(val('input_height') || '0', 10) || null, steps: parseInt(val('input_steps') || '0', 10) || null, cfg: parseFloat(val('input_cfgscale') || val('input_cfg') || '') || null, sigma_shift: parseFloat(val('input_sigmashift') || '') || null, seed: val('input_seed') || null, sampler: val('input_sampler') || val('input_samplerate') || null, scheduler: val('input_scheduler') || null, batch: parseInt(val('input_images') || val('input_batchsize') || '0', 10) || null, prompt_image_count: countPromptImages(), selected_loras: [], available_loras: slimInventoryLoras(inv.loras || [], 100), available_checkpoints: slimInventoryCheckpoints(inv.checkpoints || [], 40), wildcards: (inv.wildcards || []).map((w) => w.name || w).slice(0, 60), inventory_at: inv.inventory_at || null, has_vision_image: attachableSlots().length > 0, image_slots: slotCatalog(), attached_slot_ids: attachableSlots().map((s) => s.id), has_civitai_key: !!inv.has_civitai_key, auto_apply: !!$('sa_auto_apply')?.checked, auto_generate: !!$('sa_auto_generate')?.checked, persona: $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral', model_cards: [], taste_profile: summarizeTaste(), ...initCtx, }; try { const model = resolveCurrentCheckpoint(); if (model.name || model.architecture) { ctx.checkpoint = { name: model.name || model.title || null, architecture: model.architecture || model.compat_class || model.class || null, title: model.title || null, }; } } catch (e) { /* ignore */ } try { if (typeof loraHelper !== 'undefined' && loraHelper && Array.isArray(loraHelper.selected)) { ctx.selected_loras = loraHelper.selected.map((l) => ({ name: l.name || l, weight: (loraHelper.loraWeightPref && loraHelper.loraWeightPref[l.name || l]) || 1, })); } } catch (e) { /* ignore */ } // Recommendation cards: checkpoint + selected LoRAs + other has_card entries (capped). const cardKeys = []; const seenCard = new Set(); const addKey = (kind, name) => { if (!kind || !name) { return; } const key = `${kind}:${name}`; if (seenCard.has(key)) { return; } seenCard.add(key); cardKeys.push({ kind, name }); }; if (ctx.checkpoint?.name) { addKey('checkpoint', ctx.checkpoint.name); } for (const l of ctx.selected_loras || []) { if (l?.name) { addKey('lora', l.name); } } for (const l of inv.loras || []) { if (l?.has_card && l?.name) { addKey('lora', l.name); } if (cardKeys.length >= 14) { break; } } for (const c of inv.checkpoints || []) { if (c?.has_card && c?.name) { addKey('checkpoint', c.name); } if (cardKeys.length >= 16) { break; } } for (const k of cardKeys) { const cached = state.modelCards[`${k.kind}:${k.name}`]; if (cached) { ctx.model_cards.push(slimCardForContext(cached)); } } // Fallback if inventory empty if (!ctx.available_loras.length) { try { const models = (typeof allModels !== 'undefined' && allModels) || (typeof model_list !== 'undefined' && model_list) || []; const list = Array.isArray(models) ? models : Object.values(models || {}); for (const m of list) { if (!m) { continue; } const folder = `${m.folder || m.path || ''}`; const isLora = /lora/i.test(m.category || m.type || '') || (m.name && String(m.name).toLowerCase().includes('lora')); const inLoraFolder = /lora/i.test(folder); if (!(isLora || inLoraFolder)) { continue; } ctx.available_loras.push({ name: m.name || m.title, title: m.title || m.name, trigger_phrase: m.trigger_phrase || m.trigger || (m.metadata && (m.metadata.trigger_phrase || m.metadata.trigger)) || null, architecture: m.architecture || null, }); } if (ctx.available_loras.length > 80) { ctx.available_loras = ctx.available_loras.slice(0, 80); } } catch (e) { /* ignore */ } } try { const model = ctx.checkpoint || {}; const blob = `${model.name || ''} ${model.title || ''}`.toLowerCase(); const hasRaw = /\braw\b/.test(blob); const hasTurbo = /\bturbo\b/.test(blob); const profile = hasRaw && !hasTurbo ? 'raw' : 'turbo'; ctx.krea_profile = profile; const defaults = mergedGenerationDefaults(profile); ctx.recommended_params = { steps: defaults.steps ?? 8, cfg: defaults.cfg ?? 1, sigma_shift: defaults.sigma_shift ?? 1.15, }; if (defaults.aspect) { ctx.recommended_params.aspect = defaults.aspect; } } catch (e) { ctx.krea_profile = 'turbo'; const defaults = mergedGenerationDefaults('turbo'); ctx.recommended_params = { steps: defaults.steps ?? 8, cfg: defaults.cfg ?? 1, sigma_shift: defaults.sigma_shift ?? 1.15, }; } ctx.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(); } function isPatchObject(obj) { if (!obj || typeof obj !== 'object') { return false; } return ( obj.prompt != null || obj.loras || obj.width || obj.height || obj.steps || obj.cfg || obj.seed != null || obj.sigma_shift != null || obj.sampler || obj.actions || obj.search_query || obj.civitai_query || obj.use_init_image != null || obj.clear_init_image != null || obj.init_creativity != null || obj.denoise != null || obj.use_mask_image != null || obj.clear_mask_image != null || obj.mask_blur != null || obj.mask_grow != null || obj.look_at != null || obj.vision_from != null || obj.vision_slots != null || obj.slot_to_init != null || obj.slot_to_mask != null || obj.snapshot_generate != null || obj.select_slot != null || obj.aspect != null || obj.images != null || obj.batch != null || obj.vary != null || obj.lock_seed != null || obj.creativity != null || obj.intensity != null || obj.complexity != null || obj.movement != null || obj.clear_prompt_images != null || obj.slot_to_prompt_image != null || obj.pack != null ); } function isCardObject(obj) { if (!obj || typeof obj !== 'object') { return false; } // Prefer card shape over gen patch when both could match. const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint); const genish = !!(obj.prompt != null || obj.loras || obj.actions || obj.width || obj.height || obj.steps || obj.cfg || obj.aspect || obj.seed != null); if (cardish && !genish && (obj.name || obj.triggers || obj.when)) { return true; } return !!(obj.kind && obj.name && (obj.triggers || obj.when || obj.prompt_hint || obj.notes != null)); } function extractCardJson(text) { if (!text) { return null; } const re = /```(?:json)?\s*([\s\S]*?)```/gi; let match; let last = null; while ((match = re.exec(text)) !== null) { try { const obj = JSON.parse(match[1].trim()); if (isCardObject(obj)) { last = obj; } } catch (e) { /* ignore */ } } if (last) { return last; } try { const obj = JSON.parse(text.trim()); return isCardObject(obj) ? obj : null; } catch (e) { return null; } } function extractPatch(text) { if (!text) { return { prose: text || '', patch: null }; } const re = /```(?:json)?\s*([\s\S]*?)```/gi; let match; let lastPatch = null; let prose = text; while ((match = re.exec(text)) !== null) { const raw = match[1].trim(); try { const obj = JSON.parse(raw); if (isPatchObject(obj)) { lastPatch = obj; prose = (text.slice(0, match.index) + text.slice(match.index + match[0].length)).trim(); } } catch (e) { /* not json */ } } return { prose, patch: lastPatch }; } function normalizeAspect(raw) { if (raw == null) { return null; } let s = String(raw).trim().toLowerCase().replace(/\s+/g, ''); if (!s) { return null; } if (s === 'square') { s = '1:1'; } else if (s === 'portrait' || s === 'vert') { s = '2:3'; } else if (s === 'landscape' || s === 'horiz') { s = '16:9'; } else if (s === 'cinematic' || s === 'ultrawide') { s = '2.35:1'; } return ASPECT_TABLE[s] ? s : null; } function sizeFromAspect(aspect) { const key = normalizeAspect(aspect); return key ? ASPECT_TABLE[key] : null; } function guessAspectFromSize(w, h) { const width = parseInt(w, 10); const height = parseInt(h, 10); if (!width || !height) { return null; } let best = null; let bestDist = Infinity; for (const [key, [aw, ah]] of Object.entries(ASPECT_TABLE)) { const dist = Math.abs(width / height - aw / ah) + Math.abs(width - aw) / 4000 + Math.abs(height - ah) / 4000; if (dist < bestDist) { bestDist = dist; best = key; } } return bestDist < 0.12 ? best : null; } function clearPromptImagesInBox() { const box = document.getElementById('alt_prompt_textbox') || document.getElementById('input_prompt'); if (!box) { return false; } const before = box.value || ''; const next = before .replace(/]*>[\s\S]*?<\/image>/gi, '') .replace(/]*\/?>/gi, '') .replace(/data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=]+/g, '') .replace(/\n{3,}/g, '\n\n') .trim(); if (next === before.trim()) { return false; } box.value = next; box.dispatchEvent(new Event('input', { bubbles: true })); box.dispatchEvent(new Event('change', { bubbles: true })); return true; } function setPackValue(packName, { flash, user } = {}) { const pack = $('sa_pack'); if (!pack || !packName) { return false; } const resolved = PACK_ALIASES[String(packName).trim()] || String(packName).trim(); if (![...pack.options].some((o) => o.value === resolved)) { return false; } if (pack.value !== resolved) { pack.value = resolved; saveSettings(); } if (user) { state.packUserTouched = true; } if (flash) { pack.classList.add('sa-pack-flash'); setTimeout(() => pack.classList.remove('sa-pack-flash'), 900); } return true; } function autoSelectPack(text) { if (state.packUserTouched) { return null; } const t = String(text || '').toLowerCase(); if (!t.trim()) { return null; } if (/\b(поправь|исправь|перепиши|улучши|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)'); } } syncChipHighlight(); setStatus('Applied patch'); } function triggerGenerate() { try { if (typeof mainGenHandler !== 'undefined' && mainGenHandler && typeof mainGenHandler.doGenerate === 'function') { mainGenHandler.doGenerate(); return true; } } catch (e) { console.warn('Assistent: doGenerate failed', e); } const btn = document.getElementById('generate_button') || document.getElementById('alt_generate_button') || document.querySelector('button.generate-button') || document.querySelector('#generate_button, button[id*="generate"]'); if (btn) { btn.click(); return true; } return false; } function cancelWaitForNewImage() { if (state.waitImageTimer) { clearInterval(state.waitImageTimer); state.waitImageTimer = null; } } function bumpChatEpoch() { state.chatEpoch = (state.chatEpoch || 0) + 1; return state.chatEpoch; } function doInterruptNow() { bumpChatEpoch(); cancelWaitForNewImage(); try { if (typeof doInterrupt === 'function') { doInterrupt(false); return; } } catch (e) { /* ignore */ } if (typeof genericRequest === 'function') { genericRequest('InterruptAll', { other_sessions: false }, () => {}, 0, () => {}); } } function waitForNewImage(prevSrc, timeoutMs = 180000) { cancelWaitForNewImage(); const epoch = state.chatEpoch; return new Promise((resolve) => { const start = Date.now(); state.waitImageTimer = setInterval(() => { if (epoch !== state.chatEpoch) { cancelWaitForNewImage(); resolve(null); return; } const src = findCurrentGenerateSrc(); if (src && src !== prevSrc && !looksLikeModelPreview(src)) { cancelWaitForNewImage(); resolve(src); } else if (Date.now() - start > timeoutMs) { cancelWaitForNewImage(); resolve(null); } }, 400); }); } async function runGenerateFromPatch(patch, opts = {}) { const force = !!opts.force; if ((!force && !$('sa_auto_generate')?.checked) || !patchHasGenTrigger(patch)) { return null; } const prev = findCurrentGenerateSrc(); setStatus('Генерация…'); startBusyUi('generating'); state.generating = true; setInterruptVisible(true); const ok = triggerGenerate(); if (!ok) { state.generating = false; setInterruptVisible(state.busy); setStatus('Не удалось запустить Generate'); return null; } const src = await waitForNewImage(prev); state.generating = false; setInterruptVisible(state.busy); if (!state.busy) { stopBusyUi(src ? 'Generate готов' : 'Generate завершён (новое изображение не найдено)'); } if (src) { const gen = generateSlot(); if (gen) { gen.src = src; renderBoard(); } setStatus('Generate готов'); return src; } if (state.busy) { setStatus('Generate завершён (новое изображение не найдено)'); } return null; } async function maybeAutoCritique(imageSrc) { if (!$('sa_auto_critique')?.checked || state.critiqueHopUsed || !imageSrc) { return; } state.critiqueHopUsed = true; const pack = $('sa_pack'); if (pack) { pack.value = 'critique_image'; saveSettings(); } if ($('sa_input')) { $('sa_input').value = 'Critique this result and improve the prompt for the next generation.'; } const gen = generateSlot(); if (gen) { gen.attach = true; if (imageSrc) { gen.src = imageSrc; } renderBoard(); } setStatus('Auto-critique…'); await sendChat({ fromAutoCritique: true, forceSlotIds: [GEN_ID] }); } function currentPersonaInfo() { const id = ($('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral').trim() || 'neutral'; const known = (state.personas || []).find((p) => p && p.id === id); return { id, title: (known && known.title) || ({ neutral: 'Нейтральный', lewd: 'Пошляк', aggressive: 'Агрессивный', }[id] || id), }; } function mountAssistantMeta(div, meta = {}) { if (!div || div.querySelector('.sa-msg-meta')) { return; } const persona = meta.persona || currentPersonaInfo(); const pack = meta.pack || $('sa_pack')?.value || ''; div.dataset.persona = persona.id || 'neutral'; if (pack) { div.dataset.pack = pack; } const row = document.createElement('div'); row.className = 'sa-msg-meta'; const chip = document.createElement('span'); chip.className = `sa-persona-mark sa-persona-${persona.id || 'neutral'}`; chip.textContent = persona.title || persona.id; chip.title = `Характер: ${persona.title || persona.id}${pack ? ` · режим ${pack}` : ''}`; row.appendChild(chip); if (pack && pack !== 'write_prompt') { const packEl = document.createElement('span'); packEl.className = 'sa-pack-mark'; packEl.textContent = pack.replace(/_/g, ' '); packEl.title = `Режим: ${pack}`; row.appendChild(packEl); } div.insertBefore(row, div.firstChild); } function setAssistantBody(div, text) { if (!div) { return; } let body = div.querySelector('.sa-msg-body'); if (!body) { body = document.createElement('div'); body.className = 'sa-msg-body'; div.appendChild(body); } body.textContent = text || ''; } function appendMessage(role, text, patch, civitaiResults, meta) { const box = $('sa_messages'); if (!box) { return null; } hideChatEmpty(); const div = document.createElement('div'); div.className = `sa-msg ${role}`; if (role === 'assistant') { mountAssistantMeta(div, meta); } const { prose, patch: extracted } = role === 'assistant' ? extractPatch(text) : { prose: text, patch: null }; const finalPatch = patch || extracted; if (role === 'assistant') { setAssistantBody(div, prose || text || ''); } else { div.textContent = prose || text || ''; } if (finalPatch) { const wrap = document.createElement('div'); wrap.className = 'sa-patch'; const pre = document.createElement('pre'); pre.textContent = JSON.stringify(finalPatch, null, 2); wrap.appendChild(pre); mountPatchActions(wrap, finalPatch); div.appendChild(wrap); } if (civitaiResults && civitaiResults.length) { div.appendChild(buildCivitaiCards(civitaiResults)); } box.appendChild(div); box.scrollTop = box.scrollHeight; return div; } function beginStreamMessage(meta) { const box = $('sa_messages'); if (!box) { return null; } hideChatEmpty(); const div = document.createElement('div'); div.className = 'sa-msg assistant sa-streaming sa-typing'; mountAssistantMeta(div, meta); const body = document.createElement('div'); body.className = 'sa-msg-body'; body.innerHTML = 'Waiting for the model…'; div.appendChild(body); box.appendChild(div); box.scrollTop = box.scrollHeight; state.streamEl = div; state.streamMeta = meta || null; return div; } function appendStreamDelta(delta) { if (!state.streamEl) { beginStreamMessage(state.streamMeta || undefined); } if (state.streamEl) { if (state.streamEl.classList.contains('sa-typing')) { state.streamEl.classList.remove('sa-typing'); setAssistantBody(state.streamEl, ''); } state.gotDelta = true; if (state.busyPhase !== 'refining') { setBusyPhase('streaming'); } let body = state.streamEl.querySelector('.sa-msg-body'); if (!body) { setAssistantBody(state.streamEl, ''); body = state.streamEl.querySelector('.sa-msg-body'); } body.textContent += delta; const box = $('sa_messages'); if (box) { box.scrollTop = box.scrollHeight; } } } function finalizeStreamMessage(fullReply, civitaiResults) { const el = state.streamEl; const meta = state.streamMeta; state.streamEl = null; state.streamMeta = null; if (!el) { appendMessage('assistant', fullReply, null, civitaiResults, meta || undefined); return; } el.classList.remove('sa-streaming', 'sa-typing'); mountAssistantMeta(el, meta || undefined); const card = extractCardJson(fullReply); const { prose, patch } = extractPatch(fullReply); setAssistantBody(el, prose || fullReply || ''); el.querySelectorAll('.sa-patch, .sa-civitai-list').forEach((n) => n.remove()); if (patch && !isCardObject(patch) && !(card && !patch.prompt && !patch.actions && !patch.loras)) { const wrap = document.createElement('div'); wrap.className = 'sa-patch'; const pre = document.createElement('pre'); pre.textContent = JSON.stringify(patch, null, 2); wrap.appendChild(pre); mountPatchActions(wrap, patch); el.appendChild(wrap); } else if (card) { const wrap = document.createElement('div'); wrap.className = 'sa-patch sa-card-json-preview'; const pre = document.createElement('pre'); pre.textContent = JSON.stringify(card, null, 2); wrap.appendChild(pre); el.appendChild(wrap); } if (civitaiResults && civitaiResults.length) { el.appendChild(buildCivitaiCards(civitaiResults)); } const box = $('sa_messages'); if (box) { box.scrollTop = box.scrollHeight; } } function buildCivitaiCards(results) { const list = document.createElement('div'); list.className = 'sa-civitai-list'; for (const r of results) { const card = document.createElement('div'); card.className = 'sa-civitai-card' + (r.already_installed ? ' sa-installed' : ''); const title = document.createElement('div'); title.className = 'sa-civitai-title'; title.textContent = r.name || r.file_name || 'LoRA'; card.appendChild(title); const meta = document.createElement('div'); meta.className = 'sa-civitai-meta'; const bits = [ r.base_model || '?', r.krea_likely ? 'Krea?' : null, r.already_installed ? 'already installed' : null, (r.triggers || []).slice(0, 3).join(', ') || null, ].filter(Boolean); meta.textContent = bits.join(' · '); card.appendChild(meta); const actions = document.createElement('div'); actions.className = 'sa-civitai-actions'; if (r.already_installed) { const note = document.createElement('span'); note.textContent = 'Installed'; actions.appendChild(note); } else if (r.download_url) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'basic-button sa-primary'; btn.textContent = 'Confirm download'; btn.addEventListener('click', () => downloadCivitaiLoRA(r, btn)); actions.appendChild(btn); } else { const note = document.createElement('span'); note.textContent = 'No download URL'; actions.appendChild(note); } card.appendChild(actions); list.appendChild(card); } return list; } function downloadCivitaiLoRA(card, btn) { if (!card.download_url) { return; } if (btn) { btn.disabled = true; btn.textContent = 'Downloading…'; } setStatus(`Downloading ${card.file_name || card.name}…`); setInterruptVisible(true); const payload = { url: card.download_url, type: 'LoRA', name: card.file_name || card.name || 'lora', }; const onDone = (ok, msg) => { setInterruptVisible(state.busy || state.generating); if (ok) { setStatus(`Downloaded ${payload.name}`); if (btn) { btn.textContent = 'Downloaded'; } refreshInventory(async () => { await maybeWriteCardAfterDownload({ kind: 'lora', name: payload.name, civitai: card, }); }, { rescan: true }); } else { setStatus(msg || 'Download failed'); if (btn) { btn.disabled = false; btn.textContent = 'Confirm download'; } appendMessage('error', msg || 'Download failed'); } }; if (typeof makeWSRequest === 'function') { makeWSRequest( 'DoModelDownloadWS', payload, (data) => { if (data.error) { onDone(false, String(data.error)); return; } if (data.success || data.overall_percent >= 1 || data.current_percent >= 1) { if (data.success || data.overall_percent >= 0.99) { // Swarm docs: download does not always refresh model list — force both. triggerSwarmModelRefresh(() => onDone(true)); } else if (data.current_percent != null) { setStatus(`Download ${(data.current_percent * 100).toFixed(0)}%`); } } }, 0, (err) => onDone(false, String(err || 'Download failed')), ); } else { onDone(false, 'makeWSRequest unavailable'); } } async function maybeWriteCardAfterDownload({ kind, name, civitai }) { const display = name || civitai?.file_name || civitai?.name || 'model'; appendSystemNote(`Downloaded ${display}. Writing a recommendation card…`); setPackValue('catalog_card', { flash: true }); const meta = { triggers: civitai?.triggers || [], base_model: civitai?.base_model, civitai_url: civitai?.url || civitai?.civitai_url, version_id: civitai?.version_id || civitai?.modelVersionId, name: display, }; if ($('sa_input')) { $('sa_input').value = ''; } await sendChat({ forcedUserText: `LoRA "${display}" is now installed. Write a recommendation card (JSON) using its triggers/metadata. Then briefly suggest how to enable it in the next generate.`, skipSlash: true, skipAutoPack: true, fromDownload: true, fromCards: true, cardTarget: { kind: kind || 'lora', name: display, meta }, }); } function wantsAutoVision() { return !!$('sa_auto_vision')?.checked; } function looksLikeModelPreview(src) { const s = String(src || '').toLowerCase(); if (!s) { return false; } // 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 saveSettings() { localStorage.setItem(LS_BASE, $('sa_base_url')?.value || ''); localStorage.setItem(LS_MODEL, $('sa_model')?.value || ''); localStorage.setItem(LS_EMBED, $('sa_embed_model')?.value || state.preferredEmbed || ''); localStorage.setItem(LS_PACK, $('sa_pack')?.value || 'write_prompt'); localStorage.setItem(LS_PERSONA, $('sa_persona')?.value || 'neutral'); localStorage.setItem(LS_VIEW, state.view || 'chat'); localStorage.setItem(LS_AUTO_VISION, $('sa_auto_vision')?.checked ? '1' : '0'); localStorage.setItem(LS_AUTO_APPLY, $('sa_auto_apply')?.checked ? '1' : '0'); localStorage.setItem(LS_AUTO_GENERATE, $('sa_auto_generate')?.checked ? '1' : '0'); localStorage.setItem(LS_AUTO_CRITIQUE, $('sa_auto_critique')?.checked ? '1' : '0'); localStorage.setItem(LS_AUTO_DOWNLOAD, $('sa_auto_download')?.checked ? '1' : '0'); persistServerSettings(); } function persistServerSettings() { if (typeof genericRequest !== 'function') { return; } const skills = {}; document.querySelectorAll('#sa_skills_box input[data-skill]')?.forEach((el) => { skills[el.getAttribute('data-skill')] = !!el.checked; }); const persona = $('sa_persona')?.value || 'neutral'; const settings = { embed_model: $('sa_embed_model')?.value || state.preferredEmbed || '', base_url: $('sa_base_url')?.value || '', [persona]: { skills }, }; genericRequest('AssistentSaveSettings', { settings }, () => {}, 0, () => {}); } function applyConfigPayload(data, { applyDefaults = false } = {}) { if (!data || data.error) { return; } state.config = data; if (data.exact && typeof data.exact === 'object') { state.exact = data.exact; } const aspectSource = data.exact?.aspect_table || data.model?.aspect_table; if (aspectSource && typeof aspectSource === 'object') { applyAspectTableFrom(aspectSource); } const profileSource = data.exact?.profiles || data.model?.profiles; if (profileSource && typeof profileSource === 'object') { state.kreaProfiles = profileSource; } if (data.ui?.pack_aliases) { PACK_ALIASES = { ...PACK_ALIASES, ...data.ui.pack_aliases }; } if (data.ui?.welcome_html) { WELCOME_HTML = data.ui.welcome_html; } if (data.ui?.help_text) { HELP_TEXT = data.ui.help_text; } if (Array.isArray(data.ui?.slash) && data.ui.slash.length) { SLASH_COMMANDS = data.ui.slash.map((s) => ({ cmd: s.cmd || '', hint: s.hint || '', action: s.action || '', })); } state.enabledSkills = Array.isArray(data.enabled_skills) ? data.enabled_skills.slice() : []; if (Array.isArray(data.personas)) { state.personas = data.personas; } renderPersonaOptions(data.personas || [], data.persona || data.default_persona); renderPackOptions(data.packs || [], applyDefaults ? data.assistant?.default_pack : null); renderChips(data.ui?.chips || []); renderSkillChecks(data.skills || [], state.enabledSkills); if (applyDefaults && data.assistant?.default_pack && $('sa_pack') && !localStorage.getItem(LS_PACK)) { $('sa_pack').value = data.assistant.default_pack; } if (data.assistant?.embed_model && !state.preferredEmbed) { state.preferredEmbed = data.assistant.embed_model; } if (applyDefaults || data.exact) { fillEmptyParamsFromExact(); } } function renderPersonaOptions(personas, selected) { const sel = $('sa_persona'); if (!sel) { return; } const cur = selected || sel.value || localStorage.getItem(LS_PERSONA) || 'neutral'; sel.innerHTML = ''; for (const p of personas) { const opt = document.createElement('option'); opt.value = p.id; opt.textContent = p.title || p.id; if (p.accent) { opt.dataset.accent = p.accent; } sel.appendChild(opt); } if ([...sel.options].some((o) => o.value === cur)) { sel.value = cur; } } function renderPackOptions(packs, preferred) { const sel = $('sa_pack'); if (!sel) { return; } const cur = preferred || sel.value || localStorage.getItem(LS_PACK) || 'write_prompt'; sel.innerHTML = ''; const list = (packs || []).slice().sort((a, b) => (a.order || 100) - (b.order || 100)); for (const p of list) { const opt = document.createElement('option'); opt.value = p.id; opt.textContent = p.title || p.id; sel.appendChild(opt); } if ([...sel.options].some((o) => o.value === cur)) { sel.value = cur; } } function renderChips(chips) { const box = $('sa_chips'); if (!box || !Array.isArray(chips) || !chips.length) { return; } box.innerHTML = ''; for (const c of chips) { if (c.sep) { const sep = document.createElement('span'); sep.className = 'sa-chip-sep'; sep.setAttribute('aria-hidden', 'true'); box.appendChild(sep); continue; } const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'sa-chip'; btn.textContent = c.label || c.value || ''; if (c.title) { btn.title = c.title; } const action = c.action || ''; const value = c.value ?? ''; if (action === 'aspect') { btn.setAttribute('data-aspect', value); } else if (action === 'seed') { btn.setAttribute('data-seed', value); } else if (action === 'vary') { btn.setAttribute('data-vary', value || '1'); } else if (action === 'krea_profile') { btn.setAttribute('data-krea-profile', value); } box.appendChild(btn); } } function renderSkillChecks(skills, enabled) { const box = $('sa_skills_box'); if (!box) { return; } const on = new Set(enabled || []); box.innerHTML = ''; for (const s of skills || []) { const label = document.createElement('label'); label.className = 'sa-check'; const input = document.createElement('input'); input.type = 'checkbox'; input.setAttribute('data-skill', s.id); input.checked = on.has(s.id) || (!enabled?.length && !!s.default); input.addEventListener('change', () => { state.enabledSkills = [...document.querySelectorAll('#sa_skills_box input[data-skill]:checked')].map((el) => el.getAttribute('data-skill')); saveSettings(); }); label.appendChild(input); label.appendChild(document.createTextNode(` ${s.title || s.id}`)); box.appendChild(label); } state.enabledSkills = [...document.querySelectorAll('#sa_skills_box input[data-skill]:checked')].map((el) => el.getAttribute('data-skill')); } function loadConfig(persona, done) { if (typeof genericRequest !== 'function') { done?.(null); return; } genericRequest( 'AssistentGetConfig', { persona: persona || $('sa_persona')?.value || 'neutral' }, (data) => { applyConfigPayload(data, { applyDefaults: true }); done?.(data); }, 0, () => done?.(null), ); } function setModelOptions(models, { error } = {}) { const sel = $('sa_model'); if (!sel) { return; } const names = (models || []).map((n) => String(n || '').trim()).filter(Boolean); sel.innerHTML = ''; if (error) { const opt = document.createElement('option'); opt.value = ''; opt.textContent = `⚠ ${String(error).replace(/\s+/g, ' ').slice(0, 90)}`; sel.appendChild(opt); sel.disabled = true; return; } sel.disabled = false; if (!names.length) { const opt = document.createElement('option'); opt.value = ''; opt.textContent = 'No Ollama models — pull / Refresh'; sel.appendChild(opt); return; } for (const name of names) { const opt = document.createElement('option'); opt.value = name; opt.textContent = name; sel.appendChild(opt); } const prefer = state.preferredModel || localStorage.getItem(LS_MODEL); if (prefer && names.includes(prefer)) { sel.value = prefer; } } function setEmbedModelOptions(models) { const sel = $('sa_embed_model'); if (!sel) { return; } const names = (models || []).map((n) => String(n || '').trim()).filter(Boolean); sel.innerHTML = ''; if (!names.length) { const opt = document.createElement('option'); opt.value = state.preferredEmbed || 'nomic-embed-text'; opt.textContent = opt.value + ' (ожидается pull)'; sel.appendChild(opt); return; } for (const name of names) { const opt = document.createElement('option'); opt.value = name; opt.textContent = name; sel.appendChild(opt); } const prefer = state.preferredEmbed || localStorage.getItem(LS_EMBED) || state.config?.assistant?.embed_model; if (prefer && names.includes(prefer)) { sel.value = prefer; } else if (prefer && !names.includes(prefer)) { const opt = document.createElement('option'); opt.value = prefer; opt.textContent = prefer; sel.appendChild(opt); sel.value = prefer; } } function refreshModels() { const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434'; setStatus('Loading models…'); if (typeof genericRequest !== 'function') { setStatus('SwarmUI API not ready'); setModelOptions([], { error: 'SwarmUI API not ready' }); return; } genericRequest( 'AssistentListModels', { baseUrl }, (data) => { const models = data.models || []; const memoryModels = data.memory_models || []; setModelOptions(models); setEmbedModelOptions(memoryModels); const prefer = state.preferredModel || localStorage.getItem(LS_MODEL); if (prefer && models.includes(prefer) && $('sa_model')) { $('sa_model').value = prefer; } setStatus(models.length ? `${models.length} chat · ${memoryModels.length} memory` : 'No Ollama models (gpu-rent: ollama pull)'); saveSettings(); }, 0, (err) => { const msg = String(err || 'Ollama unreachable'); setStatus(msg); setModelOptions([], { error: msg }); appendMessage('error', msg); }, ); } function refreshInventory(done, opts = {}) { if (typeof genericRequest !== 'function') { if (done) { done(); } return; } const rescan = !!opts.rescan; genericRequest( 'AssistentListInventory', { rescan }, (data) => { state.inventory = { loras: data.loras || [], checkpoints: data.checkpoints || [], wildcards: data.wildcards || [], has_civitai_key: !!data.has_civitai_key, inventory_at: data.inventory_at || Math.floor(Date.now() / 1000), rescanned: !!data.rescanned, }; state.inventoryFetchedAt = Date.now(); const n = state.inventory.loras.length; const ck = state.inventory.checkpoints.length; setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts${rescan ? ' (rescanned)' : ''}`); prefetchActiveModelCards(); if (state.view === 'cards') { renderCardsList(); } if (done) { done(state.inventory); } }, 0, (err) => { console.warn('Assistent inventory', err); if (done) { done(null); } }, ); } function refreshInventoryAsync(opts = {}) { return new Promise((resolve) => refreshInventory(resolve, opts)); } function setCardStatus(msg) { const el = $('sa_card_status'); if (el) { el.textContent = msg || ''; } } function setView(view) { state.view = view === 'cards' ? 'cards' : 'chat'; const chat = $('sa_view_chat'); const cards = $('sa_view_cards'); if (chat) { chat.hidden = state.view !== 'chat'; } if (cards) { cards.hidden = state.view !== 'cards'; } $('sa_tab_chat')?.classList.toggle('sa-subtab-active', state.view === 'chat'); $('sa_tab_cards')?.classList.toggle('sa-subtab-active', state.view === 'cards'); saveSettings(); if (state.view === 'cards') { renderCardsList(); } } function refreshPersonas() { loadConfig($('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral', (data) => { if (data?.personas) { state.personas = data.personas; } }); } function prefetchCard(kind, name) { return new Promise((resolve) => { if (!kind || !name || typeof genericRequest !== 'function') { resolve(null); return; } const key = `${kind}:${name}`; genericRequest( 'AssistentGetCard', { kind, name }, (data) => { if (data?.card) { state.modelCards[key] = data.card; } resolve(data?.card || null); }, 0, () => resolve(null), ); }); } async function prefetchActiveModelCards() { const keys = []; const seen = new Set(); const add = (kind, name) => { if (!kind || !name) { return; } const key = `${kind}:${name}`; if (seen.has(key)) { return; } seen.add(key); keys.push({ kind, name }); }; try { const ck = resolveCurrentCheckpoint(); if (ck?.name) { add('checkpoint', ck.name); } } catch (e) { /* ignore */ } try { if (typeof loraHelper !== 'undefined' && Array.isArray(loraHelper?.selected)) { for (const l of loraHelper.selected) { add('lora', l?.name || l); } } } catch (e) { /* ignore */ } for (const l of state.inventory?.loras || []) { if (l?.has_card) { add('lora', l.name); } if (keys.length >= 14) { break; } } for (const c of state.inventory?.checkpoints || []) { if (c?.has_card) { add('checkpoint', c.name); } if (keys.length >= 16) { break; } } await Promise.all(keys.map((k) => prefetchCard(k.kind, k.name))); } function cardsCatalog() { const kind = $('sa_cards_kind')?.value || 'all'; const inv = state.inventory || {}; const rows = []; if (kind === 'all' || kind === 'checkpoint') { for (const c of inv.checkpoints || []) { rows.push({ kind: 'checkpoint', name: c.name, title: c.title || c.name, has_card: !!c.has_card, hash: c.hash || '', preview_url: c.preview_url || null, has_sidecar: !!c.has_sidecar, }); } } if (kind === 'all' || kind === 'lora') { for (const l of inv.loras || []) { rows.push({ kind: 'lora', name: l.name, title: l.title || l.name, has_card: !!l.has_card, trigger: l.trigger_phrase, hash: l.hash || '', preview_url: l.preview_url || null, has_sidecar: !!l.has_sidecar, }); } } return rows; } function renderCardsList() { const root = $('sa_cards_list'); if (!root) { return; } root.innerHTML = ''; const rows = cardsCatalog(); if (!rows.length) { root.innerHTML = '
Inventory пуст — Обновить.
'; return; } for (const row of rows) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'sa-card-row'; if (state.cardsSelection && state.cardsSelection.kind === row.kind && state.cardsSelection.name === row.name) { btn.classList.add('sa-selected'); } const thumb = row.preview_url ? `` : '
'; const metaBits = []; metaBits.push(row.has_card ? 'card ✓' : 'нет card'); if (row.has_sidecar) { metaBits.push('sidecar'); } if (row.trigger) { metaBits.push(String(row.trigger).slice(0, 40)); } btn.innerHTML = `${thumb}
${escapeHtml(row.kind)}
${escapeHtml(row.title || row.name)}
${escapeHtml(metaBits.join(' · '))}
`; btn.addEventListener('click', () => selectCardModel(row)); root.appendChild(btn); } } function wireCardForm() { const sync = () => { if ($('sa_card_show_json')?.checked) { syncCardJsonFromForm(); } }; ['sa_card_triggers', 'sa_card_weight', 'sa_card_when', 'sa_card_avoid', 'sa_card_hint', 'sa_card_notes', 'sa_card_url'] .forEach((id) => $(id)?.addEventListener('change', sync)); $('sa_card_show_json')?.addEventListener('change', () => { const on = !!$('sa_card_show_json')?.checked; const ta = $('sa_card_json'); if (ta) { ta.hidden = !on; if (on) { syncCardJsonFromForm(); } } }); $('sa_card_json')?.addEventListener('change', () => { if ($('sa_card_show_json')?.checked) { applyCardToForm(readCardDraft() || {}); } }); } function applyCardToForm(card) { card = card || {}; const triggers = Array.isArray(card.triggers) ? card.triggers.join(', ') : (card.triggers || ''); if ($('sa_card_triggers')) { $('sa_card_triggers').value = triggers; } if ($('sa_card_weight')) { $('sa_card_weight').value = card.weight != null ? card.weight : (state.cardsSelection?.kind === 'lora' ? 0.8 : 1); } if ($('sa_card_when')) { $('sa_card_when').value = card.when || ''; } if ($('sa_card_avoid')) { $('sa_card_avoid').value = card.avoid || ''; } if ($('sa_card_hint')) { $('sa_card_hint').value = card.prompt_hint || ''; } if ($('sa_card_notes')) { $('sa_card_notes').value = card.notes || ''; } if ($('sa_card_url')) { $('sa_card_url').value = card.civitai_url || ''; } if ($('sa_card_json')) { $('sa_card_json').value = JSON.stringify(card, null, 2); } } function syncCardJsonFromForm() { const sel = state.cardsSelection || {}; let base = {}; try { base = JSON.parse($('sa_card_json')?.value || '{}'); } catch (e) { base = {}; } const triggers = String($('sa_card_triggers')?.value || '') .split(/[,;]/) .map((s) => s.trim()) .filter(Boolean); const card = { ...base, kind: sel.kind || base.kind || 'lora', name: sel.name || base.name || '', triggers, weight: parseFloat($('sa_card_weight')?.value || '0.8') || 0.8, when: $('sa_card_when')?.value || '', avoid: $('sa_card_avoid')?.value || '', prompt_hint: $('sa_card_hint')?.value || '', notes: $('sa_card_notes')?.value || '', civitai_url: $('sa_card_url')?.value || '', version_id: base.version_id != null ? base.version_id : null, }; if ($('sa_card_json')) { $('sa_card_json').value = JSON.stringify(card, null, 2); } return card; } function renderCardPreviews(urls) { const root = $('sa_card_previews'); if (!root) { return; } const list = (urls || []).filter(Boolean).slice(0, 6); root.innerHTML = ''; if (!list.length) { root.hidden = true; return; } root.hidden = false; for (const url of list) { const img = document.createElement('img'); img.className = 'sa-card-thumb'; img.src = url; img.alt = 'preview'; img.title = 'Клик — на вкладку Refs'; img.addEventListener('click', () => { setBoardTab('refs'); addRefFromUrl(url); setStatus('Превью → Refs'); }); root.appendChild(img); } } function mergeCivitaiIntoCard(card, data) { const out = { ...(card || {}) }; const civ = data?.civitai; if (!out.triggers?.length && data?.trigger_phrase) { out.triggers = [data.trigger_phrase]; } if (civ) { const trained = civ.trainedWords || civ.trained_words; if ((!out.triggers || !out.triggers.length) && Array.isArray(trained) && trained.length) { out.triggers = trained.slice(0, 12); } if (!out.civitai_url) { const mid = civ.modelId || civ.model?.id || civ.model?.modelId; const vid = civ.id || data.version_id; if (mid && vid) { out.civitai_url = `https://civitai.red/models/${mid}?modelVersionId=${vid}`; } else if (vid) { out.civitai_url = `https://civitai.red/models/0?modelVersionId=${vid}`; } } if (out.version_id == null && (civ.id || data.version_id)) { out.version_id = civ.id || data.version_id; } if (!out.notes && civ.description) { out.notes = String(civ.description).replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 400); } } if (out.version_id == null && data?.version_id) { out.version_id = data.version_id; } return out; } function formatMetaStatus(data) { if (!data) { return 'Нет ответа'; } if (data.error) { return String(data.error); } const parts = []; if (data.has_sidecar) { parts.push(`Сидикарь ✓ · version ${data.version_id || '?'}`); } else if (data.fetched) { parts.push(`Civitai ✓ · version ${data.version_id || '?'}`); } else { parts.push('Сидикаря нет'); } const n = (data.example_urls || data.preview_urls || []).length; if (n) { parts.push(`${n} кадр${n === 1 ? '' : 'а'}`); } if (data.has_card) { parts.push('карточка Assistent ✓'); } else { parts.push('карточки Assistent нет'); } if (data.fetch_error) { parts.push(String(data.fetch_error)); } return parts.join(' · '); } function applyCardMetaResponse(row, data, { preserveUser } = {}) { let card = data.card || { kind: row.kind, name: row.name, triggers: data.trigger_phrase ? [data.trigger_phrase] : [], weight: row.kind === 'lora' ? 0.8 : 1, when: '', avoid: '', prompt_hint: '', notes: '', civitai_url: '', version_id: data.version_id || null, }; if (preserveUser) { const current = syncCardJsonFromForm(); card = { ...mergeCivitaiIntoCard(card, data), when: current.when || card.when || '', avoid: current.avoid || card.avoid || '', prompt_hint: current.prompt_hint || card.prompt_hint || '', notes: current.notes || card.notes || '', }; } else { card = mergeCivitaiIntoCard(card, data); } applyCardToForm(card); state.modelCards[`${row.kind}:${row.name}`] = card; const urls = [ ...(data.preview_urls || []), ...(data.example_urls || []), ]; renderCardPreviews(urls); const badge = $('sa_card_badge'); if (badge) { badge.hidden = false; badge.textContent = data.has_card ? 'card ✓' : (data.has_sidecar || data.fetched ? 'meta ✓' : 'нет меты'); } setCardStatus(formatMetaStatus(data)); } function selectCardModel(row) { state.cardsSelection = row; renderCardsList(); if ($('sa_card_title')) { $('sa_card_title').textContent = row.title || row.name; } const badge = $('sa_card_badge'); if (badge) { badge.hidden = false; badge.textContent = '…'; } setCardStatus('Читаю локальную мету…'); genericRequest( 'AssistentGetCardMeta', { kind: row.kind, name: row.name, fetch: false }, (data) => applyCardMetaResponse(row, data || {}), 0, (err) => setCardStatus(String(err || 'Ошибка загрузки')), ); } function fetchCardMetaLive() { const row = state.cardsSelection; if (!row) { setCardStatus('Выбери модель'); return; } setCardStatus('Сидикаря нет · ищу по SHA…'); genericRequest( 'AssistentGetCardMeta', { kind: row.kind, name: row.name, fetch: true }, (data) => applyCardMetaResponse(row, data || {}, { preserveUser: true }), 0, (err) => setCardStatus(String(err || 'Civitai: ошибка запроса')), ); } async function addRefFromUrl(url) { if (!url) { return; } addRefSlot({ src: url, select: false }); } function readCardDraft() { const fromForm = syncCardJsonFromForm(); if ($('sa_card_show_json')?.checked) { const raw = $('sa_card_json')?.value || ''; try { return JSON.parse(raw); } catch (e) { setCardStatus('Невалидный JSON'); return null; } } return fromForm; } function saveCurrentCard({ enqueue } = {}) { const sel = state.cardsSelection; if (!sel) { setCardStatus('Выбери модель'); return; } const card = readCardDraft(); if (!card) { return; } card.kind = card.kind || sel.kind; card.name = card.name || sel.name; setCardStatus('Сохраняю…'); genericRequest( 'AssistentSaveCard', { kind: sel.kind, name: sel.name, card, enqueue_wanted: !!enqueue }, (data) => { if (data.error) { setCardStatus(data.error); return; } state.modelCards[`${sel.kind}:${sel.name}`] = card; setCardStatus(data.installed ? `Карточка Assistent сохранена · ${data.path}` : `Черновик + wanted · ${data.path}`); refreshInventory(() => renderCardsList()); }, 0, (err) => setCardStatus(String(err || 'Ошибка сохранения')), ); } function enqueueWantedOnly() { const sel = state.cardsSelection; const card = readCardDraft() || {}; if (!sel && !card.civitai_url) { setCardStatus('Нужна модель или civitai_url'); return; } genericRequest( 'AssistentEnqueueWanted', { kind: (card.kind || sel?.kind || 'lora'), url: card.civitai_url || '', version_id: card.version_id || 0, title: card.name || sel?.name || '', card, }, (data) => { setCardStatus(data.already ? 'Уже в wanted' : `Wanted → ${data.path}`); }, 0, (err) => setCardStatus(String(err || 'Ошибка enqueue')), ); } function shortLoraName(name) { const s = String(name || ''); const base = s.split(/[/\\]/).pop() || s; return base.replace(/\.safetensors$/i, '').slice(0, 28); } function renderLoraChips() { const root = $('sa_lora_chips'); if (!root) { return; } root.innerHTML = ''; let selected = []; try { if (typeof loraHelper !== 'undefined' && Array.isArray(loraHelper?.selected)) { selected = loraHelper.selected.map((l) => ({ name: l.name || l, weight: (loraHelper.loraWeightPref && loraHelper.loraWeightPref[l.name || l]) || 1, })); } } catch (e) { /* ignore */ } for (const l of selected) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'sa-lora-chip'; btn.title = `${l.name} ×${l.weight} — клик снять`; btn.textContent = `${shortLoraName(l.name)} ${Number(l.weight).toFixed(2)}`; btn.addEventListener('click', () => { try { if (typeof loraHelper !== 'undefined' && typeof loraHelper.removeLora === 'function') { loraHelper.removeLora(l.name); } else if (loraHelper?.selected) { loraHelper.selected = loraHelper.selected.filter((x) => (x.name || x) !== l.name); if (typeof loraHelper.rebuildUI === 'function') { loraHelper.rebuildUI(); } } } catch (e) { /* ignore */ } renderLoraChips(); }); root.appendChild(btn); } const add = document.createElement('button'); add.type = 'button'; add.className = 'sa-lora-chip sa-lora-add'; add.textContent = '+ LoRA'; add.title = 'Добавить из inventory'; add.addEventListener('click', (e) => { e.stopPropagation(); openLoraPicker(add); }); root.appendChild(add); } function openLoraPicker(anchor) { document.querySelectorAll('.sa-lora-picker').forEach((n) => n.remove()); const picker = document.createElement('div'); picker.className = 'sa-lora-picker'; const inv = (state.inventory?.loras || []).slice().sort((a, b) => (b.krea_likely ? 1 : 0) - (a.krea_likely ? 1 : 0)); const filter = document.createElement('input'); filter.type = 'search'; filter.placeholder = 'Фильтр LoRA…'; filter.style.cssText = 'width:100%;box-sizing:border-box;margin-bottom:0.25rem;padding:0.3rem;'; picker.appendChild(filter); const list = document.createElement('div'); picker.appendChild(list); const draw = () => { list.innerHTML = ''; const q = filter.value.trim().toLowerCase(); let n = 0; for (const l of inv) { const name = l.name || ''; if (q && !String(name).toLowerCase().includes(q) && !String(l.title || '').toLowerCase().includes(q)) { continue; } const btn = document.createElement('button'); btn.type = 'button'; btn.textContent = `${shortLoraName(name)}${l.krea_likely ? ' · krea' : ''}`; btn.title = name; btn.addEventListener('click', async () => { await applyPatch({ loras: [ ...((() => { try { return (loraHelper?.selected || []).map((x) => ({ name: x.name || x, weight: (loraHelper.loraWeightPref && loraHelper.loraWeightPref[x.name || x]) || 1, })); } catch (e) { return []; } })()), { name, weight: l.default_weight ? parseFloat(l.default_weight) : 0.8, triggers: l.triggers || (l.trigger_phrase ? [l.trigger_phrase] : []) }, ], }, 'loras'); picker.remove(); renderLoraChips(); }); list.appendChild(btn); if (++n >= 40) { break; } } if (!n) { list.innerHTML = '
Нет LoRA
'; } }; filter.addEventListener('input', draw); draw(); const composer = $('sa_composer') || document.body; composer.style.position = composer.style.position || 'relative'; composer.appendChild(picker); const onDoc = (ev) => { if (!picker.contains(ev.target) && ev.target !== anchor) { picker.remove(); document.removeEventListener('mousedown', onDoc); } }; setTimeout(() => document.addEventListener('mousedown', onDoc), 0); filter.focus(); } function loadTasteFromServer() { if (typeof genericRequest !== 'function') { return; } genericRequest( 'AssistentGetTaste', {}, (data) => { const remote = data?.taste; if (!remote || typeof remote !== 'object') { return; } const remoteUpdated = remote.updated || 0; const localUpdated = state.taste?.updated || 0; if (remoteUpdated >= localUpdated) { state.taste = { styles: Array.isArray(remote.styles) ? remote.styles.slice(0, 12) : [], likes: Array.isArray(remote.likes) ? remote.likes.slice(0, 16) : [], avoid: Array.isArray(remote.avoid) ? remote.avoid.slice(0, 12) : [], notes: String(remote.notes || '').slice(0, 400), updated: remoteUpdated || Date.now(), }; saveTasteLocalOnly(); } }, 0, () => {}, ); } function saveTasteLocalOnly() { try { localStorage.setItem(LS_TASTE, JSON.stringify(state.taste || {})); } catch (e) { /* ignore */ } } function saveTasteToServerDebounced() { if (state.tasteSaveTimer) { clearTimeout(state.tasteSaveTimer); } state.tasteSaveTimer = setTimeout(() => { if (typeof genericRequest !== 'function') { return; } genericRequest( 'AssistentSaveTaste', { taste: state.taste || {} }, () => {}, 0, () => {}, ); }, 800); } async function generateCardWithAssistent() { const sel = state.cardsSelection; if (!sel) { setCardStatus('Выбери модель'); return; } if (state.busy) { setCardStatus('Чат занят'); return; } setPackValue('catalog_card', { flash: true }); setView('chat'); const meta = await new Promise((resolve) => { genericRequest( 'AssistentGetCardMeta', { kind: sel.kind, name: sel.name }, (data) => resolve(data), 0, () => resolve(null), ); }); const forced = `Write a recommendation card for this ${sel.kind}: ${sel.name}. Use metadata/triggers only; output one JSON card.`; await sendChat({ forcedUserText: forced, skipSlash: true, skipAutoPack: true, fromCards: true, cardTarget: { kind: sel.kind, name: sel.name, meta, }, }); } function inventoryIsStale(maxAgeMs = 20000) { if (!state.inventoryFetchedAt) { return true; } return (Date.now() - state.inventoryFetchedAt) > maxAgeMs; } async function ensureFreshInventory({ forceRescan } = {}) { const rescan = forceRescan || inventoryIsStale(20000); await refreshInventoryAsync({ rescan }); } function triggerSwarmModelRefresh(done) { if (typeof genericRequest !== 'function') { if (done) { done(); } return; } genericRequest( 'TriggerRefresh', { strong: true }, () => { if (done) { done(); } }, 0, () => { if (done) { done(); } }, ); } async function handleReplySideEffects(reply, civitaiResults, opts = {}) { const { fromAutoCritique, fromVisionHop, fromCards } = opts; if (fromCards) { const card = extractCardJson(reply); if (card) { if ($('sa_card_json')) { $('sa_card_json').value = JSON.stringify(card, null, 2); } if (opts.fromDownload || opts.cardTarget) { const kind = card.kind || opts.cardTarget?.kind || 'lora'; const name = card.name || opts.cardTarget?.name; if (name && typeof genericRequest === 'function') { genericRequest( 'AssistentSaveCard', { kind, name, card, enqueue_wanted: false }, (data) => { if (data?.path) { state.modelCards[`${kind}:${name}`] = card; setCardStatus(data.installed ? `Card saved → ${data.path}` : `Card draft → ${data.path}`); setStatus(`Card saved for ${name}`); } }, 0, () => setCardStatus('Card draft ready — Save manually'), ); } } else { setView('cards'); setCardStatus('Draft from Assistent — review & Save'); } } return; } const { patch } = extractPatch(reply); if (Array.isArray(patch?.actions) && patch.actions.map(String).includes('interrupt')) { doInterruptNow(); } if (civitaiResults && civitaiResults.length && $('sa_auto_download')?.checked) { const pick = civitaiResults.find((r) => !r.already_installed && r.download_url && r.krea_likely) || civitaiResults.find((r) => !r.already_installed && r.download_url); if (pick) { downloadCivitaiLoRA(pick, null); } } if (patch && !fromVisionHop && !fromAutoCritique) { const hopped = await maybeVisionHop(patch, opts.attachedSlotIds || []); if (hopped) { return; } } if (patch && $('sa_auto_apply')?.checked) { await applyPatch(patch, 'all'); updateTasteFromPatch(patch, opts.userText || ''); if (!fromAutoCritique) { const src = await runGenerateFromPatch(patch); if (src) { await maybeAutoCritique(src); } } } } async function applyQuickPatch(patch, note) { const withActions = { ...patch }; if (!Array.isArray(withActions.actions) && patchHasGenTrigger(withActions)) { withActions.actions = ['generate']; } const prevIntent = state.lastUserParamIntent; state.lastUserParamIntent = true; await applyPatch(withActions, 'all'); state.lastUserParamIntent = prevIntent; setStatus(note || 'Applied'); if ($('sa_auto_generate')?.checked) { await runGenerateFromPatch(withActions); } syncChipHighlight(); } function syncChipHighlight() { const bar = $('sa_chips'); if (!bar) { return; } const cur = guessAspectFromSize(val('input_width'), val('input_height')); const seed = val('input_seed'); bar.querySelectorAll('[data-aspect]').forEach((btn) => { btn.classList.toggle('sa-chip-active', btn.getAttribute('data-aspect') === cur); }); bar.querySelectorAll('[data-seed]').forEach((btn) => { const mode = btn.getAttribute('data-seed'); const active = (mode === 'lock' && seed && seed !== '-1') || (mode === 'random' && (!seed || seed === '-1')); btn.classList.toggle('sa-chip-active', active); }); } function appendSystemNote(text) { const box = $('sa_messages'); if (!box) { return; } hideChatEmpty(); const div = document.createElement('div'); div.className = 'sa-msg assistant sa-system-note'; div.textContent = text; box.appendChild(div); box.scrollTop = box.scrollHeight; } async function handleSlashCommand(raw) { const text = String(raw || '').trim(); if (!text.startsWith('/')) { return false; } const parts = text.slice(1).split(/\s+/); const cmd = (parts[0] || '').toLowerCase(); const arg = parts.slice(1).join(' ').trim(); if (cmd === 'help' || cmd === '?') { appendSystemNote(HELP_TEXT); setStatus('/help'); return true; } if (cmd === 'gen' || cmd === 'generate') { const prev = findCurrentGenerateSrc(); startBusyUi('generating'); state.generating = true; setInterruptVisible(true); if (!triggerGenerate()) { state.generating = false; stopBusyUi('Could not start Generate'); return true; } const src = await waitForNewImage(prev); state.generating = false; setInterruptVisible(state.busy); if (src) { const gen = generateSlot(); if (gen) { gen.src = src; renderBoard(); } stopBusyUi('Generate done'); } else { stopBusyUi('Generate finished'); } return true; } if (cmd === 'look') { const id = normalizeSlotId(arg || GEN_ID) || GEN_ID; const slot = slotById(id); if (!slot) { setStatus(`Неизвестный слот: ${arg || GEN_ID}`); return true; } if (slot.type !== 'generate') { setBoardTab('refs'); } else { setBoardTab('generate'); } if (!slot.src && id === GEN_ID) { const src = findCurrentGenerateSrc(); if (src) { slot.src = src; } } if (!slot.src) { setStatus(`Slot ${id} is empty`); return true; } slot.attach = true; renderBoard(); if ($('sa_input')) { $('sa_input').value = `Look at ${id} and describe what you see.`; } setPackValue('critique_image', { flash: true }); await sendChat({ forceSlotIds: [id], skipAutoPack: true }); return true; } if (cmd === 'init') { const src = selectedSrc() || findCurrentGenerateSrc(); if (!src) { setStatus('No image for Init'); return true; } await setInitFromSrc(src); setPackValue('inpaint_edit', { flash: true }); return true; } if (cmd === 'mask') { const src = selectedSrc(); if (!src) { setStatus('Select a window with a mask image'); return true; } await setMaskFromSrc(src); setPackValue('inpaint_edit', { flash: true }); return true; } if (cmd === 'clear') { clearInitAndMask(); return true; } if (cmd === 'interrupt' || cmd === 'stop') { doInterruptNow(); state.busy = false; state.generating = false; setInterruptVisible(false); syncGenerateBusy(); syncPatchActionAvailability(); stopBusyUi('Interrupted'); return true; } if (cmd === 'aspect') { const key = normalizeAspect(arg); if (!key) { setStatus(`Unknown aspect. Try: ${Object.keys(ASPECT_TABLE).join(', ')}`); return true; } await applyQuickPatch({ aspect: key, actions: ['generate'] }, `Aspect ${key}`); return true; } if (cmd === 'seed') { const mode = (arg || 'random').toLowerCase(); if (mode === 'lock' || mode === 'keep') { await applyQuickPatch({ lock_seed: true }, 'Seed locked'); } else { await applyQuickPatch({ seed: -1, vary: true, actions: ['generate'] }, 'Seed random'); } return true; } if (cmd === 'vary') { await applyQuickPatch({ vary: true, seed: -1, actions: ['generate'] }, 'Vary (new seed)'); return true; } if (cmd === 'inventory' || cmd === 'inv') { setStatus('Rescanning models…'); triggerSwarmModelRefresh(async () => { await refreshInventoryAsync({ rescan: true }); const n = state.inventory?.loras?.length || 0; const ck = state.inventory?.checkpoints?.length || 0; appendSystemNote(`Inventory refreshed: ${n} LoRAs, ${ck} checkpoints.`); setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts (rescanned)`); }); return true; } if (cmd === 'pack') { if (!setPackValue(arg, { flash: true, user: true })) { setStatus('Pack: write|critique|compose|params|inpaint|describe'); } else { setStatus(`Pack → ${$('sa_pack')?.value}`); } return true; } if (cmd === 'civitai') { if (!arg) { setStatus('/civitai '); return true; } if ($('sa_input')) { $('sa_input').value = `Find a Krea 2 LoRA for: ${arg}`; } setPackValue('write_prompt', { flash: true }); await sendChat({ skipAutoPack: true, forcedUserText: `Search Civitai for Krea-compatible LoRA: ${arg}. Prefer actions search_civitai.`, }); return true; } appendSystemNote(`Unknown command /${cmd}.\n\n${HELP_TEXT}`); setStatus(`Unknown /${cmd}`); return true; } async function maybeVisionHop(patch, attachedSlotIds) { const ids = lookAtIdsFromPatch(patch); if (!ids.length || state.visionHopUsed) { return false; } 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 && !opts.fromVisionHop && !opts.fromAutoCritique) { return; } const rawInput = ($('sa_input')?.value || '').trim(); const text = (opts.forcedUserText || rawInput).trim(); if (!text) { return; } if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop) { state.lastUserParamIntent = userTextMentionsParams(text); } if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.skipSlash) { if (rawInput.startsWith('/')) { if ($('sa_input')) { $('sa_input').value = ''; } const handled = await handleSlashCommand(rawInput); if (handled) { return; } } } if (!updateGate()) { setStatus('Выбери модель Krea 2'); return; } if (!opts.skipAutoPack && !opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.fromCards) { const guessed = autoSelectPack(text); if (guessed) { setPackValue(guessed, { flash: true }); } } // Cards mode must not be overridden by auto-pack; keep catalog_card. if (opts.fromCards || state.view === 'cards') { setPackValue('catalog_card', { flash: false }); } const pack = $('sa_pack')?.value || 'write_prompt'; const persona = $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral'; const model = $('sa_model')?.value; if (!model) { setStatus('Выбери модель Ollama в ⚙'); refreshModels(); return; } const chatEpoch = bumpChatEpoch(); state.busy = true; setInterruptVisible(true); startBusyUi('thinking'); saveSettings(); // Always pull latest LoRA/checkpoint list before the LLM sees context // (rescans disk when inventory is older than ~20s or after downloads). setStatus('Обновляю inventory…'); try { await ensureFreshInventory({ forceRescan: !!opts.fromDownload }); await prefetchActiveModelCards(); } catch (e) { console.warn('Assistent inventory refresh', e); } if (chatEpoch !== state.chatEpoch) { return; } if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop) { state.critiqueHopUsed = false; state.visionHopUsed = false; } let wantedIds = (opts.forceSlotIds || []).map(normalizeSlotId).filter(Boolean); if (!wantedIds.length) { wantedIds = attachableSlots().map((s) => s.id); } if (wantsAutoVision() && generateSlot()?.src && !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, }; state.history.push({ role: 'user', content: text }); if (state.pendingPersonaNote) { state.history.push({ role: 'user', content: state.pendingPersonaNote, systemish: true }); state.pendingPersonaNote = null; } appendMessage('user', text); $('sa_input').value = ''; persistHistory(); const context = collectLiveContext(); context.has_vision_image = !!images; context.attached_slot_ids = visionSlots.map((s) => s.id); context.persona = persona; if (opts.cardTarget) { context.card_target = opts.cardTarget; } if (opts.fromCards || pack === 'catalog_card') { context.auto_apply = false; context.auto_generate = false; } await prefetchActiveModelCards(); if (chatEpoch !== state.chatEpoch) { return; } // Refresh cards into context after prefetch const refreshed = collectLiveContext(); context.model_cards = refreshed.model_cards; const messages = state.history.slice(-12).map((m) => ({ role: m.role, content: m.content })); if (images && messages.length) { messages[messages.length - 1].images = images; } startBusyUi('thinking'); const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434'; // Flat fields: SwarmUI JObject params receive the whole request body. const payload = { baseUrl, model, pack, persona, includeBase: true, messages, context_json: JSON.stringify(context), skills: state.enabledSkills || [], embed_model: $('sa_embed_model')?.value || state.preferredEmbed || '', raw: { messages, context_json: JSON.stringify(context), pack, persona, base_url: baseUrl, model, skills: state.enabledSkills || [], embed_model: $('sa_embed_model')?.value || state.preferredEmbed || '', }, }; const finishOk = async (reply, civitaiResults) => { if (chatEpoch !== state.chatEpoch) { return; } state.busy = false; setInterruptVisible(state.generating); const prose = extractPatch(reply).prose || reply; state.history.push({ role: 'assistant', content: prose, persona, pack }); persistHistory(); stopBusyUi('Готово'); await handleReplySideEffects(reply, civitaiResults, { ...opts, userText: text, attachedSlotIds: visionSlots.map((s) => s.id), }); }; const finishErr = (msg) => { if (chatEpoch !== state.chatEpoch) { return; } state.busy = false; setInterruptVisible(state.generating); stopBusyUi(msg); if (state.streamEl) { state.streamEl.classList.remove('sa-streaming', 'sa-typing'); state.streamEl.classList.add('error'); setAssistantBody(state.streamEl, msg); state.streamEl = null; state.streamMeta = null; } else { appendMessage('error', msg); } }; if (typeof makeWSRequest === 'function') { beginStreamMessage(msgMeta); makeWSRequest( 'AssistentChatWS', payload, (data) => { if (chatEpoch !== state.chatEpoch) { return; } if (data.phase === 'waiting_ollama') { setBusyPhase('loading'); const label = state.streamEl?.querySelector('.sa-typing-label'); if (label) { label.textContent = `Загружаю ${modelShort(model)} в GPU…`; } return; } if (data.error) { finishErr(String(data.error)); return; } if (data.clear_stream) { if (state.streamEl) { state.streamEl.classList.add('sa-typing'); const body = state.streamEl.querySelector('.sa-msg-body') || state.streamEl; body.innerHTML = 'Уточняю…'; } setBusyPhase('refining'); return; } if (data.delta) { appendStreamDelta(data.delta); return; } if (data.done || data.reply != null) { const reply = data.reply || (state.streamEl?.querySelector('.sa-msg-body')?.textContent) || ''; const civitai = data.civitai_results || []; finalizeStreamMessage(reply, civitai); finishOk(reply, civitai); } }, 0, (err) => { if (chatEpoch !== state.chatEpoch) { return; } // Fallback to HTTP AssistentChat console.warn('AssistentChatWS failed, falling back', err); if (state.streamEl) { state.streamEl.remove(); state.streamEl = null; state.streamMeta = null; } genericRequest( 'AssistentChat', payload, (data) => { if (chatEpoch !== state.chatEpoch) { return; } if (data.error) { finishErr(String(data.error)); return; } const reply = data.reply || ''; appendMessage('assistant', reply, null, data.civitai_results || [], msgMeta); finishOk(reply, data.civitai_results || []); }, 0, (err2) => finishErr(String(err2 || err || 'Chat failed')), ); }, ); return; } genericRequest( 'AssistentChat', payload, (data) => { if (chatEpoch !== state.chatEpoch) { return; } if (data.error) { finishErr(String(data.error)); return; } const reply = data.reply || ''; appendMessage('assistant', reply, null, data.civitai_results || [], msgMeta); finishOk(reply, data.civitai_results || []); }, 0, (err) => finishErr(String(err || 'Chat failed')), ); } function wireDropZone() { const board = $('sa_board'); const layout = $('sa_layout'); layout?.addEventListener('dragover', (e) => { if (e.dataTransfer?.types?.includes('Files') || e.dataTransfer?.types?.includes('text/uri-list')) { e.preventDefault(); } }); layout?.addEventListener('drop', async (e) => { if (!e.dataTransfer) { return; } if (e.target && e.target.closest && e.target.closest('.sa-slot, .sa-add-cell')) { return; } e.preventDefault(); await handleDropDataTransfer(e.dataTransfer); }); board?.addEventListener('keydown', (e) => { if (e.key === 'Delete' || e.key === 'Backspace') { if (e.target && (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT')) { return; } e.preventDefault(); clearSlot(state.selectedSlotId); } }); document.addEventListener('paste', async (e) => { const pane = document.getElementById('assistent'); if (!pane || !pane.classList.contains('active')) { return; } if (e.target && (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT')) { const items = e.clipboardData?.items; let hasImage = false; if (items) { for (const item of items) { if (item.type.startsWith('image/')) { hasImage = true; break; } } } if (!hasImage) { return; } } const items = e.clipboardData?.items; if (!items) { return; } for (const item of items) { if (item.type.startsWith('image/')) { e.preventDefault(); const file = item.getAsFile(); if (file) { const sel = selectedSlot(); await acceptImageFile(file, sel && sel.type === 'ref' ? sel.id : null); } return; } } }); } function wireSplitter() { const splitter = $('sa_splitter'); const layout = $('sa_layout'); const pane = $('sa_image_pane'); if (!splitter || !layout || !pane) { return; } let dragging = false; splitter.addEventListener('mousedown', (e) => { e.preventDefault(); dragging = true; splitter.classList.add('sa-dragging'); document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }); window.addEventListener('mousemove', (e) => { if (!dragging) { return; } const rect = layout.getBoundingClientRect(); const x = e.clientX - rect.left; const pct = Math.min(56, Math.max(22, (x / rect.width) * 100)); const value = `${pct}%`; document.documentElement.style.setProperty('--sa-image-width', value); localStorage.setItem(LS_PANE_WIDTH, value); }); window.addEventListener('mouseup', () => { if (!dragging) { return; } dragging = false; splitter.classList.remove('sa-dragging'); document.body.style.cursor = ''; document.body.style.userSelect = ''; }); } function registerSendButton() { if (typeof registerMediaButton !== 'function') { setTimeout(registerSendButton, 500); return; } if (window.__swarmAssistentMediaRegistered) { return; } window.__swarmAssistentMediaRegistered = true; registerMediaButton( 'Send to Assistent', (src) => { putImageOnBoard(src, { switchTab: true, note: 'Image sent to Assistent', preferSelected: false, }); const pack = $('sa_pack'); if (pack && pack.value === 'write_prompt') { pack.value = 'critique_image'; saveSettings(); } }, 'Open Assistent with this image (vision / critique / prompt help)', ['image'], true, true, ); } function wire() { if (!$('swarm_assistent_root')) { return; } if (typeof genericRequest !== 'function') { setTimeout(wire, 300); return; } if (window.__swarmAssistentWired) { return; } window.__swarmAssistentWired = true; loadSettings(); loadTaste(); loadTasteFromServer(); setView(state.view || 'chat'); updateGate(); ensureBoard(); setBoardTab(state.boardTab || 'generate', { persist: false }); syncGenerateSlot(); if (wantsAutoVision()) { refreshImagePreview(); } restoreHistory(); maybeWelcome(); loadConfig(localStorage.getItem(LS_PERSONA) || 'neutral', () => { refreshModels(); refreshInventory(() => { renderCardsList(); renderLoraChips(); }); }); wireDropZone(); wireSplitter(); registerSendButton(); wireSlashInput(); wireCardForm(); $('sa_tab_chat')?.addEventListener('click', () => setView('chat')); $('sa_tab_cards')?.addEventListener('click', () => setView('cards')); $('sa_board_tab_gen')?.addEventListener('click', () => setBoardTab('generate')); $('sa_board_tab_refs')?.addEventListener('click', () => setBoardTab('refs')); $('sa_persona')?.addEventListener('change', onPersonaChanged); $('sa_cards_kind')?.addEventListener('change', renderCardsList); $('sa_btn_cards_refresh')?.addEventListener('click', () => refreshInventory(() => renderCardsList(), { rescan: true })); $('sa_btn_card_meta')?.addEventListener('click', () => fetchCardMetaLive()); $('sa_btn_card_generate')?.addEventListener('click', () => generateCardWithAssistent()); $('sa_btn_card_save')?.addEventListener('click', () => saveCurrentCard()); $('sa_btn_card_wanted')?.addEventListener('click', () => enqueueWantedOnly()); $('sa_btn_settings')?.addEventListener('click', () => { const s = $('sa_settings'); if (s) { s.hidden = !s.hidden; } }); $('sa_btn_refresh_models')?.addEventListener('click', () => { saveSettings(); refreshModels(); }); $('sa_btn_refresh_inventory')?.addEventListener('click', () => refreshInventory(() => { renderCardsList(); renderLoraChips(); }, { rescan: true })); $('sa_btn_add_ref')?.addEventListener('click', () => { setBoardTab('refs'); addRefSlot({ select: true }); }); $('sa_btn_use_current')?.addEventListener('click', () => snapshotGenerateToRef()); $('sa_btn_as_init')?.addEventListener('click', async () => { const src = selectedSrc() || findCurrentGenerateSrc(); if (!src) { setStatus('Нет изображения для Init'); return; } await setInitFromSrc(src); const pack = $('sa_pack'); if (pack && pack.value === 'write_prompt') { pack.value = 'inpaint_edit'; saveSettings(); } }); $('sa_btn_as_mask')?.addEventListener('click', async () => { const src = selectedSrc(); if (!src) { setStatus('Выбери окно с маской'); return; } await setMaskFromSrc(src); const pack = $('sa_pack'); if (pack) { pack.value = 'inpaint_edit'; saveSettings(); } }); $('sa_btn_clear_init')?.addEventListener('click', clearInitAndMask); $('sa_btn_clear_image')?.addEventListener('click', () => clearSlot(state.selectedSlotId)); $('sa_btn_send')?.addEventListener('click', () => sendChat()); $('sa_btn_interrupt')?.addEventListener('click', () => { doInterruptNow(); state.busy = false; state.generating = false; setInterruptVisible(false); syncGenerateBusy(); syncPatchActionAvailability(); if (state.streamEl?.classList.contains('sa-typing')) { state.streamEl.remove(); state.streamEl = null; } stopBusyUi('Прервано'); }); $('sa_btn_clear')?.addEventListener('click', () => { state.history = []; state.critiqueHopUsed = false; state.visionHopUsed = false; state.packUserTouched = false; state.pendingPersonaNote = null; state.sessionExact = {}; state.lastUserParamIntent = false; clearPersistedHistory(); const box = $('sa_messages'); if (box) { box.innerHTML = ''; const empty = document.createElement('div'); empty.className = 'sa-chat-empty'; empty.id = 'sa_chat_empty'; empty.innerHTML = '
Совместная работа с Krea 2
Напиши промпт, кинь refs, чипсы aspect или /help.
'; box.appendChild(empty); } stopBusyUi(''); setStatus(''); }); $('sa_base_url')?.addEventListener('change', saveSettings); $('sa_model')?.addEventListener('change', saveSettings); $('sa_embed_model')?.addEventListener('change', () => { state.preferredEmbed = $('sa_embed_model')?.value || ''; saveSettings(); }); $('sa_pack')?.addEventListener('change', () => { state.packUserTouched = true; saveSettings(); }); $('sa_chips')?.addEventListener('click', async (e) => { const btn = e.target.closest('.sa-chip'); if (!btn || state.busy || state.generating) { return; } const aspect = btn.getAttribute('data-aspect'); const seed = btn.getAttribute('data-seed'); const vary = btn.getAttribute('data-vary'); const profile = btn.getAttribute('data-krea-profile'); if (aspect) { await applyQuickPatch({ aspect, actions: ['generate'] }, `Aspect ${aspect}`); } else if (seed === 'lock') { await applyQuickPatch({ lock_seed: true }, 'Seed locked'); } else if (seed === 'random') { await applyQuickPatch({ seed: -1, actions: ['generate'] }, 'Seed random'); } else if (vary) { await applyQuickPatch({ vary: true, seed: -1, actions: ['generate'] }, 'Vary'); } else if (profile === 'turbo') { const p = state.kreaProfiles?.turbo || mergedGenerationDefaults('turbo'); await applyQuickPatch({ steps: p.steps ?? 8, cfg: p.cfg ?? 1, sigma_shift: p.sigma_shift ?? 1.15, actions: ['generate'] }, 'Turbo'); } else if (profile === 'raw') { const p = state.kreaProfiles?.raw || mergedGenerationDefaults('raw'); await applyQuickPatch({ steps: p.steps ?? 28, cfg: p.cfg ?? 4.5, sigma_shift: p.sigma_shift, actions: ['generate'] }, 'RAW'); } renderLoraChips(); }); $('sa_auto_vision')?.addEventListener('change', () => { saveSettings(); const gen = generateSlot(); if (gen) { gen.attach = wantsAutoVision(); renderBoard(); } }); $('sa_auto_apply')?.addEventListener('change', saveSettings); $('sa_auto_generate')?.addEventListener('change', saveSettings); $('sa_auto_critique')?.addEventListener('change', saveSettings); $('sa_auto_download')?.addEventListener('change', saveSettings); syncChipHighlight(); setInterval(syncChipHighlight, 2500); setInterval(renderLoraChips, 4000); setInterval(updateGate, 2000); setInterval(syncGenerateSlot, 700); setInterval(() => { if (!state.busy) { const tabOn = !!document.getElementById(TAB_BUTTON_ID)?.classList.contains('tab-button-selected') || !!document.getElementById('swarm_assistent_root')?.offsetParent; refreshInventory(null, { rescan: inventoryIsStale(tabOn ? 45000 : 120000) }); } }, 30000); window.swarmAssistent = { setImageFromSrc, putImageOnBoard, clearVisionImage, snapshotGenerateToRef, openAssistentTab, sendToAssistent: (src) => { putImageOnBoard(src, { switchTab: true, note: 'Изображение отправлено в Assistent', preferSelected: false }); setBoardTab('refs'); }, isKreaSelected, resolveCurrentCheckpoint, refreshInventory, applyPatch, triggerGenerate, setInitFromSrc, setMaskFromSrc, clearInitAndMask, slotById, renderBoard, setBoardTab, }; } function wireSlashInput() { const input = $('sa_input'); if (!input || input.dataset.saSlashWired) { return; } input.dataset.saSlashWired = '1'; input.addEventListener('input', () => updateSlashMenuFromInput()); input.addEventListener('keydown', (e) => { const menu = $('sa_slash_menu'); const open = menu && !menu.hidden; if (open) { const items = slashMatches((input.value.split(/\s/)[0] || '')); if (e.key === 'ArrowDown') { e.preventDefault(); state.slashIndex = Math.min(items.length - 1, (state.slashIndex || 0) + 1); renderSlashMenu(items); return; } if (e.key === 'ArrowUp') { e.preventDefault(); state.slashIndex = Math.max(0, (state.slashIndex || 0) - 1); renderSlashMenu(items); return; } if (e.key === 'Tab' || (e.key === 'Enter' && !e.shiftKey)) { const pick = items[state.slashIndex || 0]; if (pick && input.value.trim() === (input.value.split(/\s/)[0] || '')) { e.preventDefault(); applySlashPick(pick); return; } } if (e.key === 'Escape') { hideSlashMenu(); return; } } if (e.key === 'Enter' && !e.shiftKey && !e.altKey) { e.preventDefault(); hideSlashMenu(); sendChat(); } }); input.addEventListener('blur', () => setTimeout(hideSlashMenu, 150)); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', wire); } else { wire(); } })();