diff --git a/.gitignore b/.gitignore index 8f27a56..a6b7859 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ bin/ obj/ +node_modules/ .vs/ *.user *.suo diff --git a/Assets/assistent.api.js b/Assets/assistent.api.js deleted file mode 100644 index 6302df3..0000000 --- a/Assets/assistent.api.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Swarm Assistent — promise wrapper around SwarmUI's genericRequest. - * Loaded before assistent.js. - */ -window.SA = window.SA || {}; - -SA.request = function (name, body) { - return new Promise((resolve, reject) => { - if (typeof genericRequest !== 'function') { - reject(new Error('genericRequest unavailable')); - return; - } - genericRequest( - name, - body || {}, - (data) => { - if (data && data.error) { - reject(new Error(String(data.error))); - } else { - resolve(data); - } - }, - 0, - (err) => reject(err instanceof Error ? err : new Error(String(err || 'request failed'))), - ); - }); -}; diff --git a/Assets/assistent.bundle.js b/Assets/assistent.bundle.js new file mode 100644 index 0000000..1c869ab --- /dev/null +++ b/Assets/assistent.bundle.js @@ -0,0 +1,9066 @@ +(() => { + // src/api.js + function createRequest() { + return function request(name, body) { + return new Promise((resolve, reject) => { + if (typeof genericRequest !== "function") { + reject(new Error("genericRequest unavailable")); + return; + } + genericRequest( + name, + body || {}, + (data) => { + if (data && data.error) { + reject(new Error(String(data.error))); + } else { + resolve(data); + } + }, + 0, + (err) => reject(err instanceof Error ? err : new Error(String(err || "request failed"))) + ); + }); + }; + } + function attachApi(SA2) { + SA2.request = createRequest(); + } + + // src/patch.js + var DEFAULT_PATCH_KEYS = [ + "prompt", + "negative", + "loras", + "width", + "height", + "steps", + "cfg", + "seed", + "sigma_shift", + "sampler", + "scheduler", + "actions", + "search_query", + "civitai_query", + "use_init_image", + "clear_init_image", + "init_creativity", + "denoise", + "use_mask_image", + "clear_mask_image", + "mask_blur", + "mask_grow", + "look_at", + "vision_from", + "vision_slots", + "slot_to_init", + "slot_to_mask", + "snapshot_generate", + "select_slot", + "aspect", + "images", + "batch", + "vary", + "lock_seed", + "creativity", + "intensity", + "complexity", + "movement", + "clear_prompt_images", + "slot_to_prompt_image", + "pack", + "memories", + "memory", + "memory_query", + "memory_kind", + "tag_query", + "user_prefs", + "inventory_query", + "skills", + "persona_shelves", + "persona_clone", + "persona", + "controls", + "variants" + ]; + var PATCH_KEYS = DEFAULT_PATCH_KEYS.slice(); + function setPatchKeys(keys) { + if (Array.isArray(keys) && keys.length) { + PATCH_KEYS = keys.map(String); + } + } + var FENCE_RE = /```(?:json)?\s*([\s\S]*?)```/gi; + function has(obj, key) { + return obj[key] !== void 0 && obj[key] !== null; + } + function isCardObject(obj) { + if (!obj || typeof obj !== "object") { + return false; + } + const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint); + const genish = !!(obj.prompt != null || obj.negative != null || obj.loras || obj.actions || obj.width || obj.height || obj.steps != null || obj.cfg != null || obj.aspect || obj.seed != null || obj.search_query || obj.civitai_query || obj.look_at || obj.controls); + 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 isPatchObject2(obj) { + if (!obj || typeof obj !== "object") { + return false; + } + if (isCardObject(obj)) { + return false; + } + return PATCH_KEYS.some((k) => has(obj, k)); + } + function normalizePatch(patch) { + if (!patch || typeof patch !== "object") { + return patch; + } + if (!has(patch, "search_query") && has(patch, "civitai_query")) { + patch.search_query = patch.civitai_query; + } + if (!has(patch, "init_creativity") && has(patch, "denoise")) { + patch.init_creativity = patch.denoise; + } + if (!has(patch, "look_at")) { + if (has(patch, "vision_from")) { + patch.look_at = patch.vision_from; + } else if (has(patch, "vision_slots")) { + patch.look_at = patch.vision_slots; + } + } + return patch; + } + function extractPatch(text) { + if (!text) { + return { prose: text || "", patch: null }; + } + const re = new RegExp(FENCE_RE.source, "gi"); + let match; + let lastPatch = null; + let prose = text; + while ((match = re.exec(text)) !== null) { + try { + const obj = JSON.parse(match[1].trim()); + if (isPatchObject2(obj)) { + lastPatch = normalizePatch(obj); + prose = (text.slice(0, match.index) + text.slice(match.index + match[0].length)).trim(); + } + } catch (e) { + } + } + return { prose, patch: lastPatch }; + } + function isTerminalStreamPatch(obj) { + if (!obj || typeof obj !== "object") { + return false; + } + if (isCardObject(obj)) { + return true; + } + if (Array.isArray(obj.variants) && obj.variants.length) { + return true; + } + if (obj.look_at != null || obj.vision_from != null || obj.vision_slots != null) { + return true; + } + if (obj.search_query != null || obj.civitai_query != null || obj.memory_query != null || obj.tag_query != null || obj.inventory_query != null) { + return true; + } + const acts = Array.isArray(obj.actions) ? obj.actions.map(String) : []; + const hopOrGen = [ + "skill_load", + "persona_read", + "memory_get", + "memory_search", + "lookup_tags", + "list_inventory", + "search_civitai", + "interrupt", + "generate", + "memory_upsert", + "user_pref_upsert" + ]; + if (acts.some((a) => hopOrGen.includes(a))) { + return true; + } + if (String(obj.prompt || "").trim().length >= 48) { + return true; + } + if (obj.loras != null || obj.aspect != null || obj.steps != null || obj.width != null || obj.height != null || obj.cfg != null || obj.seed != null || obj.controls != null || obj.memories != null || obj.user_prefs != null) { + return true; + } + return false; + } + function attachPatch(SA2) { + SA2.PATCH_KEYS = PATCH_KEYS; + SA2.setPatchKeys = setPatchKeys; + SA2.isCardObject = isCardObject; + SA2.isPatchObject = isPatchObject2; + SA2.isTerminalStreamPatch = isTerminalStreamPatch; + SA2.normalizePatch = normalizePatch; + SA2.extractPatch = extractPatch; + } + + // src/persist.js + var LS_CHATS = "swarm_assistent_chats_v1"; + var SAVE_DEBOUNCE_MS = 700; + var timers = { chats: /* @__PURE__ */ new Map(), ui: null }; + function normalizeChat(raw) { + if (!raw || !raw.id) { + return null; + } + return { + id: String(raw.id), + title: String(raw.title || "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442"), + createdAt: Number(raw.createdAt) || Date.now(), + updatedAt: Number(raw.updatedAt) || Date.now(), + messages: Array.isArray(raw.messages) ? raw.messages : [], + messages_count: Number(raw.messages_count) || (Array.isArray(raw.messages) ? raw.messages.length : 0), + params: raw.params && typeof raw.params === "object" ? raw.params : null + }; + } + function attachPersist(SA2, request = SA2.request) { + async function loadChats() { + let chats = []; + try { + const data = await request("AssistentListChats", { with_messages: true }); + chats = (data?.chats || []).map(normalizeChat).filter(Boolean); + } catch (e) { + console.warn("Assistent: disk chats unavailable", e); + return null; + } + return chats.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)); + } + async function getChat(id) { + if (!id) { + return null; + } + const data = await request("AssistentGetChat", { id }); + return normalizeChat(data?.chat); + } + async function searchChats(q) { + const query = String(q || "").trim(); + if (query.length < 2) { + return []; + } + const data = await request("AssistentListChats", { q: query, with_messages: false, limit: 40 }); + return (data?.chats || []).map(normalizeChat).filter(Boolean); + } + function saveChat(chat, { immediate = false } = {}) { + const clean = normalizeChat(chat); + if (!clean) { + return Promise.resolve(null); + } + const send = () => { + timers.chats.delete(clean.id); + return request("AssistentSaveChat", { + id: clean.id, + title: clean.title, + messages: clean.messages, + params: clean.params, + createdAt: clean.createdAt, + updatedAt: clean.updatedAt + }); + }; + if (immediate) { + const pending2 = timers.chats.get(clean.id); + if (pending2) { + clearTimeout(pending2); + } + return send(); + } + const pending = timers.chats.get(clean.id); + if (pending) { + clearTimeout(pending); + } + timers.chats.set(clean.id, setTimeout(() => { + send().catch((e) => console.warn("Assistent: chat save failed", clean.id, e)); + }, SAVE_DEBOUNCE_MS)); + return Promise.resolve(null); + } + function deleteChat(id) { + if (!id) { + return Promise.resolve(null); + } + const pending = timers.chats.get(id); + if (pending) { + clearTimeout(pending); + timers.chats.delete(id); + } + return request("AssistentDeleteChat", { id }); + } + async function loadUiState() { + try { + const data = await request("AssistentGetUiState", {}); + const ui = data?.ui_state; + return ui && typeof ui === "object" ? ui : null; + } catch (e) { + return null; + } + } + function saveUiState(uiState, { immediate = false } = {}) { + if (!uiState || typeof uiState !== "object") { + return Promise.resolve(null); + } + const send = () => { + timers.ui = null; + return request("AssistentSaveUiState", { ui_state: uiState }); + }; + if (timers.ui) { + clearTimeout(timers.ui); + timers.ui = null; + } + if (immediate) { + return send(); + } + timers.ui = setTimeout(() => { + send().catch((e) => console.warn("Assistent: ui-state save failed", e)); + }, SAVE_DEBOUNCE_MS); + return Promise.resolve(null); + } + SA2.persist = { + LS_CHATS, + loadChats, + getChat, + searchChats, + saveChat, + deleteChat, + loadUiState, + saveUiState + }; + } + + // src/app.js + (function() { + const LS_BASE = "swarm_assistent_base_url"; + const LS_MODEL = "swarm_assistent_model"; + const LS_EMBED = "swarm_assistent_embed_model"; + const LS_PACK = "swarm_assistent_pack"; + const LS_PERSONA = "swarm_assistent_persona"; + const LS_VIEW = "swarm_assistent_view"; + const LS_AUTO_VISION = "swarm_assistent_auto_vision"; + const LS_AUTO_APPLY = "swarm_assistent_auto_apply"; + const LS_AUTO_GENERATE = "swarm_assistent_auto_generate"; + const LS_AUTO_CRITIQUE = "swarm_assistent_auto_critique"; + const LS_AUTO_DOWNLOAD = "swarm_assistent_auto_download"; + const LS_PARK_LLM = "swarm_assistent_park_llm"; + const LS_PANE_WIDTH = "swarm_assistent_pane_width"; + const LS_WELCOMED = "swarm_assistent_welcomed"; + const LS_CHATS2 = "swarm_assistent_chats_v1"; + const LS_BOARD_TAB = "swarm_assistent_board_tab"; + const MAX_CHATS = 40; + const MAX_CHAT_MSGS = 24; + const TAB_BUTTON_ID = "maintab_assistent"; + const GEN_ID = "generate"; + let MAX_REF_SLOTS = 4; + let MAX_GEN_VARIANTS = 4; + let CONTEXT_PROMPT_MAX = 2e3; + let HISTORY_KEEP_TURNS = 4; + let INVENTORY_PROMPT_RICH = 12; + let INVENTORY_PROMPT_NAMES = 24; + 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 = { + ordinary: "ordinary", + combine: "ordinary", + normal: "ordinary", + general: "ordinary", + default: "ordinary", + 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 \xB7 Krea 2
+ + \u041D\u0430\u043F\u0438\u0448\u0438, \u0447\u0442\u043E \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u2014 \u0438\u043B\u0438 \u043A\u0438\u043D\u044C \u0440\u0435\u0444\u0435\u0440\u0435\u043D\u0441 \u0438 \u043F\u043E\u043F\u0440\u043E\u0441\u0438 \u043F\u0440\u0430\u0432\u043A\u0443.`; + let HELP_TEXT = `Slash-\u043A\u043E\u043C\u0430\u043D\u0434\u044B (\u0431\u0435\u0437 LLM): +/help \u2014 \u044D\u0442\u043E\u0442 \u0441\u043F\u0438\u0441\u043E\u043A +/new \u2014 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442 (\u0442\u0435\u043A\u0443\u0449\u0438\u0439 \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u0441\u044F \u0432 \u0418\u0441\u0442\u043E\u0440\u0438\u044E) +/history \u2014 \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u0441\u043F\u0438\u0441\u043E\u043A \u0447\u0430\u0442\u043E\u0432 +/debug \u2014 \u0441\u0432\u043E\u0434\u043A\u0430 UI/Exact (\u0431\u0435\u0437 LLM) +/debug ask \u2014 \u0442\u043E \u0436\u0435 + \u043A\u043E\u0440\u043E\u0442\u043A\u0438\u0439 \u043E\u0442\u0432\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0438 +/why \u2014 \u0441\u0440\u0430\u0437\u0443 /debug ask +/gen \u2014 Generate \u0441\u0435\u0439\u0447\u0430\u0441 +/look generate|refN \u2014 \u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043A\u0430\u0434\u0440 \u043C\u043E\u0434\u0435\u043B\u0438 (vision) +/init /mask /clear \u2014 Init / Mask / Clear Init +/interrupt \u2014 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044E +/aspect 16:9 \u2014 \u0440\u0430\u0437\u043C\u0435\u0440 \u0438\u0437 \u0442\u0430\u0431\u043B\u0438\u0446\u044B 1K +/seed lock|random \u2014 \u0437\u0430\u0444\u0438\u043A\u0441\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0438\u043B\u0438 \u0440\u0430\u043D\u0434\u043E\u043C\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C seed +/vary \u2014 \u043D\u043E\u0432\u044B\u0439 seed, \u0442\u043E\u0442 \u0436\u0435 \u043F\u0440\u043E\u043C\u043F\u0442 +/pack write|critique|compose|params|inpaint|describe|card +/civitai \u2014 \u043F\u043E\u0438\u0441\u043A LoRA (Confirm \u0432 \u0447\u0430\u0442\u0435) +/inventory \u2014 rescan \u043C\u043E\u0434\u0435\u043B\u0435\u0439 + \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u043F\u0438\u0441\u043E\u043A LoRA + +\u0427\u0438\u043F\u0441\u044B \u043D\u0430\u0434 \u043F\u043E\u043B\u0435\u043C \u0432\u0432\u043E\u0434\u0430 \u0434\u0435\u043B\u0430\u044E\u0442 \u0442\u043E \u0436\u0435 \u0434\u043B\u044F aspect / seed / vary / Turbo\xB7RAW. +\u041F\u0440\u0438 \u0441\u0442\u0430\u0440\u0442\u0435 \u0432\u0441\u0435\u0433\u0434\u0430 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442; \u0441\u043C\u0435\u043D\u0430 \u0447\u0430\u0442\u0430 \u0432 \u0418\u0441\u0442\u043E\u0440\u0438\u0438 \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u0435\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B.`; + let SLASH_COMMANDS = [ + { cmd: "/help", hint: "\u0441\u043F\u0438\u0441\u043E\u043A \u043A\u043E\u043C\u0430\u043D\u0434" }, + { cmd: "/new", hint: "\u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442" }, + { cmd: "/history", hint: "\u0438\u0441\u0442\u043E\u0440\u0438\u044F \u0447\u0430\u0442\u043E\u0432" }, + { cmd: "/debug", hint: "\u0441\u0432\u043E\u0434\u043A\u0430 \xB7 ask = \u0441 LLM" }, + { cmd: "/why", hint: "debug + \u043F\u043E\u044F\u0441\u043D\u0435\u043D\u0438\u0435 LLM" }, + { cmd: "/gen", hint: "Generate \u0441\u0435\u0439\u0447\u0430\u0441" }, + { cmd: "/look ", hint: "generate|refN" }, + { cmd: "/init", hint: "\u043A\u0430\u043A Init" }, + { cmd: "/mask", hint: "\u043A\u0430\u043A Mask" }, + { cmd: "/clear", hint: "\u0441\u0431\u0440\u043E\u0441 Init/Mask" }, + { cmd: "/interrupt", hint: "\u0441\u0442\u043E\u043F" }, + { cmd: "/aspect ", hint: "16:9" }, + { cmd: "/seed ", hint: "lock|random" }, + { cmd: "/vary", hint: "\u043D\u043E\u0432\u044B\u0439 seed" }, + { cmd: "/pack ", hint: "write|critique|\u2026" }, + { cmd: "/civitai ", hint: "\u0437\u0430\u043F\u0440\u043E\u0441 LoRA" }, + { cmd: "/inventory", hint: "rescan \u043C\u043E\u0434\u0435\u043B\u0435\u0439" } + ]; + const state = { + history: [], + config: null, + exact: null, + sessionExact: {}, + lastUserParamIntent: false, + lastUserControlIntent: false, + lastPatch: null, + pendingSilentGen: false, + pendingPromptEnMerge: null, + 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, + inventory: { loras: [], checkpoints: [], wildcards: [], has_civitai_key: false }, + inventoryFetchedAt: 0, + streamEl: null, + streamMeta: null, + streamText: "", + streamFenceDone: false, + turnHops: [], + lastSystemChars: 0, + lastSystemLayers: null, + lastContextChars: 0, + busyPhase: "idle", + busyStarted: 0, + gotDelta: false, + busyTimer: null, + slots: [], + selectedSlotId: "ref1", + refSeq: 1, + genResults: [], + selectedGenResultId: null, + lightboxIndex: -1, + packUserTouched: false, + view: "chat", + boardTab: "generate", + personas: [], + modelCards: {}, + cardsSelection: null, + pendingPersonaNote: null, + chats: [], + activeChatId: null, + restoringChat: false, + chatsPanelOpen: false, + chatsQuery: "", + chatsSearchHits: null, + slashIndex: 0, + llmParked: false, + expectColdLoad: false, + memoryRows: [], + userPrefs: [], + settingsTab: "behavior", + settingsPersonaId: null, + wanted: { count: 0, items: [] }, + wantedKeys: /* @__PURE__ */ new Set(), + ollamaHealth: "unknown" + }; + const HOP_BUDGET = 4; + function isContinuationTurn(opts) { + return !!(opts && (opts.fromVisionHop || opts.fromAutoCritique || opts.fromPromptEnRetry || opts.fromEmptyPatchRetry)); + } + function isMachineTurn(opts) { + return isContinuationTurn(opts) || !!(opts && (opts.fromCards || opts.fromDownload || opts.fromDebug)); + } + function resetTurnHops() { + state.turnHops = []; + state.pendingPromptEnMerge = null; + } + function turnHopUsed(kind) { + return (state.turnHops || []).includes(kind); + } + function claimTurnHop(kind) { + if (!Array.isArray(state.turnHops)) { + state.turnHops = []; + } + if (state.turnHops.includes(kind) || state.turnHops.length >= HOP_BUDGET) { + return false; + } + state.turnHops.push(kind); + return true; + } + function diskPersist() { + return window.SA && window.SA.persist || null; + } + function $(id) { + return document.getElementById(id); + } + function modelShort(name) { + const s = String(name || ""); + const slash = s.lastIndexOf("/"); + return (slash >= 0 ? s.slice(slash + 1) : s) || "model"; + } + function fmtElapsed(ms) { + const s = Math.max(0, Math.floor(ms / 1e3)); + 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; + } + } + let scrollMessagesRaf = 0; + function messagesNearBottom(thresholdPx = 96) { + const box = $("sa_messages"); + if (!box) { + return true; + } + return box.scrollHeight - box.scrollTop - box.clientHeight <= thresholdPx; + } + function scrollMessagesToBottom({ force = false } = {}) { + const box = $("sa_messages"); + if (!box) { + return; + } + if (!force && !messagesNearBottom()) { + return; + } + if (scrollMessagesRaf) { + return; + } + scrollMessagesRaf = requestAnimationFrame(() => { + scrollMessagesRaf = 0; + const el = $("sa_messages"); + if (el && (force || messagesNearBottom(120))) { + el.scrollTop = el.scrollHeight; + } + }); + } + 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 = state.llmParked || state.expectColdLoad ? "loading" : "waiting"; + } + const model = modelShort($("sa_model")?.value); + const labels = { + encoding: "Encoding image\u2026", + waiting: "\u0416\u0434\u0443 Ollama / \u043F\u0435\u0440\u0432\u044B\u0439 \u0442\u043E\u043A\u0435\u043D\u2026", + loading: `\u0417\u0430\u0433\u0440\u0443\u0436\u0430\u044E ${model} \u0432 GPU\u2026 \u043E\u0431\u044B\u0447\u043D\u043E 30\u2013120 \u0441 \u043F\u043E\u0441\u043B\u0435 park`, + warming: `\u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E ${model} \u0432 GPU\u2026`, + thinking: "Thinking\u2026", + streaming: "Writing\u2026", + generating: "Generating image\u2026", + parking: "\u041E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0430\u044E VRAM (park LLM)\u2026", + applying: "Applying patch\u2026", + silent_gen: "\u041F\u0440\u0438\u043C\u0435\u043D\u044F\u044E \u043F\u0430\u0442\u0447 \u2192 Generate\u2026", + refining: "Civitai search done \u2014 refining\u2026" + }; + const text = labels[state.busyPhase] || "Working\u2026"; + 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 >= 1e3 ? ` \xB7 ${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) { + } + 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) { + } + 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) { + } + 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(" \xB7 "); + const p = gate.querySelector("p"); + if (p) { + p.innerHTML = seen ? `Swarm Assistent is for Krea 2 models only. Current: ${escapeHtml( + seen + )} \u2014 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, """); + } + const PROSE_SECTION_TITLES = { + critique: "\u041A\u0440\u0438\u0442\u0438\u043A\u0430", + \u043A\u0440\u0438\u0442\u0438\u043A\u0430: "\u041A\u0440\u0438\u0442\u0438\u043A\u0430", + analysis: "\u0420\u0430\u0437\u0431\u043E\u0440", + \u0440\u0430\u0437\u0431\u043E\u0440: "\u0420\u0430\u0437\u0431\u043E\u0440", + notes: "\u0417\u0430\u043C\u0435\u0442\u043A\u0438", + \u0437\u0430\u043C\u0435\u0442\u043A\u0438: "\u0417\u0430\u043C\u0435\u0442\u043A\u0438", + summary: "\u041A\u0440\u0430\u0442\u043A\u043E", + \u043A\u0440\u0430\u0442\u043A\u043E: "\u041A\u0440\u0430\u0442\u043A\u043E", + prompt: "\u041F\u0440\u043E\u043C\u043F\u0442", + \u043F\u0440\u043E\u043C\u043F\u0442: "\u041F\u0440\u043E\u043C\u043F\u0442", + "improved prompt": "\u041F\u0440\u043E\u043C\u043F\u0442", + "next prompt": "\u041F\u0440\u043E\u043C\u043F\u0442", + deliverable: "\u0418\u0442\u043E\u0433", + \u0438\u0442\u043E\u0433: "\u0418\u0442\u043E\u0433", + verdict: "\u0412\u0435\u0440\u0434\u0438\u043A\u0442", + \u0432\u0435\u0440\u0434\u0438\u043A\u0442: "\u0412\u0435\u0440\u0434\u0438\u043A\u0442", + issues: "\u041F\u0440\u043E\u0431\u043B\u0435\u043C\u044B", + \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u044B: "\u041F\u0440\u043E\u0431\u043B\u0435\u043C\u044B", + fixes: "\u041F\u0440\u0430\u0432\u043A\u0438", + \u043F\u0440\u0430\u0432\u043A\u0438: "\u041F\u0440\u0430\u0432\u043A\u0438", + suggestion: "\u041F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435", + suggestions: "\u041F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F", + \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F: "\u041F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F" + }; + function localizeProseHeading(raw) { + const cleaned = String(raw || "").replace(/[*_`#]/g, "").trim(); + if (!cleaned) { + return null; + } + const key = cleaned.toLowerCase().replace(/\s+/g, " "); + if (/^json\s*patch$/.test(key) || /^патч$/.test(key) || /^json\s*патч$/.test(key)) { + return null; + } + if (PROSE_SECTION_TITLES[key]) { + return PROSE_SECTION_TITLES[key]; + } + const head = key.split(/[—:\-|]/)[0].trim(); + if (PROSE_SECTION_TITLES[head]) { + return PROSE_SECTION_TITLES[head]; + } + return cleaned; + } + function formatProseInline(escapedLine) { + let t = escapedLine; + t = t.replace(/`([^`]+)`/g, '$1'); + t = t.replace(/\*\*([^*]+)\*\*/g, "$1"); + t = t.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, "$1$2"); + return t; + } + function formatAssistantProseHtml(raw) { + let text = String(raw || "").replace(/\r\n/g, "\n"); + text = text.replace(/(?:^|\n)#{1,6}\s*JSON\s*Patch\s*(?=\n|$)/gi, "\n"); + text = text.replace(/(?:^|\n)\s*JSON\s*Patch\s*:?\s*(?=\n|$)/gi, "\n"); + text = text.replace(/\n{3,}/g, "\n\n").trim(); + if (!text) { + return ""; + } + const lines = text.split("\n"); + const parts = []; + let listItems = []; + const flushList = () => { + if (!listItems.length) { + return; + } + parts.push( + `` + ); + listItems = []; + }; + for (const line of lines) { + const heading = line.match(/^#{1,3}\s+(.+?)\s*$/); + if (heading) { + flushList(); + const title = localizeProseHeading(heading[1]); + if (!title) { + continue; + } + const level = Math.min((line.match(/^#+/) || ["###"])[0].length, 3); + parts.push( + `
${escapeHtml(title)}
` + ); + continue; + } + const bullet = line.match(/^\s*[-*•]\s+(.+)$/); + if (bullet) { + listItems.push(bullet[1]); + continue; + } + flushList(); + if (!line.trim()) { + parts.push(''); + continue; + } + parts.push(`

${formatProseInline(escapeHtml(line))}

`); + } + flushList(); + return parts.join(""); + } + function setAssistantBody(div, text, { live = false } = {}) { + if (!div) { + return; + } + let body = div.querySelector(".sa-msg-body"); + if (!body) { + body = document.createElement("div"); + body.className = "sa-msg-body"; + div.appendChild(body); + } + const raw = text || ""; + if (live) { + body.classList.add("sa-prose", "sa-prose-live"); + body.classList.remove("sa-prose-rich"); + body.textContent = raw; + return; + } + body.classList.add("sa-prose", "sa-prose-rich"); + body.classList.remove("sa-prose-live"); + const html = formatAssistantProseHtml(raw); + if (html) { + body.innerHTML = html; + } else { + body.textContent = ""; + } + } + 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 liveNegativePrompt() { + return String(val("input_negativeprompt") || val("alt_negativeprompt_textbox") || "").trim(); + } + function exactDefaultNegative() { + const { exact } = resolveExactBundle(); + const n = exact?.generation?.negative ?? exact?.negative; + return n != null ? String(n).trim() : ""; + } + function setNegativePrompt(text) { + const s = text != null ? String(text) : ""; + if (document.getElementById("input_negativeprompt")) { + setVal("input_negativeprompt", s); + } + if (document.getElementById("alt_negativeprompt_textbox")) { + setVal("alt_negativeprompt_textbox", s); + } + } + function ensureNegativeForGenerate(patch) { + let neg = ""; + if (patch && patch.negative != null && String(patch.negative).trim() !== "") { + neg = String(patch.negative).trim(); + } else { + neg = liveNegativePrompt() || exactDefaultNegative(); + } + if (neg) { + setNegativePrompt(neg); + if (patch && (patch.negative == null || String(patch.negative).trim() === "")) { + patch.negative = neg; + } + } + return neg; + } + 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 cyrTokenRe(alts) { + const boundary = "(^|[^0-9A-Za-z_\u0410-\u042F\u0430-\u044F\u0401\u0451])"; + const end = "(?=$|[^0-9A-Za-z_\u0410-\u042F\u0430-\u044F\u0401\u0451])"; + return new RegExp(`${boundary}(?:${alts})${end}`, "i"); + } + function parseAspectFromUserText(text) { + const t = String(text || ""); + if (!t.trim()) { + return null; + } + const ratio = t.match(/(?:^|[^0-9])(\d+(?:\.\d+)?)\s*[:x×хX]\s*(\d+(?:\.\d+)?)(?=$|[^0-9])/); + if (ratio) { + const key = normalizeAspect(`${ratio[1]}:${ratio[2]}`); + if (key) { + return key; + } + } + const na = t.match(/(?:^|[^0-9])(\d+(?:\.\d+)?)\s*(?:на|к|to)\s*(\d+(?:\.\d+)?)(?=$|[^0-9])/i); + if (na) { + const key = normalizeAspect(`${na[1]}:${na[2]}`); + if (key) { + return key; + } + } + const named = t.match(/\b(16:9|9:16|1:1|4:5|2:3|3:2|4:3|2\.35:1)\b/i); + if (named) { + return normalizeAspect(named[1]); + } + if (cyrTokenRe("\u043F\u043E\u0440\u0442\u0440\u0435\u0442|\u0432\u0435\u0440\u0442\u0438\u043A\u0430\u043B[\u0430-\u044F\u0451]*").test(t) || /\b(portrait|vertical)\b/i.test(t)) { + return normalizeAspect("9:16") || normalizeAspect("2:3"); + } + if (cyrTokenRe("\u0430\u043B\u044C\u0431\u043E\u043C|\u0433\u043E\u0440\u0438\u0437\u043E\u043D\u0442\u0430\u043B[\u0430-\u044F\u0451]*").test(t) || /\b(landscape|horizontal|widescreen)\b/i.test(t)) { + return normalizeAspect("16:9"); + } + return null; + } + function isSameButAspectRequest(text) { + const t = String(text || ""); + if (!parseAspectFromUserText(t)) { + return false; + } + return /такую\s+же|тот\s+же\s+промпт|same\s+(one|prompt|thing|again)|только\s+(поменя|смени|поставь)|поменяй\s+на|смени\s+на|only\s+change|just\s+change/i.test(t) || /поменяй\s+(размер|aspect|соотношен)/i.test(t) || /смени\s+(размер|aspect|соотношен)/i.test(t); + } + function userTextMentionsControls(text) { + const t = String(text || ""); + if (/\b(horny|controls?|preference[_\s-]?bias)\b/i.test(t)) { + return true; + } + return cyrTokenRe("\u0445\u043E\u0440\u043D\u0438|\u043E\u0441\u0442\u044B\u043D\u044C|\u0441\u043B\u0430\u0439\u0434\u0435\u0440").test(t) || /\/\s*(остынь|ostyn|horny-game)/i.test(t) || /слайдер\s*вкус|вкус\s*(на|в)\s*\d|поставь\s*вкус|крутани\s*вкус/i.test(t); + } + function patchLooksLikeGeneration(patch) { + if (!patch || typeof patch !== "object") { + return false; + } + if (patch.prompt != null || patch.loras || patch.aspect != null || patch.width != null || patch.height != null || patch.steps != null || patch.cfg != null || patch.seed != null) { + return true; + } + return Array.isArray(patch.actions) && patch.actions.map(String).includes("generate"); + } + function filterControlPatch(incoming, patch) { + const schema = state.config?.controls || {}; + const out = {}; + if (!incoming || typeof incoming !== "object") { + return out; + } + if (patchLooksLikeGeneration(patch) && !state.lastUserControlIntent) { + return out; + } + for (const [id, raw] of Object.entries(incoming)) { + if (!schema[id]) { + continue; + } + const n = Number(raw); + if (!Number.isFinite(n)) { + continue; + } + const def = Number(schema[id]?.default); + const cur = getControlValue(id, Number.isFinite(def) ? def : n); + if (Math.abs(n - cur) < 5e-4) { + continue; + } + if (!state.lastUserControlIntent && Number.isFinite(def) && Math.abs(n - def) < 5e-4 && Math.abs(cur - def) > 5e-4) { + continue; + } + out[id] = n; + } + return out; + } + function userTextMentionsParams(text) { + const t = String(text || ""); + if (parseAspectFromUserText(t)) { + return true; + } + if (/\b(steps?|cfg|seed|sigma|aspect|resolution|batch|turbo|raw)\b/i.test(t)) { + return true; + } + return cyrTokenRe("\u0440\u0430\u0437\u043C\u0435\u0440|\u0448\u0438\u0440\u0438\u043D[\u0430-\u044F\u0451]*|\u0432\u044B\u0441\u043E\u0442[\u0430-\u044F\u0451]*|\u0441\u043E\u043E\u0442\u043D\u043E\u0448\u0435\u043D[\u0430-\u044F\u0451]*|\u0442\u0443\u0440\u0431\u043E|\u043F\u043E\u0440\u0442\u0440\u0435\u0442|\u0432\u0435\u0440\u0442\u0438\u043A\u0430\u043B[\u0430-\u044F\u0451]*").test(t); + } + function replyMissingJsonPatch(reply) { + const t = String(reply || ""); + if (!t.trim()) { + return false; + } + if (/```(?:json)?\s*\{[\s\S]*?\}```/i.test(t)) { + return false; + } + return /###\s*JSON\s*Patch\b/i.test(t) || /JSON\s*Patch\s*:?\s*$/im.test(t); + } + function userAsksGenerate(text) { + const t = String(text || "").trim(); + if (!t) { + return false; + } + if (/^(gen|generate|go|рисуй|нарисуй)([!.…\s]|$)/i.test(t)) { + return true; + } + if (/^(ещё|еще)(\s+раз)?([!.…\s]|$)/i.test(t)) { + return true; + } + const letter = "[0-9A-Za-z_\u0410-\u042F\u0430-\u044F\u0401\u0451]"; + const stem = `${letter}*`; + return cyrTokenRe( + `\u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439|\u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C|\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439|generate|\u043D\u0430\u0440\u0438\u0441\u0443\u0439|\u043F\u0435\u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439|\u043F\u0435\u0440\u0435\u0440\u0438\u0441\u0443\u0439|\u0441\u0434\u0435\u043B\u0430\u0439\\s+(\u043A\u0430\u0440\u0442\u0438\u043D\u043A${stem}|\u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D${stem}|\u0444\u043E\u0442\u043E${stem})|\u0445\u043E\u0447\u0443\\s+(\u043A\u0430\u0440\u0442\u0438\u043D\u043A${stem}|\u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D${stem}|\u0444\u043E\u0442\u043E${stem})|run\\s+generat|/gen` + ).test(t); + } + function userAsksContinue(text) { + const t = String(text || "").trim(); + if (!t) { + return false; + } + if (/^(давай\s+дальше|продолжай|продолжим|go\s+on|continue|keep\s+going|next(\s+one)?|next\s+frame)([!.…\s]|$)/i.test(t)) { + return true; + } + return cyrTokenRe( + "\u0434\u0430\u0432\u0430\u0439\\s+\u0434\u0430\u043B\u044C\u0448\u0435|\u0441\u043B\u0435\u0434\u0443\u044E\u0449(\u0438\u0439|\u0430\u044F|\u0435\u0435|\u0443\u044E)\\s+\u043A\u0430\u0434\u0440|\u0435\u0449\u0451\\s+\u043A\u0430\u0434\u0440|\u0435\u0449\u0435\\s+\u043A\u0430\u0434\u0440|\u043A\u0430\u0434\u0440\\s*\u2116?\\s*\\d+|\u0441\u0434\u0435\u043B\u0430\u0439\\s+\u0441\u043B\u0435\u0434\u0443\u044E\u0449" + ).test(t); + } + function extractPromptFromProse(reply) { + const t = String(reply || "").replace(/\r\n/g, "\n"); + if (!t.trim()) { + return null; + } + const bq = []; + for (const line of t.split("\n")) { + const m = line.match(/^\s{0,3}>\s?(.*)$/); + if (m) { + bq.push(m[1]); + continue; + } + if (bq.length) { + break; + } + } + const fromBq = bq.join("\n").trim(); + if (fromBq.length >= 48) { + return fromBq.slice(0, 4e3); + } + const section = t.match( + /(?:^|\n)#{1,6}\s*(?:📷\s*)?(?:prompt|промпт|improved\s+prompt|next\s+prompt|кадр[^\n]*)\s*\n+([\s\S]+?)(?=\n#{1,6}\s|\n```|$)/i + ); + if (section) { + const body = section[1].replace(/^\s{0,3}>\s?/gm, "").trim(); + if (body.length >= 48) { + return body.slice(0, 4e3); + } + } + return null; + } + function synthesizePatchAfterEmptyFence(reply, userText, opts = {}) { + const wantsFrame = !!opts.userWantsGenerate || !isMachineTurn(opts) && userImpliesGenerate(userText); + if (!wantsFrame && !replyMissingJsonPatch(reply)) { + return null; + } + const prompt = extractPromptFromProse(reply) || state.lastPatch?.prompt || null; + if (!prompt) { + return null; + } + const patch = wantsFrame ? { prompt, actions: ["generate"] } : { prompt }; + if (Array.isArray(state.lastPatch?.loras) && state.lastPatch.loras.length) { + patch.loras = state.lastPatch.loras; + } + if (state.lastPatch?.aspect) { + patch.aspect = state.lastPatch.aspect; + } + return patch; + } + function promptLooksKreaReady(prompt) { + const t = String(prompt || "").trim(); + if (t.length < 80) { + return false; + } + const cyr = (t.match(/[\u0400-\u04FF]/g) || []).length; + const lat = (t.match(/[A-Za-z]/g) || []).length; + if (cyr >= 12) { + return false; + } + if (lat < 55) { + return false; + } + if (t.length < 120 && (t.match(/[,.;:]/g) || []).length < 2) { + return false; + } + return true; + } + function promptNeedsKreaPrep(prompt) { + return !promptLooksKreaReady(prompt); + } + function buildKreaPromptPrepRequest(patch) { + const keep = { + actions: Array.isArray(patch.actions) && patch.actions.length ? patch.actions : ["generate"] + }; + if (patch.aspect) { + keep.aspect = patch.aspect; + } + if (patch.negative != null && String(patch.negative).trim() !== "") { + keep.negative = patch.negative; + } else { + const liveNeg = liveNegativePrompt() || exactDefaultNegative(); + if (liveNeg) { + keep.negative = liveNeg; + } + } + if (Array.isArray(patch.loras)) { + keep.loras = patch.loras; + } + if (patch.width != null) { + keep.width = patch.width; + } + if (patch.height != null) { + keep.height = patch.height; + } + return `You are the Krea 2 prompt prep step (chat model). Rewrite SOURCE into the final Swarm Generate box text. +HARD RULES: +- JSON "prompt": English only (no Cyrillic) \u2014 translate if needed. +- Natural photographer/director prose for Qwen3-VL \u2014 not Danbooru tags, not (word:1.5), not masterpiece/best quality/8k. +- Structure & front-load: subject \u2192 pose/action \u2192 body/wardrobe \u2192 setting \u2192 materials/textures \u2192 camera/framing \u2192 lighting \u2192 medium/mood. +- Expand thin ideas; fix anti-patterns; one coherent scene. +- Keep LoRA trigger phrases in English near the subject they affect. +- Always include JSON "negative": keep/supplement SOURCE+live negative, or use Exact default if empty. Never drop it. +- Put \u201Cno blur / empty street\u201D ideas as positives in prompt, not as a huge negative dump. +- One short ack in the user language max, then ONE fenced JSON merging these keys: ${JSON.stringify(keep)} plus the new English "prompt" and "negative". +- Include actions:["generate"] when an image was requested. + +SOURCE: +${patch.prompt}`; + } + function mergePromptEnRewrite(effective) { + const base = state.pendingPromptEnMerge; + state.pendingPromptEnMerge = null; + if (!base || !effective) { + return effective; + } + return { + ...base, + ...effective, + prompt: effective.prompt || base.prompt, + negative: effective.negative != null && String(effective.negative).trim() !== "" ? effective.negative : base.negative || liveNegativePrompt() || exactDefaultNegative() || void 0, + actions: Array.isArray(effective.actions) && effective.actions.length ? effective.actions : base.actions || ["generate"], + loras: effective.loras || base.loras, + aspect: effective.aspect || base.aspect + }; + } + function userAsksNoGenerate(text) { + const t = String(text || "").trim(); + if (!t || userAsksGenerate(t)) { + return false; + } + if (/\b(remember|save\s+(this\s+)?(as\s+)?(the\s+)?(base\s+)?(prompt|template)|don'?t\s+generat|do\s+not\s+generat|no\s+generat|without\s+generat)\b/i.test(t)) { + return true; + } + return cyrTokenRe( + "\u0437\u0430\u043F\u043E\u043C\u043D|\u0437\u0430\u043F\u043E\u043C\u043D\u0438|\u0437\u0430\u043F\u043E\u043C\u043D\u0438\u043C|\u0441\u043E\u0445\u0440\u0430\u043D\u0438|\u0441\u043E\u0445\u0440\u0430\u043D\u0438\u043C|\u0448\u0430\u0431\u043B\u043E\u043D|\u0431\u0430\u0437\u043E\u0432(\u044B\u0439|\u043E\u0433\u043E|\u043E\u043C\u0443|\u044B\u043C|\u0430\u044F|\u0443\u044E|\u043E\u0435)?\\s+\u043F\u0440\u043E\u043C\u043F\u0442|\u043D\u0435\\s+\u0433\u0435\u043D\u0435\u0440\u0438\u0440|\u0431\u0435\u0437\\s+\u0433\u0435\u043D\u0435\u0440\u0430\u0446|\u043D\u0435\\s+\u043D\u0430\u0434\u043E\\s+\u0433\u0435\u043D\u0435\u0440|\u0442\u043E\u043B\u044C\u043A\u043E\\s+\u0437\u0430\u043F\u043E\u043C\u043D|\u043F\u043E\u043A\u0430\\s+\u0437\u0430\u043F\u043E\u043C\u043D|\u043D\u0435\\s+\u0440\u0438\u0441\u0443\u0439|\u043D\u0435\\s+\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439\\s+\u0433\u0435\u043D\u0435\u0440" + ).test(t); + } + function userCommandsGenerate(text) { + const t = String(text || "").trim(); + if (!t || userAsksNoGenerate(t)) { + return false; + } + return userAsksGenerate(t) || userAsksContinue(t) || isSameButAspectRequest(t); + } + function userAsksLook(text) { + const t = String(text || "").trim(); + if (!t) { + return false; + } + if (/\b(look\s+at|critique|criticize|describe\s+(this|the|ref|image)|what\s+do\s+you\s+see)\b/i.test(t)) { + return true; + } + if (cyrTokenRe("\u043A\u0440\u0438\u0442\u0438\u043A[\u0430-\u044F\u0451]*|\u0447\u0442\u043E\\s+\u043D\u0435\\s+\u0442\u0430\u043A|\u0440\u0430\u0437\u0431\u0435\u0440\u0438").test(t)) { + return true; + } + if (cyrTokenRe("\u043E\u043F\u0438\u0448\u0438\\s+(\u044D\u0442\u043E|\u044D\u0442\u0443|\u0440\u0435\u0444|\u0438\u0437\u043E\u0431\u0440\u0430\u0436[\u0430-\u044F\u0451]*|\u043A\u0430\u0440\u0442\u0438\u043D\u043A[\u0430-\u044F\u0451]*|\u043A\u0430\u0434\u0440|\u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442|\u0440\u0435\u0444\u0435\u0440\u0435\u043D\u0441)").test(t)) { + return true; + } + return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t); + } + function userIsChatNotFrame(text) { + const t = String(text || "").trim(); + if (!t) { + return false; + } + if (/^(ок|окей|ok|okay|ладно|хорошо|понял|ясно|спасибо|thanks)([!.…\s]*)$/i.test(t)) { + return true; + } + if (cyrTokenRe( + "\u0447\u0442\u043E\\s+\u0442\u0430\u043A\u043E\u0435|\u043A\u0430\u043A\\s+\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442|\u0437\u0430\u0447\u0435\u043C\\s+|\u043A\u0430\u043A\u0438\u0435\\s+(\u043B\u043E\u0440|\u043C\u043E\u0434\u0435\u043B|\u0447\u0435\u043A\u043F\u043E\u0438\u043D\u0442)|\u0441\u043F\u0438\u0441\u043E\u043A\\s+\u043B\u043E\u0440|\u0433\u0434\u0435\\s+\u043D\u0430\u0441\u0442\u0440\u043E\u0439|\u0447\u0442\u043E\\s+\u0437\u043D\u0430\u0447\u0438\u0442|\u043D\u0440\u0430\u0432\u0438\u0442|\u0441\u043F\u0430\u0441\u0438\u0431\u043E|\u0431\u043B\u0430\u0433\u043E\u0434\u0430\u0440|\u043F\u043E\u0447\u0435\u043C\u0443\\s+\u0442\u0430\u043A|\u0447\u0442\u043E\\s+\u0442\u044B\\s+(\u0441\u0434\u0435\u043B\u0430\u043B|\u0438\u0437\u043C\u0435\u043D\u0438\u043B)|\u0442\u043E\u043B\u044C\u043A\u043E\\s+(\u043E\u0442\u0432\u0435\u0442\u044C|\u0441\u043A\u0430\u0436\u0438|\u043E\u0431\u044A\u044F\u0441\u043D\u0438)|\u0431\u0435\u0437\\s+(\u043A\u0430\u0434\u0440|\u0433\u0435\u043D\u0435\u0440\u0430\u0446)|\u043D\u0435\\s+\u043D\u0430\u0434\u043E\\s+\u043A\u0430\u0434\u0440" + ).test(t) && !userAsksGenerate(t) && !userAsksContinue(t)) { + return true; + } + return false; + } + function userImpliesGenerate(text) { + const t = String(text || "").trim(); + if (!t || userAsksNoGenerate(t) || userIsChatNotFrame(t)) { + return false; + } + if (userCommandsGenerate(t)) { + return true; + } + if (t.length < 8) { + return false; + } + const wantsLook = userAsksLook(t); + const wantsRedraw = cyrTokenRe("\u043F\u043E\u043F\u0440\u0430\u0432\u044C|\u0438\u0441\u043F\u0440\u0430\u0432\u044C|\u043F\u0435\u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0438\u0440|\u043F\u0435\u0440\u0435\u0440\u0438\u0441\u0443\u0439|\u0443\u043B\u0443\u0447\u0448\u0438|\u043F\u0435\u0440\u0435\u0434\u0435\u043B\u0430\u0439").test(t) || /\b(fix|redo|redraw|improve)\b/i.test(t); + if (wantsLook && !wantsRedraw) { + return false; + } + if (cyrTokenRe( + "\u043D\u0430\u0440\u0438\u0441\u0443|\u0441\u0433\u0435\u043D\u0435\u0440|\u043F\u0435\u0440\u0435\u0440\u0438\u0441\u0443|\u0441\u0434\u0435\u043B\u0430\u0439\\s+(\u043A\u0430\u0440\u0442\u0438\u043D\u043A|\u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D|\u0444\u043E\u0442\u043E|\u043A\u0430\u0434\u0440)|\u0445\u043E\u0447\u0443\\s+(\u043A\u0430\u0440\u0442\u0438\u043D\u043A|\u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D|\u0444\u043E\u0442\u043E|\u0443\u0432\u0438\u0434\u0435\u0442\u044C|\u0432\u0438\u0434\u0435\u0442\u044C)|\u043F\u043E\u043A\u0430\u0436\u0438\\s+\u043A\u0430\u043A\\s+(\u043E\u043D\u0430|\u043E\u043D|\u044D\u0442\u043E)|\u0441\u0434\u0435\u043B\u0430\u0439\\s+(\u0435\u0451|\u0435\u0435|\u0435\u0433\u043E|\u043C\u043D\u0435)\\s|\u043F\u0443\u0441\u0442\u044C\\s+\u0431\u0443\u0434\u0435\u0442|\u0434\u0440\u0443\u0433\u043E\u0439\\s+(\u0440\u0430\u043A\u0443\u0440\u0441|\u0441\u0432\u0435\u0442|\u043D\u0430\u0440\u044F\u0434|\u043F\u043E\u0437\u0430)|\u043F\u043E\u043C\u0435\u043D\u044F\u0439\\s+(\u043F\u043E\u0437\u0443|\u0441\u0432\u0435\u0442|\u043E\u0434\u0435\u0436\u0434|\u0444\u043E\u043D)|\u0434\u043E\u0431\u0430\u0432\u044C\\s+(\u0441\u0432\u0435\u0442|\u0434\u0435\u0442\u0430\u043B)|\u0435\u0449\u0451\\s+\u043E\u0434\u043D|\u0435\u0449\u0435\\s+\u043E\u0434\u043D" + ).test(t)) { + return true; + } + if (/\b(draw|paint|render|make her|make him|another one|new frame)\b/i.test(t)) { + return true; + } + const isQuestion = /[??]\s*$/.test(t); + if (isQuestion) { + return cyrTokenRe("\u043D\u0430\u0440\u0438\u0441\u0443|\u0441\u0433\u0435\u043D\u0435\u0440|\u043C\u043E\u0436\u0435\u0448\u044C\\s+(\u0441\u0434\u0435\u043B\u0430\u0442\u044C|\u043D\u0430\u0440\u0438\u0441\u043E\u0432\u0430\u0442\u044C)|\u043C\u043E\u0436\u043D\u043E\\s+(\u043A\u0430\u0440\u0442\u0438\u043D\u043A|\u0441\u0433\u0435\u043D\u0435\u0440)").test(t); + } + return false; + } + function packBlocksAutoGenerate(pack) { + const p = String(pack || ""); + return p === "describe_ref" || p === "catalog_card" || p === "author_persona" || p === "debug_explain"; + } + function packWantsVision(pack) { + const p = String(pack || ""); + return p === "critique_image" || p === "describe_ref" || p === "compose_scene" || p === "inpaint_edit"; + } + function resolveTurnIntent(patch, userText, opts = {}) { + const machine = isMachineTurn(opts); + const vetoed = !machine && userAsksNoGenerate(userText); + const commanded = !!opts.userWantsGenerate || !machine && userCommandsGenerate(userText); + const implied = !machine && userImpliesGenerate(userText); + const modelAsked = Array.isArray(patch?.actions) && patch.actions.map(String).includes("generate"); + let generate; + if (vetoed || opts.fromAutoCritique) { + generate = false; + } else if (commanded) { + generate = true; + } else if (packBlocksAutoGenerate($("sa_pack")?.value || "")) { + generate = false; + } else { + generate = modelAsked || implied; + } + const hasLook = !!patch && (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null); + const honorLook = opts.fromAutoCritique || opts.fromVisionHop || !machine && userAsksLook(userText) || packWantsVision($("sa_pack")?.value); + const look = !!(hasLook && !vetoed && !generate && honorLook); + return { generate, look, vetoed }; + } + function stripGenerateAction(patch) { + if (!patch || typeof patch !== "object") { + return patch; + } + if (!Array.isArray(patch.actions)) { + return patch; + } + const next = patch.actions.map(String).filter((a) => a !== "generate"); + if (next.length === patch.actions.length) { + return patch; + } + const out = { ...patch }; + if (next.length) { + out.actions = next; + } else { + delete out.actions; + } + return out; + } + function stripLookAt(patch) { + if (!patch || typeof patch !== "object") { + return patch; + } + if (patch.look_at == null && patch.vision_from == null && patch.vision_slots == null) { + return patch; + } + const out = { ...patch }; + delete out.look_at; + delete out.vision_from; + delete out.vision_slots; + return out; + } + function rememberLastPatch(patch) { + if (patch && typeof patch === "object" && !isCardObject2(patch)) { + state.lastPatch = patch; + syncBuildGenButton(); + } + } + function syncBuildGenButton() { + const btn = $("sa_btn_build_gen"); + if (!btn) { + return; + } + if (state.lastPatch) { + const keys = Object.keys(state.lastPatch).filter((k) => state.lastPatch[k] != null).slice(0, 6); + btn.title = `\u0415\u0441\u0442\u044C \u043F\u0430\u0442\u0447 Assistent (${keys.join(", ") || "\u2026"}) \u2192 Apply + Generate`; + btn.classList.add("sa-has-patch"); + } else { + btn.title = "\u041D\u0435\u0442 \u043F\u0430\u0442\u0447\u0430 \u2014 Generate \u0441 \u0442\u0435\u043A\u0443\u0449\u0438\u043C \u043F\u0440\u043E\u043C\u043F\u0442\u043E\u043C SwarmUI"; + btn.classList.remove("sa-has-patch"); + } + } + function defaultPackId() { + return state.config?.assistant?.default_pack || $("sa_pack")?.querySelector("option")?.value || "ordinary"; + } + function syncModeBadge() { + const badge = $("sa_mode_badge"); + const pack = $("sa_pack")?.value || defaultPackId(); + if (!badge) { + return; + } + const shortMap = { + ordinary: "\u043E\u0431\u044B\u0447\u043D\u044B\u0439", + write_prompt: "write", + critique_image: "critique", + compose_scene: "compose", + fix_params: "params", + inpaint_edit: "inpaint", + describe_ref: "describe", + catalog_card: "card", + author_persona: "persona" + }; + const short = shortMap[pack] || pack.replace(/_/g, " ").slice(0, 12); + badge.textContent = short; + badge.dataset.pack = pack; + badge.title = `\u0420\u0435\u0436\u0438\u043C: ${pack}`; + badge.classList.toggle("sa-mode-hot", pack === "critique_image" || pack === "inpaint_edit"); + } + function syncLiveParamsBar() { + const el = $("sa_live_params"); + if (!el) { + return; + } + const w = parseInt(val("input_width") || "0", 10) || null; + const h = parseInt(val("input_height") || "0", 10) || null; + const aspect = guessAspectFromSize(w, h) || "\u2014"; + const steps = val("input_steps") || "\u2014"; + const cfg = val("input_cfgscale") || val("input_cfg") || "\u2014"; + const seed = val("input_seed") || "\u2014"; + const profile = detectKreaProfileName(); + el.textContent = `${aspect} \xB7 ${w || "?"}\xD7${h || "?"} \xB7 steps ${steps} \xB7 cfg ${cfg} \xB7 ${profile} \xB7 seed ${seed}`; + } + function applyAspectTableFrom(obj) { + if (!obj || typeof obj !== "object") { + return false; + } + const next = {}; + for (const [k, v] of Object.entries(obj)) { + if (Array.isArray(v) && v.length >= 2) { + next[k] = [Number(v[0]), Number(v[1])]; + } + } + if (!Object.keys(next).length) { + return false; + } + ASPECT_TABLE = next; + return true; + } + function resolveExactBundle() { + const exact = state.exact || state.config?.exact || {}; + const profiles = exact.profiles || state.kreaProfiles || {}; + return { exact, profiles }; + } + function detectKreaProfileName() { + try { + const model = resolveCurrentCheckpoint(); + const blob = `${model?.name || ""} ${model?.title || ""}`.toLowerCase(); + const hasRaw = /\braw\b/.test(blob); + const hasTurbo = /\bturbo\b/.test(blob); + return hasRaw && !hasTurbo ? "raw" : "turbo"; + } catch (e) { + return state.exact?.generation?.profile || "turbo"; + } + } + function mergedGenerationDefaults(profileName) { + const { exact, profiles } = resolveExactBundle(); + const gen = exact.generation && typeof exact.generation === "object" ? { ...exact.generation } : {}; + const profile = profileName || gen.profile || detectKreaProfileName(); + const fromProfile = profiles[profile] && typeof profiles[profile] === "object" ? { ...profiles[profile] } : {}; + const session = state.sessionExact && typeof state.sessionExact === "object" ? { ...state.sessionExact } : {}; + return { ...gen, ...fromProfile, profile, ...session }; + } + function exactDefaultFor(key, profileName) { + const { exact, profiles } = resolveExactBundle(); + const profile = profileName || exact.generation?.profile || detectKreaProfileName(); + const fromProfile = profiles[profile]?.[key]; + if (fromProfile != null) { + return fromProfile; + } + return exact.generation?.[key]; + } + function rememberSessionExact(partial) { + if (state.restoringChat || !partial || typeof partial !== "object") { + return; + } + const keys = ["steps", "cfg", "sigma_shift", "aspect", "width", "height", "images", "batch", "seed", "sampler", "scheduler"]; + for (const k of keys) { + if (partial[k] != null) { + state.sessionExact[k] = partial[k]; + } + } + if (partial.images == null && partial.batch != null) { + state.sessionExact.images = partial.batch; + } + } + function shouldRememberSessionParam(key, value) { + if (state.restoringChat || value == null) { + return false; + } + if (state.lastUserParamIntent) { + return true; + } + const exactVal = exactDefaultFor(key); + if (exactVal == null) { + return true; + } + return String(value) !== String(exactVal); + } + 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)); + } + } + if (!liveNegativePrompt()) { + const neg = exactDefaultNegative(); + if (neg) { + setNegativePrompt(neg); + } + } + } + function shouldSkipSessionRollback(key, patchValue) { + if (state.restoringChat) { + return false; + } + if (state.lastUserParamIntent) { + return false; + } + if (state.sessionExact[key] == null) { + return false; + } + const sessionVal = state.sessionExact[key]; + if (String(sessionVal) === String(patchValue)) { + return false; + } + const exactVal = exactDefaultFor(key); + if (exactVal == null) { + return false; + } + return String(patchValue) === String(exactVal); + } + function openAssistentTab() { + const tab = document.getElementById(TAB_BUTTON_ID); + if (tab) { + tab.click(); + setTimeout(() => $("sa_input")?.focus(), 50); + return true; + } + const pane = document.getElementById("assistent"); + if (pane && typeof bootstrap !== "undefined" && bootstrap.Tab) { + try { + bootstrap.Tab.getOrCreateInstance(tab || pane).show(); + } catch (e) { + } + } + setTimeout(() => $("sa_input")?.focus(), 50); + return !!tab; + } + function historyMessageLimit() { + const turns = Math.max(1, Number(HISTORY_KEEP_TURNS) || 4); + return turns * 2; + } + function flashImagePane(slotId) { + const el = document.querySelector(`.sa-slot[data-id="${slotId || state.selectedSlotId}"]`); + if (!el) { + return; + } + el.classList.remove("sa-flash"); + void el.offsetWidth; + el.classList.add("sa-flash"); + } + function ensureBoard() { + if (state.slots.length) { + return; + } + state.slots = [ + { id: GEN_ID, type: "generate", label: "Generate", src: null, attach: false }, + { id: "ref1", type: "ref", label: "Ref 1", src: null, attach: true } + ]; + state.refSeq = 1; + state.selectedSlotId = "ref1"; + } + function slotById(id) { + ensureBoard(); + const key = normalizeSlotId(id); + return state.slots.find((s) => s.id === key) || null; + } + function generateSlot() { + return slotById(GEN_ID); + } + function refSlots() { + ensureBoard(); + return state.slots.filter((s) => s.type === "ref"); + } + function normalizeSlotId(id) { + const raw = String(id || "").trim().toLowerCase(); + if (!raw) { + return ""; + } + if (raw === "gen" || raw === "current" || raw === "live" || raw === "generation") { + return GEN_ID; + } + if (raw === "selected" || raw === "sel") { + return state.selectedSlotId; + } + const m = raw.match(/^ref\s*[_-]?\s*(\d+)$/); + if (m) { + return `ref${m[1]}`; + } + return raw; + } + function selectedSlot() { + return slotById(state.selectedSlotId) || generateSlot(); + } + function selectedSrc() { + return selectedSlot()?.src || null; + } + function syncLastImageAlias() { + const attached = attachableSlots(); + state.lastImageDataUrl = (attached[0] || selectedSlot() || generateSlot())?.src || null; + } + function attachableSlots() { + ensureBoard(); + return state.slots.filter((s) => s.attach && s.src); + } + function visionReadySlots() { + ensureBoard(); + return state.slots.filter((s) => s && s.src && !looksLikeModelPreview(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("\u041F\u0440\u043E\u043F\u0443\u0441\u043A \u043F\u0440\u0435\u0432\u044C\u044E \u043C\u043E\u0434\u0435\u043B\u0438 (\u043D\u0443\u0436\u043D\u0430 \u0440\u0435\u0430\u043B\u044C\u043D\u0430\u044F \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044F)"); + 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 \u2014 use Snapshot gen to copy it"); + } + return; + } + slot.src = null; + slot.attach = true; + syncLastImageAlias(); + renderBoard(); + if (!silent) { + setStatus(`${slot.label} cleared`); + } + } + function snapshotGenerateToRef() { + const src = generateSlot()?.src && !looksLikeModelPreview(generateSlot().src) ? generateSlot().src : findCurrentGenerateSrc({ allowPreview: false }); + if (!src) { + setStatus("\u041D\u0435\u0442 \u0442\u0435\u043A\u0443\u0449\u0435\u0433\u043E \u043A\u0430\u0434\u0440\u0430 Generate (\u043F\u0440\u0435\u0432\u044C\u044E \u043C\u043E\u0434\u0435\u043B\u0438 \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F)"); + return false; + } + const empty = refSlots().find((s) => !s.src); + let ok = false; + if (empty) { + ok = setSlotSrc(empty.id, src, { note: `\u0421\u043D\u0438\u043C\u043E\u043A \u2192 ${empty.label}` }); + } else { + const created = addRefSlot({ src, select: true }); + if (created?.src) { + setStatus(`\u0421\u043D\u0438\u043C\u043E\u043A \u2192 ${created.label}`); + flashImagePane(created.id); + ok = true; + } else { + const last = refSlots()[refSlots().length - 1]; + if (last) { + ok = setSlotSrc(last.id, src, { note: `\u0421\u043D\u0438\u043C\u043E\u043A \u2192 ${last.label} (\u0437\u0430\u043C\u0435\u043D\u0430)` }); + } + } + } + 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 isSwarmGenerateRunning() { + try { + if (typeof num_live_gens === "number" && num_live_gens > 0) { + return true; + } + if (typeof num_waiting_gens === "number" && num_waiting_gens > 0) { + return true; + } + } catch (e) { + } + try { + if (typeof mainGenHandler !== "undefined" && mainGenHandler) { + if (mainGenHandler.isGenerating === true || mainGenHandler.running === true) { + return true; + } + } + } catch (e) { + } + 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 isGenerateUnavailable() { + if (state.generating || state.busy) { + return true; + } + return isSwarmGenerateRunning(); + } + function syncGenerateBusy() { + const overlay = document.querySelector(".sa-slot-gen .sa-slot-busy"); + if (overlay) { + const stuck = state.generating && !isSwarmGenerateRunning(); + overlay.hidden = !state.generating && state.busyPhase !== "generating" || stuck; + } + const running = state.generating || state.busyPhase === "generating"; + document.querySelectorAll(".sa-slot-gen-result").forEach((el) => { + const busy = el.querySelector(".sa-slot-busy"); + if (!busy) { + return; + } + const hasImg = el.classList.contains("sa-has-image"); + busy.hidden = !running || hasImg; + }); + } + 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 \u2014 use the latest proposal"; + el.replaceWith(note); + }); + } + function mountPatchBlock(host, patch, { silent = false } = {}) { + if (!host || !patch) { + return; + } + rememberLastPatch(patch); + const wrap = document.createElement("div"); + wrap.className = "sa-patch" + (silent ? " sa-patch-auto" : ""); + const details = document.createElement("details"); + details.className = "sa-patch-details"; + const summary = document.createElement("summary"); + const keys = Object.keys(patch).filter((k) => patch[k] != null && k !== "notes" && k !== "actions"); + summary.textContent = silent ? `\u041F\u0430\u0442\u0447 \u043F\u0440\u0438\u043C\u0435\u043D\u0451\u043D \xB7 ${keys.slice(0, 6).join(", ") || "generate"}` : `JSON \u043F\u0430\u0442\u0447 \xB7 ${keys.slice(0, 8).join(", ") || "\u2026"}`; + const pre = document.createElement("pre"); + pre.textContent = JSON.stringify(patch, null, 2); + details.appendChild(summary); + details.appendChild(pre); + wrap.appendChild(details); + mountPatchActions(wrap, patch, { silent }); + host.appendChild(wrap); + } + function mountPatchActions(parent, patch, { silent = false } = {}) { + if (!parent || !patch) { + return; + } + rememberLastPatch(patch); + retireStalePatchActions(); + const wrap = parent.classList.contains("sa-patch") ? parent : null; + const host = wrap || parent; + if (silent) { + const note = document.createElement("div"); + note.className = "sa-patch-actions sa-patch-silent sa-patch-current"; + const willGen = Array.isArray(patch.actions) && patch.actions.map(String).includes("generate") || !!state.pendingSilentGen; + note.textContent = willGen ? "\u041F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u043E \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \xB7 Generate\u2026" : "\u041F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u043E \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438"; + host.appendChild(note); + return; + } + const actions = document.createElement("div"); + actions.className = "sa-patch-actions sa-patch-current"; + for (const [label, which] of [ + ["\u041F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C \u0432\u0441\u0451", "all"], + ["\u041F\u0440\u043E\u043C\u043F\u0442", "prompt"], + ["LoRAs", "loras"], + ["\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B", "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 = "\u041F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C + Generate"; + genBtn.addEventListener("click", async () => { + if (isGenerateUnavailable()) { + return; + } + startBusyUi("silent_gen"); + await applyPatch(patch, "all"); + await runGenerateFromPatch({ ...patch, actions: ["generate"] }, { force: true }); + }); + actions.appendChild(genBtn); + host.appendChild(actions); + syncPatchActionAvailability(); + } + async function buildCurrentAndGenerate() { + if (state.busy || state.generating) { + setStatus("\u0417\u0430\u043D\u044F\u0442\u043E \u2014 \u043F\u043E\u0434\u043E\u0436\u0434\u0438 \u0438\u043B\u0438 \u043D\u0430\u0436\u043C\u0438 \u0421\u0442\u043E\u043F"); + return; + } + if (isGenerateUnavailable()) { + setStatus("Generate \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u2014 \u0434\u043E\u0436\u0434\u0438\u0441\u044C SwarmUI"); + return; + } + const patch = state.lastPatch; + if (patch) { + startBusyUi("silent_gen"); + setStatus("\u0421\u043E\u0431\u0438\u0440\u0430\u044E \u043F\u0430\u0442\u0447 \u2192 Generate\u2026"); + await applyPatch(patch, "all"); + syncLiveParamsBar(); + await runGenerateFromPatch({ ...patch, actions: ["generate"] }, { force: true }); + return; + } + startBusyUi("generating"); + setStatus("Generate \u0441 \u0442\u0435\u043A\u0443\u0449\u0438\u043C \u043F\u0440\u043E\u043C\u043F\u0442\u043E\u043C\u2026"); + await runGenerateFromPatch({ actions: ["generate"] }, { force: true }); + } + function renderBoard() { + const board = $("sa_board"); + if (!board) { + return; + } + ensureBoard(); + const tab = state.boardTab === "refs" ? "refs" : "generate"; + const refs = refSlots(); + const showVariantGrid = tab === "generate" && (state.genResults || []).length > 1; + board.classList.toggle("sa-board-many", tab === "refs" && (refs.some((s) => s.src) || refs.length > 1) || showVariantGrid); + board.classList.toggle("sa-board-gen-only", tab === "generate" && !showVariantGrid); + board.classList.toggle("sa-board-variants", showVariantGrid); + board.innerHTML = ""; + if (tab === "generate" && showVariantGrid) { + for (const row of state.genResults) { + board.appendChild(buildGenResultEl(row)); + } + } else { + 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 = "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043E\u043A\u043D\u043E \u0440\u0435\u0444\u0435\u0440\u0435\u043D\u0441\u0430"; + 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 buildGenResultEl(row) { + const el = document.createElement("div"); + el.className = "sa-slot sa-slot-gen-result"; + el.dataset.genResultId = row.id; + if (row.src) { + el.classList.add("sa-has-image"); + } + if (row.id === state.selectedGenResultId) { + 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 sa-live"; + chip.textContent = row.label || row.id; + bar.appendChild(chip); + const openBtn = document.createElement("button"); + openBtn.type = "button"; + openBtn.className = "sa-slot-open"; + openBtn.textContent = "\u041E\u0442\u043A\u0440\u044B\u0442\u044C"; + openBtn.title = "\u041F\u0440\u043E\u0441\u043C\u043E\u0442\u0440"; + openBtn.hidden = !row.src; + openBtn.addEventListener("click", (e) => { + e.stopPropagation(); + selectGenResult(row.id, { restore: true, openViewer: true }); + }); + bar.appendChild(openBtn); + el.appendChild(bar); + if (row.src) { + const img = document.createElement("img"); + img.alt = row.label || row.id; + img.src = row.src; + el.appendChild(img); + } else { + const empty = document.createElement("div"); + empty.className = "sa-image-empty"; + empty.innerHTML = '
\u2026
\u0416\u0434\u0443 \u043A\u0430\u0434\u0440
'; + el.appendChild(empty); + } + const busy = document.createElement("div"); + busy.className = "sa-slot-busy"; + const pending = !row.src && (state.generating || state.busyPhase === "generating"); + busy.hidden = !pending; + busy.innerHTML = ''; + el.appendChild(busy); + el.addEventListener("click", () => { + const already = row.id === state.selectedGenResultId; + selectGenResult(row.id, { restore: true, openViewer: already && !!row.src }); + }); + el.addEventListener("dblclick", (e) => { + e.preventDefault(); + if (row.src) { + selectGenResult(row.id, { restore: true, openViewer: true }); + } + }); + return el; + } + function ensureGenLightbox() { + let root = $("sa_gen_lightbox"); + if (root) { + return root; + } + const host = $("swarm_assistent_root") || document.body; + root = document.createElement("div"); + root.id = "sa_gen_lightbox"; + root.className = "sa-lightbox"; + root.hidden = true; + root.innerHTML = ` +
+ `; + host.appendChild(root); + root.addEventListener("click", async (e) => { + const act = e.target?.closest?.("[data-lb]")?.getAttribute("data-lb"); + if (!act) { + return; + } + e.preventDefault(); + e.stopPropagation(); + if (act === "close") { + closeGenLightbox(); + } else if (act === "prev") { + stepGenLightbox(-1); + } else if (act === "next") { + stepGenLightbox(1); + } else if (act === "to_ref") { + const row = currentLightboxRow(); + if (row?.src) { + const created = addRefSlot({ select: true }); + if (created) { + created.src = row.src; + setBoardTab("refs"); + renderBoard(); + setStatus(`\u0421\u043D\u0438\u043C\u043E\u043A \u2192 ${created.label}`); + } + } + } else if (act === "as_init") { + const row = currentLightboxRow(); + if (row?.src) { + await setInitFromSrc(row.src); + } + } + }); + return root; + } + function currentLightboxRow() { + const list = (state.genResults || []).filter((r) => r.src); + if (!list.length || state.lightboxIndex < 0) { + return null; + } + return list[state.lightboxIndex] || null; + } + function syncGenLightbox() { + const root = ensureGenLightbox(); + const list = (state.genResults || []).filter((r) => r.src); + const row = list[state.lightboxIndex]; + if (!row) { + root.hidden = true; + return; + } + root.hidden = false; + const img = $("sa_lb_img"); + const title = $("sa_lb_title"); + const idx = $("sa_lb_idx"); + if (img) { + img.src = row.src; + img.alt = row.label || row.id; + } + if (title) { + title.textContent = row.label || row.id; + } + if (idx) { + idx.textContent = `${state.lightboxIndex + 1} / ${list.length}`; + } + } + function openGenLightbox(id) { + const list = (state.genResults || []).filter((r) => r.src); + let idx = list.findIndex((r) => r.id === id); + if (idx < 0) { + idx = 0; + } + if (!list.length) { + return; + } + state.lightboxIndex = idx; + ensureGenLightbox(); + syncGenLightbox(); + selectGenResult(list[idx].id, { restore: true, openViewer: false }); + } + function closeGenLightbox() { + state.lightboxIndex = -1; + const root = $("sa_gen_lightbox"); + if (root) { + root.hidden = true; + } + } + function stepGenLightbox(delta) { + const list = (state.genResults || []).filter((r) => r.src); + if (list.length < 2) { + return; + } + state.lightboxIndex = (state.lightboxIndex + delta + list.length) % list.length; + const row = list[state.lightboxIndex]; + if (row) { + selectGenResult(row.id, { restore: true, openViewer: false }); + } + syncGenLightbox(); + } + 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} \xB7 vision ${withVision}` : String(withImg); + } else { + badge.hidden = true; + } + } + let genBadge = $("sa_gen_badge"); + if (!genBadge) { + const genTab = $("sa_board_tab_gen"); + if (genTab) { + genBadge = document.createElement("span"); + genBadge.id = "sa_gen_badge"; + genBadge.className = "sa-board-badge"; + genBadge.hidden = true; + genTab.appendChild(genBadge); + } + } + if (genBadge) { + const n = finishedGenResultCount(); + if (n > 1) { + genBadge.hidden = false; + genBadge.textContent = String(n); + } else { + genBadge.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) { + } + } + 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
\u0416\u0438\u0432\u043E\u0439 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438
' : '
Reference
Drop \xB7 paste \xB7 \u0421\u043D\u0438\u043C\u043E\u043A 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; + } + 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 === src) { + if (state.generating && !isSwarmGenerateRunning()) { + const img = document.querySelector(".sa-slot-gen img"); + if (img) { + const bump = src.includes("?") ? `${src}&sa_t=${Date.now()}` : `${src}?sa_t=${Date.now()}`; + img.src = bump; + } + } + } 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); + scrollMessagesToBottom({ force: true }); + } + function chatUid() { + return `c_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; + } + function stripJsonFencesForHistory(content) { + return String(content || "").replace(/```(?:json)?\s*[\s\S]*?```/gi, "").replace(/###\s*Critique\b[\s\S]*?(?=###|$)/gi, "").replace(/###\s*JSON\s*Patch\b[\s\S]*$/gi, "").replace(/\n{3,}/g, "\n\n").trim(); + } + function slimHistoryMessages(list) { + return (list || []).filter((m) => m && (m.role === "user" || m.role === "assistant") && !m.systemish).slice(-MAX_CHAT_MSGS).map((m) => { + let content = String(m.content || ""); + if (m.role === "assistant") { + content = stripJsonFencesForHistory(content); + } + return { + role: m.role, + content: content.slice(0, 4e3), + persona: m.persona || void 0, + pack: m.pack || void 0 + }; + }); + } + function titleFromMessages(messages) { + const u = (messages || []).find((m) => m.role === "user" && m.content); + const t = String(u?.content || "").replace(/\s+/g, " ").trim(); + return t ? t.slice(0, 52) : "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442"; + } + function snapshotChatParams() { + let loras = []; + try { + if (typeof loraHelper !== "undefined" && loraHelper && Array.isArray(loraHelper.selected)) { + loras = loraHelper.selected.map((l) => ({ + name: l.name || l, + weight: loraHelper.loraWeightPref && loraHelper.loraWeightPref[l.name || l] || 1 + })); + } + } catch (e) { + } + let lastPatch = null; + try { + lastPatch = state.lastPatch ? JSON.parse(JSON.stringify(state.lastPatch)) : null; + } catch (e) { + lastPatch = null; + } + let sessionExact = {}; + try { + sessionExact = state.sessionExact && typeof state.sessionExact === "object" ? JSON.parse(JSON.stringify(state.sessionExact)) : {}; + } catch (e) { + sessionExact = {}; + } + return { + prompt: val("alt_prompt_textbox") || val("input_prompt") || "", + negative: val("input_negativeprompt") || val("alt_negativeprompt_textbox") || "", + width: parseInt(val("input_width") || "0", 10) || null, + height: parseInt(val("input_height") || "0", 10) || null, + steps: parseInt(val("input_steps") || "0", 10) || null, + cfg: parseFloat(val("input_cfgscale") || val("input_cfg") || "") || null, + sigma_shift: parseFloat(val("input_sigmashift") || "") || null, + seed: val("input_seed") || null, + sampler: val("input_sampler") || null, + scheduler: val("input_scheduler") || null, + batch: parseInt(val("input_images") || val("input_batchsize") || "0", 10) || null, + loras, + persona: $("sa_persona")?.value || "neutral", + pack: $("sa_pack")?.value || defaultPackId(), + sessionExact, + lastPatch, + genResults: Array.isArray(state.genResults) ? state.genResults.map((r) => ({ + id: r.id, + label: r.label, + src: r.src || null, + patch: r.patch || null + })) : [], + selectedGenResultId: state.selectedGenResultId || null + }; + } + async function restoreChatParams(params) { + state.restoringChat = true; + try { + state.sessionExact = {}; + state.lastPatch = null; + state.lastUserParamIntent = false; + if (!params || typeof params !== "object") { + clearGenResults(); + syncBuildGenButton(); + syncLiveParamsBar(); + syncModeBadge(); + renderBoard(); + return { restored: false }; + } + const promptBox = document.getElementById("alt_prompt_textbox") || document.getElementById("input_prompt"); + if (promptBox) { + promptBox.value = params.prompt != null ? String(params.prompt) : ""; + promptBox.dispatchEvent(new Event("input", { bubbles: true })); + promptBox.dispatchEvent(new Event("change", { bubbles: true })); + } + setVal("input_negativeprompt", params.negative != null ? String(params.negative) : ""); + if (document.getElementById("alt_negativeprompt_textbox")) { + setVal("alt_negativeprompt_textbox", params.negative != null ? String(params.negative) : ""); + } + if (params.width != null) { + setVal("input_width", String(params.width)); + } + if (params.height != null) { + setVal("input_height", String(params.height)); + } + if (params.steps != null) { + setVal("input_steps", String(params.steps)); + } + if (params.cfg != null) { + if (document.getElementById("input_cfgscale")) { + setVal("input_cfgscale", String(params.cfg)); + } else { + setVal("input_cfg", String(params.cfg)); + } + } + if (params.sigma_shift != null) { + setVal("input_sigmashift", String(params.sigma_shift)); + } + if (params.seed != null && params.seed !== "") { + setVal("input_seed", String(params.seed)); + } + if (params.sampler) { + setVal("input_sampler", String(params.sampler)); + } + if (params.scheduler) { + setVal("input_scheduler", String(params.scheduler)); + } + if (params.batch != null) { + if (document.getElementById("input_images")) { + setVal("input_images", String(params.batch)); + } else if (document.getElementById("input_batchsize")) { + setVal("input_batchsize", String(params.batch)); + } + } + const loras = Array.isArray(params.loras) ? params.loras : []; + await applyPatch({ loras }, "loras"); + if (params.pack) { + setPackValue(params.pack, { flash: false }); + } + if (params.persona) { + await applyPersonaForChat(params.persona, { quiet: true }); + } + state.sessionExact = params.sessionExact && typeof params.sessionExact === "object" ? { ...params.sessionExact } : {}; + state.lastPatch = params.lastPatch || null; + if (Array.isArray(params.genResults) && params.genResults.length) { + state.genResults = params.genResults.map((r, i) => ({ + id: r.id || `var${i + 1}`, + label: r.label || `\u0412\u0430\u0440\u0438\u0430\u043D\u0442 ${i + 1}`, + src: r.src || null, + patch: r.patch || null + })); + state.selectedGenResultId = params.selectedGenResultId || state.genResults.find((r) => r.src)?.id || state.genResults[0]?.id || null; + const selected = state.genResults.find((r) => r.id === state.selectedGenResultId); + const gen = generateSlot(); + if (gen && selected?.src) { + gen.src = selected.src; + } + } else { + clearGenResults(); + } + syncBuildGenButton(); + syncLiveParamsBar(); + syncModeBadge(); + renderLoraChips(); + syncChipHighlight(); + renderBoard(); + return { restored: true }; + } finally { + state.restoringChat = false; + } + } + function applyPersonaForChat(personaId, { quiet = false } = {}) { + const id = String(personaId || "neutral").trim() || "neutral"; + return new Promise((resolve) => { + const sel = $("sa_persona"); + if (sel && [...sel.options].some((o) => o.value === id)) { + sel.value = id; + } + if (!quiet) { + onPersonaChanged(); + resolve(); + return; + } + saveSettings(); + if (typeof genericRequest !== "function") { + resolve(); + return; + } + const packKeep = $("sa_pack")?.value; + genericRequest( + "AssistentGetConfig", + { persona: id }, + (data) => { + applyConfigPayload(data, { applyDefaults: false }); + if (sel && [...sel.options].some((o) => o.value === id)) { + sel.value = id; + } + if (packKeep) { + setPackValue(packKeep, { flash: false }); + } + resolve(); + }, + 0, + () => resolve() + ); + }); + } + function persistChatsStore() { + try { + const chats = (state.chats || []).filter((c) => c && c.id && (c.id === state.activeChatId || (c.messages || []).length > 0)).slice().sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)).slice(0, MAX_CHATS).map((c) => ({ + id: c.id, + title: c.title || "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442", + createdAt: c.createdAt || Date.now(), + updatedAt: c.updatedAt || Date.now(), + messages: slimHistoryMessages(c.messages || []), + params: c.params || null + })); + state.chats = chats; + localStorage.setItem(LS_CHATS2, JSON.stringify({ version: 1, chats })); + saveActiveChatToDisk(); + } catch (e) { + console.warn("Assistent: persist chats failed", e); + try { + const slim = (state.chats || []).filter((c) => c && c.id && (c.id === state.activeChatId || (c.messages || []).length > 0)).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)).slice(0, 12).map((c) => ({ + ...c, + messages: slimHistoryMessages(c.messages).slice(-historyMessageLimit()).map((m) => ({ + ...m, + content: String(m.content || "").slice(0, 1500) + })) + })); + state.chats = slim; + localStorage.setItem(LS_CHATS2, JSON.stringify({ version: 1, chats: slim })); + saveActiveChatToDisk(); + } catch (e2) { + console.warn("Assistent: chats quota fallback failed", e2); + } + } + } + function saveActiveChatToDisk() { + const persist = diskPersist(); + if (!persist) { + return; + } + for (const chat of state.chats || []) { + if (!chat?.id || !(chat.messages || []).length) { + continue; + } + persist.saveChat(chat); + } + } + function loadChatsStore() { + state.chats = []; + try { + const raw = localStorage.getItem(LS_CHATS2); + if (raw) { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed?.chats)) { + state.chats = parsed.chats.filter((c) => c && c.id); + } + } + } catch (e) { + } + } + async function loadChatsFromDisk() { + const persist = diskPersist(); + if (!persist) { + return; + } + let diskChats = null; + try { + diskChats = await persist.loadChats(); + } catch (e) { + console.warn("Assistent: disk chats failed", e); + return; + } + if (!Array.isArray(diskChats)) { + return; + } + const byId = /* @__PURE__ */ new Map(); + for (const c of state.chats || []) { + if (c?.id) { + byId.set(c.id, c); + } + } + for (const c of diskChats) { + if (!c?.id) { + continue; + } + const prev = byId.get(c.id); + if (!prev || (c.updatedAt || 0) >= (prev.updatedAt || 0)) { + byId.set(c.id, c); + } + } + state.chats = [...byId.values()].sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)).slice(0, MAX_CHATS); + try { + localStorage.setItem(LS_CHATS2, JSON.stringify({ version: 1, chats: state.chats })); + } catch (e) { + } + } + function findChat(id) { + return (state.chats || []).find((c) => c.id === id) || null; + } + function saveActiveChatToStore({ dropEmpty = false } = {}) { + if (!state.activeChatId || state.restoringChat) { + return; + } + const chat = findChat(state.activeChatId); + if (!chat) { + return; + } + chat.messages = slimHistoryMessages(state.history); + chat.params = snapshotChatParams(); + chat.updatedAt = Date.now(); + chat.title = titleFromMessages(chat.messages); + if (dropEmpty && !chat.messages.length) { + state.chats = state.chats.filter((c) => c.id !== chat.id); + if (state.activeChatId === chat.id) { + state.activeChatId = null; + } + } + persistChatsStore(); + } + function resetMessagesUi(emptyHint) { + const box = $("sa_messages"); + if (!box) { + return; + } + box.innerHTML = ""; + const empty = document.createElement("div"); + empty.className = "sa-chat-empty"; + empty.id = "sa_chat_empty"; + empty.innerHTML = emptyHint || '
\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442
\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B Generate \u043E\u0441\u0442\u0430\u044E\u0442\u0441\u044F \u043A\u0430\u043A \u0441\u0435\u0439\u0447\u0430\u0441.
+ \u2014 \u0435\u0449\u0451 \u043E\u0434\u0438\u043D \u0447\u0430\u0442 \xB7 \u0418\u0441\u0442\u043E\u0440\u0438\u044F \u2014 \u0432\u0435\u0440\u043D\u0443\u0442\u044C\u0441\u044F \u043A \u043F\u0440\u043E\u0448\u043B\u043E\u043C\u0443 (\u0441 \u0435\u0433\u043E \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u0430\u043C\u0438).
'; + box.appendChild(empty); + } + function renderHistoryIntoUi(messages) { + const box = $("sa_messages"); + if (!box) { + return; + } + box.innerHTML = ""; + const list = slimHistoryMessages(messages); + if (!list.length) { + resetMessagesUi(); + return; + } + for (const m of list) { + if (m.role === "user") { + appendMessage("user", m.content, null, null, { historical: true }); + } else { + appendMessage("assistant", m.content, null, null, { + persona: m.persona ? { id: m.persona, title: m.persona } : null, + pack: m.pack, + historical: true + }); + } + } + } + function updateSessionLabel() { + const el = $("sa_session_label"); + if (!el) { + return; + } + const chat = findChat(state.activeChatId); + el.textContent = chat?.title || "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442"; + el.title = (chat?.title || "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442") + " \u2014 \u043A\u043B\u0438\u043A: \u0418\u0441\u0442\u043E\u0440\u0438\u044F"; + } + function savedChatsCount() { + return (state.chats || []).filter((c) => (c.messages || []).length > 0).length; + } + function syncHistoryBadge() { + const btn = $("sa_btn_chats"); + if (!btn) { + return; + } + const n = savedChatsCount(); + btn.textContent = n > 0 ? `\u0418\u0441\u0442\u043E\u0440\u0438\u044F (${n})` : "\u0418\u0441\u0442\u043E\u0440\u0438\u044F"; + btn.title = n > 0 ? `\u0421\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0445 \u0447\u0430\u0442\u043E\u0432: ${n}. \u041F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u0435\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B.` : "\u0418\u0441\u0442\u043E\u0440\u0438\u044F \u0447\u0430\u0442\u043E\u0432 (\u043F\u043E\u043A\u0430 \u043F\u0443\u0441\u0442\u043E)"; + } + function formatChatWhen(ts) { + if (!ts) { + return ""; + } + try { + return new Date(ts).toLocaleString(void 0, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); + } catch (e) { + return ""; + } + } + function chatMatchesQuery(chat, q) { + if (!q) { + return true; + } + const title = String(chat?.title || "").toLowerCase(); + if (title.includes(q)) { + return true; + } + const msgs = chat?.messages || []; + for (const m of msgs) { + if (String(m?.content || "").toLowerCase().includes(q)) { + return true; + } + } + return false; + } + function renderChatsList() { + const root = $("sa_chats_list"); + if (!root) { + return; + } + root.innerHTML = ""; + syncHistoryBadge(); + const q = (state.chatsQuery || "").trim().toLowerCase(); + let chats = (state.chats || []).slice().sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)).filter((c) => (c.messages || []).length > 0 || (c.messages_count || 0) > 0); + if (q) { + const local = chats.filter((c) => chatMatchesQuery(c, q)); + const seen = new Set(local.map((c) => c.id)); + const extra = (state.chatsSearchHits || []).filter((h) => h && h.id && !seen.has(h.id)); + chats = local.concat(extra); + } + if (!chats.length) { + root.innerHTML = q ? '
\u041D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u043D\u0430\u0448\u043B\u043E\u0441\u044C.
' : '
\u041F\u043E\u043A\u0430 \u043F\u0443\u0441\u0442\u043E. \u041D\u0430\u043F\u0438\u0448\u0438 \u0447\u0442\u043E-\u043D\u0438\u0431\u0443\u0434\u044C \u0432 \u0447\u0430\u0442 \u2014 \u043E\u043D \u043F\u043E\u044F\u0432\u0438\u0442\u0441\u044F \u0437\u0434\u0435\u0441\u044C. \u041F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u0432\u0435\u0440\u043D\u0451\u0442 \u0438 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F, \u0438 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B Generate.
'; + return; + } + for (const c of chats) { + const row = document.createElement("div"); + row.className = "sa-chat-row" + (c.id === state.activeChatId ? " sa-chat-row-active" : ""); + row.dataset.id = c.id; + const n = (c.messages || []).length || Number(c.messages_count) || 0; + const bits = []; + if (c.params?.width && c.params?.height) { + bits.push(`${c.params.width}\xD7${c.params.height}`); + } + if (c.params?.steps != null) { + bits.push(`steps ${c.params.steps}`); + } + if (c.params?.cfg != null) { + bits.push(`cfg ${c.params.cfg}`); + } + if (Array.isArray(c.params?.loras) && c.params.loras.length) { + bits.push(`LoRA ${c.params.loras.length}`); + } + const noParams = !c.params ? " \xB7 \u0431\u0435\u0437 \u0441\u043D\u0438\u043C\u043A\u0430 params" : ""; + row.innerHTML = ``; + root.appendChild(row); + } + } + function setChatsPanelOpen(open) { + state.chatsPanelOpen = !!open; + const panel = $("sa_chats_panel"); + const btn = $("sa_btn_chats"); + if (panel) { + panel.hidden = !state.chatsPanelOpen; + } + btn?.setAttribute("aria-expanded", state.chatsPanelOpen ? "true" : "false"); + if (state.chatsPanelOpen) { + saveActiveChatToStore(); + const search = $("sa_chats_search"); + if (search) { + search.value = state.chatsQuery || ""; + search.focus(); + } + renderChatsList(); + } + } + async function startNewChat({ saveCurrent = true, force = false } = {}) { + if (!force && (state.busy || state.generating)) { + setStatus("\u0417\u0430\u043D\u044F\u0442\u043E \u2014 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043A\u043E\u043D\u0446\u0430 \u043E\u0442\u0432\u0435\u0442\u0430 \u0438\u043B\u0438 \u0421\u0442\u043E\u043F"); + return; + } + if (force) { + abortInFlightWork({ status: "" }); + } + setChatsPanelOpen(false); + if (saveCurrent) { + saveActiveChatToStore({ dropEmpty: true }); + } + state.sessionExact = {}; + state.lastUserParamIntent = false; + state.pendingSilentGen = false; + state.lastPatch = null; + clearGenResults(); + const chat = { + id: chatUid(), + title: "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442", + createdAt: Date.now(), + updatedAt: Date.now(), + messages: [], + params: snapshotChatParams() + }; + state.chats.unshift(chat); + state.activeChatId = chat.id; + state.history = []; + resetTurnHops(); + state.packUserTouched = false; + state.pendingPersonaNote = null; + if (state.streamEl) { + try { + state.streamEl.remove(); + } catch (e) { + } + state.streamEl = null; + } + syncBuildGenButton(); + resetMessagesUi(); + persistChatsStore(); + updateSessionLabel(); + renderBoard(); + syncHistoryBadge(); + renderChatsList(); + setStatus("\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442 \u2014 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B Generate \u043A\u0430\u043A \u0441\u0435\u0439\u0447\u0430\u0441"); + maybeWelcome(); + } + async function switchToChat(id) { + if (!id || id === state.activeChatId) { + setChatsPanelOpen(false); + return; + } + if (state.busy || state.generating) { + setStatus("\u0417\u0430\u043D\u044F\u0442\u043E \u2014 \u043D\u0435\u043B\u044C\u0437\u044F \u0441\u043C\u0435\u043D\u0438\u0442\u044C \u0447\u0430\u0442 \u0441\u0435\u0439\u0447\u0430\u0441"); + return; + } + saveActiveChatToStore({ dropEmpty: true }); + let chat = findChat(id); + if (!chat || !(chat.messages || []).length) { + try { + const full = await diskPersist()?.getChat?.(id); + if (full) { + const idx = (state.chats || []).findIndex((c) => c.id === id); + if (idx >= 0) { + state.chats[idx] = full; + } else { + state.chats.unshift(full); + } + chat = full; + } + } catch (e) { + console.warn("Assistent: getChat failed", id, e); + } + } + if (!chat) { + setStatus("\u0427\u0430\u0442 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D"); + return; + } + state.activeChatId = chat.id; + state.history = slimHistoryMessages(chat.messages); + resetTurnHops(); + state.packUserTouched = false; + state.pendingPersonaNote = null; + state.pendingSilentGen = false; + state.lastUserParamIntent = false; + if (state.streamEl) { + try { + state.streamEl.remove(); + } catch (e) { + } + state.streamEl = null; + } + renderHistoryIntoUi(state.history); + const result = await restoreChatParams(chat.params); + updateSessionLabel(); + syncHistoryBadge(); + renderChatsList(); + setChatsPanelOpen(false); + setView("chat"); + if (result?.restored) { + setStatus(`\u0427\u0430\u0442 \xAB${chat.title}\xBB \xB7 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u044B`); + } else { + setStatus(`\u0427\u0430\u0442 \xAB${chat.title}\xBB \xB7 \u0441\u043D\u0438\u043C\u043E\u043A \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u2014 Generate \u043D\u0435 \u043C\u0435\u043D\u044F\u043B\u0441\u044F`); + } + } + function deleteChat(id) { + if (!id) { + return; + } + const wasActive = id === state.activeChatId; + state.chats = state.chats.filter((c) => c.id !== id); + if (wasActive) { + state.activeChatId = null; + } + diskPersist()?.deleteChat(id)?.catch?.((e) => console.warn("Assistent: disk delete failed", e)); + persistChatsStore(); + syncHistoryBadge(); + if (wasActive) { + startNewChat({ saveCurrent: false, force: true }); + } else { + renderChatsList(); + } + } + async function initChatSessions() { + loadChatsStore(); + await loadChatsFromDisk(); + startNewChat({ saveCurrent: false, force: true }); + syncHistoryBadge(); + renderChatsList(); + } + function persistHistory() { + if (state.restoringChat) { + return; + } + if (!state.activeChatId) { + const chat2 = { + id: chatUid(), + title: "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442", + createdAt: Date.now(), + updatedAt: Date.now(), + messages: [], + params: snapshotChatParams() + }; + state.chats.unshift(chat2); + state.activeChatId = chat2.id; + } + const chat = findChat(state.activeChatId); + if (!chat) { + return; + } + chat.messages = slimHistoryMessages(state.history); + chat.params = snapshotChatParams(); + chat.updatedAt = Date.now(); + chat.title = titleFromMessages(chat.messages); + persistChatsStore(); + updateSessionLabel(); + syncHistoryBadge(); + } + function clearPersistedHistory() { + if (state.activeChatId) { + const chat = findChat(state.activeChatId); + if (chat) { + chat.messages = []; + chat.title = "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442"; + chat.params = snapshotChatParams(); + chat.updatedAt = Date.now(); + } + persistChatsStore(); + } + updateSessionLabel(); + renderChatsList(); + } + function clearChatHistory() { + abortInFlightWork({ status: "" }); + state.history = []; + resetTurnHops(); + state.packUserTouched = false; + state.pendingPersonaNote = null; + state.sessionExact = {}; + state.lastUserParamIntent = false; + state.pendingSilentGen = false; + state.lastPatch = null; + clearGenResults(); + syncBuildGenButton(); + clearPersistedHistory(); + resetMessagesUi('
\u0427\u0430\u0442 \u043E\u0447\u0438\u0449\u0435\u043D
\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u0441\u0431\u0440\u043E\u0448\u0435\u043D\u044B. \u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B Generate \u043D\u0430 \u043C\u0435\u0441\u0442\u0435. + \u2014 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442 \u0432 \u0418\u0441\u0442\u043E\u0440\u0438\u044E, \u0418\u0441\u0442\u043E\u0440\u0438\u044F \u2014 \u043F\u0440\u043E\u0448\u043B\u044B\u0435 \u0434\u0438\u0430\u043B\u043E\u0433\u0438.
'); + setStatus("\u0427\u0430\u0442 \u043E\u0447\u0438\u0449\u0435\u043D"); + updateSessionLabel(); + syncHistoryBadge(); + renderBoard(); + } + 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())} \u2014 ${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(" ")) { + const token2 = text.split(/\s/)[0] || ""; + if (!token2.startsWith("/") || text.includes(" ") && !SLASH_COMMANDS.some((c) => c.cmd.startsWith(token2))) { + if (!(token2.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(`\u0422\u043E\u043D \u2192 ${title}`); + state.pendingPersonaNote = `Persona is now ${id} (${title}). Adopt this voice from now on.`; + if (data?.assistant?.default_pack && $("sa_pack") && !state.packUserTouched) { + const packId = data.assistant.default_pack; + if ([...$("sa_pack").options || []].some((o) => o.value === packId)) { + $("sa_pack").value = packId; + } + } + fillEmptyParamsFromExact(); + renderPersonaControls(data?.controls || {}, data?.control_values || data?.exact?.controls || {}); + syncPersonaDeleteButton(data?.persona_source || data?.personas?.find((p) => p.id === id)?.source); + if (state.view === "settings") { + if (state.settingsTab === "user") { + refreshUserPrefs(); + } + if (state.settingsTab === "craft") { + renderMemoryList(); + } + if (state.settingsTab === "more") { + fillKnobsFromConfig(data); + } + } + }); + } + 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) { + } + 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) { + } + 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) + "\u2026"; + } + 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: [], + available_checkpoints: slimInventoryCheckpoints(inv.checkpoints || [], 8), + wildcards: (inv.wildcards || []).map((w) => w.name || w).slice(0, 20), + inventory_at: inv.inventory_at || null, + has_vision_image: visionReadySlots().length > 0, + image_slots: slotCatalog(), + attached_slot_ids: attachableSlots().map((s) => s.id), + gen_results: (state.genResults || []).map((r) => ({ + id: r.id, + label: r.label, + has_image: !!r.src, + selected: r.id === state.selectedGenResultId + })), + selected_gen_result: state.selectedGenResultId || null, + 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: [], + user_prefs_count: 0, + ...initCtx + }; + { + const slim = slimInventoryLoras(inv.loras || [], INVENTORY_PROMPT_NAMES); + ctx.available_loras = slim; + if ((inv.loras || []).length > slim.length) { + ctx.available_loras_truncated = true; + ctx.available_loras_total = (inv.loras || []).length; + } + } + 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) { + } + try { + if (typeof loraHelper !== "undefined" && loraHelper && Array.isArray(loraHelper.selected)) { + const byName = /* @__PURE__ */ new Map(); + for (const l of inv.loras || []) { + if (l?.name) { + byName.set(String(l.name).toLowerCase(), l); + } + } + ctx.selected_loras = loraHelper.selected.map((l) => { + const name = l.name || l; + const invRow = byName.get(String(name).toLowerCase()) || {}; + const out = { + name, + weight: loraHelper.loraWeightPref && loraHelper.loraWeightPref[name] || invRow.default_weight || 1 + }; + if (invRow.trigger_phrase) { + out.trigger_phrase = invRow.trigger_phrase; + } + if (Array.isArray(invRow.triggers) && invRow.triggers.length) { + out.triggers = invRow.triggers.slice(0, 8); + } + if (invRow.blurb) { + out.blurb = invRow.blurb; + } + return out; + }); + } + } catch (e) { + } + const cardKeys = []; + const seenCard = /* @__PURE__ */ 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 k of cardKeys) { + const cached = state.modelCards[`${k.kind}:${k.name}`]; + if (!cached) { + continue; + } + const slim = slimCardForContext(cached); + if (!slim) { + continue; + } + if (k.kind === "lora") { + const sel = (ctx.selected_loras || []).find( + (l) => String(l.name || "").toLowerCase() === String(k.name).toLowerCase() + ); + const inventoryRich = !!(sel && (sel.triggers?.length || sel.trigger_phrase || sel.blurb)); + const cardExtra = !!(slim.when || slim.avoid || slim.prompt_hint || slim.notes); + if (inventoryRich && !cardExtra) { + continue; + } + } + ctx.model_cards.push(slim); + } + 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 > INVENTORY_PROMPT_NAMES) { + ctx.available_loras = ctx.available_loras.slice(0, INVENTORY_PROMPT_NAMES); + } + } catch (e) { + } + } + 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); + const rec = { + steps: defaults.steps ?? 8, + cfg: defaults.cfg ?? 1, + sigma_shift: defaults.sigma_shift ?? 1.15 + }; + if (defaults.aspect) { + rec.aspect = defaults.aspect; + } + const liveDiffers = ctx.steps != null && ctx.steps !== rec.steps || ctx.cfg != null && ctx.cfg !== rec.cfg || ctx.sigma_shift != null && ctx.sigma_shift !== rec.sigma_shift; + if (liveDiffers) { + ctx.recommended_params = rec; + } + } catch (e) { + ctx.krea_profile = "turbo"; + } + ctx.session_exact = state.sessionExact && Object.keys(state.sessionExact).length ? { ...state.sessionExact } : void 0; + if (!ctx.session_exact) { + delete ctx.session_exact; + } + 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) : void 0, + weight: card.weight != null ? card.weight : void 0, + when: card.when ? String(card.when).slice(0, 160) : void 0, + avoid: card.avoid ? String(card.avoid).slice(0, 120) : void 0, + prompt_hint: card.prompt_hint ? String(card.prompt_hint).slice(0, 160) : void 0, + notes: card.notes ? String(card.notes).slice(0, 200) : void 0 + }; + const clean = {}; + for (const [k, v] of Object.entries(out)) { + if (v != null && v !== "") { + clean[k] = v; + } + } + return clean; + } + function slimInventoryLoras(list, limit) { + const selected = /* @__PURE__ */ 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) { + } + const namesCap = Math.min(limit || INVENTORY_PROMPT_NAMES, INVENTORY_PROMPT_NAMES); + const richCap = Math.max(4, Math.min(INVENTORY_PROMPT_RICH, namesCap)); + 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) : void 0, + architecture: l.architecture || null, + compat_class: l.compat_class || null, + has_card: hasCard, + krea_likely: krea, + blurb, + default_weight: l.default_weight || void 0, + tags: Array.isArray(l.tags) ? l.tags.slice(0, 6) : void 0, + _score: (sel ? 1e3 : 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))); + let richUsed = 0; + const out = []; + for (const row of rows) { + if (out.length >= namesCap) { + break; + } + const sel = selected.has(String(row.name || "").toLowerCase()); + let wantRich = sel; + if (!wantRich && richUsed < richCap && (row.krea_likely || row.has_card || row.blurb)) { + wantRich = true; + } + if (wantRich) { + const rich = { name: row.name, title: row.title }; + if (row.trigger_phrase) { + rich.trigger_phrase = row.trigger_phrase; + } + if (row.triggers) { + rich.triggers = row.triggers; + } + if (row.krea_likely) { + rich.krea_likely = true; + } + if (row.has_card) { + rich.has_card = true; + } + if (row.blurb) { + rich.blurb = row.blurb; + } + if (row.default_weight) { + rich.default_weight = row.default_weight; + } + if (row.architecture) { + rich.architecture = row.architecture; + } + out.push(rich); + if (!sel) { + richUsed++; + } + } else { + const nameOnly = { name: row.name }; + if (row.krea_likely) { + nameOnly.krea_likely = true; + } + out.push(nameOnly); + } + } + 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 || 8).map((c) => { + const out = { + name: c.name, + title: c.title || c.name + }; + if (c.architecture) { + out.architecture = c.architecture; + } + if (c.krea_likely) { + out.krea_likely = true; + } + if (c.has_card) { + out.has_card = true; + } + return out; + }); + } + 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 isCardObject2(obj) { + if (window.SA && typeof SA.isCardObject === "function") { + return SA.isCardObject(obj); + } + if (!obj || typeof obj !== "object") { + return false; + } + 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 != null || obj.cfg != null || obj.aspect || obj.seed != null || obj.search_query || obj.civitai_query || obj.look_at || obj.controls); + 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 (isCardObject2(obj)) { + last = obj; + } + } catch (e) { + } + } + if (last) { + return last; + } + try { + const obj = JSON.parse(text.trim()); + return isCardObject2(obj) ? obj : null; + } catch (e) { + return null; + } + } + function extractPatch2(text) { + if (window.SA && typeof SA.extractPatch === "function") { + return SA.extractPatch(text); + } + return { prose: text || "", patch: null }; + } + 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) / 4e3 + Math.abs(height - ah) / 4e3; + if (dist < bestDist) { + bestDist = dist; + best = key; + } + } + return bestDist < 0.12 ? best : null; + } + function clearPromptImagesInBox() { + const box = document.getElementById("alt_prompt_textbox") || document.getElementById("input_prompt"); + if (!box) { + return false; + } + const before = box.value || ""; + const next = before.replace(/]*>[\s\S]*?<\/image>/gi, "").replace(/]*\/?>/gi, "").replace(/data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=]+/g, "").replace(/\n{3,}/g, "\n\n").trim(); + if (next === before.trim()) { + return false; + } + box.value = next; + box.dispatchEvent(new Event("input", { bubbles: true })); + box.dispatchEvent(new Event("change", { bubbles: true })); + return true; + } + function clearPatchBlocksOnly() { + document.querySelectorAll("#sa_messages .sa-patch, #sa_messages .sa-patch-stale, #sa_messages .sa-civitai-list").forEach((el) => el.remove()); + state.lastPatch = null; + syncBuildGenButton(); + setStatus("\u041F\u0430\u0442\u0447\u0438 \u0443\u0431\u0440\u0430\u043D\u044B \u0438\u0437 \u0447\u0430\u0442\u0430"); + } + function toggleMoreMenu(menuId, btnId) { + const menu = $(menuId); + const btn = $(btnId); + if (!menu) { + return; + } + const open = menu.hidden; + document.querySelectorAll(".sa-more-menu").forEach((m) => { + m.hidden = true; + }); + document.querySelectorAll("#sa_btn_board_more, #sa_btn_clear_more").forEach((b) => b.setAttribute("aria-expanded", "false")); + if (open) { + menu.hidden = false; + btn?.setAttribute("aria-expanded", "true"); + } + } + function closeAllMoreMenus() { + document.querySelectorAll(".sa-more-menu").forEach((m) => { + m.hidden = true; + }); + document.querySelectorAll("#sa_btn_board_more, #sa_btn_clear_more").forEach((b) => b.setAttribute("aria-expanded", "false")); + } + function setPackValue(packName, { flash, user } = {}) { + const pack = $("sa_pack"); + if (!pack || !packName) { + return false; + } + const resolved = PACK_ALIASES[String(packName).trim()] || String(packName).trim(); + if (![...pack.options].some((o) => o.value === resolved)) { + return false; + } + if (pack.value !== resolved) { + pack.value = resolved; + saveSettings(); + } + if (user) { + state.packUserTouched = true; + } + if (flash) { + pack.classList.add("sa-pack-flash"); + setTimeout(() => pack.classList.remove("sa-pack-flash"), 900); + } + syncModeBadge(); + return true; + } + function autoSelectPack(text) { + if (state.packUserTouched) { + return null; + } + const cur = $("sa_pack")?.value || defaultPackId(); + if (cur === "ordinary") { + return null; + } + const t = String(text || "").toLowerCase(); + if (!t.trim()) { + return null; + } + if (userTextMentionsParams(t) || parseAspectFromUserText(t)) { + return cur === "critique_image" || cur === "describe_ref" ? "ordinary" : "fix_params"; + } + if (cyrTokenRe("\u043F\u043E\u043F\u0440\u0430\u0432\u044C|\u0438\u0441\u043F\u0440\u0430\u0432\u044C|\u043F\u0435\u0440\u0435\u043F\u0438\u0448\u0438|\u0443\u043B\u0443\u0447\u0448\u0438").test(t) || /\b(fix\s+it|make\s+it\s+better|rewrite)\b/i.test(t)) { + return "write_prompt"; + } + if (cyrTokenRe("\u043E\u043F\u0438\u0448\u0438\\s+(\u0440\u0435\u0444|\u0438\u0437\u043E\u0431\u0440\u0430\u0436[\u0430-\u044F\u0451]*|\u044D\u0442\u043E\u0442|\u044D\u0442\u0443|\u043A\u0430\u0440\u0442\u0438\u043D\u043A[\u0430-\u044F\u0451]*|\u0440\u0435\u0444\u0435\u0440\u0435\u043D\u0441)").test(t) || /\b(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|criticize)\b/i.test(t) || cyrTokenRe("\u043A\u0440\u0438\u0442\u0438\u043A[\u0430-\u044F\u0451]*|\u0447\u0442\u043E\\s+\u043D\u0435\\s+\u0442\u0430\u043A|\u0440\u0430\u0437\u0431\u0435\u0440\u0438").test(t) || /(?:^|[^а-яёa-z0-9_])(посмотри|смотри)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген)/i.test(t)) { + return "critique_image"; + } + if (/\b(inpaint|mask|img2img)\b/i.test(t) || cyrTokenRe("\u0437\u0430\u043C\u0430\u0436\u044C|\u0437\u0430\u043A\u0440\u0430\u0441\u044C|\u0440\u0443\u043A\u0438|\u043B\u0438\u0446\u043E|\u043C\u0430\u0441\u043A[\u0430-\u044F\u0451]*").test(t) || /init\s*image/i.test(t)) { + return "inpaint_edit"; + } + if (cur === "critique_image") { + return "ordinary"; + } + if (/\b(moodboard|compose|scene)\b/i.test(t) || cyrTokenRe("\u0441\u0446\u0435\u043D[\u0430-\u044F\u0451]*|\u0430\u0442\u043C\u043E\u0441\u0444\u0435\u0440[\u0430-\u044F\u0451]*|\u043C\u0438\u0437\u0430\u043D\u0441\u0446\u0435\u043D[\u0430-\u044F\u0451]*").test(t)) { + return "compose_scene"; + } + return "write_prompt"; + } + function restoreDefaultPackAfterHop() { + if (state.packUserTouched) { + return; + } + const cur = $("sa_pack")?.value || ""; + if (cur === "critique_image" || cur === "describe_ref") { + setPackValue(defaultPackId(), { flash: true }); + } + } + 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 || Array.isArray(patch.variants) && patch.variants.length > 0 || 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 (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 (doPrompt) { + if (patch.negative != null) { + setNegativePrompt(patch.negative); + } else if (patchHasGenTrigger(patch)) { + ensureNegativeForGenerate(patch); + } + } + if (doLoras && Array.isArray(patch.loras) && typeof loraHelper !== "undefined" && loraHelper) { + try { + if (typeof loraHelper.clearLoras === "function") { + loraHelper.clearLoras(); + } + } catch (e) { + } + 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) { + } + } + if (doParams) { + const defaults = mergedGenerationDefaults(); + 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 (shouldRememberSessionParam("aspect", patch.aspect)) { + 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 (shouldRememberSessionParam("width", patch.width)) { + 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 (shouldRememberSessionParam("height", patch.height)) { + 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 (shouldRememberSessionParam("steps", patch.steps)) { + 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 (shouldRememberSessionParam("cfg", patch.cfg)) { + 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 (shouldRememberSessionParam("seed", patch.seed)) { + rememberSessionExact({ seed: patch.seed }); + } + } + if (patch.sigma_shift != null && !shouldSkipSessionRollback("sigma_shift", patch.sigma_shift)) { + setVal("input_sigmashift", String(patch.sigma_shift)); + if (shouldRememberSessionParam("sigma_shift", patch.sigma_shift)) { + 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 (shouldRememberSessionParam("sampler", patch.sampler)) { + rememberSessionExact({ sampler: patch.sampler }); + } + } + if (patch.scheduler != null && document.getElementById("input_scheduler")) { + setVal("input_scheduler", String(patch.scheduler)); + if (shouldRememberSessionParam("scheduler", patch.scheduler)) { + 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 (shouldRememberSessionParam("images", batch)) { + 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 \u2014 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 \u2014 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)"); + } + } + if (patch.controls && typeof patch.controls === "object" && !Array.isArray(patch.controls)) { + const schema = state.config?.controls || {}; + const filtered = filterControlPatch(patch.controls, patch); + if (Object.keys(filtered).length) { + const next = { ...state.config?.control_values || state.exact?.controls || {}, ...filtered }; + savePersonaControls(filtered); + renderPersonaControls(schema, next); + } + } + const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : []; + const wantSwitch = acts.includes("persona_switch") || patch.persona && typeof patch.persona === "string" || patch._persona_cloned || patch._persona_written; + if (wantSwitch) { + const newId = String(patch.persona || patch._persona_cloned || patch._persona_written || "").trim(); + if (newId && AssistentConfigSafeIdClient(newId)) { + await refreshPersonasAndSwitch(newId); + } else if (acts.includes("persona_clone") || acts.includes("persona_write") || patch.persona_clone) { + await refreshPersonasAndSwitch(null); + } + } + syncChipHighlight(); + syncLiveParamsBar(); + syncBuildGenButton(); + if (state.activeChatId && !state.restoringChat) { + const chat = findChat(state.activeChatId); + if (chat) { + chat.params = snapshotChatParams(); + chat.updatedAt = Date.now(); + persistChatsStore(); + } + } + if (!state.restoringChat) { + setStatus(patch._persona_error ? `Persona: ${patch._persona_error}` : "Applied patch"); + } + } + function AssistentConfigSafeIdClient(id) { + return /^[A-Za-z0-9][A-Za-z0-9_\-]{0,63}$/.test(String(id || "")); + } + async function refreshPersonasAndSwitch(preferId) { + await new Promise((resolve) => { + genericRequest( + "AssistentListPersonas", + {}, + async (data) => { + if (Array.isArray(data?.personas)) { + state.personas = data.personas.map((p) => ({ + id: p.id, + title: p.title, + accent: p.accent, + source: p.source + })); + renderPersonaOptions(state.personas, preferId || $("sa_persona")?.value); + } + if (preferId && $("sa_persona")) { + if ([...$("sa_persona").options].some((o) => o.value === preferId)) { + $("sa_persona").value = preferId; + await applyPersonaForChat(preferId, { quiet: true }); + } + } else { + loadConfig($("sa_persona")?.value, () => resolve()); + return; + } + resolve(); + }, + 0, + () => resolve() + ); + }); + } + function triggerGenerate() { + try { + if (typeof mainGenHandler !== "undefined" && mainGenHandler && typeof mainGenHandler.doGenerate === "function") { + mainGenHandler.doGenerate(); + return true; + } + } catch (e) { + console.warn("Assistent: doGenerate failed", e); + } + const btn = document.getElementById("generate_button") || document.getElementById("alt_generate_button") || document.querySelector("button.generate-button") || document.querySelector('#generate_button, button[id*="generate"]'); + if (btn) { + btn.click(); + return true; + } + return false; + } + function shouldParkLlmBeforeGen() { + return !!$("sa_park_llm")?.checked; + } + function parkLlm() { + return new Promise((resolve) => { + const model = $("sa_model")?.value; + if (!shouldParkLlmBeforeGen() || !model || state.llmParked || typeof genericRequest !== "function") { + resolve(false); + return; + } + const baseUrl = $("sa_base_url")?.value || "http://127.0.0.1:11434"; + let settled = false; + const finish = (ok) => { + if (settled) { + return; + } + settled = true; + if (ok) { + state.llmParked = true; + state.expectColdLoad = true; + } + resolve(!!ok); + }; + setTimeout(() => finish(false), 8e3); + genericRequest("AssistentParkLlm", { baseUrl, model }, () => finish(true), 0, () => finish(false)); + }); + } + function warmLlm({ force = false } = {}) { + return new Promise((resolve) => { + const model = $("sa_model")?.value; + if (!model || typeof genericRequest !== "function") { + resolve(false); + return; + } + if (!force && !state.llmParked) { + resolve(false); + return; + } + const baseUrl = $("sa_base_url")?.value || "http://127.0.0.1:11434"; + let settled = false; + const finish = (ok) => { + if (settled) { + return; + } + settled = true; + state.llmParked = false; + if (ok) { + state.expectColdLoad = false; + } + resolve(!!ok); + }; + setTimeout(() => finish(false), 18e4); + genericRequest("AssistentWarmLlm", { baseUrl, model }, () => finish(true), 0, () => finish(false)); + }); + } + function cancelWaitForNewImage() { + if (state.waitImageTimer) { + clearInterval(state.waitImageTimer); + state.waitImageTimer = null; + } + } + function bumpChatEpoch() { + state.chatEpoch = (state.chatEpoch || 0) + 1; + return state.chatEpoch; + } + function clearInFlightUi({ status } = {}) { + state.busy = false; + state.generating = false; + state.pendingSilentGen = false; + if (state.streamEl) { + try { + state.streamEl.remove(); + } catch (e) { + } + state.streamEl = null; + state.streamMeta = null; + } + setInterruptVisible(false); + syncGenerateBusy(); + syncPatchActionAvailability(); + if (status != null) { + stopBusyUi(status); + } else { + stopBusyUi(""); + } + } + function abortInFlightWork({ status, interruptSwarm = false } = {}) { + bumpChatEpoch(); + cancelWaitForNewImage(); + if (interruptSwarm) { + try { + if (typeof doInterrupt === "function") { + doInterrupt(false); + } else if (typeof genericRequest === "function") { + genericRequest("InterruptAll", { other_sessions: false }, () => { + }, 0, () => { + }); + } + } catch (e) { + } + } + clearInFlightUi({ status: status != null ? status : "" }); + } + function doInterruptNow() { + bumpChatEpoch(); + cancelWaitForNewImage(); + try { + if (typeof doInterrupt === "function") { + doInterrupt(false); + } + } catch (e) { + } + if (typeof genericRequest === "function") { + genericRequest("InterruptAll", { other_sessions: false }, () => { + }, 0, () => { + }); + } + clearInFlightUi({ status: "\u041F\u0440\u0435\u0440\u0432\u0430\u043D\u043E" }); + } + function waitForNewImage(prevSrc, timeoutMs = 18e4) { + cancelWaitForNewImage(); + const epoch = state.chatEpoch; + const prev = String(prevSrc || ""); + return new Promise((resolve) => { + const start = Date.now(); + let sawRunning = false; + let idleTicks = 0; + let candidate = null; + state.waitImageTimer = setInterval(() => { + if (epoch !== state.chatEpoch) { + cancelWaitForNewImage(); + resolve(null); + return; + } + const running = isSwarmGenerateRunning(); + if (running) { + sawRunning = true; + idleTicks = 0; + } else if (sawRunning) { + idleTicks += 1; + } + const raw = findCurrentGenerateSrc(); + const src = raw && !looksLikeModelPreview(raw) ? raw : null; + if (src && src !== prev) { + candidate = src; + } + if (sawRunning && !running && idleTicks >= 2) { + cancelWaitForNewImage(); + resolve(candidate || src || null); + return; + } + if (candidate && !running && Date.now() - start > 500) { + cancelWaitForNewImage(); + resolve(candidate); + return; + } + if (Date.now() - start > timeoutMs) { + cancelWaitForNewImage(); + resolve(candidate || src || null); + } + }, 400); + }); + } + const VARIANT_STRIP_KEYS = [ + "variants", + "label", + "notes", + "actions", + "look_at", + "vision_from", + "vision_slots", + "search_query", + "civitai_query", + "memories", + "memory", + "memory_query", + "memory_kind", + "tag_query", + "user_prefs", + "controls", + "skills", + "persona_shelves", + "inventory_query", + "pack" + ]; + function normalizeVariantList(patch) { + if (!patch || !Array.isArray(patch.variants)) { + return null; + } + const items = patch.variants.filter((v) => v && typeof v === "object" && !Array.isArray(v)); + if (items.length < 2) { + return null; + } + return items.slice(0, MAX_GEN_VARIANTS); + } + function stripMetaPatchKeys(obj) { + const out = { ...obj || {} }; + for (const k of VARIANT_STRIP_KEYS) { + delete out[k]; + } + return out; + } + function mergeVariantPatch(base, item, index) { + const merged = { ...stripMetaPatchKeys(base), ...stripMetaPatchKeys(item) }; + merged.images = 1; + delete merged.batch; + if (merged.seed == null && merged.lock_seed !== true) { + merged.seed = -1; + merged.vary = true; + } + merged.actions = ["generate"]; + const labelRaw = item?.label != null ? String(item.label).trim() : ""; + return { + id: `var${index + 1}`, + label: (labelRaw || `\u0412\u0430\u0440\u0438\u0430\u043D\u0442 ${index + 1}`).slice(0, 48), + patch: merged + }; + } + function finishedGenResultCount() { + return (state.genResults || []).filter((r) => r && r.src).length; + } + function isMultiGenResults() { + return finishedGenResultCount() > 1 || (state.genResults || []).length > 1; + } + function clearGenResults() { + state.genResults = []; + state.selectedGenResultId = null; + if (state.lightboxIndex >= 0) { + closeGenLightbox(); + } + } + function selectGenResult(id, { restore = true, openViewer = false } = {}) { + const row = (state.genResults || []).find((r) => r.id === id); + if (!row) { + return false; + } + state.selectedGenResultId = row.id; + const gen = generateSlot(); + if (gen && row.src) { + gen.src = row.src; + } + if (restore && row.patch) { + applyPatch(row.patch, "all").catch(() => { + }); + syncLiveParamsBar(); + } + renderBoard(); + if (openViewer && row.src) { + openGenLightbox(row.id); + } + return true; + } + async function runGenerateFromPatch(patch, opts = {}) { + const force = !!opts.force; + if (!force && !$("sa_auto_generate")?.checked || !patchHasGenTrigger(patch)) { + return null; + } + const variantItems = normalizeVariantList(patch); + const jobs = variantItems ? variantItems.map((item, i) => mergeVariantPatch(patch, item, i)) : null; + if (jobs) { + state.genResults = jobs.map((j) => ({ + id: j.id, + label: j.label, + src: null, + patch: j.patch + })); + state.selectedGenResultId = null; + setBoardTab("generate", { persist: true }); + renderBoard(); + } else { + clearGenResults(); + } + const epoch = state.chatEpoch; + if (shouldParkLlmBeforeGen()) { + startBusyUi("parking"); + setStatus("\u041E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0430\u044E VRAM\u2026"); + await parkLlm(); + if (epoch !== state.chatEpoch) { + return null; + } + } + state.generating = true; + setInterruptVisible(true); + startBusyUi("generating"); + let lastSrc = null; + const steps = jobs || [{ + id: "var1", + label: "Generate", + patch: { ...stripMetaPatchKeys(patch), actions: ["generate"] } + }]; + for (let i = 0; i < steps.length; i++) { + if (epoch !== state.chatEpoch) { + break; + } + const job = steps[i]; + if (jobs) { + setStatus(`\u0412\u0430\u0440\u0438\u0430\u043D\u0442 ${i + 1}/${steps.length}: ${job.label}`); + } else { + setStatus("\u0413\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044F\u2026"); + } + startBusyUi("generating"); + if (jobs) { + await applyPatch(job.patch, "all"); + syncLiveParamsBar(); + } + ensureNegativeForGenerate(job.patch); + if (epoch !== state.chatEpoch) { + break; + } + const prev = findCurrentGenerateSrc(); + const ok = triggerGenerate(); + if (!ok) { + setStatus(jobs ? `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0432\u0430\u0440\u0438\u0430\u043D\u0442 ${i + 1}/${steps.length}` : "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C Generate"); + if (!jobs) { + break; + } + continue; + } + const src = await waitForNewImage(prev); + if (epoch !== state.chatEpoch) { + break; + } + if (src) { + lastSrc = src; + if (jobs) { + const row = state.genResults.find((r) => r.id === job.id); + if (row) { + row.src = src; + } + state.selectedGenResultId = job.id; + } + const gen = generateSlot(); + if (gen) { + gen.src = src; + } + renderBoard(); + } + } + state.generating = false; + setInterruptVisible(state.busy); + state.expectColdLoad = true; + if (jobs && lastSrc && state.selectedGenResultId && epoch === state.chatEpoch) { + const row = state.genResults.find((r) => r.id === state.selectedGenResultId); + if (row?.patch) { + await applyPatch(row.patch, "all"); + syncLiveParamsBar(); + } + } + const paneVisible = !!document.getElementById("swarm_assistent_root")?.offsetParent; + const multiDone = !!(jobs && finishedGenResultCount() > 1); + const willAutoCritique = !multiDone && !!$("sa_auto_critique")?.checked; + if (state.view === "chat" && paneVisible && !willAutoCritique && epoch === state.chatEpoch) { + startBusyUi("warming"); + setStatus("\u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E LLM \u0432 GPU\u2026"); + await warmLlm({ force: true }); + } + if (epoch !== state.chatEpoch) { + return null; + } + if (jobs) { + const n = finishedGenResultCount(); + const msg = n > 0 ? n > 1 ? `\u0413\u043E\u0442\u043E\u0432\u043E \xB7 ${n} \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u043E\u0432` : `\u0413\u043E\u0442\u043E\u0432\u043E \xB7 1 \u0432\u0430\u0440\u0438\u0430\u043D\u0442` : "Generate \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D (\u043D\u043E\u0432\u043E\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E)"; + if (!state.busy) { + stopBusyUi(msg); + } + setStatus(msg); + return multiDone ? null : lastSrc; + } + if (!state.busy) { + stopBusyUi(lastSrc ? "Generate \u0433\u043E\u0442\u043E\u0432" : "Generate \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D (\u043D\u043E\u0432\u043E\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E)"); + } + if (lastSrc) { + const gen = generateSlot(); + if (gen) { + gen.src = lastSrc; + renderBoard(); + } + setStatus("Generate \u0433\u043E\u0442\u043E\u0432"); + return lastSrc; + } + if (state.busy) { + setStatus("Generate \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D (\u043D\u043E\u0432\u043E\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E)"); + } + return null; + } + async function resolveFinishedGenerateSrc(hint, { settleMs = 2e4 } = {}) { + scrubPreviewFromGenerateSlot(); + let src = hint && !looksLikeModelPreview(hint) ? hint : null; + if (!src) { + src = findCurrentGenerateSrc(); + } + if (isGenerateUnavailable()) { + const settled = await waitForNewImage(src, settleMs); + if (settled) { + src = settled; + } + } + return src && !looksLikeModelPreview(src) ? src : null; + } + async function maybeAutoCritique(imageSrc) { + if (!$("sa_auto_critique")?.checked || turnHopUsed("critique") || isMultiGenResults()) { + return; + } + const src = await resolveFinishedGenerateSrc(imageSrc); + if (!src) { + setStatus("\u0410\u0432\u0442\u043E-\u043A\u0440\u0438\u0442\u0438\u043A\u0430 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u0430 \u2014 \u043D\u0435\u0442 \u0433\u043E\u0442\u043E\u0432\u043E\u0433\u043E \u043A\u0430\u0434\u0440\u0430 Generate"); + return; + } + if (!claimTurnHop("critique")) { + return; + } + setPackValue("critique_image", { flash: true }); + if ($("sa_input")) { + $("sa_input").value = "Critique this result and improve the prompt for the next generation."; + } + const gen = generateSlot(); + if (gen) { + gen.attach = true; + gen.src = src; + renderBoard(); + } + setStatus("Auto-critique\u2026"); + await sendChat({ fromAutoCritique: true, forceSlotIds: [GEN_ID] }); + restoreDefaultPackAfterHop(); + } + async function maybeAutoVisionLook(imageSrc) { + if (!wantsAutoVision() || $("sa_auto_critique")?.checked || turnHopUsed("vision") || state.busy || isMultiGenResults()) { + return; + } + const src = await resolveFinishedGenerateSrc(imageSrc); + if (!src) { + return; + } + const gen = generateSlot(); + if (gen) { + gen.src = src; + gen.attach = true; + renderBoard(); + } + if (!claimTurnHop("vision")) { + return; + } + setPackValue("critique_image", { flash: true }); + if ($("sa_input")) { + $("sa_input").value = "Look at the Generate result and briefly say what worked and what to fix next."; + } + setStatus("Auto look_at\u2026"); + await sendChat({ fromVisionHop: true, forceSlotIds: [GEN_ID], skipAutoPack: true }); + restoreDefaultPackAfterHop(); + } + async function askLookAtResult() { + if (state.busy || state.generating) { + setStatus("\u0417\u0430\u043D\u044F\u0442\u043E \u2014 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043A\u043E\u043D\u0446\u0430 \u043E\u0442\u0432\u0435\u0442\u0430 \u0438\u043B\u0438 \u0421\u0442\u043E\u043F"); + return; + } + if (!updateGate()) { + setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Krea 2"); + return; + } + const preferred = (state.genResults || []).find((r) => r.id === state.selectedGenResultId && r.src)?.src || generateSlot()?.src; + const src = await resolveFinishedGenerateSrc(preferred, { settleMs: 8e3 }); + if (!src) { + setStatus("\u041D\u0435\u0442 \u0433\u043E\u0442\u043E\u0432\u043E\u0433\u043E \u043A\u0430\u0434\u0440\u0430 Generate \u2014 \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439"); + return; + } + const gen = generateSlot(); + if (gen) { + gen.src = src; + gen.attach = true; + } + setBoardTab("generate"); + renderBoard(); + setView("chat"); + const label = (state.genResults || []).find((r) => r.id === state.selectedGenResultId)?.label; + setPackValue("critique_image", { flash: true }); + if ($("sa_input")) { + $("sa_input").value = label ? `\u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442 \xAB${label}\xBB: \u0447\u0442\u043E \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C, \u0447\u0442\u043E \u0441\u043B\u043E\u043C\u0430\u043B\u043E\u0441\u044C, \u0438 \u043A\u0430\u043A \u043F\u043E\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u043E\u043C\u043F\u0442 \u0438 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B \u0434\u043B\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043A\u0430\u0434\u0440\u0430.` : "\u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442: \u0447\u0442\u043E \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C, \u0447\u0442\u043E \u0441\u043B\u043E\u043C\u0430\u043B\u043E\u0441\u044C, \u0438 \u043A\u0430\u043A \u043F\u043E\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u043E\u043C\u043F\u0442 \u0438 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B \u0434\u043B\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043A\u0430\u0434\u0440\u0430."; + } + await sendChat({ forceSlotIds: [GEN_ID], skipAutoPack: true }); + } + function currentPersonaInfo() { + const id = ($("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral").trim() || "neutral"; + const known = (state.personas || []).find((p) => p && p.id === id); + return { + id, + title: known && known.title || ({ + neutral: "\u041D\u0435\u0439\u0442\u0440\u0430\u043B\u044C\u043D\u044B\u0439", + lewd: "\u041F\u043E\u0448\u043B\u044F\u043A", + aggressive: "\u0410\u0433\u0440\u0435\u0441\u0441\u0438\u0432\u043D\u044B\u0439" + }[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 = `\u0425\u0430\u0440\u0430\u043A\u0442\u0435\u0440: ${persona.title || persona.id}${pack ? ` \xB7 \u0440\u0435\u0436\u0438\u043C ${pack}` : ""}`; + row.appendChild(chip); + if (pack && pack !== "ordinary" && pack !== "write_prompt") { + const packEl = document.createElement("span"); + packEl.className = "sa-pack-mark"; + packEl.textContent = pack.replace(/_/g, " "); + packEl.title = `\u0420\u0435\u0436\u0438\u043C: ${pack}`; + row.appendChild(packEl); + } + div.insertBefore(row, div.firstChild); + } + 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" ? extractPatch2(text) : { prose: text, patch: null }; + const finalPatch = patch || extracted; + if (role === "assistant") { + setAssistantBody(div, prose || text || ""); + } else { + div.textContent = prose || text || ""; + } + if (finalPatch && !(meta && meta.historical)) { + const silent = !!(meta && meta.silentPatch); + mountPatchBlock(div, finalPatch, { silent }); + } + if (civitaiResults && civitaiResults.length) { + div.appendChild(buildCivitaiCards(civitaiResults)); + } + box.appendChild(div); + scrollMessagesToBottom({ force: true }); + 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\u2026'; + div.appendChild(body); + box.appendChild(div); + scrollMessagesToBottom({ force: true }); + state.streamEl = div; + state.streamMeta = meta || null; + state.streamFenceDone = false; + return div; + } + function streamHasClosedPatchFence(text) { + const t = String(text || ""); + if (!/```[\s\S]*```/.test(t)) { + return false; + } + const re = /```(?:json)?\s*([\s\S]*?)```/gi; + let match; + while ((match = re.exec(t)) !== null) { + try { + const obj = JSON.parse(match[1].trim()); + const terminal = window.SA && typeof SA.isTerminalStreamPatch === "function" ? SA.isTerminalStreamPatch(obj) : isPatchObject(obj) || isCardObject2(obj); + if (terminal) { + return true; + } + } catch (e) { + } + } + return false; + } + function trimToClosedPatchFence(text) { + const t = String(text || ""); + const re = /```(?:json)?\s*([\s\S]*?)```/gi; + let match; + let lastEnd = -1; + while ((match = re.exec(t)) !== null) { + try { + const obj = JSON.parse(match[1].trim()); + const terminal = window.SA && typeof SA.isTerminalStreamPatch === "function" ? SA.isTerminalStreamPatch(obj) : isPatchObject(obj) || isCardObject2(obj); + if (terminal) { + lastEnd = match.index + match[0].length; + } + } catch (e) { + } + } + return lastEnd > 0 ? t.slice(0, lastEnd).trimEnd() : t; + } + function appendStreamDelta(delta) { + if (state.streamFenceDone) { + return; + } + if (!state.streamEl) { + beginStreamMessage(state.streamMeta || void 0); + } + if (state.streamEl) { + if (state.streamEl.classList.contains("sa-typing")) { + state.streamEl.classList.remove("sa-typing"); + state.streamText = ""; + setAssistantBody(state.streamEl, "", { live: true }); + } + state.gotDelta = true; + state.expectColdLoad = false; + if (state.busyPhase !== "refining") { + setBusyPhase("streaming"); + } + state.streamText = (state.streamText || "") + (delta || ""); + if (streamHasClosedPatchFence(state.streamText)) { + state.streamText = trimToClosedPatchFence(state.streamText); + state.streamFenceDone = true; + } + setAssistantBody(state.streamEl, state.streamText, { live: true }); + scrollMessagesToBottom(); + } + } + function finalizeStreamMessage(fullReply, civitaiResults) { + const el = state.streamEl; + const meta = state.streamMeta; + state.streamEl = null; + state.streamMeta = null; + state.streamText = ""; + state.streamFenceDone = false; + if (!el) { + appendMessage("assistant", fullReply, null, civitaiResults, meta || void 0); + return; + } + el.classList.remove("sa-streaming", "sa-typing"); + mountAssistantMeta(el, meta || void 0); + const card = extractCardJson(fullReply); + const { prose, patch } = extractPatch2(fullReply); + setAssistantBody(el, prose || fullReply || ""); + el.querySelectorAll(".sa-patch, .sa-civitai-list").forEach((n) => n.remove()); + if (patch && !isCardObject2(patch) && !(card && !patch.prompt && !patch.actions && !patch.loras)) { + const silent = !!(meta && meta.silentPatch) || !!state.pendingSilentGen; + mountPatchBlock(el, patch, { silent }); + } else if (card) { + const wrap = document.createElement("div"); + wrap.className = "sa-patch sa-card-json-preview"; + const pre = document.createElement("pre"); + pre.textContent = JSON.stringify(card, null, 2); + wrap.appendChild(pre); + el.appendChild(wrap); + } + if (civitaiResults && civitaiResults.length) { + el.appendChild(buildCivitaiCards(civitaiResults)); + } + scrollMessagesToBottom(); + } + 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(" \xB7 "); + card.appendChild(meta); + const actions = document.createElement("div"); + actions.className = "sa-civitai-actions"; + if (r.already_installed) { + const note = document.createElement("span"); + note.textContent = "\u0423\u0436\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430"; + actions.appendChild(note); + } else if (r.download_url) { + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "basic-button sa-primary"; + btn.textContent = "\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u0435"; + btn.addEventListener("click", () => downloadCivitaiLoRA(r, btn)); + actions.appendChild(btn); + } else { + const note = document.createElement("span"); + note.textContent = "\u041D\u0435\u0442 URL \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F"; + 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 = "\u0421\u043A\u0430\u0447\u0438\u0432\u0430\u044E\u2026"; + } + setStatus(`\u0421\u043A\u0430\u0447\u0438\u0432\u0430\u044E ${card.file_name || card.name}\u2026`); + 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(`\u0421\u043A\u0430\u0447\u0430\u043D\u043E ${payload.name}`); + if (btn) { + btn.textContent = "\u0421\u043A\u0430\u0447\u0430\u043D\u043E"; + } + refreshInventory(async () => { + await maybeWriteCardAfterDownload({ + kind: "lora", + name: payload.name, + civitai: card + }); + }, { rescan: true }); + } else { + setStatus(msg || "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F"); + if (btn) { + btn.disabled = false; + btn.textContent = "\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u0435"; + } + appendMessage("error", msg || "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F"); + } + }; + 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) { + 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\u2026`); + setPackValue("catalog_card", { flash: true }); + const meta = { + triggers: civitai?.triggers || [], + base_model: civitai?.base_model, + civitai_url: civitai?.url || civitai?.civitai_url, + version_id: civitai?.version_id || civitai?.modelVersionId, + name: display + }; + if ($("sa_input")) { + $("sa_input").value = ""; + } + await sendChat({ + forcedUserText: `LoRA "${display}" is now installed. Write a recommendation card (JSON) using its triggers/metadata. Then briefly suggest how to enable it in the next generate.`, + skipSlash: true, + skipAutoPack: true, + fromDownload: true, + fromCards: true, + cardTarget: { kind: kind || "lora", name: display, meta } + }); + } + function wantsAutoVision() { + return !!$("sa_auto_vision")?.checked; + } + function looksLikeModelPreview(src) { + const s = String(src || "").toLowerCase(); + if (!s) { + return false; + } + return s.includes(".preview.") || s.includes("placeholder") || 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) { + } + if (!src) { + try { + if (typeof currentMetadataMap !== "undefined" && currentMetadataMap && currentMetadataMap.image) { + src = currentMetadataMap.image; + } + } catch (e) { + } + } + if (!src) { + return null; + } + 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); + let persona = localStorage.getItem(LS_PERSONA); + if (persona === "terse") { + persona = "aggressive"; + localStorage.setItem(LS_PERSONA, 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 parkLlm2 = localStorage.getItem(LS_PARK_LLM); + 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 ($("sa_park_llm")) { + $("sa_park_llm").checked = parkLlm2 === "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" || view === "settings") { + state.view = view; + } + const boardTab = localStorage.getItem(LS_BOARD_TAB); + if (boardTab === "refs" || boardTab === "generate") { + state.boardTab = boardTab; + } + } + function collectUiState() { + return { + pack: $("sa_pack")?.value || defaultPackId(), + persona: $("sa_persona")?.value || "neutral", + auto_vision: !!$("sa_auto_vision")?.checked, + auto_apply: !!$("sa_auto_apply")?.checked, + auto_generate: !!$("sa_auto_generate")?.checked, + auto_critique: !!$("sa_auto_critique")?.checked, + auto_download: !!$("sa_auto_download")?.checked, + park_llm: !!$("sa_park_llm")?.checked, + pane_width: localStorage.getItem(LS_PANE_WIDTH) || "", + embed_model: $("sa_embed_model")?.value || state.preferredEmbed || "", + base_url: $("sa_base_url")?.value || "", + model: $("sa_model")?.value || "", + view: state.view || "chat", + board_tab: state.boardTab || "generate" + }; + } + async function applyDiskUiState() { + const persist = diskPersist(); + if (!persist) { + return; + } + let ui = null; + try { + ui = await persist.loadUiState(); + } catch (e) { + return; + } + if (!ui || typeof ui !== "object") { + return; + } + const fill = (lsKey, value, apply) => { + if (value == null || value === "" || localStorage.getItem(lsKey) != null) { + return; + } + localStorage.setItem(lsKey, String(value)); + apply?.(String(value)); + }; + fill(LS_BASE, ui.base_url, (v) => { + if ($("sa_base_url")) { + $("sa_base_url").value = v; + } + }); + fill(LS_MODEL, ui.model, (v) => { + state.preferredModel = v; + }); + fill(LS_EMBED, ui.embed_model, (v) => { + state.preferredEmbed = v; + }); + fill(LS_PACK, ui.pack, (v) => { + if ($("sa_pack")) { + $("sa_pack").value = v; + } + }); + fill(LS_PERSONA, ui.persona, (v) => { + if ($("sa_persona")) { + $("sa_persona").value = v; + } + }); + fill(LS_PANE_WIDTH, ui.pane_width, (v) => document.documentElement.style.setProperty("--sa-image-width", v)); + if (ui.view === "cards" || ui.view === "chat" || ui.view === "settings") { + fill(LS_VIEW, ui.view, (v) => { + state.view = v; + }); + } + if (ui.board_tab === "refs" || ui.board_tab === "generate") { + fill(LS_BOARD_TAB, ui.board_tab, (v) => { + state.boardTab = v; + }); + } + for (const [key, lsKey, id] of [ + ["auto_vision", LS_AUTO_VISION, "sa_auto_vision"], + ["auto_apply", LS_AUTO_APPLY, "sa_auto_apply"], + ["auto_generate", LS_AUTO_GENERATE, "sa_auto_generate"], + ["auto_critique", LS_AUTO_CRITIQUE, "sa_auto_critique"], + ["auto_download", LS_AUTO_DOWNLOAD, "sa_auto_download"], + ["park_llm", LS_PARK_LLM, "sa_park_llm"] + ]) { + if (ui[key] == null || localStorage.getItem(lsKey) != null) { + continue; + } + const on = ui[key] === true || ui[key] === "1" || ui[key] === 1; + if (on && key === "auto_download") { + continue; + } + localStorage.setItem(lsKey, on ? "1" : "0"); + const el = $(id); + if (el) { + el.checked = on; + } + } + } + function saveUiStateToDisk() { + diskPersist()?.saveUiState(collectUiState()); + } + function saveSettings() { + localStorage.setItem(LS_BASE, $("sa_base_url")?.value || ""); + localStorage.setItem(LS_MODEL, $("sa_model")?.value || ""); + localStorage.setItem(LS_EMBED, $("sa_embed_model")?.value || state.preferredEmbed || ""); + localStorage.setItem(LS_PACK, $("sa_pack")?.value || defaultPackId()); + 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"); + localStorage.setItem(LS_PARK_LLM, $("sa_park_llm")?.checked ? "1" : "0"); + persistServerSettings(); + saveUiStateToDisk(); + } + function persistServerSettings() { + if (typeof genericRequest !== "function") { + return; + } + const skills = {}; + document.querySelectorAll("#sa_skills_box input[data-skill]")?.forEach((el) => { + skills[el.getAttribute("data-skill")] = !!el.checked; + }); + const persona = $("sa_persona")?.value || "neutral"; + const settings = { + embed_model: $("sa_embed_model")?.value || state.preferredEmbed || "", + base_url: $("sa_base_url")?.value || "", + [persona]: { skills } + }; + genericRequest("AssistentSaveSettings", { settings }, () => { + }, 0, () => { + }); + } + function applyConfigPayload(data, { applyDefaults = false } = {}) { + if (!data || data.error) { + return; + } + const prevPersona = state.config?.persona || $("sa_persona")?.value || ""; + const prevControls = state.config?.control_values && typeof state.config.control_values === "object" ? { ...state.config.control_values } : null; + state.config = data; + if (window.SA?.applyConfigPatchKeys) { + window.SA.applyConfigPatchKeys(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 || "" + })); + } + if (Array.isArray(data.ui?.slash_extra) && data.ui.slash_extra.length) { + for (const s of data.ui.slash_extra) { + const cmd = s.cmd || ""; + if (!cmd || SLASH_COMMANDS.some((c) => c.cmd === cmd)) { + continue; + } + SLASH_COMMANDS.push({ + cmd, + hint: s.hint || "", + action: s.action || "" + }); + } + } + if (data.ui?.help_extra) { + HELP_TEXT = `${HELP_TEXT || ""} + +${data.ui.help_extra}`.trim(); + } + state.enabledSkills = Array.isArray(data.enabled_skills) ? data.enabled_skills.slice() : []; + if (Array.isArray(data.personas)) { + state.personas = data.personas; + } + renderPersonaOptions(data.personas || [], data.persona || data.default_persona); + renderPackOptions(data.packs || [], applyDefaults ? data.assistant?.default_pack : null); + renderChips(data.ui?.chips || []); + renderSkillChecks(data.skills || [], state.enabledSkills); + if (applyDefaults && data.assistant?.default_pack && $("sa_pack") && !localStorage.getItem(LS_PACK)) { + $("sa_pack").value = data.assistant.default_pack; + } + if (data.assistant?.embed_model && !state.preferredEmbed) { + state.preferredEmbed = data.assistant.embed_model; + } + const asst = data.assistant || {}; + if (asst.history_keep_turns != null) { + HISTORY_KEEP_TURNS = Math.max(1, Number(asst.history_keep_turns) || 4); + } + if (asst.max_ref_slots != null) { + MAX_REF_SLOTS = Math.max(1, Number(asst.max_ref_slots) || 4); + } + if (asst.max_gen_variants != null) { + MAX_GEN_VARIANTS = Math.max(2, Math.min(8, Number(asst.max_gen_variants) || 4)); + } + if (asst.context_prompt_max != null) { + CONTEXT_PROMPT_MAX = Math.max(200, Number(asst.context_prompt_max) || 2e3); + } + if (asst.inventory_prompt_rich != null) { + INVENTORY_PROMPT_RICH = Math.max(4, Number(asst.inventory_prompt_rich) || 12); + } + if (asst.inventory_prompt_names != null) { + INVENTORY_PROMPT_NAMES = Math.max(INVENTORY_PROMPT_RICH, Number(asst.inventory_prompt_names) || 24); + } + fillKnobsFromConfig(data); + if (applyDefaults || data.exact) { + fillEmptyParamsFromExact(); + } + const nextPersona = data.persona || $("sa_persona")?.value || ""; + let controlValues = data.control_values || data.exact?.controls || {}; + if (!applyDefaults && prevControls && nextPersona === prevPersona) { + controlValues = { ...controlValues, ...prevControls }; + state.config.control_values = controlValues; + if (state.exact) { + state.exact.controls = { ...state.exact.controls || {}, ...prevControls }; + } + } + renderPersonaControls(data.controls || {}, controlValues); + syncPersonaDeleteButton(data.persona_source || data.personas?.find((p) => p.id === (data.persona || $("sa_persona")?.value))?.source); + } + function syncPersonaDeleteButton(source) { + const btn = $("sa_persona_delete"); + if (!btn) { + return; + } + const src = String(source || ""); + const canDelete = src === "overlay" || src === "overlay+bundled"; + btn.hidden = !canDelete; + btn.disabled = !canDelete; + } + let controlSaveTimer = null; + let controlsPointerDown = false; + let pendingControlsRender = null; + function renderPersonaControls(schema, values) { + const box = $("sa_persona_controls"); + if (!box) { + return; + } + if (controlsPointerDown) { + pendingControlsRender = { schema, values }; + return; + } + pendingControlsRender = null; + box.innerHTML = ""; + const keys = schema && typeof schema === "object" ? Object.keys(schema) : []; + if (!keys.length) { + box.hidden = true; + return; + } + box.hidden = false; + const ordered = keys.slice().sort((a, b) => { + const oa = Number(schema[a]?.order ?? 100); + const ob = Number(schema[b]?.order ?? 100); + if (oa !== ob) { + return oa - ob; + } + return String(a).localeCompare(String(b)); + }); + for (const id of ordered) { + const def = schema[id]; + if (!def || typeof def !== "object") { + continue; + } + if (String(def.type || "slider").toLowerCase() !== "slider") { + continue; + } + const min = Number(def.min ?? -1); + const max = Number(def.max ?? 1); + const step = Number(def.step ?? 0.05); + const defVal = Number(def.default ?? 0); + let cur = values && values[id] != null ? Number(values[id]) : defVal; + if (Number.isNaN(cur)) { + cur = defVal; + } + const asPercent = String(def.display || "").toLowerCase() === "percent"; + const fmt = (v) => asPercent ? `${Math.round(v)}%` : Number(v).toFixed(2); + const row = document.createElement("div"); + row.className = "sa-control-row"; + row.title = def.hint || id; + const lab = document.createElement("label"); + lab.textContent = def.label || id; + const input = document.createElement("input"); + input.type = "range"; + input.min = String(min); + input.max = String(max); + input.step = String(step); + input.value = String(cur); + input.dataset.controlId = id; + const valEl = document.createElement("span"); + valEl.className = "sa-control-val"; + valEl.textContent = fmt(cur); + const applyLocal = (v) => { + valEl.textContent = fmt(v); + if (state.config) { + state.config.control_values = { ...state.config.control_values || {}, [id]: v }; + } + if (state.exact) { + state.exact.controls = { ...state.exact.controls || {}, [id]: v }; + } + }; + input.addEventListener("pointerdown", () => { + controlsPointerDown = true; + }); + const endPointer = () => { + const v = Number(input.value); + applyLocal(v); + controlsPointerDown = false; + if (pendingControlsRender) { + const schema2 = pendingControlsRender.schema; + pendingControlsRender = null; + renderPersonaControls( + schema2, + state.config?.control_values || state.exact?.controls || {} + ); + } + if (controlSaveTimer) { + clearTimeout(controlSaveTimer); + } + controlSaveTimer = setTimeout(() => savePersonaControls({ [id]: v }), 50); + }; + input.addEventListener("pointerup", endPointer); + input.addEventListener("pointercancel", endPointer); + input.addEventListener("input", () => { + applyLocal(Number(input.value)); + }); + input.addEventListener("change", () => { + const v = Number(input.value); + applyLocal(v); + if (controlSaveTimer) { + clearTimeout(controlSaveTimer); + } + controlSaveTimer = setTimeout(() => savePersonaControls({ [id]: v }), 50); + }); + row.appendChild(lab); + row.appendChild(input); + row.appendChild(valEl); + box.appendChild(row); + } + } + function getControlValue(id, fallback) { + const v = state.config?.control_values?.[id] ?? state.exact?.controls?.[id]; + const n = Number(v); + return Number.isFinite(n) ? n : fallback; + } + function coolDownHorny() { + const persona = $("sa_persona")?.value || ""; + if (persona !== "leonid") { + setStatus("/\u043E\u0441\u0442\u044B\u043D\u044C \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F Leonid"); + return; + } + state.lastUserControlIntent = true; + const schema = state.config?.controls || {}; + if (!schema.horny) { + setStatus("\u0423 \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438 \u043D\u0435\u0442 \u0441\u043B\u0430\u0439\u0434\u0435\u0440\u0430 \u0425\u043E\u0440\u043D\u0438"); + return; + } + const min = Number(schema.horny.min ?? 0); + const max = Number(schema.horny.max ?? 100); + const cur = getControlValue("horny", Number(schema.horny.default ?? 35)); + const next = Math.max(min, Math.min(max, cur - 30)); + const values = { + ...state.config?.control_values || state.exact?.controls || {}, + horny: next + }; + if (state.config) { + state.config.control_values = values; + } + if (state.exact) { + state.exact.controls = values; + } + renderPersonaControls(schema, values); + savePersonaControls({ horny: next }); + appendSystemNote(`\u0425\u043E\u0440\u043D\u0438: ${Math.round(cur)}% \u2192 ${Math.round(next)}% (\u221230)`); + setStatus(`/\u043E\u0441\u0442\u044B\u043D\u044C \u2192 ${Math.round(next)}%`); + } + async function startHornyGame() { + const persona = $("sa_persona")?.value || ""; + if (persona !== "leonid") { + setStatus("/horny-game \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F Leonid"); + return; + } + const cur = getControlValue("horny", 35); + state.lastUserControlIntent = true; + await sendChat({ + skipSlash: true, + skipAutoPack: true, + forcedUserText: `\u041A\u043E\u043C\u0430\u043D\u0434\u0430 /horny-game. \u0422\u0435\u043A\u0443\u0449\u0438\u0439 controls.horny = ${Math.round(cur)} (0\u2013100). +\u041E\u0446\u0435\u043D\u0438, \u043D\u0430\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0432\u043A\u0443\u0441\u044B \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F \u0432 \u044D\u0442\u043E\u043C \u0447\u0430\u0442\u0435 / \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u043C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0438 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u044E\u0442 \u0441 \u0442\u0432\u043E\u0438\u043C\u0438 (roleplay, outfits, realism, fetishes). +\u041F\u043E\u0441\u0442\u0430\u0432\u044C \u043D\u043E\u0432\u044B\u0439 controls.horny: \u0443\u043C\u043D\u043E\u0436\u044C/\u0441\u0434\u0432\u0438\u043D\u044C \u0442\u0435\u043A\u0443\u0449\u0435\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u043F\u043E\u0440\u0446\u0438\u043E\u043D\u0430\u043B\u044C\u043D\u043E \xAB\u043D\u0430\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0442\u0435\u0431\u0435 \u044D\u0442\u043E \u0437\u0430\u0448\u043B\u043E\xBB (\u0441\u043B\u0430\u0431\u043E\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0435 \u2192 \u0447\u0443\u0442\u044C \u0432\u043D\u0438\u0437 \u0438\u043B\u0438 \u043F\u043E\u0447\u0442\u0438 \u0431\u0435\u0437 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439; \u0441\u0438\u043B\u044C\u043D\u043E\u0435 \u2192 \u0437\u0430\u043C\u0435\u0442\u043D\u044B\u0439 \u0440\u043E\u0441\u0442, clamp 0\u2013100). +\u0412 \u043F\u0440\u043E\u0437\u0435 \u0441\u043A\u0430\u0436\u0438 \u043A\u0440\u0430\u0442\u043A\u043E: \u0441\u043E\u0432\u043F\u0430\u043B\u043E \u043B\u0438, \u043A\u0430\u043A\u043E\u0439 \u043C\u043D\u043E\u0436\u0438\u0442\u0435\u043B\u044C/\u0441\u0434\u0432\u0438\u0433 \u0438 \u043D\u043E\u0432\u044B\u0439 %. \u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D JSON patch \u0441 "controls": { "horny": }. \u0411\u0435\u0437 generate, \u0435\u0441\u043B\u0438 \u043D\u0435 \u043F\u0440\u043E\u0441\u0438\u043B\u0438 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443.` + }); + setStatus("/horny-game\u2026"); + } + function savePersonaControls(partial) { + const persona = $("sa_persona")?.value || "neutral"; + if (typeof genericRequest !== "function") { + return; + } + if (partial && typeof partial === "object") { + if (state.config) { + state.config.control_values = { ...state.config.control_values || {}, ...partial }; + } + if (state.exact) { + state.exact.controls = { ...state.exact.controls || {}, ...partial }; + } + } + genericRequest( + "AssistentSaveControls", + { persona, controls: partial || {} }, + (data) => { + if (data?.error) { + setStatus(data.error); + return; + } + if (data?.control_values && state.config) { + state.config.control_values = data.control_values; + if (state.exact) { + state.exact.controls = data.control_values; + } + } + if (!controlsPointerDown && data?.control_values) { + syncPersonaControlInputs(data.control_values); + } + }, + 0, + () => setStatus("controls save failed") + ); + } + function syncPersonaControlInputs(values) { + const box = $("sa_persona_controls"); + if (!box || !values || typeof values !== "object") { + return; + } + box.querySelectorAll("input[data-control-id]").forEach((input) => { + const id = input.dataset.controlId; + if (values[id] == null) { + return; + } + const v = Number(values[id]); + if (!Number.isFinite(v) || input.value === String(v)) { + return; + } + input.value = String(v); + const valEl = input.parentElement?.querySelector(".sa-control-val"); + if (valEl) { + const schema = state.config?.controls?.[id]; + const asPercent = String(schema?.display || "").toLowerCase() === "percent"; + valEl.textContent = asPercent ? `${Math.round(v)}%` : Number(v).toFixed(2); + } + }); + } + async function deleteCurrentOverlayPersona() { + const id = $("sa_persona")?.value; + if (!id) { + return; + } + const meta = (state.personas || []).find((p) => p.id === id); + const title = meta?.title || id; + const src = meta?.source || state.config?.persona_source || ""; + if (src !== "overlay" && src !== "overlay+bundled") { + setStatus("Bundled personas cannot be deleted"); + return; + } + if (!window.confirm(`\u0423\u0434\u0430\u043B\u0438\u0442\u044C \xAB${title}\xBB? +\u041F\u043E\u0441\u0442\u0430\u0432\u043A\u0430 (bundled) \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u0435\u0442\u0441\u044F.`)) { + return; + } + await new Promise((resolve) => { + genericRequest( + "AssistentDeletePersona", + { persona: id }, + async (data) => { + if (data?.error) { + setStatus(data.error); + resolve(); + return; + } + const next = data?.default_persona || "neutral"; + if (Array.isArray(data?.personas)) { + state.personas = data.personas; + } + renderPersonaOptions(state.personas || [], next); + if ($("sa_persona")) { + $("sa_persona").value = next; + } + await applyPersonaForChat(next, { quiet: false }); + setStatus(`\u0423\u0434\u0430\u043B\u0435\u043D\u043E: ${id}`); + resolve(); + }, + 0, + () => { + setStatus("delete failed"); + resolve(); + } + ); + }); + } + function renderPersonaOptions(personas, selected) { + const sel = $("sa_persona"); + if (!sel) { + return; + } + const cur = selected || sel.value || localStorage.getItem(LS_PERSONA) || "neutral"; + sel.innerHTML = ""; + for (const p of personas) { + const opt = document.createElement("option"); + opt.value = p.id; + opt.textContent = p.title || p.id; + if (p.accent) { + opt.dataset.accent = p.accent; + } + sel.appendChild(opt); + } + if ([...sel.options].some((o) => o.value === cur)) { + sel.value = cur; + } + const meta = (personas || []).find((p) => p.id === sel.value); + syncPersonaDeleteButton(meta?.source || state.config?.persona_source); + } + function renderPackOptions(packs, preferred) { + const sel = $("sa_pack"); + if (!sel) { + return; + } + const cur = preferred || sel.value || localStorage.getItem(LS_PACK) || defaultPackId(); + 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 chatModelSeniority(name) { + const n = String(name || "").toLowerCase(); + let score = 0; + const m = n.match(/(?:^|[:\-/])(\d+)\s*b\b/); + if (m) { + score += Number(m[1]) * 1e6; + } + if (n.includes("instruct")) { + score += 5e4; + } + if (n.includes("qwen3")) { + score += 2e4; + } + if (n.includes("thinking") || n.endsWith(":latest")) { + score -= 1e4; + } + return score; + } + function pickSeniorChatModel(names) { + const list = (names || []).map((n) => String(n || "").trim()).filter(Boolean); + if (!list.length) { + return ""; + } + return [...list].sort((a, b) => chatModelSeniority(b) - chatModelSeniority(a) || a.localeCompare(b))[0]; + } + function resolveChatModel(names, apiPreferred) { + const list = (names || []).map((n) => String(n || "").trim()).filter(Boolean); + if (!list.length) { + return ""; + } + const preferred = apiPreferred && list.includes(apiPreferred) ? apiPreferred : pickSeniorChatModel(list); + const ls = state.preferredModel || localStorage.getItem(LS_MODEL) || ""; + if (ls && list.includes(ls)) { + return ls; + } + return preferred || list[0]; + } + function setModelOptions(models, { error, preferred } = {}) { + const sel = $("sa_model"); + const sel2 = $("sa_settings_chat_model"); + const apply = (target) => { + if (!target) { + return; + } + let names = (models || []).map((n) => String(n || "").trim()).filter(Boolean); + names = [...names].sort((a, b) => chatModelSeniority(b) - chatModelSeniority(a) || a.localeCompare(b)); + target.innerHTML = ""; + if (error) { + const opt = document.createElement("option"); + opt.value = ""; + opt.textContent = `\u26A0 ${String(error).replace(/\s+/g, " ").slice(0, 90)}`; + target.appendChild(opt); + target.disabled = true; + return; + } + target.disabled = false; + if (!names.length) { + const opt = document.createElement("option"); + opt.value = ""; + opt.textContent = "No Ollama models \u2014 pull / Refresh"; + target.appendChild(opt); + return; + } + for (const name of names) { + const opt = document.createElement("option"); + opt.value = name; + opt.textContent = name; + target.appendChild(opt); + } + const pick = resolveChatModel(names, preferred); + if (pick) { + target.value = pick; + } + }; + apply(sel); + apply(sel2); + } + 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 + " (\u043E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F 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\u2026"); + 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 || []; + const preferred = (data.preferred || "").trim(); + setModelOptions(models, { preferred }); + setEmbedModelOptions(memoryModels); + const pick = resolveChatModel(models, preferred); + if (pick && $("sa_model")) { + $("sa_model").value = pick; + if ($("sa_settings_chat_model")) { + $("sa_settings_chat_model").value = pick; + } + state.preferredModel = pick; + localStorage.setItem(LS_MODEL, pick); + } + setStatus(models.length ? `${models.length} chat \xB7 ${memoryModels.length} memory` : "No Ollama models (gpu-rent: ollama pull)"); + if (models.length) { + setOllamaHealth("ok", `Ollama \xB7 ${models.length}`, `\u0427\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u0435\u0439: ${models.length}, \u043F\u0430\u043C\u044F\u0442\u044C: ${memoryModels.length}`); + } else { + setOllamaHealth("warn", "Ollama \xB7 0 \u043C\u043E\u0434\u0435\u043B\u0435\u0439", "\u041D\u0435\u0442 \u0447\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u2014 \u0441\u0434\u0435\u043B\u0430\u0439 ollama pull"); + } + saveSettings(); + }, + 0, + (err) => { + const msg = String(err || "Ollama unreachable"); + setStatus(msg); + setModelOptions([], { error: msg }); + setOllamaHealth("down", "Ollama \u2715", 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() / 1e3), + rescanned: !!data.rescanned + }; + state.inventoryFetchedAt = Date.now(); + const n = state.inventory.loras.length; + const ck = state.inventory.checkpoints.length; + setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts${rescan ? " (rescanned)" : ""}`); + prefetchActiveModelCards(); + if (state.view === "cards") { + renderCardsList(); + } + if (done) { + done(state.inventory); + } + }, + 0, + (err) => { + console.warn("Assistent inventory", err); + if (done) { + done(null); + } + } + ); + } + function refreshInventoryAsync(opts = {}) { + return new Promise((resolve) => refreshInventory(resolve, opts)); + } + function memoryKindFilter() { + return $("sa_mem_kind")?.value || "all"; + } + function memoryScopeFilter() { + return $("sa_mem_scope")?.value || "all"; + } + function memorySearchFilter() { + return ($("sa_mem_search")?.value || "").trim().toLowerCase(); + } + function renderMemoryKinds(kinds) { + const sel = $("sa_mem_kind"); + if (!sel) { + return; + } + const cur = sel.value || "all"; + sel.innerHTML = ""; + const all = document.createElement("option"); + all.value = "all"; + all.textContent = "\u0412\u0441\u0435 \u0442\u0438\u043F\u044B"; + sel.appendChild(all); + for (const kind of kinds || []) { + const opt = document.createElement("option"); + opt.value = kind; + opt.textContent = kind; + sel.appendChild(opt); + } + if ([...sel.options].some((o) => o.value === cur)) { + sel.value = cur; + } + } + function filteredMemoryRows() { + const filter = memoryKindFilter(); + const scope = memoryScopeFilter(); + const q = memorySearchFilter(); + const persona = $("sa_persona")?.value || "neutral"; + return (state.memoryRows || []).filter((m) => { + if (filter !== "all" && m.kind !== filter) { + return false; + } + if (scope === "shared" && m.scope !== "shared") { + return false; + } + if (scope === "personal" && !(m.scope === "personal" && (m.persona === persona || !m.persona))) { + return false; + } + if (q) { + const hay = `${m.kind || ""} ${m.key || ""} ${m.text || ""}`.toLowerCase(); + if (!hay.includes(q)) { + return false; + } + } + return true; + }); + } + function renderMemoryList() { + const root = $("sa_mem_list"); + if (!root) { + return; + } + const rows = filteredMemoryRows(); + root.innerHTML = ""; + if (!rows.length) { + root.innerHTML = '
\u041A\u0440\u0430\u0444\u0442-\u043F\u0430\u043C\u044F\u0442\u044C \u043F\u0443\u0441\u0442\u0430 \u2014 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0438, seed \u0438 \u043F\u0430\u0442\u0447\u0438 memory_upsert.
'; + return; + } + for (const row of rows) { + const el = document.createElement("div"); + el.className = "sa-mem-row"; + const bundled = row.source === "bundled"; + const when = row.updated ? formatChatWhen(row.updated * 1e3) : ""; + const scope = row.scope === "personal" ? `\u043F\u0435\u0440\u0441\u043E\u043D\u0430 ${row.persona || "\u2014"}` : "\u043E\u0431\u0449\u0430\u044F"; + el.innerHTML = `
${escapeHtml(row.kind || "note")}${escapeHtml(row.key || "")}
${escapeHtml(clipDebug(row.text, 220))}
${escapeHtml([scope, row.source || "user", when].filter(Boolean).join(" \xB7 "))}
`; + const forget = document.createElement("button"); + forget.type = "button"; + forget.className = "basic-button sa-mem-forget"; + forget.textContent = "\xD7"; + if (bundled) { + forget.disabled = true; + forget.title = "Bundled \u2014 \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u043F\u0440\u0438 reseed, \u043F\u0440\u0430\u0432\u044C Config/_base/memory-seed/"; + } else { + forget.title = "\u0417\u0430\u0431\u044B\u0442\u044C"; + forget.addEventListener("click", () => forgetMemory(row)); + } + el.appendChild(forget); + root.appendChild(el); + } + } + function refreshMemoryList() { + if (typeof genericRequest !== "function") { + return; + } + const list = $("sa_mem_list"); + if (list && !state.memoryRows.length) { + list.innerHTML = '
\u0427\u0438\u0442\u0430\u044E \u043F\u0430\u043C\u044F\u0442\u044C\u2026
'; + } + genericRequest( + "AssistentListMemory", + { limit: 200 }, + (data) => { + state.memoryRows = Array.isArray(data?.memories) ? data.memories : []; + renderMemoryKinds(data?.kinds || []); + renderMemoryList(); + const foot = $("sa_mem_total"); + if (foot) { + foot.textContent = `\u0412\u0441\u0435\u0433\u043E: ${data?.total ?? state.memoryRows.length} \xB7 ${data?.embed_model || "\u2014"}`; + } + }, + 0, + (err) => { + if (list) { + list.innerHTML = `
\u041F\u0430\u043C\u044F\u0442\u044C \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430: ${escapeHtml(String(err || "\u043E\u0448\u0438\u0431\u043A\u0430"))}
`; + } + } + ); + } + function forgetMemory(row) { + if (!row?.kind || !row?.key || typeof genericRequest !== "function") { + return; + } + genericRequest( + "AssistentForgetMemory", + { + kind: row.kind, + key: row.key, + source: row.source || "", + scope: row.scope || "", + persona: row.scope === "personal" ? row.persona || "" : "" + }, + () => { + state.memoryRows = (state.memoryRows || []).filter((m) => !(m.kind === row.kind && m.key === row.key && m.source === row.source && m.persona === row.persona)); + renderMemoryList(); + setStatus(`\u0417\u0430\u0431\u044B\u0442\u043E: ${row.kind}/${row.key}`); + }, + 0, + (err) => setStatus(String(err || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0431\u044B\u0442\u044C")) + ); + } + function clearCraftMemory(opts = {}) { + if (typeof genericRequest !== "function") { + return; + } + const label = opts.label || "\u043A\u0440\u0430\u0444\u0442-\u043F\u0430\u043C\u044F\u0442\u044C"; + if (!window.confirm(`\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C ${label}? Bundled seed \u043E\u0441\u0442\u0430\u043D\u0435\u0442\u0441\u044F.`)) { + return; + } + const body = { + scope: opts.scope || "", + kind: opts.kind || "", + persona: opts.persona || "" + }; + genericRequest( + "AssistentClearMemory", + body, + (data) => { + setStatus(`\u0423\u0434\u0430\u043B\u0435\u043D\u043E \u043A\u0440\u0430\u0444\u0442-\u0437\u0430\u043F\u0438\u0441\u0435\u0439: ${data?.deleted ?? 0}`); + refreshMemoryList(); + }, + 0, + (err) => setStatus(String(err || "\u041E\u0447\u0438\u0441\u0442\u043A\u0430 \u043D\u0435 \u0443\u0434\u0430\u043B\u0430\u0441\u044C")) + ); + } + function setSettingsTab(id) { + state.settingsTab = id || "behavior"; + document.querySelectorAll("#sa_settings .sa-stab").forEach((btn) => { + const on = btn.getAttribute("data-stab") === state.settingsTab; + btn.classList.toggle("sa-stab-active", on); + btn.setAttribute("aria-selected", on ? "true" : "false"); + }); + document.querySelectorAll("#sa_settings .sa-spane").forEach((pane) => { + pane.hidden = pane.getAttribute("data-spane") !== state.settingsTab; + }); + if (state.settingsTab === "craft") { + refreshMemoryList(); + refreshWantedQueue(); + } + if (state.settingsTab === "user") { + refreshUserPrefs(); + } + if (state.settingsTab === "personas") { + renderPersonaSettingsList(); + } + if (state.settingsTab === "models") { + syncSettingsHealthLine(); + const m = $("sa_model")?.value; + if (m && $("sa_settings_chat_model")) { + $("sa_settings_chat_model").value = m; + } + } + if (state.settingsTab === "more") { + fillKnobsFromConfig(state.config); + } + } + function fillKnobsFromConfig(data) { + const asst = data?.assistant || state.config?.assistant || {}; + const exact = data?.exact || state.config?.exact || state.exact || {}; + const setNum = (id, v) => { + const el = $(id); + if (el && v != null && Number.isFinite(Number(v))) { + el.value = String(v); + } + }; + setNum("sa_num_ctx", asst.num_ctx); + setNum("sa_history_keep", asst.history_keep_turns); + setNum("sa_memory_top_k", asst.memory_top_k); + const w = asst.user_prefs_weight != null ? Number(asst.user_prefs_weight) : 1; + const weightEl = $("sa_user_prefs_weight"); + if (weightEl) { + weightEl.value = String(Math.max(0, Math.min(1.5, w))); + const lab = $("sa_user_prefs_weight_val"); + if (lab) { + lab.textContent = Number(weightEl.value).toFixed(1); + } + } + const turbo = exact.profiles?.turbo || {}; + const raw = exact.profiles?.raw || {}; + setNum("sa_exact_turbo_steps", turbo.steps); + setNum("sa_exact_turbo_cfg", turbo.cfg); + setNum("sa_exact_turbo_sigma", turbo.sigma_shift); + setNum("sa_exact_raw_steps", raw.steps); + setNum("sa_exact_raw_cfg", raw.cfg); + setNum("sa_exact_raw_sigma", raw.sigma_shift); + } + function saveKnobs() { + if (typeof genericRequest !== "function") { + return; + } + const num = (id) => { + const v = parseFloat($(id)?.value); + return Number.isFinite(v) ? v : null; + }; + const assistant = { + num_ctx: num("sa_num_ctx"), + history_keep_turns: num("sa_history_keep"), + memory_top_k: num("sa_memory_top_k"), + user_prefs_weight: num("sa_user_prefs_weight") + }; + Object.keys(assistant).forEach((k) => { + if (assistant[k] == null) { + delete assistant[k]; + } + }); + const exact = { + profiles: { + turbo: { + steps: num("sa_exact_turbo_steps"), + cfg: num("sa_exact_turbo_cfg"), + sigma_shift: num("sa_exact_turbo_sigma") + }, + raw: { + steps: num("sa_exact_raw_steps"), + cfg: num("sa_exact_raw_cfg"), + sigma_shift: num("sa_exact_raw_sigma") + } + } + }; + genericRequest( + "AssistentSaveKnobs", + { assistant, exact }, + (data) => { + if (data?.assistant || data?.exact) { + applyConfigPayload({ + ...state.config, + assistant: data.assistant || state.config?.assistant, + exact: data.exact || state.config?.exact + }); + } + setStatus("Knobs \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u044B \u0432 overlay"); + }, + 0, + (err) => setStatus(String(err || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C knobs")) + ); + } + function syncSettingsHealthLine() { + const line = $("sa_settings_health_line"); + const badge = $("sa_ollama_health"); + if (line && badge) { + line.textContent = badge.textContent || "Ollama \xB7 \u2026"; + line.className = "sa-settings-health " + (badge.className || "").replace("sa-health", "").trim(); + } + } + function personaSourceLabel(source) { + if (source === "overlay") { + return "\u043C\u043E\u044F"; + } + if (source === "overlay+bundled") { + return "\u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043E+\u043F\u0440\u0430\u0432\u043A\u0430"; + } + return "\u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043E"; + } + function renderPersonaSettingsList() { + const root = $("sa_persona_list"); + if (!root) { + return; + } + const list = state.personas || state.config?.personas || []; + const cur = state.settingsPersonaId || $("sa_persona")?.value || list[0]?.id; + state.settingsPersonaId = cur; + root.innerHTML = ""; + for (const p of list) { + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "sa-persona-item" + (p.id === cur ? " sa-persona-item-active" : ""); + const accent = p.accent || "currentColor"; + btn.innerHTML = `
${escapeHtml(p.title || p.id)}
${escapeHtml(personaSourceLabel(p.source))}
`; + btn.addEventListener("click", () => { + state.settingsPersonaId = p.id; + renderPersonaSettingsList(); + loadPersonaPreview(p.id); + }); + root.appendChild(btn); + } + syncPersonaPanelActions(); + if (cur) { + loadPersonaPreview(cur); + } + } + function syncPersonaPanelActions() { + const id = state.settingsPersonaId; + const p = (state.personas || []).find((x) => x.id === id); + const canDelete = p && (p.source === "overlay" || p.source === "overlay+bundled"); + const del = $("sa_btn_persona_delete_panel"); + if (del) { + del.disabled = !canDelete; + } + } + function loadPersonaPreview(id) { + const box = $("sa_persona_preview"); + if (!box || typeof genericRequest !== "function") { + return; + } + box.innerHTML = '
\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026
'; + genericRequest( + "AssistentGetPersonaShelves", + { persona: id }, + (data) => { + const summary = data?.identity_summary || ""; + const src = data?.source || ""; + box.textContent = `${id} \xB7 ${personaSourceLabel(src)} + +${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; + syncPersonaPanelActions(); + }, + 0, + (err) => { + box.innerHTML = `
${escapeHtml(String(err || "\u043E\u0448\u0438\u0431\u043A\u0430"))}
`; + } + ); + } + function exportSelectedPersona() { + const id = state.settingsPersonaId || $("sa_persona")?.value; + if (!id || typeof genericRequest !== "function") { + return; + } + genericRequest( + "AssistentExportPersona", + { persona: id }, + (data) => { + const pack = data?.pack; + if (!pack) { + setStatus("\u041F\u0443\u0441\u0442\u043E\u0439 \u044D\u043A\u0441\u043F\u043E\u0440\u0442"); + return; + } + const blob = new Blob([JSON.stringify(pack, null, 2)], { type: "application/json" }); + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = `${pack.id || id}.assistent-persona.json`; + a.click(); + URL.revokeObjectURL(a.href); + setStatus(`\u042D\u043A\u0441\u043F\u043E\u0440\u0442: ${a.download}`); + }, + 0, + (err) => setStatus(String(err || "\u042D\u043A\u0441\u043F\u043E\u0440\u0442 \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F")) + ); + } + function importPersonaFile(file) { + if (!file || typeof genericRequest !== "function") { + return; + } + const reader = new FileReader(); + reader.onload = () => { + let pack; + try { + pack = JSON.parse(String(reader.result || "")); + } catch (e) { + setStatus("\u041D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 JSON"); + return; + } + let newId = pack?.id || ""; + if ((state.personas || []).some((p) => p.id === newId && (p.source === "bundled" || p.source === "overlay+bundled"))) { + newId = window.prompt("Id \u0437\u0430\u043D\u044F\u0442 bundled \u2014 \u043D\u043E\u0432\u044B\u0439 id:", `${newId}_import`) || ""; + } + genericRequest( + "AssistentImportPersona", + { pack, new_id: newId || null, overwrite: false }, + (data) => { + if (Array.isArray(data?.personas)) { + state.personas = data.personas; + renderPersonaOptions(data.personas, data.persona?.id); + } + state.settingsPersonaId = data?.persona?.id || newId; + renderPersonaSettingsList(); + setStatus(`\u0418\u043C\u043F\u043E\u0440\u0442: ${data?.persona?.id || newId}`); + }, + 0, + (err) => setStatus(String(err || "\u0418\u043C\u043F\u043E\u0440\u0442 \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F")) + ); + }; + reader.readAsText(file); + } + function cloneSelectedPersona() { + const from = state.settingsPersonaId || $("sa_persona")?.value; + if (!from) { + return; + } + const to = window.prompt("\u041D\u043E\u0432\u044B\u0439 id \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438:", `${from}_copy`); + if (!to) { + return; + } + genericRequest( + "AssistentClonePersona", + { from, to, title: to }, + (data) => { + if (Array.isArray(data?.personas)) { + state.personas = data.personas; + renderPersonaOptions(data.personas, to); + } + state.settingsPersonaId = to; + renderPersonaSettingsList(); + setStatus(`\u041A\u043B\u043E\u043D: ${to}`); + }, + 0, + (err) => setStatus(String(err || "\u041A\u043B\u043E\u043D \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F")) + ); + } + function deleteSelectedOverlayPersona() { + const id = state.settingsPersonaId; + const p = (state.personas || []).find((x) => x.id === id); + if (!p || p.source !== "overlay" && p.source !== "overlay+bundled") { + setStatus("\u041C\u043E\u0436\u043D\u043E \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E overlay"); + return; + } + if (!window.confirm(`\u0423\u0434\u0430\u043B\u0438\u0442\u044C overlay-\u043B\u0438\u0447\u043D\u043E\u0441\u0442\u044C \xAB${id}\xBB?`)) { + return; + } + genericRequest( + "AssistentDeletePersona", + { persona: id }, + (data) => { + if (Array.isArray(data?.personas)) { + state.personas = data.personas; + renderPersonaOptions(data.personas, data.default_persona); + } + state.settingsPersonaId = data?.default_persona || null; + renderPersonaSettingsList(); + setStatus(`\u0423\u0434\u0430\u043B\u0435\u043D\u043E: ${id}`); + }, + 0, + (err) => setStatus(String(err || "\u0423\u0434\u0430\u043B\u0435\u043D\u0438\u0435 \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C")) + ); + } + function refreshUserPrefs() { + if (typeof genericRequest !== "function") { + return; + } + const persona = $("sa_persona")?.value || "neutral"; + genericRequest( + "AssistentListUserPrefs", + { persona, limit: 200 }, + (data) => { + state.userPrefs = Array.isArray(data?.prefs) ? data.prefs : []; + renderUserPrefsLists(); + }, + 0, + (err) => setStatus(String(err || "User prefs \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B")) + ); + } + function renderUserPrefsLists() { + const persona = $("sa_persona")?.value || "neutral"; + const global = (state.userPrefs || []).filter((p) => p.scope === "global"); + const personal = (state.userPrefs || []).filter((p) => p.scope === "persona" && (p.persona_id === persona || p.persona === persona)); + const fill = (rootId, rows) => { + const root = $(rootId); + if (!root) { + return; + } + root.innerHTML = ""; + if (!rows.length) { + root.innerHTML = '
\u041F\u0443\u0441\u0442\u043E
'; + return; + } + for (const row of rows) { + const el = document.createElement("div"); + el.className = "sa-mem-row"; + const pin = row.pinned ? " \u2605" : ""; + el.innerHTML = `
${escapeHtml(row.key || "")}${pin}
${escapeHtml(clipDebug(row.text, 200))}
`; + el.querySelector(".sa-mem-row-body")?.addEventListener("click", () => editUserPref(row)); + el.querySelector(".sa-mem-row-body")?.setAttribute("title", "\u041A\u043B\u0438\u043A \u2014 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C"); + const pinBtn = document.createElement("button"); + pinBtn.type = "button"; + pinBtn.className = "basic-button sa-mem-forget"; + pinBtn.textContent = row.pinned ? "\u2605" : "\u2606"; + pinBtn.title = row.pinned ? "Unpin" : "Pin"; + pinBtn.addEventListener("click", (e) => { + e.stopPropagation(); + toggleUserPrefPin(row); + }); + el.appendChild(pinBtn); + const forget = document.createElement("button"); + forget.type = "button"; + forget.className = "basic-button sa-mem-forget"; + forget.textContent = "\xD7"; + forget.title = "\u0417\u0430\u0431\u044B\u0442\u044C"; + forget.addEventListener("click", (e) => { + e.stopPropagation(); + forgetUserPref(row); + }); + el.appendChild(forget); + root.appendChild(el); + } + }; + fill("sa_prefs_global", global); + fill("sa_prefs_persona", personal); + } + function editUserPref(row) { + const text = window.prompt("\u0422\u0435\u043A\u0441\u0442 \u0444\u0430\u043A\u0442\u0430:", row.text || ""); + if (text == null || !String(text).trim()) { + return; + } + genericRequest( + "AssistentUpsertUserPref", + { + key: row.key, + text: String(text).trim(), + scope: row.scope || "global", + persona: row.persona_id || row.persona || $("sa_persona")?.value || "neutral", + source: "user", + pinned: !!row.pinned + }, + () => refreshUserPrefs(), + 0, + (err) => setStatus(String(err || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C")) + ); + } + function toggleUserPrefPin(row) { + genericRequest( + "AssistentUpsertUserPref", + { + key: row.key, + text: row.text, + scope: row.scope || "global", + persona: row.persona_id || row.persona || $("sa_persona")?.value || "neutral", + source: row.source || "user", + pinned: !row.pinned + }, + () => refreshUserPrefs(), + 0, + (err) => setStatus(String(err || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C pin")) + ); + } + function addUserPref(scope) { + const key = window.prompt("\u041A\u043B\u044E\u0447 (stable-id):", scope === "global" ? "prefer" : "tone"); + if (!key) { + return; + } + const text = window.prompt("\u0422\u0435\u043A\u0441\u0442 \u0444\u0430\u043A\u0442\u0430:", ""); + if (!text) { + return; + } + genericRequest( + "AssistentUpsertUserPref", + { + key: key.trim(), + text: text.trim(), + scope, + persona: $("sa_persona")?.value || "neutral", + source: "user", + pinned: false + }, + () => { + refreshUserPrefs(); + setStatus("\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043E"); + }, + 0, + (err) => setStatus(String(err || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C")) + ); + } + function forgetUserPref(row) { + genericRequest( + "AssistentForgetUserPref", + { + key: row.key, + scope: row.scope || "global", + persona: row.persona_id || row.persona || $("sa_persona")?.value + }, + () => refreshUserPrefs(), + 0, + (err) => setStatus(String(err || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0431\u044B\u0442\u044C")) + ); + } + function clearUserPrefs(scope) { + const labels = { global: "\u043E\u0431\u0449\u0438\u0435 prefs", persona: "prefs \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438", all: "\u0432\u0441\u0435 prefs \u043E \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435" }; + if (!window.confirm(`\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C ${labels[scope] || scope}?`)) { + return; + } + genericRequest( + "AssistentClearUserPrefs", + { scope, persona: $("sa_persona")?.value || "neutral" }, + (data) => { + setStatus(`\u0423\u0434\u0430\u043B\u0435\u043D\u043E: ${data?.deleted ?? 0}`); + refreshUserPrefs(); + }, + 0, + (err) => setStatus(String(err || "\u041E\u0447\u0438\u0441\u0442\u043A\u0430 \u043D\u0435 \u0443\u0434\u0430\u043B\u0430\u0441\u044C")) + ); + } + function resetUiState() { + if (!window.confirm("\u0421\u0431\u0440\u043E\u0441\u0438\u0442\u044C UI-state (local + disk)? \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 Ollama \u0438 prefs \u043E\u0441\u0442\u0430\u043D\u0443\u0442\u0441\u044F.")) { + return; + } + const keys = Object.keys(localStorage).filter((k) => k.startsWith("swarm_assistent_")); + for (const k of keys) { + localStorage.removeItem(k); + } + diskPersist()?.saveUiState?.({}); + setStatus("UI-state \u0441\u0431\u0440\u043E\u0448\u0435\u043D \u2014 \u043E\u0431\u043D\u043E\u0432\u0438 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0443"); + } + function modelKeyLeaf(name) { + return String(name || "").replace(/\\/g, "/").split("/").pop().replace(/\.(safetensors|ckpt|pt|pth|gguf|bin)$/i, "").trim().toLowerCase(); + } + function refreshWantedQueue() { + if (typeof genericRequest !== "function") { + return; + } + genericRequest( + "AssistentListWanted", + {}, + (data) => { + const items = Array.isArray(data?.items) ? data.items : []; + state.wanted = { count: data?.count ?? items.length, items }; + const keys = /* @__PURE__ */ new Set(); + for (const item of items) { + const leaf = modelKeyLeaf(item?.title); + if (leaf) { + keys.add(leaf); + } + if (item?.version_id) { + keys.add(`v${item.version_id}`); + } + } + state.wantedKeys = keys; + const el = $("sa_mem_wanted"); + if (el) { + el.textContent = state.wanted.count ? `\u041E\u0447\u0435\u0440\u0435\u0434\u044C wanted: ${state.wanted.count} (\u0441\u043A\u0430\u0447\u0430\u0435\u0442\u0441\u044F \u043D\u0430 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u043C up)` : "\u041E\u0447\u0435\u0440\u0435\u0434\u044C wanted: \u043F\u0443\u0441\u0442\u043E"; + el.title = items.slice(0, 12).map((i) => `${i.kind}: ${i.title || i.url}`).join("\n"); + } + if (state.view === "cards") { + renderCardsList(); + } + }, + 0, + () => { + const el = $("sa_mem_wanted"); + if (el) { + el.textContent = "\u041E\u0447\u0435\u0440\u0435\u0434\u044C wanted: \u2014"; + } + } + ); + } + function isWantedModel(row) { + const keys = state.wantedKeys; + if (!keys || !keys.size) { + return false; + } + for (const candidate of [row?.name, row?.title]) { + const leaf = modelKeyLeaf(candidate); + if (leaf && keys.has(leaf)) { + return true; + } + } + return false; + } + function setOllamaHealth(level, text, title) { + state.ollamaHealth = level; + const el = $("sa_ollama_health"); + if (!el) { + return; + } + el.hidden = false; + el.textContent = text; + el.title = title || text; + el.classList.remove("sa-health-ok", "sa-health-warn", "sa-health-down"); + el.classList.add(`sa-health-${level}`); + syncSettingsHealthLine(); + } + function probeOllamaHealth() { + if (typeof genericRequest !== "function") { + return; + } + const baseUrl = $("sa_base_url")?.value || "http://127.0.0.1:11434"; + genericRequest( + "AssistentListModels", + { baseUrl }, + (data) => { + if (data?.error) { + setOllamaHealth("down", "Ollama \u2715", String(data.error)); + return; + } + const chat = (data.models || []).length; + const mem = (data.memory_models || []).length; + if (!chat) { + setOllamaHealth("warn", "Ollama \xB7 0 \u043C\u043E\u0434\u0435\u043B\u0435\u0439", "\u041D\u0435\u0442 \u0447\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u2014 \u0441\u0434\u0435\u043B\u0430\u0439 ollama pull"); + return; + } + setOllamaHealth("ok", `Ollama \xB7 ${chat}`, `\u0427\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u0435\u0439: ${chat}, \u043F\u0430\u043C\u044F\u0442\u044C: ${mem} \xB7 ${baseUrl}`); + }, + 0, + (err) => setOllamaHealth("down", "Ollama \u2715", `\u041D\u0435\u0442 \u0441\u0432\u044F\u0437\u0438: ${String(err || "")} \xB7 ${baseUrl}`) + ); + } + function setCardStatus(msg) { + const el = $("sa_card_status"); + if (el) { + el.textContent = msg || ""; + } + } + function setView(view) { + if (view === "cards") { + state.view = "cards"; + } else if (view === "settings") { + state.view = "settings"; + } else { + state.view = "chat"; + } + const chat = $("sa_view_chat"); + const cards = $("sa_view_cards"); + const settings = $("sa_view_settings"); + if (chat) { + chat.hidden = state.view !== "chat"; + } + if (cards) { + cards.hidden = state.view !== "cards"; + } + if (settings) { + settings.hidden = state.view !== "settings"; + } + $("sa_tab_chat")?.classList.toggle("sa-subtab-active", state.view === "chat"); + $("sa_tab_cards")?.classList.toggle("sa-subtab-active", state.view === "cards"); + $("sa_tab_settings")?.classList.toggle("sa-subtab-active", state.view === "settings"); + $("sa_btn_settings")?.classList.toggle("sa-subtab-active", state.view === "settings"); + $("sa_tab_chat")?.setAttribute("aria-selected", state.view === "chat" ? "true" : "false"); + $("sa_tab_cards")?.setAttribute("aria-selected", state.view === "cards" ? "true" : "false"); + $("sa_tab_settings")?.setAttribute("aria-selected", state.view === "settings" ? "true" : "false"); + saveSettings(); + if (state.view === "cards") { + renderCardsList(); + } else if (state.view === "settings") { + setSettingsTab(state.settingsTab || "behavior"); + } else if ((state.llmParked || state.expectColdLoad) && !state.generating) { + warmLlm({ force: true }); + } + } + function openSettings(tab) { + if (tab) { + state.settingsTab = tab; + } + setView("settings"); + } + function closeSettings() { + setView("chat"); + } + 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 = /* @__PURE__ */ 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) { + } + try { + if (typeof loraHelper !== "undefined" && Array.isArray(loraHelper?.selected)) { + for (const l of loraHelper.selected) { + add("lora", l?.name || l); + } + } + } catch (e) { + } + 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 \u043F\u0443\u0441\u0442 \u2014 \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C.
'; + 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 \u2713" : "\u043D\u0435\u0442 card"); + if (row.has_sidecar) { + metaBits.push("sidecar"); + } + if (isWantedModel(row)) { + metaBits.push("\u23F3 wanted"); + btn.classList.add("sa-card-row-wanted"); + } + if (row.trigger) { + metaBits.push(String(row.trigger).slice(0, 40)); + } + btn.innerHTML = `${thumb}
${escapeHtml(row.kind)}
${escapeHtml(row.title || row.name)}
${escapeHtml(metaBits.join(" \xB7 "))}
`; + btn.addEventListener("click", (e) => { + if (e.target?.closest?.("[data-chat]")) { + e.preventDefault(); + e.stopPropagation(); + sendCardToChat(row); + return; + } + selectCardModel(row); + }); + root.appendChild(btn); + } + } + function sendCardToChat(row) { + if (!row?.name) { + return; + } + selectCardModel(row); + setView("chat"); + const kind = row.kind === "checkpoint" ? "checkpoint" : "LoRA"; + const triggers = row.trigger ? ` Triggers: ${row.trigger}.` : ""; + if ($("sa_input")) { + $("sa_input").value = `\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 ${kind} \xAB${row.name}\xBB.${triggers} \u0423\u0447\u0442\u0438 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0443/triggers \u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438 \u043F\u0430\u0442\u0447.`; + $("sa_input").focus(); + } + setStatus(`\u0412 \u0447\u0430\u0442 \u2192 ${row.name}`); + } + function wireCardForm() { + const sync = () => { + if ($("sa_card_show_json")?.checked) { + syncCardJsonFromForm(); + } + }; + ["sa_card_triggers", "sa_card_weight", "sa_card_when", "sa_card_avoid", "sa_card_hint", "sa_card_notes", "sa_card_url"].forEach((id) => $(id)?.addEventListener("change", sync)); + $("sa_card_show_json")?.addEventListener("change", () => { + const on = !!$("sa_card_show_json")?.checked; + const ta = $("sa_card_json"); + if (ta) { + ta.hidden = !on; + if (on) { + syncCardJsonFromForm(); + } + } + }); + $("sa_card_json")?.addEventListener("change", () => { + if ($("sa_card_show_json")?.checked) { + applyCardToForm(readCardDraft() || {}); + } + }); + } + function applyCardToForm(card) { + card = card || {}; + const triggers = Array.isArray(card.triggers) ? card.triggers.join(", ") : card.triggers || ""; + if ($("sa_card_triggers")) { + $("sa_card_triggers").value = triggers; + } + if ($("sa_card_weight")) { + $("sa_card_weight").value = card.weight != null ? card.weight : state.cardsSelection?.kind === "lora" ? 0.8 : 1; + } + if ($("sa_card_when")) { + $("sa_card_when").value = card.when || ""; + } + if ($("sa_card_avoid")) { + $("sa_card_avoid").value = card.avoid || ""; + } + if ($("sa_card_hint")) { + $("sa_card_hint").value = card.prompt_hint || ""; + } + if ($("sa_card_notes")) { + $("sa_card_notes").value = card.notes || ""; + } + if ($("sa_card_url")) { + $("sa_card_url").value = card.civitai_url || ""; + } + if ($("sa_card_json")) { + $("sa_card_json").value = JSON.stringify(card, null, 2); + } + } + function syncCardJsonFromForm() { + const sel = state.cardsSelection || {}; + let base = {}; + try { + base = JSON.parse($("sa_card_json")?.value || "{}"); + } catch (e) { + base = {}; + } + const triggers = String($("sa_card_triggers")?.value || "").split(/[,;]/).map((s) => s.trim()).filter(Boolean); + const card = { + ...base, + kind: sel.kind || base.kind || "lora", + name: sel.name || base.name || "", + triggers, + weight: parseFloat($("sa_card_weight")?.value || "0.8") || 0.8, + when: $("sa_card_when")?.value || "", + avoid: $("sa_card_avoid")?.value || "", + prompt_hint: $("sa_card_hint")?.value || "", + notes: $("sa_card_notes")?.value || "", + civitai_url: $("sa_card_url")?.value || "", + version_id: base.version_id != null ? base.version_id : null + }; + if ($("sa_card_json")) { + $("sa_card_json").value = JSON.stringify(card, null, 2); + } + return card; + } + function renderCardPreviews(urls) { + const root = $("sa_card_previews"); + if (!root) { + return; + } + const list = (urls || []).filter(Boolean).slice(0, 6); + root.innerHTML = ""; + if (!list.length) { + root.hidden = true; + return; + } + root.hidden = false; + for (const url of list) { + const img = document.createElement("img"); + img.className = "sa-card-thumb"; + img.src = url; + img.alt = "preview"; + img.title = "\u041A\u043B\u0438\u043A \u2014 \u043D\u0430 \u0432\u043A\u043B\u0430\u0434\u043A\u0443 Refs"; + img.addEventListener("click", () => { + setBoardTab("refs"); + addRefFromUrl(url); + setStatus("\u041F\u0440\u0435\u0432\u044C\u044E \u2192 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 "\u041D\u0435\u0442 \u043E\u0442\u0432\u0435\u0442\u0430"; + } + if (data.error) { + return String(data.error); + } + const parts = []; + if (data.has_sidecar) { + parts.push(`\u0421\u0438\u0434\u0438\u043A\u0430\u0440\u044C \u2713 \xB7 version ${data.version_id || "?"}`); + } else if (data.fetched) { + parts.push(`Civitai \u2713 \xB7 version ${data.version_id || "?"}`); + } else { + parts.push("\u0421\u0438\u0434\u0438\u043A\u0430\u0440\u044F \u043D\u0435\u0442"); + } + const n = (data.example_urls || data.preview_urls || []).length; + if (n) { + parts.push(`${n} \u043A\u0430\u0434\u0440${n === 1 ? "" : "\u0430"}`); + } + if (data.has_card) { + parts.push("\u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0430 Assistent \u2713"); + } else { + parts.push("\u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0438 Assistent \u043D\u0435\u0442"); + } + if (data.fetch_error) { + parts.push(String(data.fetch_error)); + } + return parts.join(" \xB7 "); + } + 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 \u2713" : data.has_sidecar || data.fetched ? "meta \u2713" : "\u043D\u0435\u0442 \u043C\u0435\u0442\u044B"; + } + 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 = "\u2026"; + } + setCardStatus("\u0427\u0438\u0442\u0430\u044E \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u0443\u044E \u043C\u0435\u0442\u0443\u2026"); + genericRequest( + "AssistentGetCardMeta", + { kind: row.kind, name: row.name, fetch: false }, + (data) => applyCardMetaResponse(row, data || {}), + 0, + (err) => setCardStatus(String(err || "\u041E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438")) + ); + } + function fetchCardMetaLive() { + const row = state.cardsSelection; + if (!row) { + setCardStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C"); + return; + } + setCardStatus("\u0421\u0438\u0434\u0438\u043A\u0430\u0440\u044F \u043D\u0435\u0442 \xB7 \u0438\u0449\u0443 \u043F\u043E SHA\u2026"); + genericRequest( + "AssistentGetCardMeta", + { kind: row.kind, name: row.name, fetch: true }, + (data) => applyCardMetaResponse(row, data || {}, { preserveUser: true }), + 0, + (err) => setCardStatus(String(err || "Civitai: \u043E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u043F\u0440\u043E\u0441\u0430")) + ); + } + 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("\u041D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 JSON"); + return null; + } + } + return fromForm; + } + function saveCurrentCard({ enqueue } = {}) { + const sel = state.cardsSelection; + if (!sel) { + setCardStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C"); + return; + } + const card = readCardDraft(); + if (!card) { + return; + } + card.kind = card.kind || sel.kind; + card.name = card.name || sel.name; + setCardStatus("\u0421\u043E\u0445\u0440\u0430\u043D\u044F\u044E\u2026"); + 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 ? `\u041A\u0430\u0440\u0442\u043E\u0447\u043A\u0430 Assistent \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430 \xB7 ${data.path}` : `\u0427\u0435\u0440\u043D\u043E\u0432\u0438\u043A + wanted \xB7 ${data.path}`); + refreshInventory(() => renderCardsList()); + if (enqueue || !data.installed) { + refreshWantedQueue(); + } + }, + 0, + (err) => setCardStatus(String(err || "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F")) + ); + } + function enqueueWantedOnly() { + const sel = state.cardsSelection; + const card = readCardDraft() || {}; + if (!sel && !card.civitai_url) { + setCardStatus("\u041D\u0443\u0436\u043D\u0430 \u043C\u043E\u0434\u0435\u043B\u044C \u0438\u043B\u0438 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 ? "\u0423\u0436\u0435 \u0432 wanted" : `Wanted \u2192 ${data.path}`); + refreshWantedQueue(); + }, + 0, + (err) => setCardStatus(String(err || "\u041E\u0448\u0438\u0431\u043A\u0430 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) { + } + for (const l of selected) { + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "sa-lora-chip"; + btn.title = `${l.name} \xD7${l.weight} \u2014 \u043A\u043B\u0438\u043A \u0441\u043D\u044F\u0442\u044C`; + 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) { + } + 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 = "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0438\u0437 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 = "\u0424\u0438\u043B\u044C\u0442\u0440 LoRA\u2026"; + 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 ? " \xB7 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 = '
\u041D\u0435\u0442 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(); + } + async function generateCardWithAssistent() { + const sel = state.cardsSelection; + if (!sel) { + setCardStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C"); + return; + } + if (state.busy) { + setCardStatus("\u0427\u0430\u0442 \u0437\u0430\u043D\u044F\u0442"); + 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 = 2e4) { + if (!state.inventoryFetchedAt) { + return true; + } + return Date.now() - state.inventoryFetchedAt > maxAgeMs; + } + async function ensureFreshInventory({ forceRescan } = {}) { + const rescan = forceRescan || inventoryIsStale(2e4); + await refreshInventoryAsync({ rescan }); + } + function triggerSwarmModelRefresh(done) { + if (typeof genericRequest !== "function") { + if (done) { + done(); + } + return; + } + genericRequest( + "TriggerRefresh", + { strong: true }, + () => { + if (done) { + done(); + } + }, + 0, + () => { + if (done) { + done(); + } + } + ); + } + async function handleReplySideEffects(reply, civitaiResults, opts = {}) { + const { fromAutoCritique, fromVisionHop, fromCards, fromDebug } = opts; + if (fromCards) { + const card = extractCardJson(reply); + if (card) { + if ($("sa_card_json")) { + $("sa_card_json").value = JSON.stringify(card, null, 2); + } + if (opts.fromDownload || opts.cardTarget) { + const kind = card.kind || opts.cardTarget?.kind || "lora"; + const name = card.name || opts.cardTarget?.name; + if (name && typeof genericRequest === "function") { + genericRequest( + "AssistentSaveCard", + { kind, name, card, enqueue_wanted: false }, + (data) => { + if (data?.path) { + state.modelCards[`${kind}:${name}`] = card; + setCardStatus(data.installed ? `Card saved \u2192 ${data.path}` : `Card draft \u2192 ${data.path}`); + setStatus(`Card saved for ${name}`); + } + }, + 0, + () => setCardStatus("Card draft ready \u2014 Save manually") + ); + } + } else { + setView("cards"); + setCardStatus("Draft from Assistent \u2014 review & Save"); + } + } + return; + } + const { patch } = extractPatch2(reply); + if (fromDebug) { + state.pendingSilentGen = false; + return; + } + const commanded = !!opts.userWantsGenerate || !isMachineTurn(opts) && userImpliesGenerate(opts.userText || ""); + let effective = patch; + if (!effective && !fromVisionHop && !fromAutoCritique) { + const aspect = parseAspectFromUserText(opts.userText || ""); + if (aspect && (replyMissingJsonPatch(reply) || isSameButAspectRequest(opts.userText || ""))) { + effective = { aspect, actions: ["generate"] }; + if (state.lastPatch?.prompt) { + effective.prompt = state.lastPatch.prompt; + } + appendSystemNote(`\u041F\u0430\u0442\u0447 \u043F\u0443\u0441\u0442\u043E\u0439 \u2014 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043B aspect ${aspect} \u0441\u0430\u043C.`); + } + } + if (!effective && !fromVisionHop && !fromAutoCritique) { + const synthesized = synthesizePatchAfterEmptyFence(reply, opts.userText || "", opts); + if (synthesized) { + effective = synthesized; + appendSystemNote(synthesized.actions ? "\u041F\u0430\u0442\u0447 \u043F\u0443\u0441\u0442\u043E\u0439 \u2014 \u0441\u043E\u0431\u0440\u0430\u043B prompt \u0438\u0437 \u043E\u0442\u0432\u0435\u0442\u0430 \u0438 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u043B Generate." : "\u041F\u0430\u0442\u0447 \u043F\u0443\u0441\u0442\u043E\u0439 \u2014 \u0441\u043E\u0431\u0440\u0430\u043B prompt \u0438\u0437 \u043E\u0442\u0432\u0435\u0442\u0430."); + } + } + if (!effective && !fromVisionHop && !fromAutoCritique && commanded && claimTurnHop("empty_patch")) { + appendSystemNote("\u041D\u0443\u0436\u0435\u043D \u043A\u0430\u0434\u0440 \u2014 \u043F\u0440\u043E\u0448\u0443 JSON \u0441 prompt + generate."); + await sendChat({ + skipSlash: true, + skipAutoPack: true, + fromEmptyPatchRetry: true, + userWantsGenerate: true, + forcedUserText: '\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u0443\u0436\u0435 \u043F\u0440\u043E\u0441\u0438\u0442 \u043A\u0430\u0434\u0440 (\u044D\u0442\u043E \u0441\u043B\u0435\u0434\u0443\u0435\u0442 \u0438\u0437 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F, \u0434\u0430\u0436\u0435 \u0431\u0435\u0437 \u0441\u043B\u043E\u0432\u0430 \xAB\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439\xBB). \u041E\u0442\u0432\u0435\u0442\u044C \u0422\u041E\u041B\u042C\u041A\u041E \u043E\u0434\u043D\u0438\u043C fenced JSON: {"prompt":"","negative":"","actions":["generate"]}. prompt \u2014 \u0430\u043D\u0433\u043B\u0438\u0439\u0441\u043A\u0438\u0439. \u0411\u0435\u0437 \u043F\u0440\u043E\u0437\u044B, \u0431\u0435\u0437 \xAB\u0441\u043A\u0430\u0436\u0438 \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439\xBB.' + }); + return; + } + if (opts.fromPromptEnRetry && effective) { + effective = mergePromptEnRewrite(effective); + } + if (effective) { + rememberLastPatch(effective); + } + if (Array.isArray(effective?.actions) && effective.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); + } + } + const intent = resolveTurnIntent(effective, opts.userText || "", opts); + if (effective) { + if (intent.generate) { + const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : []; + if (!acts.includes("generate")) { + effective = { ...effective, actions: acts.concat("generate") }; + } + } else { + effective = stripGenerateAction(effective); + } + if (!intent.look) { + effective = stripLookAt(effective); + } + rememberLastPatch(effective); + } + if (intent.vetoed) { + state.pendingSilentGen = false; + } + if (effective && intent.look && !fromVisionHop && !fromAutoCritique) { + const hopped = await maybeVisionHop(effective, opts.attachedSlotIds || []); + if (hopped) { + return; + } + } + if (intent.generate && effective?.prompt && promptNeedsKreaPrep(effective.prompt) && !fromVisionHop && claimTurnHop("krea_prep")) { + state.pendingPromptEnMerge = { ...effective }; + appendSystemNote("\u0413\u043E\u0442\u043E\u0432\u043B\u044E \u043F\u0440\u043E\u043C\u043F\u0442 \u0434\u043B\u044F Krea 2 \u0447\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u044C\u044E (EN + \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430)\u2026"); + setBusyPhase("refining"); + await sendChat({ + skipSlash: true, + skipAutoPack: true, + fromPromptEnRetry: true, + userWantsGenerate: true, + forcedUserText: buildKreaPromptPrepRequest(effective) + }); + return; + } + const doApply = !!(effective && (intent.generate || $("sa_auto_apply")?.checked)); + if (doApply) { + if (intent.generate) { + startBusyUi("silent_gen"); + } else { + setBusyPhase("applying"); + } + await applyPatch(effective, "all"); + syncLiveParamsBar(); + if (intent.generate) { + if (effective?.prompt && promptNeedsKreaPrep(effective.prompt)) { + setStatus("\u041F\u0440\u043E\u043C\u043F\u0442 \u0432\u0441\u0451 \u0435\u0449\u0451 \u043D\u0435 EN/Krea-ready \u2014 Generate \u0441 \u0442\u0435\u043C \u0447\u0442\u043E \u0435\u0441\u0442\u044C"); + } + const src = await runGenerateFromPatch( + { ...effective, actions: Array.isArray(effective.actions) ? effective.actions : ["generate"] }, + { force: true } + ); + if (src) { + await maybeAutoCritique(src); + await maybeAutoVisionLook(src); + } + } else if (!state.generating) { + stopBusyUi(intent.vetoed ? "\u0417\u0430\u043F\u043E\u043C\u043D\u0438\u043B \xB7 \u0431\u0435\u0437 Generate" : ""); + } + } else if (replyMissingJsonPatch(reply) && !fromAutoCritique && !fromVisionHop) { + setStatus("\u041E\u0442\u0432\u0435\u0442 \u0431\u0435\u0437 JSON-\u043F\u0430\u0442\u0447\u0430 \u2014 \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u043F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u043E"); + } + state.pendingSilentGen = false; + } + async function applyQuickPatch(patch, note) { + const withActions = { ...patch }; + if (!Array.isArray(withActions.actions) && patchHasGenTrigger(withActions)) { + withActions.actions = ["generate"]; + } + const prevIntent = state.lastUserParamIntent; + state.lastUserParamIntent = true; + await applyPatch(withActions, "all"); + state.lastUserParamIntent = prevIntent; + setStatus(note || "Applied"); + if ($("sa_auto_generate")?.checked) { + await runGenerateFromPatch(withActions); + } + syncChipHighlight(); + } + function syncChipHighlight() { + const bar = $("sa_chips"); + if (!bar) { + return; + } + const cur = guessAspectFromSize(val("input_width"), val("input_height")); + const seed = val("input_seed"); + bar.querySelectorAll("[data-aspect]").forEach((btn) => { + btn.classList.toggle("sa-chip-active", btn.getAttribute("data-aspect") === cur); + }); + bar.querySelectorAll("[data-seed]").forEach((btn) => { + const mode = btn.getAttribute("data-seed"); + const active = mode === "lock" && seed && seed !== "-1" || mode === "random" && (!seed || seed === "-1"); + btn.classList.toggle("sa-chip-active", active); + }); + } + function appendSystemNote(text) { + const box = $("sa_messages"); + if (!box) { + return; + } + hideChatEmpty(); + const div = document.createElement("div"); + div.className = "sa-msg assistant sa-system-note"; + div.textContent = text; + box.appendChild(div); + scrollMessagesToBottom(); + } + function clipDebug(s, max) { + const t = String(s || "").replace(/\s+/g, " ").trim(); + if (!t) { + return "\u2014"; + } + return t.length > max ? `${t.slice(0, max)}\u2026` : t; + } + function formatDebugLoras(list) { + if (!Array.isArray(list) || !list.length) { + return "\u043D\u0435\u0442"; + } + return list.slice(0, 8).map((l) => { + const name = l?.name || l; + const w = l?.weight != null ? `@${l.weight}` : ""; + return `${name}${w}`; + }).join(", "); + } + function buildDebugSummary() { + const persona = $("sa_persona")?.value || "neutral"; + const pack = $("sa_pack")?.value || defaultPackId(); + const chatModel = $("sa_model")?.value || "\u2014"; + const embed = $("sa_embed_model")?.value || state.preferredEmbed || "\u2014"; + const profile = detectKreaProfileName(); + const defaults = mergedGenerationDefaults(profile); + const session = state.sessionExact || {}; + const exactGen = state.exact?.generation || state.config?.exact?.generation || {}; + const ctx = (() => { + try { + return collectLiveContext(); + } catch (e) { + return {}; + } + })(); + const aspect = guessAspectFromSize(ctx.width, ctx.height) || defaults.aspect || "\u2014"; + const why = []; + if (Object.keys(session).length) { + why.push(`session_exact \u043F\u0435\u0440\u0435\u043A\u0440\u044B\u0432\u0430\u0435\u0442 Exact: ${Object.keys(session).join(", ")}`); + } else { + why.push("session_exact \u043F\u0443\u0441\u0442 \u2014 params \u0438\u0437 Exact + \u043F\u0440\u043E\u0444\u0438\u043B\u044C \u0447\u0435\u043A\u043F\u043E\u0438\u043D\u0442\u0430"); + } + why.push(`\u043F\u0440\u043E\u0444\u0438\u043B\u044C \u0447\u0435\u043A\u043F\u043E\u0438\u043D\u0442\u0430: ${profile} (\u0438\u043C\u044F/title \u2192 turbo|raw)`); + if (persona === "cinema" || state.exact?.generation?.aspect) { + why.push(`persona/exact aspect: ${state.exact?.generation?.aspect || exactGen.aspect || "\u2014"}`); + } + if (state.lastPatch) { + const keys = Object.keys(state.lastPatch).filter((k) => state.lastPatch[k] != null && k !== "notes"); + why.push(`\u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0439 \u043F\u0430\u0442\u0447 \u0437\u0430\u0434\u0430\u043B: ${keys.slice(0, 12).join(", ")}`); + } else { + why.push("\u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u0430\u0442\u0447\u0430 Assistent \u0435\u0449\u0451 \u043D\u0435\u0442"); + } + why.push("\u043F\u0440\u0438\u043E\u0440\u0438\u0442\u0435\u0442: user \u2192 About the user \u2192 session_exact \u2192 exact(+persona) \u2192 live UI \u2192 craft memory_hits"); + const lines = [ + "### Debug Assistent", + `persona=${persona} \xB7 pack=${pack}`, + `chat=${chatModel} \xB7 embed=${embed}`, + `skills=${(state.enabledSkills || []).join(",") || "\u2014"}`, + `auto: apply=${!!$("sa_auto_apply")?.checked} gen=${!!$("sa_auto_generate")?.checked} vision=${!!$("sa_auto_vision")?.checked} critique=${!!$("sa_auto_critique")?.checked}`, + "", + "Live SwarmUI:", + ` ckpt=${ctx.checkpoint?.name || "\u2014"} \xB7 krea_profile=${ctx.krea_profile || profile}`, + ` ${ctx.width || "?"}\xD7${ctx.height || "?"} (${aspect}) \xB7 steps=${ctx.steps ?? "\u2014"} \xB7 cfg=${ctx.cfg ?? "\u2014"} \xB7 sigma=${ctx.sigma_shift ?? "\u2014"} \xB7 seed=${ctx.seed ?? "\u2014"} \xB7 batch=${ctx.batch ?? "\u2014"}`, + ` loras: ${formatDebugLoras(ctx.selected_loras || ctx.enabled_loras)}`, + ` available_loras=${(ctx.available_loras || []).length}${ctx.available_loras_truncated ? ` truncated/${ctx.available_loras_total || "?"}` : ""}`, + ` prompt: ${clipDebug(ctx.prompt, 220)}`, + ` negative: ${clipDebug(ctx.negative, 120)}`, + ` init=${!!ctx.has_init_image} mask=${!!ctx.has_mask_image} prompt_images=${ctx.prompt_image_count || 0}`, + ` has_vision_image=${!!ctx.has_vision_image} \xB7 images_in_request=${!!ctx.images_in_request} \xB7 vision_ready=${visionReadySlots().length}`, + ` context_json_chars\u2248${JSON.stringify(ctx).length} \xB7 last_system_chars=${state.lastSystemChars || "\u2014"} \xB7 last_context_chars=${state.lastContextChars || "\u2014"}`, + state.lastSystemLayers ? ` system_layers: ${Object.entries(state.lastSystemLayers).map(([k, v]) => `${k}=${v}`).join(" \xB7 ")}` : " system_layers: \u2014 (\u043E\u0442\u043F\u0440\u0430\u0432\u044C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435, \u0447\u0442\u043E\u0431\u044B \u0437\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u044C)", + "", + "Exact defaults (merged):", + ` generation=${JSON.stringify(exactGen)}`, + ` effective=${JSON.stringify({ + steps: defaults.steps, + cfg: defaults.cfg, + sigma_shift: defaults.sigma_shift, + aspect: defaults.aspect, + images: defaults.images, + profile: defaults.profile + })}`, + ` session_exact=${Object.keys(session).length ? JSON.stringify(session) : "{}"}`, + "", + "\u041F\u043E\u0447\u0435\u043C\u0443 \u0442\u0430\u043A:", + ...why.map((w) => ` \xB7 ${w}`) + ]; + if (state.lastPatch) { + lines.push("", `last_patch: ${clipDebug(JSON.stringify(state.lastPatch), 360)}`); + } + return lines.join("\n"); + } + async function handleSlashCommand(raw) { + const text = String(raw || "").trim(); + if (!text.startsWith("/")) { + return false; + } + const parts = text.slice(1).split(/\s+/); + const cmd = (parts[0] || "").toLowerCase(); + const arg = parts.slice(1).join(" ").trim(); + if (cmd === "help" || cmd === "?") { + appendSystemNote(HELP_TEXT); + setStatus("/help"); + return true; + } + if (cmd === "new" || cmd === "newchat") { + await startNewChat({ saveCurrent: true }); + return true; + } + if (cmd === "history" || cmd === "chats" || cmd === "sessions") { + setChatsPanelOpen(true); + setStatus("/history"); + return true; + } + if (cmd === "debug" || cmd === "dbg" || cmd === "why") { + const dump = buildDebugSummary(); + appendSystemNote(dump); + const argL = String(arg || "").toLowerCase().trim(); + const wantLlm = cmd === "why" || /^(ask|llm|explain|поясни|почему|модель)(\s|$)/i.test(argL); + if (wantLlm) { + setStatus("/debug ask\u2026"); + await sendChat({ + forcedUserText: "\u041E\u0442\u043B\u0430\u0434\u043A\u0430 Assistent. \u041D\u0438\u0436\u0435 \u0444\u0430\u043A\u0442\u044B UI (\u0443\u0436\u0435 \u0441\u043E\u0431\u0440\u0430\u043D\u044B \u043A\u043B\u0438\u0435\u043D\u0442\u043E\u043C). \u041A\u0440\u0430\u0442\u043A\u043E \u0441\u0432\u043E\u0438\u043C\u0438 \u0441\u043B\u043E\u0432\u0430\u043C\u0438 (5\u201310 \u0441\u0442\u0440\u043E\u043A, \u044F\u0437\u044B\u043A \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F): \u043A\u0430\u043A\u0438\u0435 \u043F\u0440\u043E\u043C\u043F\u0442/params \u0441\u0435\u0439\u0447\u0430\u0441, \u0447\u0442\u043E \u0438\u0437 Exact vs session_exact vs live, \u0447\u0442\u043E \u0441\u0434\u0435\u043B\u0430\u043B \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0439 \u043F\u0430\u0442\u0447 \u0438 \u043F\u043E\u0447\u0435\u043C\u0443 \u0442\u0430\u043A \u043B\u043E\u0433\u0438\u0447\u043D\u043E. \u0411\u0435\u0437 JSON patch, \u0431\u0435\u0437 generate, \u0431\u0435\u0437 look_at.\n\n" + dump, + skipSlash: true, + skipAutoPack: true, + fromDebug: true, + skipAppendUser: true + }); + } else { + setStatus("/debug"); + } + return true; + } + if (cmd === "gen" || cmd === "generate") { + const prev = findCurrentGenerateSrc(); + startBusyUi("generating"); + state.generating = true; + setInterruptVisible(true); + if (!triggerGenerate()) { + state.generating = false; + stopBusyUi("Could not start Generate"); + return true; + } + const src = await waitForNewImage(prev); + state.generating = false; + setInterruptVisible(state.busy); + if (src) { + const gen = generateSlot(); + if (gen) { + gen.src = src; + renderBoard(); + } + stopBusyUi("Generate done"); + } else { + stopBusyUi("Generate finished"); + } + return true; + } + if (cmd === "look") { + const id = normalizeSlotId(arg || GEN_ID) || GEN_ID; + const slot = slotById(id); + if (!slot) { + setStatus(`\u041D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u044B\u0439 \u0441\u043B\u043E\u0442: ${arg || GEN_ID}`); + return true; + } + if (slot.type !== "generate") { + setBoardTab("refs"); + } else { + setBoardTab("generate"); + } + if (!slot.src && id === GEN_ID) { + const src = findCurrentGenerateSrc(); + if (src) { + slot.src = src; + } + } + if (!slot.src) { + setStatus(`Slot ${id} is empty`); + return true; + } + slot.attach = true; + renderBoard(); + if ($("sa_input")) { + $("sa_input").value = `Look at ${id} and describe what you see.`; + } + setPackValue("critique_image", { flash: true }); + await sendChat({ forceSlotIds: [id], skipAutoPack: true }); + return true; + } + if (cmd === "init") { + const src = selectedSrc() || findCurrentGenerateSrc(); + if (!src) { + setStatus("No image for Init"); + return true; + } + await setInitFromSrc(src); + setPackValue("inpaint_edit", { flash: true }); + return true; + } + if (cmd === "mask") { + const src = selectedSrc(); + if (!src) { + setStatus("Select a window with a mask image"); + return true; + } + await setMaskFromSrc(src); + setPackValue("inpaint_edit", { flash: true }); + return true; + } + if (cmd === "clear") { + clearInitAndMask(); + return true; + } + if (cmd === "interrupt" || cmd === "stop") { + doInterruptNow(); + clearInFlightUi({ status: "\u041F\u0440\u0435\u0440\u0432\u0430\u043D\u043E" }); + 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\u2026"); + 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|ordinary|critique|compose|params|inpaint|describe|card|persona"); + } else { + setStatus(`Pack \u2192 ${$("sa_pack")?.value}`); + } + return true; + } + if (cmd === "\u043E\u0441\u0442\u044B\u043D\u044C" || cmd === "ostyn" || cmd === "cool" || cmd === "cooldown") { + coolDownHorny(); + return true; + } + if (cmd === "horny-game" || cmd === "hornygame" || cmd === "horny_game") { + await startHornyGame(); + 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(defaultPackId(), { flash: true }); + await sendChat({ + skipAutoPack: true, + forcedUserText: `Search Civitai for Krea-compatible LoRA: ${arg}. Prefer actions search_civitai.` + }); + return true; + } + if (cmd === "persona") { + const sub = (parts[1] || "new").toLowerCase(); + const rest = parts.slice(2).join(" ").trim(); + setPackValue("author_persona", { flash: true, user: true }); + if (sub === "save") { + await sendChat({ + skipAutoPack: true, + forcedUserText: "\u0421\u043E\u0445\u0440\u0430\u043D\u0438 \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438 \u0441\u0435\u0439\u0447\u0430\u0441 (persona_clone / persona_write). \u041D\u0435 \u0443\u0434\u0430\u043B\u044F\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438." + }); + return true; + } + const fromId = sub === "clone" && rest ? rest.split(/\s+/)[0] : $("sa_persona")?.value || "neutral"; + await sendChat({ + skipAutoPack: true, + forcedUserText: `\u041D\u0430\u0447\u043D\u0438 \u0438\u043D\u0442\u0435\u0440\u0432\u044C\u044E author_persona: \u043A\u043B\u043E\u043D \u0441 \u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A\u0430 \xAB${fromId}\xBB. \u0421\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0439 \u043F\u043E \u043F\u043E\u043B\u043A\u0430\u043C \u0433\u0440\u0443\u043F\u043F\u0430\u043C\u0438. \u041D\u0435 \u043F\u0438\u0448\u0438 \u043D\u0430 \u0434\u0438\u0441\u043A, \u043F\u043E\u043A\u0430 \u043C\u0430\u043B\u043E \u043E\u0442\u0432\u0435\u0442\u043E\u0432. \u041D\u0435 \u0443\u0434\u0430\u043B\u044F\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438.` + }); + return true; + } + appendSystemNote(`Unknown command /${cmd}. + +${HELP_TEXT}`); + setStatus(`Unknown /${cmd}`); + return true; + } + async function maybeVisionHop(patch, attachedSlotIds) { + const ids = lookAtIdsFromPatch(patch); + if (!ids.length || turnHopUsed("vision")) { + 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: \u043D\u0435\u0442 \u0440\u0435\u0430\u043B\u044C\u043D\u043E\u0433\u043E \u043A\u0430\u0434\u0440\u0430 (\u043F\u0440\u0435\u0432\u044C\u044E \u043C\u043E\u0434\u0435\u043B\u0438 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u043E)"); + return false; + } + const already = new Set(attachedSlotIds || []); + const need = have.filter((s) => !already.has(s.id)); + if (!need.length) { + return false; + } + if (!claimTurnHop("vision")) { + return false; + } + 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 \u2190 ${need.map((s) => s.label).join(", ")}`); + await sendChat({ fromVisionHop: true, forceSlotIds: need.map((s) => s.id) }); + return true; + } + async function sendChat(opts = {}) { + if ((state.busy || state.generating) && !isContinuationTurn(opts)) { + return; + } + const rawInput = ($("sa_input")?.value || "").trim(); + const text = (opts.forcedUserText || rawInput).trim(); + if (!text) { + return; + } + if (!isMachineTurn(opts)) { + state.lastUserParamIntent = userTextMentionsParams(text); + state.lastUserControlIntent = userTextMentionsControls(text); + state.pendingSilentGen = userImpliesGenerate(text); + } + if (!isMachineTurn(opts) && !opts.skipSlash) { + if (rawInput.startsWith("/")) { + if ($("sa_input")) { + $("sa_input").value = ""; + } + const handled = await handleSlashCommand(rawInput); + if (handled) { + return; + } + } + } + if (!isMachineTurn(opts) && isSameButAspectRequest(text)) { + const aspect = parseAspectFromUserText(text); + if (aspect) { + if ($("sa_input")) { + $("sa_input").value = ""; + } + appendMessage("user", text); + state.history.push({ role: "user", content: text }); + persistHistory(); + restoreDefaultPackAfterHop(); + const patch = { aspect, actions: ["generate"] }; + if (state.lastPatch?.prompt) { + patch.prompt = state.lastPatch.prompt; + } + if (Array.isArray(state.lastPatch?.loras) && state.lastPatch.loras.length) { + patch.loras = state.lastPatch.loras; + } + appendSystemNote(`\u0421\u0442\u0430\u0432\u043B\u044E ${aspect} \u0438 Generate (\u0442\u043E\u0442 \u0436\u0435 \u043F\u0440\u043E\u043C\u043F\u0442) \u2014 \u0431\u0435\u0437 \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u043E\u0439 \u043A\u0440\u0438\u0442\u0438\u043A\u0438.`); + await applyQuickPatch(patch, `Aspect ${aspect}`); + return; + } + } + if (!updateGate()) { + setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Krea 2"); + return; + } + if (!opts.skipAutoPack && !isMachineTurn(opts)) { + const guessed = autoSelectPack(text); + if (guessed) { + setPackValue(guessed, { flash: true }); + } + } + if (!opts.fromDebug && (opts.fromCards || state.view === "cards")) { + setPackValue("catalog_card", { flash: false }); + } + const pack = opts.fromDebug ? "debug_explain" : $("sa_pack")?.value || defaultPackId(); + const persona = $("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral"; + const model = $("sa_model")?.value; + if (!model) { + setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Ollama \u0432 \u2699"); + refreshModels(); + return; + } + const chatEpoch = bumpChatEpoch(); + state.busy = true; + state.llmParked = false; + setInterruptVisible(true); + if (state.expectColdLoad && !isContinuationTurn(opts)) { + startBusyUi("warming"); + setStatus("\u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E LLM \u0432 GPU\u2026"); + try { + await warmLlm({ force: true }); + } catch (e) { + console.warn("Assistent warm before send", e); + } + if (chatEpoch !== state.chatEpoch) { + return; + } + } + startBusyUi(state.expectColdLoad ? "loading" : "thinking"); + saveSettings(); + setStatus("\u041E\u0431\u043D\u043E\u0432\u043B\u044F\u044E inventory\u2026"); + try { + await ensureFreshInventory({ forceRescan: !!opts.fromDownload }); + await prefetchActiveModelCards(); + } catch (e) { + console.warn("Assistent inventory refresh", e); + } + if (chatEpoch !== state.chatEpoch) { + return; + } + if (!isContinuationTurn(opts) && !opts.fromDownload) { + resetTurnHops(); + } + let wantedIds = (opts.forceSlotIds || []).map(normalizeSlotId).filter(Boolean); + const sendVision = !!(opts.fromVisionHop || wantedIds.length && opts.forceSlotIds); + if (!sendVision) { + wantedIds = []; + } + const visionSlots = wantedIds.map((id) => slotById(id)).filter((s) => s && s.src && !looksLikeModelPreview(s.src)); + let images = null; + if (visionSlots.length) { + startBusyUi("encoding"); + setStatus("\u041A\u043E\u0434\u0438\u0440\u0443\u044E \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435\u2026"); + images = []; + for (const slot of visionSlots) { + if (chatEpoch !== state.chatEpoch) { + return; + } + const b64 = await imageToBase64ForOllama(slot.src); + if (b64) { + images.push(b64); + } + } + if (!images.length) { + images = null; + } + } + if (chatEpoch !== state.chatEpoch) { + return; + } + const msgMeta = { + persona: currentPersonaInfo(), + pack, + silentPatch: !!state.pendingSilentGen + }; + if (!opts.skipAppendUser) { + state.history.push({ role: "user", content: opts.historyUserText || text }); + if (state.pendingPersonaNote) { + state.history.push({ role: "user", content: state.pendingPersonaNote, systemish: true }); + state.pendingPersonaNote = null; + } + appendMessage("user", opts.historyUserText || text); + if ($("sa_input")) { + $("sa_input").value = ""; + } + persistHistory(); + } else { + const marker = opts.historyUserText || (opts.fromDebug ? "/debug ask" : ""); + if (marker) { + state.history.push({ role: "user", content: marker }); + persistHistory(); + } + } + const context = collectLiveContext(); + context.has_vision_image = visionReadySlots().length > 0; + context.images_in_request = !!(images && images.length); + context.attached_slot_ids = attachableSlots().map((s) => s.id); + context.vision_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; + } + const refreshed = collectLiveContext(); + context.model_cards = refreshed.model_cards; + const messages = state.history.slice(-historyMessageLimit()).map((m) => { + let content = String(m.content || ""); + if (m.role === "assistant") { + content = stripJsonFencesForHistory(content); + } + return { role: m.role, content: content.slice(0, 4e3) }; + }); + if (opts.skipAppendUser) { + messages.push({ role: "user", content: text }); + } + if (images && messages.length) { + messages[messages.length - 1].images = images; + } + startBusyUi("thinking"); + const baseUrl = $("sa_base_url")?.value || "http://127.0.0.1:11434"; + const payload = { + baseUrl, + model, + pack, + persona, + includeBase: !opts.fromDebug, + messages, + context_json: JSON.stringify(context), + skills: opts.fromDebug ? [] : state.enabledSkills || [], + embed_model: $("sa_embed_model")?.value || state.preferredEmbed || "" + }; + const finishOk = async (reply, civitaiResults, meta = {}) => { + if (chatEpoch !== state.chatEpoch) { + return; + } + if (meta.system_chars != null) { + state.lastSystemChars = Number(meta.system_chars) || 0; + } + if (meta.system_layers && typeof meta.system_layers === "object") { + state.lastSystemLayers = meta.system_layers; + } + try { + state.lastContextChars = context && JSON.stringify(context).length || 0; + } catch (e) { + state.lastContextChars = 0; + } + const prose = extractPatch2(reply).prose || reply; + state.history.push({ role: "assistant", content: prose, persona, pack }); + persistHistory(); + setBusyPhase(state.pendingSilentGen ? "silent_gen" : "thinking"); + try { + await handleReplySideEffects(reply, civitaiResults, { + ...opts, + userText: text, + userWantsGenerate: !!opts.userWantsGenerate || !isMachineTurn(opts) && state.pendingSilentGen, + attachedSlotIds: visionSlots.map((s) => s.id) + }); + } finally { + if (chatEpoch !== state.chatEpoch) { + return; + } + if (!state.generating) { + state.busy = false; + setInterruptVisible(false); + stopBusyUi("\u0413\u043E\u0442\u043E\u0432\u043E"); + } else { + state.busy = false; + setInterruptVisible(true); + } + } + }; + 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(state.expectColdLoad ? "loading" : "waiting"); + const label = state.streamEl?.querySelector(".sa-typing-label"); + if (label) { + label.textContent = state.expectColdLoad ? `\u0417\u0430\u0433\u0440\u0443\u0436\u0430\u044E ${modelShort(model)} \u0432 GPU\u2026` : "\u0414\u0443\u043C\u0430\u044E\u2026"; + } + 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 = '\u0423\u0442\u043E\u0447\u043D\u044F\u044E\u2026'; + } + 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, { system_chars: data.system_chars, system_layers: data.system_layers }); + } + }, + 0, + (err) => { + if (chatEpoch !== state.chatEpoch) { + return; + } + 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 || [], { system_chars: data.system_chars, system_layers: data.system_layers }); + }, + 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 || [], { system_chars: data.system_chars, system_layers: data.system_layers }); + }, + 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 === "Escape" && state.lightboxIndex >= 0) { + e.preventDefault(); + closeGenLightbox(); + return; + } + if ((e.key === "Enter" || e.key === " ") && state.boardTab === "generate" && state.selectedGenResultId) { + const row = (state.genResults || []).find((r) => r.id === state.selectedGenResultId); + if (row?.src) { + e.preventDefault(); + openGenLightbox(row.id); + return; + } + } + if ((e.key === "ArrowLeft" || e.key === "ArrowRight") && state.lightboxIndex >= 0) { + e.preventDefault(); + stepGenLightbox(e.key === "ArrowRight" ? 1 : -1); + return; + } + 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 items2 = e.clipboardData?.items; + let hasImage = false; + if (items2) { + for (const item of items2) { + if (item.type.startsWith("image/")) { + hasImage = true; + break; + } + } + } + if (!hasImage) { + return; + } + } + const items = e.clipboardData?.items; + if (!items) { + return; + } + for (const item of items) { + if (item.type.startsWith("image/")) { + e.preventDefault(); + const file = item.getAsFile(); + if (file) { + const sel = selectedSlot(); + await acceptImageFile(file, sel && sel.type === "ref" ? sel.id : null); + } + return; + } + } + }); + } + function wireSplitter() { + const splitter = $("sa_splitter"); + const layout = $("sa_layout"); + const pane = $("sa_image_pane"); + if (!splitter || !layout || !pane) { + return; + } + let dragging = false; + splitter.addEventListener("mousedown", (e) => { + e.preventDefault(); + dragging = true; + splitter.classList.add("sa-dragging"); + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + }); + window.addEventListener("mousemove", (e) => { + if (!dragging) { + return; + } + const rect = layout.getBoundingClientRect(); + const x = e.clientX - rect.left; + const pct = Math.min(56, Math.max(22, x / rect.width * 100)); + const value = `${pct}%`; + document.documentElement.style.setProperty("--sa-image-width", value); + localStorage.setItem(LS_PANE_WIDTH, value); + }); + window.addEventListener("mouseup", () => { + if (!dragging) { + return; + } + dragging = false; + splitter.classList.remove("sa-dragging"); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + saveUiStateToDisk(); + }); + } + function registerSendButton() { + if (typeof registerMediaButton !== "function") { + setTimeout(registerSendButton, 500); + return; + } + if (window.__swarmAssistentMediaRegistered) { + return; + } + window.__swarmAssistentMediaRegistered = true; + registerMediaButton( + "Send to Assistent", + (src) => { + putImageOnBoard(src, { + switchTab: true, + note: "Image sent to Assistent", + preferSelected: false + }); + const pack = $("sa_pack"); + if (pack && (pack.value === "ordinary" || pack.value === "write_prompt")) { + pack.value = "critique_image"; + saveSettings(); + } + }, + "Open Assistent with this image (vision / critique / prompt help)", + ["image"], + true, + true + ); + } + async function bootstrapPersisted() { + try { + await applyDiskUiState(); + } catch (e) { + console.warn("Assistent: ui-state restore failed", e); + } + try { + await initChatSessions(); + } catch (e) { + console.warn("Assistent: chat sessions failed", e); + } + loadConfig(localStorage.getItem(LS_PERSONA) || "neutral", () => { + refreshModels(); + refreshInventory(() => { + renderCardsList(); + renderLoraChips(); + }); + }); + probeOllamaHealth(); + refreshWantedQueue(); + } + function wire() { + if (!$("swarm_assistent_root")) { + return; + } + if (typeof genericRequest !== "function") { + setTimeout(wire, 300); + return; + } + if (window.__swarmAssistentWired) { + return; + } + window.__swarmAssistentWired = true; + loadSettings(); + setView(state.view || "chat"); + updateGate(); + ensureBoard(); + setBoardTab(state.boardTab || "generate", { persist: false }); + syncGenerateSlot(); + if (wantsAutoVision()) { + refreshImagePreview(); + } + bootstrapPersisted(); + wireDropZone(); + wireSplitter(); + registerSendButton(); + wireSlashInput(); + wireCardForm(); + $("sa_btn_new_chat")?.addEventListener("click", () => startNewChat({ saveCurrent: true })); + $("sa_btn_chats")?.addEventListener("click", (e) => { + e.stopPropagation(); + setChatsPanelOpen(!state.chatsPanelOpen); + }); + $("sa_session_label")?.addEventListener("click", (e) => { + e.stopPropagation(); + setChatsPanelOpen(!state.chatsPanelOpen); + }); + $("sa_session_label")?.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setChatsPanelOpen(!state.chatsPanelOpen); + } + }); + $("sa_chats_panel")?.addEventListener("click", (e) => e.stopPropagation()); + $("sa_chats_list")?.addEventListener("click", (e) => { + const row = e.target.closest(".sa-chat-row"); + if (!row) { + return; + } + const id = row.dataset.id; + if (e.target.closest("[data-del]")) { + e.preventDefault(); + if (window.confirm("\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u044D\u0442\u043E\u0442 \u0447\u0430\u0442 \u0438\u0437 \u0438\u0441\u0442\u043E\u0440\u0438\u0438?")) { + deleteChat(id); + } + return; + } + if (e.target.closest("[data-open]")) { + switchToChat(id); + } + }); + let chatsSearchTimer = null; + $("sa_chats_search")?.addEventListener("input", () => { + const q = ($("sa_chats_search")?.value || "").trim(); + state.chatsQuery = q; + if (!q) { + state.chatsSearchHits = null; + renderChatsList(); + return; + } + renderChatsList(); + clearTimeout(chatsSearchTimer); + chatsSearchTimer = setTimeout(async () => { + try { + const hits = await diskPersist()?.searchChats?.(q); + if ((state.chatsQuery || "") !== q) { + return; + } + state.chatsSearchHits = Array.isArray(hits) ? hits : []; + renderChatsList(); + } catch (e) { + } + }, 220); + }); + $("sa_tab_chat")?.addEventListener("click", () => setView("chat")); + $("sa_tab_cards")?.addEventListener("click", () => setView("cards")); + $("sa_tab_settings")?.addEventListener("click", () => openSettings(state.settingsTab || "behavior")); + $("sa_board_tab_gen")?.addEventListener("click", () => setBoardTab("generate")); + $("sa_board_tab_refs")?.addEventListener("click", () => setBoardTab("refs")); + $("sa_persona")?.addEventListener("change", onPersonaChanged); + $("sa_persona_delete")?.addEventListener("click", () => deleteCurrentOverlayPersona()); + $("sa_cards_kind")?.addEventListener("change", renderCardsList); + $("sa_btn_cards_refresh")?.addEventListener("click", () => refreshInventory(() => renderCardsList(), { rescan: true })); + $("sa_btn_card_meta")?.addEventListener("click", () => fetchCardMetaLive()); + $("sa_btn_card_generate")?.addEventListener("click", () => generateCardWithAssistent()); + $("sa_btn_card_save")?.addEventListener("click", () => saveCurrentCard()); + $("sa_btn_card_wanted")?.addEventListener("click", () => enqueueWantedOnly()); + $("sa_btn_settings")?.addEventListener("click", () => { + if (state.view === "settings") { + closeSettings(); + } else { + openSettings(state.settingsTab || "behavior"); + } + }); + $("sa_settings_close")?.addEventListener("click", () => closeSettings()); + document.querySelectorAll("#sa_settings .sa-stab").forEach((btn) => { + btn.addEventListener("click", () => setSettingsTab(btn.getAttribute("data-stab"))); + }); + $("sa_btn_mem_refresh")?.addEventListener("click", () => { + refreshMemoryList(); + refreshWantedQueue(); + }); + $("sa_mem_kind")?.addEventListener("change", renderMemoryList); + $("sa_mem_scope")?.addEventListener("change", renderMemoryList); + $("sa_mem_search")?.addEventListener("input", () => renderMemoryList()); + $("sa_btn_mem_clear_kind")?.addEventListener("click", () => { + const kind = memoryKindFilter(); + clearCraftMemory({ kind: kind === "all" ? "" : kind, label: kind === "all" ? "\u0432\u0435\u0441\u044C \u043A\u0440\u0430\u0444\u0442 (\u0444\u0438\u043B\u044C\u0442\u0440 \u0442\u0438\u043F\u0430)" : `\u0442\u0438\u043F ${kind}` }); + }); + $("sa_btn_mem_clear_persona")?.addEventListener("click", () => { + clearCraftMemory({ scope: "personal", persona: $("sa_persona")?.value || "neutral", label: "\u043A\u0440\u0430\u0444\u0442 \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438" }); + }); + $("sa_btn_mem_clear_shared")?.addEventListener("click", () => { + clearCraftMemory({ scope: "shared", label: "\u043E\u0431\u0449\u0443\u044E \u043A\u0440\u0430\u0444\u0442-\u043F\u0430\u043C\u044F\u0442\u044C" }); + }); + $("sa_btn_mem_clear_all")?.addEventListener("click", () => { + clearCraftMemory({ label: "\u0432\u0435\u0441\u044C \u043A\u0440\u0430\u0444\u0442 (non-bundled)" }); + }); + $("sa_btn_prefs_refresh")?.addEventListener("click", () => refreshUserPrefs()); + $("sa_btn_pref_add_global")?.addEventListener("click", () => addUserPref("global")); + $("sa_btn_pref_add_persona")?.addEventListener("click", () => addUserPref("persona")); + $("sa_btn_prefs_clear_global")?.addEventListener("click", () => clearUserPrefs("global")); + $("sa_btn_prefs_clear_persona")?.addEventListener("click", () => clearUserPrefs("persona")); + $("sa_btn_prefs_clear_all")?.addEventListener("click", () => clearUserPrefs("all")); + $("sa_user_prefs_weight")?.addEventListener("input", () => { + const lab = $("sa_user_prefs_weight_val"); + if (lab) { + lab.textContent = Number($("sa_user_prefs_weight").value).toFixed(1); + } + }); + $("sa_user_prefs_weight")?.addEventListener("change", () => saveKnobs()); + $("sa_memory_top_k")?.addEventListener("change", () => saveKnobs()); + $("sa_btn_knobs_save")?.addEventListener("click", () => saveKnobs()); + $("sa_btn_reset_ui")?.addEventListener("click", () => resetUiState()); + $("sa_btn_persona_export")?.addEventListener("click", () => exportSelectedPersona()); + $("sa_btn_persona_import")?.addEventListener("click", () => $("sa_persona_import_file")?.click()); + $("sa_persona_import_file")?.addEventListener("change", (e) => { + const file = e.target?.files?.[0]; + if (file) { + importPersonaFile(file); + } + e.target.value = ""; + }); + $("sa_btn_persona_clone")?.addEventListener("click", () => cloneSelectedPersona()); + $("sa_btn_persona_delete_panel")?.addEventListener("click", () => deleteSelectedOverlayPersona()); + $("sa_btn_settings_health")?.addEventListener("click", () => { + probeOllamaHealth(); + setTimeout(syncSettingsHealthLine, 400); + }); + $("sa_settings_chat_model")?.addEventListener("change", () => { + const v = $("sa_settings_chat_model")?.value; + if (v && $("sa_model")) { + $("sa_model").value = v; + saveSettings(); + } + }); + $("sa_btn_look_result")?.addEventListener("click", () => askLookAtResult()); + $("sa_ollama_health")?.addEventListener("click", () => probeOllamaHealth()); + document.addEventListener("keydown", (e) => { + if (state.lightboxIndex >= 0) { + if (e.key === "Escape") { + e.preventDefault(); + closeGenLightbox(); + return; + } + if (e.key === "ArrowLeft") { + e.preventDefault(); + stepGenLightbox(-1); + return; + } + if (e.key === "ArrowRight") { + e.preventDefault(); + stepGenLightbox(1); + return; + } + } + if (e.key !== "Escape") { + return; + } + let closed = false; + if (state.view === "settings") { + closeSettings(); + closed = true; + } + if (state.chatsPanelOpen) { + setChatsPanelOpen(false); + closed = true; + } + const slash = $("sa_slash_menu"); + if (slash && !slash.hidden) { + slash.hidden = true; + closed = true; + } + closeAllMoreMenus(); + if (closed) { + e.preventDefault(); + } + }); + document.getElementById(TAB_BUTTON_ID)?.addEventListener("click", () => { + setTimeout(() => $("sa_input")?.focus(), 80); + }); + $("sa_btn_refresh_models")?.addEventListener("click", () => { + saveSettings(); + refreshModels(); + probeOllamaHealth(); + }); + $("sa_btn_refresh_inventory")?.addEventListener("click", () => refreshInventory(() => { + renderCardsList(); + renderLoraChips(); + }, { rescan: true })); + $("sa_btn_add_ref")?.addEventListener("click", () => { + setBoardTab("refs"); + addRefSlot({ select: true }); + }); + $("sa_btn_use_current")?.addEventListener("click", () => snapshotGenerateToRef()); + $("sa_btn_as_init")?.addEventListener("click", async () => { + closeAllMoreMenus(); + const src = selectedSrc() || findCurrentGenerateSrc(); + if (!src) { + setStatus("\u041D\u0435\u0442 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F \u0434\u043B\u044F Init"); + return; + } + await setInitFromSrc(src); + const pack = $("sa_pack"); + if (pack && (pack.value === "ordinary" || pack.value === "write_prompt")) { + setPackValue("inpaint_edit", { flash: true }); + } + }); + $("sa_btn_as_mask")?.addEventListener("click", async () => { + closeAllMoreMenus(); + const src = selectedSrc(); + if (!src) { + setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043E\u043A\u043D\u043E \u0441 \u043C\u0430\u0441\u043A\u043E\u0439"); + return; + } + await setMaskFromSrc(src); + setPackValue("inpaint_edit", { flash: true }); + }); + $("sa_btn_clear_init")?.addEventListener("click", () => { + if (window.confirm("\u0421\u0431\u0440\u043E\u0441\u0438\u0442\u044C Init \u0438 Mask?")) { + clearInitAndMask(); + } + closeAllMoreMenus(); + }); + $("sa_btn_clear_image")?.addEventListener("click", () => clearSlot(state.selectedSlotId)); + $("sa_btn_board_more")?.addEventListener("click", (e) => { + e.stopPropagation(); + toggleMoreMenu("sa_board_more_menu", "sa_btn_board_more"); + }); + $("sa_btn_send")?.addEventListener("click", () => sendChat()); + $("sa_btn_build_gen")?.addEventListener("click", () => buildCurrentAndGenerate()); + $("sa_btn_interrupt")?.addEventListener("click", () => { + doInterruptNow(); + clearInFlightUi({ status: "\u041F\u0440\u0435\u0440\u0432\u0430\u043D\u043E" }); + }); + $("sa_btn_clear")?.addEventListener("click", () => { + if (window.confirm("\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0435\u0441\u044C \u0447\u0430\u0442 Assistent?")) { + clearChatHistory(); + } + }); + $("sa_btn_clear_more")?.addEventListener("click", (e) => { + e.stopPropagation(); + toggleMoreMenu("sa_clear_more_menu", "sa_btn_clear_more"); + }); + $("sa_btn_clear_confirm")?.addEventListener("click", () => { + closeAllMoreMenus(); + if (window.confirm("\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0435\u0441\u044C \u0447\u0430\u0442 Assistent?")) { + clearChatHistory(); + } + }); + $("sa_btn_clear_patches")?.addEventListener("click", () => { + closeAllMoreMenus(); + clearPatchBlocksOnly(); + }); + $("sa_btn_card_to_chat")?.addEventListener("click", () => { + if (state.cardsSelection) { + sendCardToChat(state.cardsSelection); + } else { + setCardStatus("\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0432\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C \u0432 \u0441\u043F\u0438\u0441\u043A\u0435"); + } + }); + document.addEventListener("click", () => { + if (state.chatsPanelOpen) { + setChatsPanelOpen(false); + } + closeAllMoreMenus(); + }); + $("sa_board_more_menu")?.addEventListener("click", (e) => e.stopPropagation()); + $("sa_clear_more_menu")?.addEventListener("click", (e) => e.stopPropagation()); + $("sa_base_url")?.addEventListener("change", saveSettings); + $("sa_model")?.addEventListener("change", () => { + const v = $("sa_model")?.value; + if (v && $("sa_settings_chat_model")) { + $("sa_settings_chat_model").value = v; + } + saveSettings(); + }); + $("sa_embed_model")?.addEventListener("change", () => { + state.preferredEmbed = $("sa_embed_model")?.value || ""; + saveSettings(); + }); + $("sa_pack")?.addEventListener("change", () => { + state.packUserTouched = true; + saveSettings(); + syncModeBadge(); + }); + $("sa_chips")?.addEventListener("click", async (e) => { + const btn = e.target.closest(".sa-chip"); + if (!btn || state.busy || state.generating) { + return; + } + const aspect = btn.getAttribute("data-aspect"); + const seed = btn.getAttribute("data-seed"); + const vary = btn.getAttribute("data-vary"); + const profile = btn.getAttribute("data-krea-profile"); + if (aspect) { + await applyQuickPatch({ aspect, actions: ["generate"] }, `Aspect ${aspect}`); + } else if (seed === "lock") { + await applyQuickPatch({ lock_seed: true }, "Seed locked"); + } else if (seed === "random") { + await applyQuickPatch({ seed: -1, actions: ["generate"] }, "Seed random"); + } else if (vary) { + await applyQuickPatch({ vary: true, seed: -1, actions: ["generate"] }, "Vary"); + } else if (profile === "turbo") { + const p = state.kreaProfiles?.turbo || mergedGenerationDefaults("turbo"); + await applyQuickPatch({ steps: p.steps ?? 8, cfg: p.cfg ?? 1, sigma_shift: p.sigma_shift ?? 1.15, actions: ["generate"] }, "Turbo"); + } else if (profile === "raw") { + const p = state.kreaProfiles?.raw || mergedGenerationDefaults("raw"); + await applyQuickPatch({ steps: p.steps ?? 28, cfg: p.cfg ?? 4.5, sigma_shift: p.sigma_shift, actions: ["generate"] }, "RAW"); + } + renderLoraChips(); + }); + $("sa_auto_vision")?.addEventListener("change", () => { + saveSettings(); + const gen = generateSlot(); + if (gen) { + gen.attach = wantsAutoVision(); + renderBoard(); + } + }); + $("sa_auto_apply")?.addEventListener("change", saveSettings); + $("sa_auto_generate")?.addEventListener("change", saveSettings); + $("sa_auto_critique")?.addEventListener("change", saveSettings); + $("sa_auto_download")?.addEventListener("change", saveSettings); + $("sa_park_llm")?.addEventListener("change", saveSettings); + syncChipHighlight(); + setInterval(syncChipHighlight, 2500); + setInterval(renderLoraChips, 4e3); + syncLiveParamsBar(); + setInterval(syncLiveParamsBar, 1200); + syncModeBadge(); + syncBuildGenButton(); + setInterval(updateGate, 2e3); + setInterval(syncGenerateSlot, 700); + setInterval(() => { + if (!state.busy && !state.generating) { + probeOllamaHealth(); + } + }, 45e3); + setInterval(() => { + if (!state.busy && !state.generating) { + refreshWantedQueue(); + } + }, 12e4); + window.addEventListener("beforeunload", () => { + try { + saveActiveChatToStore({ dropEmpty: true }); + const chat = findChat(state.activeChatId); + if (chat && (chat.messages || []).length) { + diskPersist()?.saveChat(chat, { immediate: true }); + } + diskPersist()?.saveUiState(collectUiState(), { immediate: true }); + } catch (e) { + } + }); + 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 ? 45e3 : 12e4) }); + } + }, 3e4); + window.swarmAssistent = { + setImageFromSrc, + putImageOnBoard, + clearVisionImage, + snapshotGenerateToRef, + openAssistentTab, + sendToAssistent: (src) => { + putImageOnBoard(src, { switchTab: true, note: "\u0418\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E \u0432 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(); + } + })(); + + // src/main.js + window.SA = window.SA || {}; + attachApi(window.SA); + attachPatch(window.SA); + attachPersist(window.SA); + window.SA.applyConfigPatchKeys = function(config) { + const keys = config?.patch_keys; + if (Array.isArray(keys) && keys.length) { + setPatchKeys(keys); + window.SA.PATCH_KEYS = keys; + } + }; +})(); diff --git a/Assets/assistent.css b/Assets/assistent.css index 271cd70..ecced26 100644 --- a/Assets/assistent.css +++ b/Assets/assistent.css @@ -95,6 +95,10 @@ outline: none; } +.sa-board:focus-visible { + box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 55%, transparent); +} + .sa-board.sa-board-gen-only { grid-template-columns: 1fr; grid-auto-rows: 1fr; @@ -138,6 +142,10 @@ transition: border-color 0.15s ease, box-shadow 0.15s ease; } +.sa-slot:focus-visible { + box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 55%, transparent); +} + .sa-slot.sa-has-image { border-style: solid; } diff --git a/Assets/assistent.patch.js b/Assets/assistent.patch.js deleted file mode 100644 index dca8974..0000000 --- a/Assets/assistent.patch.js +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Swarm Assistent — patch detection / extraction / alias normalization. - * Loaded before assistent.js; mirrors AssistentPatch.cs on the server side. - */ -window.SA = window.SA || {}; - -(function () { - const PATCH_KEYS = [ - 'prompt', 'negative', 'loras', 'width', 'height', 'steps', 'cfg', 'seed', 'sigma_shift', 'sampler', - 'actions', 'search_query', 'civitai_query', - 'use_init_image', 'clear_init_image', 'init_creativity', 'denoise', - 'use_mask_image', 'clear_mask_image', 'mask_blur', 'mask_grow', - 'look_at', 'vision_from', 'vision_slots', 'slot_to_init', 'slot_to_mask', - 'snapshot_generate', 'select_slot', 'aspect', 'images', 'batch', 'vary', 'lock_seed', - 'creativity', 'intensity', 'complexity', 'movement', - 'clear_prompt_images', 'slot_to_prompt_image', 'pack', - 'memories', 'memory_query', 'memory_kind', 'tag_query', 'user_prefs', - 'controls', 'persona_clone', 'persona_shelves', 'persona', 'notes', - 'variants', - ]; - - const FENCE_RE = /```(?:json)?\s*([\s\S]*?)```/gi; - - function has(obj, key) { - return obj[key] !== undefined && obj[key] !== null; - } - - /** Model-card JSON (catalog_card) — must not be treated as a Generate patch. */ - function isCardObject(obj) { - if (!obj || typeof obj !== 'object') { - return false; - } - const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint); - const genish = !!(obj.prompt != null || obj.negative != null || obj.loras || obj.actions - || obj.width || obj.height || obj.steps || obj.cfg || obj.aspect || obj.seed != null - || obj.search_query || obj.civitai_query || obj.look_at || obj.controls); - 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)); - } - - /** True when the object looks like a generation patch rather than a catalog card / arbitrary JSON. */ - function isPatchObject(obj) { - if (!obj || typeof obj !== 'object') { - return false; - } - if (isCardObject(obj)) { - return false; - } - return PATCH_KEYS.some((k) => has(obj, k)); - } - - /** Maps alias fields onto canonical names, keeping the aliases in place. */ - function normalizePatch(patch) { - if (!patch || typeof patch !== 'object') { - return patch; - } - if (!has(patch, 'search_query') && has(patch, 'civitai_query')) { - patch.search_query = patch.civitai_query; - } - if (!has(patch, 'init_creativity') && has(patch, 'denoise')) { - patch.init_creativity = patch.denoise; - } - if (!has(patch, 'look_at')) { - if (has(patch, 'vision_from')) { - patch.look_at = patch.vision_from; - } else if (has(patch, 'vision_slots')) { - patch.look_at = patch.vision_slots; - } - } - return patch; - } - - /** Splits a reply into prose and the last fenced patch object found in it. */ - function extractPatch(text) { - if (!text) { - return { prose: text || '', patch: null }; - } - const re = new RegExp(FENCE_RE.source, 'gi'); - let match; - let lastPatch = null; - let prose = text; - while ((match = re.exec(text)) !== null) { - try { - const obj = JSON.parse(match[1].trim()); - if (isPatchObject(obj)) { - lastPatch = normalizePatch(obj); - prose = (text.slice(0, match.index) + text.slice(match.index + match[0].length)).trim(); - } - } catch (e) { /* not json */ } - } - return { prose, patch: lastPatch }; - } - - /** - * Closed fence worth freezing the stream / stopping Ollama early. - * Weak fences (pack / creativity / empty) must NOT stop — model often continues with the real patch. - */ - function isTerminalStreamPatch(obj) { - if (!obj || typeof obj !== 'object') { - return false; - } - if (isCardObject(obj)) { - return true; - } - if (Array.isArray(obj.variants) && obj.variants.length) { - return true; - } - if (obj.look_at != null || obj.vision_from != null || obj.vision_slots != null) { - return true; - } - if (obj.search_query != null || obj.civitai_query != null - || obj.memory_query != null || obj.tag_query != null || obj.inventory_query != null) { - return true; - } - const acts = Array.isArray(obj.actions) ? obj.actions.map(String) : []; - const hopOrGen = [ - 'skill_load', 'persona_read', 'memory_get', 'memory_search', 'lookup_tags', - 'list_inventory', 'search_civitai', 'interrupt', 'generate', - 'memory_upsert', 'user_pref_upsert', - ]; - if (acts.some((a) => hopOrGen.includes(a))) { - return true; - } - if (String(obj.prompt || '').trim().length >= 48) { - return true; - } - if (obj.loras != null || obj.aspect != null || obj.steps != null - || obj.width != null || obj.height != null || obj.cfg != null - || obj.seed != null || obj.controls != null - || obj.memories != null || obj.user_prefs != null) { - return true; - } - return false; - } - - SA.PATCH_KEYS = PATCH_KEYS; - SA.isCardObject = isCardObject; - SA.isPatchObject = isPatchObject; - SA.isTerminalStreamPatch = isTerminalStreamPatch; - SA.normalizePatch = normalizePatch; - SA.extractPatch = extractPatch; -})(); - diff --git a/Assets/assistent.persist.js b/Assets/assistent.persist.js deleted file mode 100644 index 144d6ab..0000000 --- a/Assets/assistent.persist.js +++ /dev/null @@ -1,202 +0,0 @@ -/** - * Swarm Assistent — sqlite persistence for chats + UI state (Assistent/memory/assistent.sqlite). - * Loaded after assistent.api.js and before assistent.js. - */ -window.SA = window.SA || {}; - -(function () { - const LS_CHATS = 'swarm_assistent_chats_v1'; - const LS_MIGRATED = 'swarm_assistent_chats_on_disk_v1'; - const SAVE_DEBOUNCE_MS = 700; - - const timers = { chats: new Map(), ui: null }; - - function request(name, body) { - if (typeof SA.request === 'function') { - return SA.request(name, body); - } - return new Promise((resolve, reject) => { - if (typeof genericRequest !== 'function') { - reject(new Error('genericRequest unavailable')); - return; - } - genericRequest( - name, - body || {}, - (data) => (data && data.error ? reject(new Error(String(data.error))) : resolve(data)), - 0, - (err) => reject(err instanceof Error ? err : new Error(String(err || 'request failed'))), - ); - }); - } - - function normalizeChat(raw) { - if (!raw || !raw.id) { - return null; - } - return { - id: String(raw.id), - title: String(raw.title || 'Новый чат'), - createdAt: Number(raw.createdAt) || Date.now(), - updatedAt: Number(raw.updatedAt) || Number(raw.createdAt) || Date.now(), - messages: Array.isArray(raw.messages) ? raw.messages : [], - messages_count: Number(raw.messages_count) || (Array.isArray(raw.messages) ? raw.messages.length : 0), - params: raw.params && typeof raw.params === 'object' ? raw.params : null, - }; - } - - function readLocalChats() { - try { - const parsed = JSON.parse(localStorage.getItem(LS_CHATS) || 'null'); - if (Array.isArray(parsed?.chats)) { - return parsed.chats.map(normalizeChat).filter(Boolean); - } - } catch (e) { /* ignore */ } - return []; - } - - /** One-shot lift of the browser-only history onto the data volume. */ - async function migrateLocalChatsToDisk() { - if (localStorage.getItem(LS_MIGRATED) === '1') { - return []; - } - const local = readLocalChats().filter((c) => (c.messages || []).length > 0); - localStorage.setItem(LS_MIGRATED, '1'); - if (!local.length) { - return []; - } - for (const chat of local) { - try { - await saveChat(chat, { immediate: true }); - } catch (e) { - console.warn('Assistent: chat migration failed', chat.id, e); - } - } - return local; - } - - /** Sqlite chats, newest first. Falls back to a localStorage migration when the store is empty. */ - async function loadChats() { - let chats = []; - try { - const data = await request('AssistentListChats', { with_messages: true }); - chats = (data?.chats || []).map(normalizeChat).filter(Boolean); - } catch (e) { - console.warn('Assistent: disk chats unavailable', e); - return null; - } - if (!chats.length) { - const migrated = await migrateLocalChatsToDisk(); - if (migrated.length) { - chats = migrated; - } - } else { - localStorage.setItem(LS_MIGRATED, '1'); - } - return chats.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)); - } - - async function getChat(id) { - if (!id) { - return null; - } - const data = await request('AssistentGetChat', { id }); - return normalizeChat(data?.chat); - } - - async function searchChats(q) { - const query = String(q || '').trim(); - if (query.length < 2) { - return []; - } - const data = await request('AssistentListChats', { q: query, with_messages: false, limit: 40 }); - return (data?.chats || []).map(normalizeChat).filter(Boolean); - } - - function saveChat(chat, { immediate = false } = {}) { - const clean = normalizeChat(chat); - if (!clean) { - return Promise.resolve(null); - } - const send = () => { - timers.chats.delete(clean.id); - return request('AssistentSaveChat', { - id: clean.id, - title: clean.title, - messages: clean.messages, - params: clean.params, - createdAt: clean.createdAt, - updatedAt: clean.updatedAt, - }); - }; - if (immediate) { - const pending = timers.chats.get(clean.id); - if (pending) { - clearTimeout(pending); - } - return send(); - } - const pending = timers.chats.get(clean.id); - if (pending) { - clearTimeout(pending); - } - timers.chats.set(clean.id, setTimeout(() => { - send().catch((e) => console.warn('Assistent: chat save failed', clean.id, e)); - }, SAVE_DEBOUNCE_MS)); - return Promise.resolve(null); - } - - function deleteChat(id) { - if (!id) { - return Promise.resolve(null); - } - const pending = timers.chats.get(id); - if (pending) { - clearTimeout(pending); - timers.chats.delete(id); - } - return request('AssistentDeleteChat', { id }); - } - - async function loadUiState() { - try { - const data = await request('AssistentGetUiState', {}); - const ui = data?.ui_state; - return ui && typeof ui === 'object' ? ui : null; - } catch (e) { - return null; - } - } - - function saveUiState(uiState, { immediate = false } = {}) { - if (!uiState || typeof uiState !== 'object') { - return Promise.resolve(null); - } - const send = () => { - timers.ui = null; - return request('AssistentSaveUiState', { ui_state: uiState }); - }; - if (timers.ui) { - clearTimeout(timers.ui); - timers.ui = null; - } - if (immediate) { - return send(); - } - timers.ui = setTimeout(() => { - send().catch((e) => console.warn('Assistent: ui-state save failed', e)); - }, SAVE_DEBOUNCE_MS); - return Promise.resolve(null); - } - - SA.persist = { - LS_CHATS, - loadChats, - getChat, - searchChats, - saveChat, - deleteChat, - loadUiState, - saveUiState, - }; -})(); diff --git a/AssistentChatPipeline.cs b/AssistentChatPipeline.cs index d8f378b..a2eec2d 100644 --- a/AssistentChatPipeline.cs +++ b/AssistentChatPipeline.cs @@ -228,12 +228,24 @@ public partial class SwarmAssistentExtension { break; } - string tool = NextToolHop(patch); - if (string.IsNullOrWhiteSpace(tool)) + string follow = null; + JArray civitaiHop = null; + HashSet hopSkip = new(StringComparer.OrdinalIgnoreCase); + while (true) { - break; + string tool = NextToolHop(patch, hopSkip); + if (string.IsNullOrWhiteSpace(tool)) + { + follow = null; + break; + } + (follow, civitaiHop) = await RunToolHop(session, root, embed, pid, chain, patch, tool, hopDone); + if (follow is not null) + { + break; + } + hopSkip.Add(tool); } - (string follow, JArray civitaiHop) = await RunToolHop(session, root, embed, pid, chain, patch, tool, hopDone); if (follow is null) { break; diff --git a/AssistentConfig.cs b/AssistentConfig.cs index 11aaf92..4777dfa 100644 --- a/AssistentConfig.cs +++ b/AssistentConfig.cs @@ -29,6 +29,10 @@ public sealed class AssistentConfig public static string SafeId(string id) { string s = (id ?? "").Replace('\\', '/').AfterLast('/').Replace("..", "").Trim(); + if (string.Equals(s, "terse", StringComparison.OrdinalIgnoreCase)) + { + s = "aggressive"; + } if (string.IsNullOrWhiteSpace(s) || !Regex.IsMatch(s, @"^[A-Za-z0-9][A-Za-z0-9_\-]{0,63}$")) { return null; @@ -122,21 +126,29 @@ public sealed class AssistentConfig } } - public JArray TryReadJsonArray(string path) + public string[] LoadPatchKeys() { - if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) + string path = ResolveUnder(_bundledRoot, "_base/patch-keys.json"); + JObject doc = TryReadJson(path); + if (doc?["keys"] is JArray arr && arr.Count > 0) { - return null; - } - try - { - return JArray.Parse(File.ReadAllText(path, Encoding.UTF8)); - } - catch (Exception ex) - { - Logs.Debug($"AssistentConfig json-array {path}: {ex.Message}"); - return null; + return arr.Select(t => t?.ToString()).Where(s => !string.IsNullOrWhiteSpace(s)).ToArray(); } + // Fallback if bundled json missing + return + [ + "prompt", "negative", "loras", "width", "height", "steps", "cfg", "seed", "sigma_shift", "sampler", "scheduler", + "actions", "search_query", "civitai_query", + "use_init_image", "clear_init_image", "init_creativity", "denoise", + "use_mask_image", "clear_mask_image", "mask_blur", "mask_grow", + "look_at", "vision_from", "vision_slots", "slot_to_init", "slot_to_mask", + "snapshot_generate", "select_slot", "aspect", "images", "batch", "vary", "lock_seed", + "creativity", "intensity", "complexity", "movement", + "clear_prompt_images", "slot_to_prompt_image", "pack", "memories", "memory", + "memory_query", "memory_kind", "tag_query", "user_prefs", + "inventory_query", "skills", "persona_shelves", "persona_clone", "persona", "controls", + "variants", + ]; } public string TryReadText(string path) @@ -272,33 +284,6 @@ public sealed class AssistentConfig byId[id] = (cur.title, cur.accent, PersonaSource(id)); } - // Legacy personas.json titles - string overlayJson = Path.Combine(_overlayRoot, "personas.json"); - JObject legacy = TryReadJson(overlayJson); - if (legacy?["personas"] is JArray arr) - { - foreach (JToken t in arr) - { - if (t is not JObject po) - { - continue; - } - string id = SafeId(po["id"]?.ToString()); - if (id is null) - { - continue; - } - if (byId.TryGetValue(id, out var cur)) - { - byId[id] = (po["title"]?.ToString() ?? cur.title, cur.accent, cur.source); - } - else - { - byId[id] = (po["title"]?.ToString() ?? id, "#8b949e", "legacy"); - } - } - } - return byId.OrderBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase) .Select(kv => (kv.Key, kv.Value.title, kv.Value.accent, kv.Value.source)) .ToList(); @@ -308,13 +293,6 @@ public sealed class AssistentConfig { JObject assistant = MergeJsonLayers("assistant.json", LayerRoots("neutral")); string def = SafeId(assistant["default_persona"]?.ToString()) ?? "neutral"; - string overlayJson = Path.Combine(_overlayRoot, "personas.json"); - JObject legacy = TryReadJson(overlayJson); - string fromLegacy = SafeId(legacy?["default"]?.ToString()); - if (fromLegacy is not null) - { - def = fromLegacy; - } var catalog = ListPersonaCatalog(); if (catalog.All(p => !string.Equals(p.id, def, StringComparison.OrdinalIgnoreCase)) && catalog.Count > 0) { @@ -410,17 +388,20 @@ public sealed class AssistentConfig /// Clamp and merge control values into overlay exact.json (controls key only). public JObject SaveControlValues(string personaId, JObject values) { - string id = SafeId(personaId) ?? "neutral"; - JObject schema = LoadControlsSchema(id); - string dir = Path.Combine(_overlayRoot, "personas", id); - Directory.CreateDirectory(dir); - string path = Path.Combine(dir, "exact.json"); - JObject existing = TryReadJson(path) ?? new JObject(); - JObject prev = existing["controls"] as JObject ?? new JObject(); - JObject clamped = ClampControls(schema, values ?? new JObject()); - existing["controls"] = DeepMerge(prev, clamped); - File.WriteAllText(path, existing.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8); - return LoadControlValues(id); + lock (_lock) + { + string id = SafeId(personaId) ?? "neutral"; + JObject schema = LoadControlsSchema(id); + string dir = Path.Combine(_overlayRoot, "personas", id); + Directory.CreateDirectory(dir); + string path = Path.Combine(dir, "exact.json"); + JObject existing = TryReadJson(path) ?? new JObject(); + JObject prev = existing["controls"] as JObject ?? new JObject(); + JObject clamped = ClampControls(schema, values ?? new JObject()); + existing["controls"] = DeepMerge(prev, clamped); + File.WriteAllText(path, existing.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8); + return LoadControlValues(id); + } } public static JObject ClampControls(JObject schema, JObject values) @@ -727,11 +708,6 @@ public sealed class AssistentConfig { string id = SafeId(personaId) ?? throw new ArgumentException("invalid persona id"); if (IsBundledPersona(id) && !allowBundledShadow && !IsOverlayPersona(id)) - { - // v1: do not shadow-write bundled; require clone to a new overlay id. - throw new InvalidOperationException($"cannot write bundled persona '{id}' — clone to a new overlay id"); - } - if (IsBundledPersona(id) && !IsOverlayPersona(id)) { throw new InvalidOperationException($"cannot write bundled persona '{id}' — clone to a new overlay id"); } @@ -1099,39 +1075,6 @@ public sealed class AssistentConfig } string extra = MergeTextLayers("extra.md", roots); - // Legacy personas.json: only when no overlay persona folder exists for this id. - string overlayPersonaDir = Path.Combine(_overlayRoot, "personas", id); - bool hasOverlayFolder = Directory.Exists(overlayPersonaDir) - && (File.Exists(Path.Combine(overlayPersonaDir, "persona.json")) - || File.Exists(Path.Combine(overlayPersonaDir, "extra.md"))); - if (!hasOverlayFolder) - { - string overlayJson = Path.Combine(_overlayRoot, "personas.json"); - JObject legacy = TryReadJson(overlayJson); - if (legacy?["personas"] is JArray arr) - { - foreach (JToken t in arr) - { - if (t is JObject po && string.Equals(SafeId(po["id"]?.ToString()), id, StringComparison.OrdinalIgnoreCase)) - { - JObject persona = shelves["persona"] as JObject ?? new JObject(); - string title = po["title"]?.ToString(); - if (!string.IsNullOrWhiteSpace(title)) - { - persona["title"] = title; - } - shelves["persona"] = persona; - string prompt = po["prompt"]?.ToString(); - if (!string.IsNullOrWhiteSpace(prompt)) - { - extra = string.IsNullOrWhiteSpace(extra) ? prompt : extra + "\n\n" + prompt; - } - break; - } - } - } - } - shelves["extra"] = extra ?? ""; return shelves; } @@ -1428,9 +1371,13 @@ public sealed class AssistentConfig public void SaveSettings(JObject settings) { - Directory.CreateDirectory(_overlayRoot); - string path = Path.Combine(_overlayRoot, "settings.json"); - File.WriteAllText(path, (settings ?? new JObject()).ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8); + lock (_lock) + { + Directory.CreateDirectory(_overlayRoot); + string path = Path.Combine(_overlayRoot, "settings.json"); + JObject merged = DeepMerge(LoadSettings(), settings ?? new JObject()); + File.WriteAllText(path, merged.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8); + } } public JObject LoadOllamaRoles() @@ -1471,7 +1418,7 @@ public sealed class AssistentConfig } } } - if (clientSkills is not null && clientSkills.Count > 0) + if (clientSkills is not null) { enabled.Clear(); foreach (JToken t in clientSkills) @@ -1534,6 +1481,7 @@ public sealed class AssistentConfig ["identity"] = identity, ["identity_summary"] = RenderIdentityBlock(id, includeAllShelves: true), ["enabled_skills"] = new JArray(ResolveEnabledSkills(id, null)), + ["patch_keys"] = new JArray(LoadPatchKeys()), }; } } diff --git a/AssistentMemory.Store.cs b/AssistentMemory.Store.cs index 4fe9a60..3d94adb 100644 --- a/AssistentMemory.Store.cs +++ b/AssistentMemory.Store.cs @@ -9,13 +9,12 @@ using SwarmUI.Utils; namespace Mrleo1nid.SwarmAssistent; -/// Runtime store in the same sqlite file: chats, ui-state, taste. +/// Runtime store in the same sqlite file: chats, ui-state. /// Config overlays, sidecar cards, and ollama-roles stay on disk. public sealed partial class AssistentMemory { public const int MaxChatsStored = 200; public const string KvUiState = "ui_state"; - public const string KvTaste = "taste"; void EnsureStoreSchema() { @@ -39,7 +38,6 @@ public sealed partial class AssistentMemory CREATE INDEX IF NOT EXISTS idx_chats_updated ON chats(updated_at DESC); """); EnsureChatsFts(); - MigrateJsonStoreOnce(); } void EnsureChatsFts() @@ -96,102 +94,6 @@ public sealed partial class AssistentMemory } } - void MigrateJsonStoreOnce() - { - if (GetMeta("json_store_migrated") == "1") - { - return; - } - string root = Path.Combine(_dataRoot, "Assistent"); - int chats = 0; - string chatsDir = Path.Combine(root, "chats"); - if (Directory.Exists(chatsDir)) - { - foreach (string file in Directory.EnumerateFiles(chatsDir, "*.json")) - { - try - { - JObject chat = JObject.Parse(File.ReadAllText(file, Encoding.UTF8)); - string id = (chat["id"]?.ToString() ?? Path.GetFileNameWithoutExtension(file) ?? "").Trim(); - if (string.IsNullOrWhiteSpace(id) || GetChatUnlocked(id) is not null) - { - continue; - } - UpsertChatUnlocked(chat, id); - chats++; - } - catch (Exception ex) - { - Logs.Debug($"AssistentMemory migrate chat {file}: {ex.Message}"); - } - } - } - ImportKvFile(Path.Combine(root, "ui-state.json"), KvUiState); - ImportKvFile(Path.Combine(root, "taste.json"), KvTaste); - SetMeta("json_store_migrated", "1"); - TryArchiveMigratedJson(root, chatsDir); - if (chats > 0) - { - Logs.Debug($"AssistentMemory: migrated {chats} chats from JSON into sqlite"); - } - } - - void ImportKvFile(string path, string key) - { - if (!File.Exists(path) || !string.IsNullOrEmpty(GetKvUnlocked(key))) - { - return; - } - try - { - SetKvUnlocked(key, File.ReadAllText(path, Encoding.UTF8)); - } - catch (Exception ex) - { - Logs.Debug($"AssistentMemory migrate {key}: {ex.Message}"); - } - } - - void TryArchiveMigratedJson(string root, string chatsDir) - { - try - { - string dest = Path.Combine(root, "_migrated_json"); - Directory.CreateDirectory(dest); - MoveIfExists(Path.Combine(root, "ui-state.json"), Path.Combine(dest, "ui-state.json")); - MoveIfExists(Path.Combine(root, "taste.json"), Path.Combine(dest, "taste.json")); - if (!Directory.Exists(chatsDir)) - { - return; - } - string chatsDest = Path.Combine(dest, "chats"); - Directory.CreateDirectory(chatsDest); - foreach (string file in Directory.EnumerateFiles(chatsDir, "*.json")) - { - MoveIfExists(file, Path.Combine(chatsDest, Path.GetFileName(file))); - } - } - catch (Exception ex) - { - Logs.Debug($"AssistentMemory archive json: {ex.Message}"); - } - } - - static void MoveIfExists(string src, string dest) - { - if (!File.Exists(src)) - { - return; - } - if (File.Exists(dest)) - { - File.Delete(src); - return; - } - Directory.CreateDirectory(Path.GetDirectoryName(dest)!); - File.Move(src, dest); - } - public string GetKv(string key) { lock (_lock) diff --git a/AssistentMemory.UserPrefs.cs b/AssistentMemory.UserPrefs.cs index 6e3f17b..b233dd5 100644 --- a/AssistentMemory.UserPrefs.cs +++ b/AssistentMemory.UserPrefs.cs @@ -10,8 +10,6 @@ namespace Mrleo1nid.SwarmAssistent; /// Facts about the human at the desk — global + per-persona, separate from craft RAG. public sealed partial class AssistentMemory { - const string MetaTasteMigrated = "user_prefs_taste_migrated"; - void EnsureUserPrefsSchema() { Exec( @@ -29,53 +27,6 @@ public sealed partial class AssistentMemory ); CREATE INDEX IF NOT EXISTS idx_user_prefs_scope ON user_prefs(scope, persona_id); """); - MigrateTasteToUserPrefsOnce(); - } - - void MigrateTasteToUserPrefsOnce() - { - if (GetMeta(MetaTasteMigrated) == "1") - { - return; - } - try - { - JObject taste = GetKvObject(KvTaste); - if (taste is not null && taste.Count > 0) - { - long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - void AddList(string prefix, JToken arr) - { - if (arr is not JArray a) - { - return; - } - int i = 0; - foreach (JToken t in a) - { - string text = (t?.ToString() ?? "").Trim(); - if (string.IsNullOrWhiteSpace(text)) - { - continue; - } - UpsertUserPrefUnlocked($"{prefix}_{i++}", text, "global", "", "migrated_taste", pinned: false, now); - } - } - AddList("like", taste["likes"]); - AddList("avoid", taste["avoid"]); - AddList("style", taste["styles"]); - string notes = (taste["notes"]?.ToString() ?? "").Trim(); - if (!string.IsNullOrWhiteSpace(notes)) - { - UpsertUserPrefUnlocked("notes", notes, "global", "", "migrated_taste", pinned: false, now); - } - } - } - catch (Exception ex) - { - Logs.Debug($"AssistentMemory taste→user_prefs: {ex.Message}"); - } - SetMeta(MetaTasteMigrated, "1"); } static string NormalizePrefScope(string scope) diff --git a/AssistentMemory.cs b/AssistentMemory.cs index 0e8d4ed..6604ff9 100644 --- a/AssistentMemory.cs +++ b/AssistentMemory.cs @@ -747,7 +747,25 @@ public sealed partial class AssistentMemory : IDisposable EnsureOpen(); Dictionary ftsRanks = FtsRowRanks(ftsMatch, Math.Max(40, options.TopK * 4)); using SqliteCommand cmd = _conn.CreateCommand(); - cmd.CommandText = "SELECT id, kind, key, text, source, embedding, persona FROM memories"; + List personaKeys = rank.Keys.ToList(); + StringBuilder sql = new("SELECT id, kind, key, text, source, embedding, persona FROM memories WHERE persona IN ("); + for (int i = 0; i < personaKeys.Count; i++) + { + if (i > 0) + { + sql.Append(','); + } + string pName = "$p" + i; + sql.Append(pName); + cmd.Parameters.AddWithValue(pName, personaKeys[i]); + } + sql.Append(')'); + if (kindFilter is not null) + { + sql.Append(" AND kind = $kind"); + cmd.Parameters.AddWithValue("$kind", kindFilter); + } + cmd.CommandText = sql.ToString(); using SqliteDataReader reader = cmd.ExecuteReader(); while (reader.Read()) { diff --git a/AssistentPatch.cs b/AssistentPatch.cs index 1639fd6..0f3fe1e 100644 --- a/AssistentPatch.cs +++ b/AssistentPatch.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using Newtonsoft.Json.Linq; @@ -9,20 +10,9 @@ public partial class SwarmAssistentExtension { static readonly Regex JsonFenceRe = new(@"```(?:json)?\s*([\s\S]*?)```", RegexOptions.IgnoreCase | RegexOptions.Compiled); - static readonly string[] PatchKeys = - [ - "prompt", "negative", "loras", "width", "height", "steps", "cfg", "seed", "sigma_shift", "sampler", - "actions", "search_query", "civitai_query", - "use_init_image", "clear_init_image", "init_creativity", "denoise", - "use_mask_image", "clear_mask_image", "mask_blur", "mask_grow", - "look_at", "vision_from", "vision_slots", "slot_to_init", "slot_to_mask", - "snapshot_generate", "select_slot", "aspect", "images", "batch", "vary", "lock_seed", - "creativity", "intensity", "complexity", "movement", - "clear_prompt_images", "slot_to_prompt_image", "pack", "memories", "memory", - "memory_query", "memory_kind", "tag_query", "user_prefs", - "inventory_query", "skills", "persona_shelves", "controls", - "variants", - ]; + static string[] PatchKeys => _patchKeys ??= Config?.LoadPatchKeys() ?? []; + + static string[] _patchKeys; static bool HasValue(JObject obj, string key) { @@ -84,6 +74,8 @@ public partial class SwarmAssistentExtension { return null; } + JObject lastAny = null; + JObject lastTerminal = null; foreach (Match match in JsonFenceRe.Matches(reply)) { string raw = match.Groups[1].Value.Trim(); @@ -96,7 +88,12 @@ public partial class SwarmAssistentExtension } if (Array.Exists(PatchKeys, k => obj[k] is not null)) { - return NormalizePatch(obj); + JObject normalized = NormalizePatch(obj); + lastAny = normalized; + if (FenceIsTerminalPatch(obj)) + { + lastTerminal = normalized; + } } } catch @@ -104,7 +101,7 @@ public partial class SwarmAssistentExtension // not json } } - return null; + return lastTerminal ?? lastAny; } /// @@ -264,43 +261,45 @@ public partial class SwarmAssistentExtension return ActionsContain(patch, "lookup_tags") ? ExtractSearchQuery(patch) : null; } - static string NextToolHop(JObject patch) + static string NextToolHop(JObject patch, HashSet skip = null) { if (patch is null) { return null; } - if (ActionsContain(patch, "memory_get")) + bool Skip(string tool) => skip is not null && skip.Contains(tool); + if (ActionsContain(patch, "memory_get") && !Skip("memory_get")) { return "memory_get"; } - if (ActionsContain(patch, "memory_search") || !string.IsNullOrWhiteSpace(patch["memory_query"]?.ToString())) + if ((ActionsContain(patch, "memory_search") || !string.IsNullOrWhiteSpace(patch["memory_query"]?.ToString())) + && !Skip("memory_search")) { return "memory_search"; } - if (ActionsContain(patch, "lookup_tags") || !string.IsNullOrWhiteSpace(patch["tag_query"]?.ToString())) + if ((ActionsContain(patch, "lookup_tags") || !string.IsNullOrWhiteSpace(patch["tag_query"]?.ToString())) + && !Skip("lookup_tags")) { return "lookup_tags"; } - if (ActionsContain(patch, "list_inventory") || !string.IsNullOrWhiteSpace(patch["inventory_query"]?.ToString())) + if ((ActionsContain(patch, "list_inventory") || !string.IsNullOrWhiteSpace(patch["inventory_query"]?.ToString())) + && !Skip("list_inventory")) { return "list_inventory"; } - if (ActionsContain(patch, "skill_load")) + if (ActionsContain(patch, "skill_load") && !Skip("skill_load")) { return "skill_load"; } - if (ActionsContain(patch, "persona_read")) + if (ActionsContain(patch, "persona_read") && !Skip("persona_read")) { return "persona_read"; } - if (ActionsContain(patch, "search_civitai") || !string.IsNullOrWhiteSpace(ExtractSearchQuery(patch))) + if ((ActionsContain(patch, "search_civitai") || !string.IsNullOrWhiteSpace(ExtractSearchQuery(patch))) + && !Skip("civitai")) { return "civitai"; } return null; } - - static bool WantsCivitaiSearch(JObject patch) - => ActionsContain(patch, "search_civitai") || !string.IsNullOrWhiteSpace(ExtractSearchQuery(patch)); } diff --git a/AssistentPersist.cs b/AssistentPersist.cs index 16de492..1a3e550 100644 --- a/AssistentPersist.cs +++ b/AssistentPersist.cs @@ -21,7 +21,7 @@ public partial class SwarmAssistentExtension static readonly string[] UiStateKeys = [ "pack", "persona", "auto_vision", "auto_apply", "auto_generate", "auto_critique", - "auto_download", "pane_width", "embed_model", "base_url", "model", "view", "board_tab", + "auto_download", "pane_width", "embed_model", "base_url", "model", "view", "board_tab", "park_llm", ]; static string SafeChatId(string id) diff --git a/AssistentWanted.cs b/AssistentWanted.cs index dea1513..d7940a0 100644 --- a/AssistentWanted.cs +++ b/AssistentWanted.cs @@ -13,6 +13,8 @@ namespace Mrleo1nid.SwarmAssistent; /// Queue of models the assistant wants downloaded (merged into gpu-rent models.yaml on next up/capture). public partial class SwarmAssistentExtension { + static readonly object WantedFileLock = new(); + string WantedModelsPath() => Path.Combine(DataRoot(), ".gpu-rent-wanted-models.yaml"); string WantedCardsDir() => Path.Combine(DataRoot(), ".gpu-rent-wanted-cards"); @@ -45,36 +47,39 @@ public partial class SwarmAssistentExtension string path = WantedModelsPath(); Directory.CreateDirectory(Path.GetDirectoryName(path) ?? DataRoot()); - Dictionary> sections = LoadWantedYaml(File.Exists(path) ? File.ReadAllText(path, Encoding.UTF8) : ""); - - if (version_id > 0) + lock (WantedFileLock) { - foreach (List list in sections.Values) + Dictionary> sections = LoadWantedYaml(File.Exists(path) ? File.ReadAllText(path, Encoding.UTF8) : ""); + + if (version_id > 0) { - if (list.Any(e => e.VersionId == version_id)) + foreach (List list in sections.Values) { - return new JObject { ["success"] = true, ["already"] = true, ["path"] = path, ["version_id"] = version_id }; + if (list.Any(e => e.VersionId == version_id)) + { + return new JObject { ["success"] = true, ["already"] = true, ["path"] = path, ["version_id"] = version_id }; + } } } - } - else - { - foreach (List list in sections.Values) + else { - if (list.Any(e => string.Equals(e.Url, url, StringComparison.OrdinalIgnoreCase))) + foreach (List list in sections.Values) { - return new JObject { ["success"] = true, ["already"] = true, ["path"] = path }; + if (list.Any(e => string.Equals(e.Url, url, StringComparison.OrdinalIgnoreCase))) + { + return new JObject { ["success"] = true, ["already"] = true, ["path"] = path }; + } } } - } - if (!sections.TryGetValue(kind, out List bucket)) - { - bucket = []; - sections[kind] = bucket; + if (!sections.TryGetValue(kind, out List bucket)) + { + bucket = []; + sections[kind] = bucket; + } + bucket.Add(new WantedEntry { Url = url, Title = title, VersionId = version_id }); + File.WriteAllText(path, WriteWantedYaml(sections), Encoding.UTF8); } - bucket.Add(new WantedEntry { Url = url, Title = title, VersionId = version_id }); - File.WriteAllText(path, WriteWantedYaml(sections), Encoding.UTF8); if (card is not null && version_id > 0) { diff --git a/Config/_base/core/core.md b/Config/_base/core/core.md index d6b0207..93d3ce3 100644 --- a/Config/_base/core/core.md +++ b/Config/_base/core/core.md @@ -28,7 +28,7 @@ Never write a “JSON Patch” section in prose without an actual fenced ```json - Rich entries (blurbs/triggers) are selected + top krea-likely. Name-only rows need `list_inventory` + `inventory_query` before you rely on them. - `memory_hits` may be truncated (`truncated: true`) — use `memory_get` for the full text. - `has_vision_image` true means a real board frame exists. `images_in_request` true means JPEG bytes are in **this** request. **Do not** `look_at` just because a frame exists. Emit `look_at` only when you cannot continue without pixels (user asked to look/critique/describe/compare, or a defect you cannot infer from the prompt). Never invent what the image looks like. A new Generate / «ещё» / prompt edit does **not** need vision. -- Prefer `krea_likely` / Krea architecture; ignore FLUX/SDXL. Respect current params unless asked or pack is `form_params`. +- Prefer `krea_likely` / Krea architecture; ignore FLUX/SDXL. Respect current params unless asked or pack is `fix_params`. - Init/inpaint flags and `image_slots` are in the JSON. Extra pack fields are documented in the active pack. ## Memory (short) @@ -40,7 +40,7 @@ Never write a “JSON Patch” section in prose without an actual fenced ```json ## Output contract (mandatory) 1. Short helpful reply in the user's language (RU or EN). -2. One fenced JSON patch with **only fields you want to change**: +2. **Frame turns only:** one fenced JSON patch with **only fields you want to change**. Chat / Q&A / opinion / remember: **prose only — omit the JSON patch.** ```json { diff --git a/Config/_base/memory-seed/krea_facts.json b/Config/_base/memory-seed/krea_facts.json deleted file mode 100644 index 60b0742..0000000 --- a/Config/_base/memory-seed/krea_facts.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/Config/_base/packs/ordinary.md b/Config/_base/packs/ordinary.md index 537b8f0..f325556 100644 --- a/Config/_base/packs/ordinary.md +++ b/Config/_base/packs/ordinary.md @@ -26,6 +26,6 @@ Otherwise **stay in ordinary** and just do the work. ## Deliverable -Same as write_prompt: short reply + fenced JSON **only if** this turn is a frame; then `actions: ["generate"]`. Chat/Q&A/opinion: no generate (prose is enough). +Short reply + fenced JSON **only on frame turns** (`actions: ["generate"]` when they want a new/updated image). Chat/Q&A/opinion/remember: prose only — no fence. «давай дальше» / next frame = new English `prompt` + `negative` (echo live if unchanged) + `actions:["generate"]` in the **same** turn — never leave an empty `### JSON Patch` header. Chat may be RU; **Generate `prompt` is always EN** (skill `prompting`). Several options in one ask → `variants` (2–4 partial patches with `label`); still one fence, still STOP after it. diff --git a/Config/_base/packs/write_prompt.json b/Config/_base/packs/write_prompt.json index b2cafb2..80af974 100644 --- a/Config/_base/packs/write_prompt.json +++ b/Config/_base/packs/write_prompt.json @@ -1,7 +1,8 @@ { "id": "write_prompt", "title": "Написать промпт", - "order": 10, + "order": 2, "aliases": ["write"], - "prompt_file": "write_prompt.md" + "prompt_file": "ordinary.md", + "enabled": true } diff --git a/Config/_base/packs/write_prompt.md b/Config/_base/packs/write_prompt.md index ce8e8c7..265af7a 100644 --- a/Config/_base/packs/write_prompt.md +++ b/Config/_base/packs/write_prompt.md @@ -1,18 +1,3 @@ -# Mode: write_prompt +# Mode: write_prompt (alias) -Goal: craft or improve a **Krea 2** prompt that will generate well on Turbo (local Swarm). Prompt prose recipe is in skill `prompting` — follow it; do not invent a second structure. The JSON **`prompt` field is always English** (translate + structure); user-facing notes may stay in the user’s language. - -## Deliverable - -- Brief note of what you changed. -- JSON patch with at least `prompt` and `negative` (create / supplement / echo live), and `loras` when relevant. -- `actions: ["generate"]` only when they want a new/updated image (scene, edit, «ещё») — they do **not** have to type «генерируй». Skip generate for chat / Q&A / remember / look-only. Do **not** `look_at` unless they asked to see/critique the last frame. -- Prefer Exact Turbo defaults / `recommended_params`. Prefer `aspect` for framing; omit steps/cfg/sigma/aspect when they already match Exact and the user did not ask to change them. -- Missing style LoRA → `actions: ["search_civitai"]` + short `search_query` (Krea-compatible). -- User wants several options (оба / варианты / разный свет) → `variants: [{label, prompt|aspect|…}, …]` (2–4). Base keys inherit; each item overrides only its diffs. Still one fence. - -### Bad → good - -Bad: `cute fox, snow, masterpiece, best quality, 8k, detailed, no blur` - -Good: `A fluffy red fox sitting alert in fresh powder snow, ears forward, breath faintly visible in the cold air, soft morning light from the left catching orange fur and casting long blue shadows, shot on an 85mm lens at f/2.8 with creamy bokeh, calm winter atmosphere, sharp eyes and whiskers.` +Alias of **ordinary** — same deliverable. Use pack id `write` / `write_prompt` when the user wants prompt-focused wording; behavior and JSON contract are identical to ordinary. diff --git a/Config/_base/patch-keys.json b/Config/_base/patch-keys.json new file mode 100644 index 0000000..b1ed1b2 --- /dev/null +++ b/Config/_base/patch-keys.json @@ -0,0 +1,15 @@ +{ + "keys": [ + "prompt", "negative", "loras", "width", "height", "steps", "cfg", "seed", "sigma_shift", "sampler", "scheduler", + "actions", "search_query", "civitai_query", + "use_init_image", "clear_init_image", "init_creativity", "denoise", + "use_mask_image", "clear_mask_image", "mask_blur", "mask_grow", + "look_at", "vision_from", "vision_slots", "slot_to_init", "slot_to_mask", + "snapshot_generate", "select_slot", "aspect", "images", "batch", "vary", "lock_seed", + "creativity", "intensity", "complexity", "movement", + "clear_prompt_images", "slot_to_prompt_image", "pack", "memories", "memory", + "memory_query", "memory_kind", "tag_query", "user_prefs", + "inventory_query", "skills", "persona_shelves", "persona_clone", "persona", "controls", + "variants" + ] +} diff --git a/Config/_base/skills/prompting.md b/Config/_base/skills/prompting.md index 649b583..5973247 100644 --- a/Config/_base/skills/prompting.md +++ b/Config/_base/skills/prompting.md @@ -26,3 +26,9 @@ The **chat model** prepares the Generate-box text for **Krea 2** (Qwen3-VL). Do User-facing prose: short, in the user’s language. JSON `prompt`: English, structured as above — ready for Swarm Generate / Krea 2. + +### Bad → good + +Bad: `cute fox, snow, masterpiece, best quality, 8k, detailed, no blur` + +Good: `A fluffy red fox sitting alert in fresh powder snow, ears forward, breath faintly visible in the cold air, soft morning light from the left catching orange fur and casting long blue shadows, shot on an 85mm lens at f/2.8 with creamy bokeh, calm winter atmosphere, sharp eyes and whiskers.` diff --git a/Config/personas/README.md b/Config/personas/README.md index 5c2167c..26712dc 100644 --- a/Config/personas/README.md +++ b/Config/personas/README.md @@ -27,7 +27,7 @@ Reserved (not identity dump): `assistant.json`, `ui.json`, `skills.json`, packs/ ## Overlay vs bundled -- **Bundled** ships with the extension (`neutral`, `lewd`, `leonid`, …). +- **Bundled** ships with the extension (`neutral`, `lewd`, `aggressive`, `cinema`, `leonid`, …). Persona `terse` was removed; saved ids map to `aggressive`. - **Overlay** on the data volume = this install. Clones from chat go here only. - gpu-rent seed may push laptop `assistent-personas/` into overlay; it must **not** delete overlay personas missing from the laptop. diff --git a/Config/personas/aggressive/rules.json b/Config/personas/aggressive/rules.json index 3bb679e..54bf488 100644 --- a/Config/personas/aggressive/rules.json +++ b/Config/personas/aggressive/rules.json @@ -1,11 +1,11 @@ { "always": [ "Short sentences", - "Say what is wrong and what to change", - "Prefer actions generate when a re-roll is obviously needed" + "Say what is wrong and what to change" ], "never": [ "Invent LoRA names — aggression is tone, not hallucination", - "Soft padding" + "Soft padding", + "Force generate on Q&A or remember-only turns — core contract decides frame vs chat" ] } diff --git a/Config/personas/terse/dislikes.json b/Config/personas/terse/dislikes.json deleted file mode 100644 index 3e5ed98..0000000 --- a/Config/personas/terse/dislikes.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "notes": ["lectures", "filler", "long explanations"] -} diff --git a/Config/personas/terse/likes.json b/Config/personas/terse/likes.json deleted file mode 100644 index 223aa67..0000000 --- a/Config/personas/terse/likes.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "notes": ["decisive patches", "one main fix"] -} diff --git a/Config/personas/terse/persona.json b/Config/personas/terse/persona.json deleted file mode 100644 index 393246c..0000000 --- a/Config/personas/terse/persona.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Короткий", - "tagline": "High-signal short replies", - "accent": "#56b6c2" -} diff --git a/Config/personas/terse/rules.json b/Config/personas/terse/rules.json deleted file mode 100644 index e8b408f..0000000 --- a/Config/personas/terse/rules.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "always": [ - "Reply in 1–2 short sentences, then the JSON patch", - "NSFW: factual, minimal words" - ], - "never": [ - "Lectures or filler", - "Invent LoRA names or triggers" - ] -} diff --git a/Config/personas/terse/voice.json b/Config/personas/terse/voice.json deleted file mode 100644 index 54085d2..0000000 --- a/Config/personas/terse/voice.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "verbosity": "minimal", - "tone": ["short", "high-signal"], - "humor": "none", - "nsfw": "factual", - "address": "peer", - "language": "match_user" -} diff --git a/README.md b/README.md index ed97ecb..1d6a14f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + board (Generate | Refs tabs), LoRA chips, **persona presets** (`Config/personas/`), **About the user** prefs + craft vector memory, model cards with Civitai fetch, img2img/inpaint, slash commands, auto Generate. +**Turn model:** one user message is one *turn*. A turn may fan out into nested LLM *hops* — Krea prompt prep, empty-patch retry, vision, auto-critique. Hops share one `HOP_BUDGET`, never re-read the user's text (their prompt is client-authored), and pass the busy gate that blocks new user sends. What a reply does to generation state is decided once, in `resolveTurnIntent`: the model's `actions:["generate"]` / `look_at` win, RU intent heuristics only back it up when the model forgets, and an explicit «запомни, не генерируй» vetoes both. + +**Version 0.11.9** — Distilled client: esbuild bundle (`Assets/assistent.bundle.js`), unified patch keys (`Config/_base/patch-keys.json`), taste stack removed (UserPrefs only), chat storage merge + all-chats disk save, `write_prompt` → alias of `ordinary`. Builds on prior 0.11.9 turn-intent work. + +**Version 0.11.9** — One turn, one decision. Nested hops (Krea prep, empty-patch retry, vision, critique) share a `turnHops` budget and pass the busy gate — Krea prep and the empty-patch retry were silently no-ops since 0.10.22/0.11.2. Generate / `look_at` are decided in a single `resolveTurnIntent`; `ensureGenerateAction`, `shouldHonorLookAt` and the `wantsGen`/`willGen`/`suppressGen` tangle are gone. Builds on 0.11.8. + **Version 0.11.8** — `session_exact` remembers applied params that differ from Exact (not only when the user typed the knob). `/debug ask` uses a hidden Q&A pack: 5–10 line explain, no JSON/generate, dump stays a system note. Builds on 0.11.7. **Version 0.11.7** — Generate only for a real frame request: chat/opinions no longer auto-run Swarm. Context still counts («нарисуй», «ещё одну», «другая поза»), not only «генерируй». Builds on 0.11.6. @@ -50,10 +56,22 @@ Assistent/ _base/ personas// # overlay presets — same names as Config/, sparse settings.json # embed_model, base_url, per-persona skills ollama-roles.json # chat vs memory model tags (gpu-rent writes this) - memory/assistent.sqlite # craft RAG + user_prefs + tags FTS + chats + ui_state + taste (legacy) - _migrated_json/ # one-shot archive of old chats/*.json, ui-state.json, taste.json + memory/assistent.sqlite # craft RAG + user_prefs + tags FTS + chats + ui_state ``` +## Client build + +Sources live in `src/` (ES modules). The VM ships the committed bundle only (no Node required at runtime): + +```bash +npm install +npm run build # → Assets/assistent.bundle.js +npm run watch # rebuild on save +npm test # intent.js + patch.js +``` + +SwarmUI loads a single script: `Assets/assistent.bundle.js`. + Copy `personas/leonid/` → new id, edit only differing JSON. See `Config/personas/README.md`. **Controls:** optional `controls.json` schema + `exact.controls` values. UI auto-draws every slider (`order`, `display: percent`). LLM may patch `"controls": {…}`. Values persist in overlay Exact (DeepMerge partial saves). Leonid: **Вкус** + **Хорни**; `/остынь`, `/horny-game`. @@ -76,7 +94,6 @@ Separate sqlite table `user_prefs` (not craft RAG): - **Persona** — only the current agent - Injected as `## About the user`; strength via `user_prefs_weight` / `user_prefs_max` in `assistant.json` (⚙ → О пользователе) - Agent write: `actions: ["user_pref_upsert"]` + `user_prefs: [{key,text,scope}]` -- Legacy `kv.taste` migrates once into global prefs ## Craft vector memory @@ -92,9 +109,8 @@ Two layers in `memory/assistent.sqlite` (`persona` column; empty = shared): ## Chats and runtime KV - Every chat (messages + Generate params snapshot) is a row in `assistent.sqlite`, newest **200** kept. History search uses FTS over title + body. -- First launch after 0.8.3 copies `chats/*.json`, `ui-state.json`, and `taste.json` into sqlite, then archives them under `_migrated_json/`. -- localStorage stays as a fast cache; on first run with an empty store the old `swarm_assistent_chats_v1` browser history is migrated up once. -- UI state seeds a **fresh** browser only — anything already in localStorage wins, and `auto_download` is never restored as on +- All chats with messages are debounced to disk (not only the active one). Load merges disk + localStorage by `updatedAt`. +- UI state whitelists `park_llm` among other keys in sqlite `kv.ui_state`. - `settings.json` and persona overlays stay files (layered merge + git). `.assistent.json` cards stay next to weights. ## VRAM handover @@ -115,6 +131,8 @@ Two layers in `memory/assistent.sqlite` (`persona` column; empty = shared): | Command | Effect | | --- | --- | | `/help` | List commands | +| `/new` | New chat (current one is saved) | +| `/history` | Open saved chats | | `/debug` | Short UI/Exact dump (no LLM) | | `/debug ask` / `/why` | Dump + short model explanation | | `/gen` | Generate now | @@ -124,7 +142,8 @@ Two layers in `memory/assistent.sqlite` (`persona` column; empty = shared): | `/aspect 16:9` | Set size from the official 1K table | | `/seed lock\|random` | Lock or randomize seed | | `/vary` | New seed, same prompt (+ generate if auto) | -| `/pack write\|critique\|…` | Switch pack | +| `/pack write\|ordinary\|critique\|compose\|params\|inpaint\|describe\|card\|persona` | Switch pack | +| `/persona new\|clone\|save` | Overlay persona authoring | | `/civitai ` | Ask LLM to search Civitai | | `/inventory` | Rescan models + refresh LoRA list | @@ -149,18 +168,21 @@ Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart. ## Packs & skills -**Packs** (one active): `ordinary` (default комбайн), `write_prompt`, `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`, `catalog_card`, `author_persona`. +**Packs** (one active): `ordinary` (default комбайн; covers write/critique/params flows), `write_prompt` (alias → same as ordinary), `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`, `catalog_card`, `author_persona`. + +Patch fence keys: single source `Config/_base/patch-keys.json` → C# + client via `AssistentGetConfig.patch_keys`. **Skills** (checkboxes): `prompting`, `creativity_sliders`, `memory` — procedures; encyclopedia numbers live in Exact, soft notes in memory-seed / RAG, human taste in UserPrefs. -**Personas:** `neutral`, `lewd`, `aggressive`, `cinema`, `terse`, `leonid` under `Config/personas/`. Overlay clones via `/persona new` or ⚙ → Личности. +**Personas:** `neutral`, `lewd`, `aggressive`, `cinema`, `leonid` under `Config/personas/`. Saved `terse` falls back to `aggressive`. Overlay clones via `/persona new` or ⚙ → Личности. ## API routes | Route | Role | | --- | --- | | `AssistentListModels` | Ollama tags → `models` (chat) + `memory_models` | -| `AssistentGetConfig` | Merged preset for persona (ui, packs, skills, identity, controls) | +| `AssistentGetConfig` | Merged preset for persona (ui, packs, skills, identity, controls, **patch_keys**) | +| `AssistentSaveSettings` | Overlay settings DeepMerge (skills, embed_model) | | `AssistentSaveControls` | Persist Exact `controls` values for a persona (overlay) | | `AssistentGetPersonaShelves` | Merged identity shelves + controls | | `AssistentClonePersona` | Snapshot clone → overlay id | @@ -168,14 +190,11 @@ Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart. | `AssistentDeletePersona` | UI-only delete of overlay persona | | `AssistentExportPersona` / `AssistentImportPersona` | Shareable `.assistent-persona.json` pack | | `AssistentSaveKnobs` | Overlay `_base/assistant.json` + Exact turbo/raw profiles | -| `AssistentGetSettings` / `AssistentSaveSettings` | Overlay settings (skills, embed_model) | | `AssistentListInventory` | LoRA / checkpoint / wildcard inventory | | `AssistentListPersonas` | Persona catalog | -| `AssistentGetPacks` | Prompt pack texts | | `AssistentGetCard` / `AssistentSaveCard` | `.assistent.json` cards (+ memory ingest) | | `AssistentGetCardMeta` | Local sidecar + optional Civitai by-hash | | `AssistentEnqueueWanted` / `AssistentListWanted` | Wanted YAML queue (write / read + count) | -| `AssistentGetTaste` / `AssistentSaveTaste` | sqlite `kv.taste` (legacy; prefer UserPrefs) | | `AssistentListUserPrefs` / `AssistentUpsertUserPref` / `AssistentForgetUserPref` / `AssistentClearUserPrefs` | About the user | | `AssistentSearchCivitai` | Civitai LoRA search | | `AssistentChat` / `AssistentChatWS` | Chat (+ user prefs + hybrid craft memory + hops) | diff --git a/SwarmAssistentExtension.cs b/SwarmAssistentExtension.cs index f655cdc..ccc8718 100644 --- a/SwarmAssistentExtension.cs +++ b/SwarmAssistentExtension.cs @@ -28,15 +28,12 @@ public partial class SwarmAssistentExtension : Extension public override void OnPreInit() { - ScriptFiles.Add("Assets/assistent.api.js"); - ScriptFiles.Add("Assets/assistent.patch.js"); - ScriptFiles.Add("Assets/assistent.persist.js"); - ScriptFiles.Add("Assets/assistent.js"); + ScriptFiles.Add("Assets/assistent.bundle.js"); StyleSheetFiles.Add("Assets/assistent.css"); ExtensionAuthor = "mrleo1nid"; Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop."; License = "MIT"; - Version = "0.11.8"; + Version = "0.11.9"; Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"]; } @@ -46,10 +43,8 @@ public partial class SwarmAssistentExtension : Extension Config = new AssistentConfig(FilePath, DataRoot()); Memory = new AssistentMemory(DataRoot(), HttpClient, Config.LoadAssistant(Config.DefaultPersonaId())["embed_model"]?.ToString() ?? "nomic-embed-text"); API.RegisterAPICall(AssistentListModels, false, PermUse); - API.RegisterAPICall(AssistentGetPacks, false, PermUse); API.RegisterAPICall(AssistentListPersonas, false, PermUse); API.RegisterAPICall(AssistentGetConfig, false, PermUse); - API.RegisterAPICall(AssistentGetSettings, false, PermUse); API.RegisterAPICall(AssistentSaveSettings, true, PermUse); API.RegisterAPICall(AssistentListInventory, false, PermUse); API.RegisterAPICall(AssistentGetCard, false, PermUse); @@ -57,8 +52,6 @@ public partial class SwarmAssistentExtension : Extension API.RegisterAPICall(AssistentEnqueueWanted, true, PermUse); API.RegisterAPICall(AssistentGetCardMeta, false, PermUse); API.RegisterAPICall(AssistentSearchCivitai, false, PermUse); - API.RegisterAPICall(AssistentGetTaste, false, PermUse); - API.RegisterAPICall(AssistentSaveTaste, true, PermUse); API.RegisterAPICall(AssistentChat, true, PermUse); API.RegisterAPICall(AssistentChatWS, true, PermUse); API.RegisterAPICall(AssistentListChats, false, PermUse); @@ -153,29 +146,6 @@ public partial class SwarmAssistentExtension : Extension return Environment.CurrentDirectory; } - public string ReadPackFile(string name) - { - return Config?.LoadPackPrompt(Config.DefaultPersonaId(), name); - } - - public async Task AssistentGetPacks(Session session, string persona = null) - { - await Task.CompletedTask; - string pid = AssistentConfig.SafeId(persona) ?? Config.DefaultPersonaId(); - JObject packs = new(); - JArray order = []; - foreach (var p in Config.ListPacks(pid)) - { - string text = Config.LoadPackPrompt(pid, p.id); - if (text is not null) - { - packs[p.id] = text; - } - order.Add(p.id); - } - return new JObject { ["success"] = true, ["packs"] = packs, ["order"] = order, ["persona"] = pid }; - } - public async Task AssistentGetConfig(Session session, string persona = null) { await Task.CompletedTask; @@ -183,12 +153,6 @@ public partial class SwarmAssistentExtension : Extension return Config.BuildMergedConfigPayload(pid); } - public async Task AssistentGetSettings(Session session) - { - await Task.CompletedTask; - return new JObject { ["success"] = true, ["settings"] = Config.LoadSettings() }; - } - public async Task AssistentSaveSettings(Session session, JObject settings) { await Task.CompletedTask; @@ -236,39 +200,4 @@ public partial class SwarmAssistentExtension : Extension ["personas"] = list, }; } - - public async Task AssistentGetTaste(Session session) - { - await Task.CompletedTask; - try - { - return new JObject { ["success"] = true, ["taste"] = Memory.GetKvObject(AssistentMemory.KvTaste) }; - } - catch (Exception ex) - { - return new JObject { ["error"] = $"taste: {ex.Message}" }; - } - } - - public async Task AssistentSaveTaste(Session session, JObject taste) - { - await Task.CompletedTask; - if (taste is null) - { - return new JObject { ["error"] = "taste required" }; - } - if (taste["updated"] == null) - { - taste["updated"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - } - try - { - Memory.SetKvObject(AssistentMemory.KvTaste, taste); - return new JObject { ["success"] = true, ["path"] = "Assistent/memory/assistent.sqlite" }; - } - catch (Exception ex) - { - return new JObject { ["error"] = $"taste save: {ex.Message}" }; - } - } } diff --git a/Tabs/Text2Image/Assistent.html b/Tabs/Text2Image/Assistent.html index 5fa15b2..30d06b2 100644 --- a/Tabs/Text2Image/Assistent.html +++ b/Tabs/Text2Image/Assistent.html @@ -43,10 +43,10 @@ Новый чат -
- - - +
+ + +
-
- обычный - @@ -90,7 +90,7 @@
- +
@@ -105,7 +105,7 @@
- + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fc3bd1c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,497 @@ +{ + "name": "swarm-assistent", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "swarm-assistent", + "devDependencies": { + "esbuild": "^0.25.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..845f464 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "swarm-assistent", + "private": true, + "type": "module", + "scripts": { + "build": "node scripts/build.mjs", + "watch": "node scripts/build.mjs --watch", + "test": "node --test test/intent.test.js test/patch.test.js" + }, + "devDependencies": { + "esbuild": "^0.25.0" + } +} diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..8ae12e8 --- /dev/null +++ b/scripts/build.mjs @@ -0,0 +1,25 @@ +import * as esbuild from 'esbuild'; +import { existsSync } from 'node:fs'; + +const watch = process.argv.includes('--watch'); +const ctxOpts = { + entryPoints: ['src/main.js'], + outfile: 'Assets/assistent.bundle.js', + bundle: true, + format: 'iife', + target: ['es2020'], + sourcemap: false, + logLevel: 'info', +}; + +if (watch) { + const ctx = await esbuild.context(ctxOpts); + await ctx.watch(); + console.log('watching src/ → Assets/assistent.bundle.js'); +} else { + await esbuild.build(ctxOpts); + if (!existsSync('Assets/assistent.bundle.js')) { + process.exit(1); + } + console.log('built Assets/assistent.bundle.js'); +} diff --git a/src/api.js b/src/api.js new file mode 100644 index 0000000..d126dda --- /dev/null +++ b/src/api.js @@ -0,0 +1,28 @@ +/** Promise wrapper around SwarmUI genericRequest. */ +export function createRequest() { + return function request(name, body) { + return new Promise((resolve, reject) => { + if (typeof genericRequest !== 'function') { + reject(new Error('genericRequest unavailable')); + return; + } + genericRequest( + name, + body || {}, + (data) => { + if (data && data.error) { + reject(new Error(String(data.error))); + } else { + resolve(data); + } + }, + 0, + (err) => reject(err instanceof Error ? err : new Error(String(err || 'request failed'))), + ); + }); + }; +} + +export function attachApi(SA) { + SA.request = createRequest(); +} diff --git a/Assets/assistent.js b/src/app.js similarity index 95% rename from Assets/assistent.js rename to src/app.js index 4e1e29b..b874115 100644 --- a/Assets/assistent.js +++ b/src/app.js @@ -7,24 +7,17 @@ const LS_MODEL = 'swarm_assistent_model'; const LS_EMBED = 'swarm_assistent_embed_model'; const LS_PACK = 'swarm_assistent_pack'; - const LS_PACK_ORDINARY_MIG = 'swarm_assistent_pack_ordinary_v1'; - /** One-shot: drop junior chat tags stuck in LS so UI/warm pick preferred/senior. */ - const LS_MODEL_SENIOR_MIG = 'swarm_assistent_model_senior_v1'; 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'; - /** One-shot: turn off auto look_at / auto-critique (vision is button / /look / model look_at). */ - const LS_VISION_OPTIN_MIG = 'swarm_assistent_vision_optin_v1'; const LS_AUTO_DOWNLOAD = 'swarm_assistent_auto_download'; /** When '1', unload chat LLM before Generate (frees VRAM; VL reload can take 1–2 min). Default off. */ const LS_PARK_LLM = 'swarm_assistent_park_llm'; const LS_PANE_WIDTH = 'swarm_assistent_pane_width'; const LS_WELCOMED = 'swarm_assistent_welcomed'; - const LS_TASTE = 'swarm_assistent_taste'; - const LS_HISTORY = 'swarm_assistent_history'; const LS_CHATS = 'swarm_assistent_chats_v1'; const LS_BOARD_TAB = 'swarm_assistent_board_tab'; const MAX_CHATS = 40; @@ -126,7 +119,6 @@ const state = { history: [], - packsLoaded: false, config: null, exact: null, sessionExact: {}, @@ -144,17 +136,13 @@ 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, streamText: '', streamFenceDone: false, - critiqueHopUsed: false, - visionHopUsed: false, + turnHops: [], lastSystemChars: 0, lastSystemLayers: null, lastContextChars: 0, @@ -174,7 +162,6 @@ personas: [], modelCards: {}, cardsSelection: null, - cardsBusy: false, pendingPersonaNote: null, chats: [], activeChatId: null, @@ -194,6 +181,48 @@ ollamaHealth: 'unknown', }; + // ---- Turn lifecycle -------------------------------------------------- + // A turn is one user utterance. It can fan out into nested LLM hops: Krea + // prompt prep, empty-patch retry, vision, auto-critique. They share one + // budget so a turn always terminates, and the text they carry is written by + // the client, not the user — intent heuristics must never read it. + + const HOP_BUDGET = 4; + + /** Nested hop continuing the current turn — allowed through the busy gate. */ + function isContinuationTurn(opts) { + return !!(opts && (opts.fromVisionHop || opts.fromAutoCritique + || opts.fromPromptEnRetry || opts.fromEmptyPatchRetry)); + } + + /** Turn whose prompt the client authored: no intent parsing, no auto-pack, no slash. */ + function isMachineTurn(opts) { + return isContinuationTurn(opts) + || !!(opts && (opts.fromCards || opts.fromDownload || opts.fromDebug)); + } + + function resetTurnHops() { + state.turnHops = []; + // Patch stashed for the Krea prep hop — stale once the turn is over. + state.pendingPromptEnMerge = null; + } + + function turnHopUsed(kind) { + return (state.turnHops || []).includes(kind); + } + + /** One shot per kind per turn, plus a hard cap on total nesting. */ + function claimTurnHop(kind) { + if (!Array.isArray(state.turnHops)) { + state.turnHops = []; + } + if (state.turnHops.includes(kind) || state.turnHops.length >= HOP_BUDGET) { + return false; + } + state.turnHops.push(kind); + return true; + } + /** Disk persistence module (assistent.persist.js) — absent means localStorage only. */ function diskPersist() { return (window.SA && window.SA.persist) || null; @@ -911,20 +940,22 @@ return null; } - /** When the model wrote ### JSON Patch with no fence — build Apply+Generate from prose / last patch. */ + /** + * The model wrote «### JSON Patch» and left the fence empty. Recover the prompt + * from prose so the turn is not a dead end — but only claim a frame when the + * user ordered one; otherwise this stays a plain prompt update. + */ function synthesizePatchAfterEmptyFence(reply, userText, opts = {}) { - const wants = !!(opts.userWantsGenerate || state.pendingSilentGen - || userAsksGenerate(userText) || userAsksContinue(userText) || userImpliesGenerate(userText)); - const missing = replyMissingJsonPatch(reply); - if (!wants && !missing) { + const wantsFrame = !!opts.userWantsGenerate + || (!isMachineTurn(opts) && userImpliesGenerate(userText)); + if (!wantsFrame && !replyMissingJsonPatch(reply)) { return null; } - const fromProse = extractPromptFromProse(reply); - const prompt = fromProse || state.lastPatch?.prompt || null; + const prompt = extractPromptFromProse(reply) || state.lastPatch?.prompt || null; if (!prompt) { return null; } - const patch = { prompt, actions: ['generate'] }; + const patch = wantsFrame ? { prompt, actions: ['generate'] } : { prompt }; if (Array.isArray(state.lastPatch?.loras) && state.lastPatch.loras.length) { patch.loras = state.lastPatch.loras; } @@ -1041,7 +1072,19 @@ ).test(t); } - /** Button / /look / «посмотри на кадр» — not casual «смотри какая». */ + /** + * The only reason the client adds a frame the model did not ask for: the + * user gave a direct order. Everything softer («другой ракурс», «покажи как + * она…») is the model's call via actions:["generate"] — see core.md. + */ + function userCommandsGenerate(text) { + const t = String(text || '').trim(); + if (!t || userAsksNoGenerate(t)) { + return false; + } + return userAsksGenerate(t) || userAsksContinue(t) || isSameButAspectRequest(t); + } + function userAsksLook(text) { const t = String(text || '').trim(); if (!t) { @@ -1059,17 +1102,36 @@ return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t); } + /** Opinion / thanks / trivia — not a new frame. */ + function userIsChatNotFrame(text) { + const t = String(text || '').trim(); + if (!t) { + return false; + } + if (/^(ок|окей|ok|okay|ладно|хорошо|понял|ясно|спасибо|thanks)([!.…\s]*)$/i.test(t)) { + return true; + } + if (cyrTokenRe( + 'что\\s+такое|как\\s+работает|зачем\\s+|какие\\s+(лор|модел|чекпоинт)|' + + 'список\\s+лор|где\\s+настрой|что\\s+значит|' + + 'нравит|спасибо|благодар|почему\\s+так|что\\s+ты\\s+(сделал|изменил)|' + + 'только\\s+(ответь|скажи|объясни)|без\\s+(кадр|генерац)|не\\s+надо\\s+кадр', + ).test(t) && !userAsksGenerate(t) && !userAsksContinue(t)) { + return true; + } + return false; + } + /** - * Scene / edit / «ещё» — Generate without the magic word «генерируй». - * Chat, opinions, trivia, look-only stay false. No noun-only fallback - * («девушка в студии» in a comment must not start a frame). + * Scene / edit / «ещё» — a frame without the magic word «генерируй». + * Chat, opinions, trivia, look-only stay false. No noun-only fallback. */ function userImpliesGenerate(text) { const t = String(text || '').trim(); if (!t || userAsksNoGenerate(t) || userIsChatNotFrame(t)) { return false; } - if (userAsksGenerate(t) || userAsksContinue(t) || isSameButAspectRequest(t)) { + if (userCommandsGenerate(t)) { return true; } if (t.length < 8) { @@ -1104,84 +1166,49 @@ return false; } - /** Opinion / thanks / trivia — not a new frame, even if the model sneaks actions:generate. */ - function userIsChatNotFrame(text) { - const t = String(text || '').trim(); - if (!t) { - return false; - } - if (/^(ок|окей|ok|okay|ладно|хорошо|понял|ясно|спасибо|thanks)([!.…\s]*)$/i.test(t)) { - return true; - } - if (cyrTokenRe( - 'что\\s+такое|как\\s+работает|зачем\\s+|какие\\s+(лор|модел|чекпоинт)|' - + 'список\\s+лор|где\\s+настрой|что\\s+значит|' - + 'нравит|спасибо|благодар|почему\\s+так|что\\s+ты\\s+(сделал|изменил)|' - + 'только\\s+(ответь|скажи|объясни)|без\\s+(кадр|генерац)|не\\s+надо\\s+кадр', - ).test(t) && !userAsksGenerate(t) && !userAsksContinue(t)) { - return true; - } - return false; - } - function packBlocksAutoGenerate(pack) { const p = String(pack || ''); return p === 'describe_ref' || p === 'catalog_card' || p === 'author_persona' || p === 'debug_explain'; } - /** Inject generate only when the user turn is a frame; strip it on chat/Q&A. */ - function ensureGenerateAction(patch, userText) { - if (!patch || typeof patch !== 'object') { - return patch; - } - if (userAsksNoGenerate(userText) || userIsChatNotFrame(userText)) { - return stripGenerateAction(patch); - } - const pack = $('sa_pack')?.value || ''; - if (packBlocksAutoGenerate(pack) && !userAsksGenerate(userText) && !userAsksContinue(userText)) { - return stripGenerateAction(patch); - } - const hasAction = Array.isArray(patch.actions) && patch.actions.map(String).includes('generate'); - if (hasAction) { - return patch; - } - if (!userImpliesGenerate(userText)) { - return patch; - } - const out = { ...patch }; - const acts = Array.isArray(patch.actions) ? patch.actions.map(String).filter((a) => a && a !== 'generate') : []; - acts.push('generate'); - out.actions = acts; - return out; - } - function packWantsVision(pack) { const p = String(pack || ''); return p === 'critique_image' || p === 'describe_ref' || p === 'compose_scene' || p === 'inpaint_edit'; } - /** Honor model look_at only when asked, pack needs it, or look-only (no Generate this turn). */ - function shouldHonorLookAt(patch, opts = {}) { - if (!patch || typeof patch !== 'object') { - return false; + /** + * Client owns Generate: a frame only when the user commanded or implied one. + * Model `actions:["generate"]` on chat/Q&A is stripped. look_at is opt-in + * (user /look, vision pack, or auto-critique hop) — not every model look_at. + */ + function resolveTurnIntent(patch, userText, opts = {}) { + const machine = isMachineTurn(opts); + const vetoed = !machine && userAsksNoGenerate(userText); + const commanded = !!opts.userWantsGenerate || (!machine && userCommandsGenerate(userText)); + const implied = !machine && userImpliesGenerate(userText); + const modelAsked = Array.isArray(patch?.actions) && patch.actions.map(String).includes('generate'); + + let generate; + if (vetoed || opts.fromAutoCritique) { + generate = false; + } else if (commanded) { + generate = true; + } else if (packBlocksAutoGenerate($('sa_pack')?.value || '')) { + generate = false; + } else { + // The model's own call, with the RU heuristics as a backstop when it forgets. + generate = modelAsked || implied; } - if (patch.look_at == null && patch.vision_from == null && patch.vision_slots == null) { - return false; - } - if (opts.fromAutoCritique || opts.fromVisionHop) { - return true; - } - if (userAsksLook(opts.userText || '')) { - return true; - } - if (packWantsVision($('sa_pack')?.value)) { - return true; - } - // look_at + generate on a write turn stares at the old frame and delays the new one. - if (patchHasGenTrigger(patch)) { - return false; - } - return true; + + const hasLook = !!patch + && (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null); + const honorLook = opts.fromAutoCritique || opts.fromVisionHop + || (!machine && userAsksLook(userText)) + || packWantsVision($('sa_pack')?.value); + // Unsolicited look_at on ordinary/write is ignored (0.11.4 opt-in). + const look = !!(hasLook && !vetoed && !generate && honorLook); + + return { generate, look, vetoed }; } function stripGenerateAction(patch) { @@ -2653,17 +2680,18 @@ } } - /** Mirrors the active chat onto the data volume (debounced inside SA.persist). */ + /** Save every chat with messages to disk (not only the active one). */ function saveActiveChatToDisk() { const persist = diskPersist(); - if (!persist || !state.activeChatId) { + if (!persist) { return; } - const chat = findChat(state.activeChatId); - if (!chat || !(chat.messages || []).length) { - return; + for (const chat of state.chats || []) { + if (!chat?.id || !(chat.messages || []).length) { + continue; + } + persist.saveChat(chat); } - persist.saveChat(chat); } function loadChatsStore() { @@ -2677,67 +2705,47 @@ } } } catch (e) { /* ignore */ } - migrateLegacyHistoryIntoChats(); } - /** Disk wins over localStorage — chats follow the data volume, not the browser. */ + /** Merge disk chats with local — keep newer updatedAt per id. */ async function loadChatsFromDisk() { const persist = diskPersist(); if (!persist) { return; } - let chats = null; + let diskChats = null; try { - chats = await persist.loadChats(); + diskChats = await persist.loadChats(); } catch (e) { console.warn('Assistent: disk chats failed', e); return; } - if (!Array.isArray(chats) || !chats.length) { + if (!Array.isArray(diskChats)) { return; } - state.chats = chats.filter((c) => c && c.id).slice(0, MAX_CHATS); + const byId = new Map(); + for (const c of state.chats || []) { + if (c?.id) { + byId.set(c.id, c); + } + } + for (const c of diskChats) { + if (!c?.id) { + continue; + } + const prev = byId.get(c.id); + if (!prev || (c.updatedAt || 0) >= (prev.updatedAt || 0)) { + byId.set(c.id, c); + } + } + state.chats = [...byId.values()] + .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)) + .slice(0, MAX_CHATS); try { localStorage.setItem(LS_CHATS, JSON.stringify({ version: 1, chats: state.chats })); } catch (e) { /* quota — disk is the source of truth anyway */ } } - function migrateLegacyHistoryIntoChats() { - try { - const raw = localStorage.getItem(LS_HISTORY); - if (!raw) { - return; - } - const parsed = JSON.parse(raw); - if (!Array.isArray(parsed) || !parsed.length) { - localStorage.removeItem(LS_HISTORY); - return; - } - const messages = slimHistoryMessages(parsed); - if (!messages.length) { - localStorage.removeItem(LS_HISTORY); - return; - } - const already = state.chats.some((c) => - (c.messages || []).length === messages.length - && (c.messages[0]?.content || '') === (messages[0]?.content || '')); - if (!already) { - state.chats.unshift({ - id: chatUid(), - title: titleFromMessages(messages), - createdAt: Date.now() - 1000, - updatedAt: Date.now() - 1000, - messages, - params: null, - }); - persistChatsStore(); - } - localStorage.removeItem(LS_HISTORY); - } catch (e) { - try { localStorage.removeItem(LS_HISTORY); } catch (e2) { /* ignore */ } - } - } - function findChat(id) { return (state.chats || []).find((c) => c.id === id) || null; } @@ -2951,8 +2959,7 @@ state.chats.unshift(chat); state.activeChatId = chat.id; state.history = []; - state.critiqueHopUsed = false; - state.visionHopUsed = false; + resetTurnHops(); state.packUserTouched = false; state.pendingPersonaNote = null; if (state.streamEl) { @@ -3003,8 +3010,7 @@ } state.activeChatId = chat.id; state.history = slimHistoryMessages(chat.messages); - state.critiqueHopUsed = false; - state.visionHopUsed = false; + resetTurnHops(); state.packUserTouched = false; state.pendingPersonaNote = null; state.pendingSilentGen = false; @@ -3084,10 +3090,6 @@ syncHistoryBadge(); } - function restoreHistory() { - // Replaced by initChatSessions — kept as no-op for safety. - } - function clearPersistedHistory() { if (state.activeChatId) { const chat = findChat(state.activeChatId); @@ -3106,8 +3108,7 @@ function clearChatHistory() { abortInFlightWork({ status: '' }); state.history = []; - state.critiqueHopUsed = false; - state.visionHopUsed = false; + resetTurnHops(); state.packUserTouched = false; state.pendingPersonaNote = null; state.sessionExact = {}; @@ -3647,19 +3648,6 @@ 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 { @@ -3763,32 +3751,6 @@ }); } - 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) { @@ -3800,90 +3762,6 @@ 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(); - syncTasteHintsToUserPrefs(taste); - } - - function syncTasteHintsToUserPrefs(taste) { - if (typeof genericRequest !== 'function' || !taste) { - return; - } - const upsert = (key, text) => { - if (!text) { - return; - } - genericRequest( - 'AssistentUpsertUserPref', - { key, text: String(text).slice(0, 240), scope: 'global', source: 'migrated_taste', pinned: false }, - () => {}, - 0, - () => {}, - ); - }; - if (taste.avoid?.[0]) { - upsert('avoid_hint', `Avoid: ${taste.avoid.slice(0, 4).join('; ')}`); - } - if (taste.styles?.[0]) { - upsert('style_hint', `Styles: ${taste.styles.slice(0, 4).join('; ')}`); - } - if (taste.likes?.[0]) { - upsert('like_hint', `Often uses: ${taste.likes.slice(0, 4).join('; ')}`); - } - } - - // Fallback key list — only used if assistent.patch.js failed to load. - const FALLBACK_PATCH_KEYS = [ - 'prompt', 'loras', 'width', 'height', 'steps', 'cfg', 'seed', 'sigma_shift', 'sampler', - 'actions', 'search_query', 'civitai_query', 'init_creativity', 'denoise', - 'look_at', 'vision_from', 'vision_slots', 'aspect', 'batch', 'vary', 'lock_seed', 'pack', - 'memories', 'user_prefs', 'variants', - ]; - - function isPatchObject(obj) { - if (window.SA && typeof SA.isPatchObject === 'function') { - return SA.isPatchObject(obj); - } - if (!obj || typeof obj !== 'object') { - return false; - } - if (isCardObject(obj)) { - return false; - } - return FALLBACK_PATCH_KEYS.some((k) => obj[k] !== undefined && obj[k] !== null); - } - function isCardObject(obj) { if (window.SA && typeof SA.isCardObject === 'function') { return SA.isCardObject(obj); @@ -3894,7 +3772,7 @@ // 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 + || obj.steps != null || obj.cfg != null || obj.aspect || obj.seed != null || obj.search_query || obj.civitai_query || obj.look_at || obj.controls); if (cardish && !genish && (obj.name || obj.triggers || obj.when)) { return true; @@ -3932,23 +3810,7 @@ if (window.SA && typeof SA.extractPatch === 'function') { return SA.extractPatch(text); } - if (!text) { - return { prose: text || '', patch: null }; - } - const re = /```(?:json)?\s*([\s\S]*?)```/gi; - let match; - let lastPatch = null; - let prose = text; - while ((match = re.exec(text)) !== null) { - try { - const obj = JSON.parse(match[1].trim()); - if (isPatchObject(obj)) { - lastPatch = obj; - prose = (text.slice(0, match.index) + text.slice(match.index + match[0].length)).trim(); - } - } catch (e) { /* not json */ } - } - return { prose, patch: lastPatch }; + return { prose: text || '', patch: null }; } function normalizeAspect(raw) { @@ -4085,7 +3947,7 @@ } // Param / aspect asks must leave a stuck critique_image pack from auto-critique. if (userTextMentionsParams(t) || parseAspectFromUserText(t)) { - return cur === 'critique_image' || cur === 'describe_ref' ? 'ordinary' : 'form_params'; + return cur === 'critique_image' || cur === 'describe_ref' ? 'ordinary' : 'fix_params'; } if (cyrTokenRe('поправь|исправь|перепиши|улучши').test(t) || /\b(fix\s+it|make\s+it\s+better|rewrite)\b/i.test(t)) { @@ -4632,12 +4494,12 @@ try { if (typeof doInterrupt === 'function') { doInterrupt(false); - return; } } catch (e) { /* ignore */ } if (typeof genericRequest === 'function') { genericRequest('InterruptAll', { other_sessions: false }, () => {}, 0, () => {}); } + clearInFlightUi({ status: 'Прервано' }); } function waitForNewImage(prevSrc, timeoutMs = 180000) { @@ -4946,7 +4808,7 @@ } async function maybeAutoCritique(imageSrc) { - if (!$('sa_auto_critique')?.checked || state.critiqueHopUsed || isMultiGenResults()) { + if (!$('sa_auto_critique')?.checked || turnHopUsed('critique') || isMultiGenResults()) { return; } const src = await resolveFinishedGenerateSrc(imageSrc); @@ -4954,7 +4816,9 @@ setStatus('Авто-критика пропущена — нет готового кадра Generate'); return; } - state.critiqueHopUsed = true; + if (!claimTurnHop('critique')) { + return; + } setPackValue('critique_image', { flash: true }); if ($('sa_input')) { $('sa_input').value = 'Critique this result and improve the prompt for the next generation.'; @@ -4972,7 +4836,7 @@ /** After Generate: send look_at with JPEG when sa_auto_vision is on (skipped if auto-critique already attaches vision). */ async function maybeAutoVisionLook(imageSrc) { - if (!wantsAutoVision() || $('sa_auto_critique')?.checked || state.visionHopUsed || state.busy || isMultiGenResults()) { + if (!wantsAutoVision() || $('sa_auto_critique')?.checked || turnHopUsed('vision') || state.busy || isMultiGenResults()) { return; } const src = await resolveFinishedGenerateSrc(imageSrc); @@ -4985,7 +4849,9 @@ gen.attach = true; renderBoard(); } - state.visionHopUsed = true; + if (!claimTurnHop('vision')) { + return; + } setPackValue('critique_image', { flash: true }); if ($('sa_input')) { $('sa_input').value = 'Look at the Generate result and briefly say what worked and what to fix next.'; @@ -5547,29 +5413,14 @@ } function loadSettings() { - // One-shot: old default was write_prompt → migrate to ordinary комбайн. - if (!localStorage.getItem(LS_PACK_ORDINARY_MIG)) { - if (localStorage.getItem(LS_PACK) === 'write_prompt') { - localStorage.setItem(LS_PACK, 'ordinary'); - } - localStorage.setItem(LS_PACK_ORDINARY_MIG, '1'); - } - // One-shot: stop staring at every Generate. Vision = button / /look / model look_at. - if (!localStorage.getItem(LS_VISION_OPTIN_MIG)) { - localStorage.setItem(LS_AUTO_VISION, '0'); - localStorage.setItem(LS_AUTO_CRITIQUE, '0'); - localStorage.setItem(LS_VISION_OPTIN_MIG, '1'); - if ($('sa_auto_vision')) { - $('sa_auto_vision').checked = false; - } - if ($('sa_auto_critique')) { - $('sa_auto_critique').checked = false; - } - } const base = localStorage.getItem(LS_BASE); const model = localStorage.getItem(LS_MODEL); const pack = localStorage.getItem(LS_PACK); - const persona = localStorage.getItem(LS_PERSONA); + let persona = localStorage.getItem(LS_PERSONA); + if (persona === 'terse') { + persona = 'aggressive'; + localStorage.setItem(LS_PERSONA, persona); + } const view = localStorage.getItem(LS_VIEW); const auto = localStorage.getItem(LS_AUTO_VISION); const autoApply = localStorage.getItem(LS_AUTO_APPLY); @@ -5752,6 +5603,9 @@ ? { ...state.config.control_values } : null; state.config = data; + if (window.SA?.applyConfigPatchKeys) { + window.SA.applyConfigPatchKeys(data); + } if (data.exact && typeof data.exact === 'object') { state.exact = data.exact; } @@ -6298,13 +6152,6 @@ ? apiPreferred : pickSeniorChatModel(list); const ls = state.preferredModel || localStorage.getItem(LS_MODEL) || ''; - if (!localStorage.getItem(LS_MODEL_SENIOR_MIG)) { - localStorage.setItem(LS_MODEL_SENIOR_MIG, '1'); - if (preferred && (!ls || !list.includes(ls) || chatModelSeniority(ls) < chatModelSeniority(preferred))) { - state.preferredModel = preferred; - return preferred; - } - } if (ls && list.includes(ls)) { return ls; } @@ -7122,9 +6969,6 @@ } const keys = Object.keys(localStorage).filter((k) => k.startsWith('swarm_assistent_')); for (const k of keys) { - if (k === LS_TASTE) { - continue; - } localStorage.removeItem(k); } diskPersist()?.saveUiState?.({}); @@ -7268,6 +7112,9 @@ $('sa_tab_cards')?.classList.toggle('sa-subtab-active', state.view === 'cards'); $('sa_tab_settings')?.classList.toggle('sa-subtab-active', state.view === 'settings'); $('sa_btn_settings')?.classList.toggle('sa-subtab-active', state.view === 'settings'); + $('sa_tab_chat')?.setAttribute('aria-selected', state.view === 'chat' ? 'true' : 'false'); + $('sa_tab_cards')?.setAttribute('aria-selected', state.view === 'cards' ? 'true' : 'false'); + $('sa_tab_settings')?.setAttribute('aria-selected', state.view === 'settings' ? 'true' : 'false'); saveSettings(); if (state.view === 'cards') { renderCardsList(); @@ -7290,14 +7137,6 @@ setView('chat'); } - 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') { @@ -7917,63 +7756,6 @@ filter.focus(); } - function loadTasteFromServer() { - if (typeof genericRequest !== 'function') { - return; - } - genericRequest( - 'AssistentGetTaste', - {}, - (data) => { - const remote = data?.taste; - if (!remote || typeof remote !== 'object') { - return; - } - const remoteUpdated = remote.updated || 0; - const localUpdated = state.taste?.updated || 0; - // sqlite taste is the source of truth; localStorage only wins when it is strictly newer. - const localEmpty = !localUpdated - && !(state.taste?.styles?.length || state.taste?.likes?.length || state.taste?.avoid?.length); - if (localEmpty || remoteUpdated >= localUpdated) { - state.taste = { - styles: Array.isArray(remote.styles) ? remote.styles.slice(0, 12) : [], - likes: Array.isArray(remote.likes) ? remote.likes.slice(0, 16) : [], - avoid: Array.isArray(remote.avoid) ? remote.avoid.slice(0, 12) : [], - notes: String(remote.notes || '').slice(0, 400), - updated: remoteUpdated || Date.now(), - }; - saveTasteLocalOnly(); - } - }, - 0, - () => {}, - ); - } - - function saveTasteLocalOnly() { - try { - localStorage.setItem(LS_TASTE, JSON.stringify(state.taste || {})); - } catch (e) { /* ignore */ } - } - - function saveTasteToServerDebounced() { - if (state.tasteSaveTimer) { - clearTimeout(state.tasteSaveTimer); - } - state.tasteSaveTimer = setTimeout(() => { - if (typeof genericRequest !== 'function') { - return; - } - genericRequest( - 'AssistentSaveTaste', - { taste: state.taste || {} }, - () => {}, - 0, - () => {}, - ); - }, 800); - } - async function generateCardWithAssistent() { const sel = state.cardsSelection; if (!sel) { @@ -8079,8 +7861,15 @@ return; } const { patch } = extractPatch(reply); + if (fromDebug) { + // Q&A only — a debug explanation never touches generation state. + state.pendingSilentGen = false; + return; + } + const commanded = !!opts.userWantsGenerate + || (!isMachineTurn(opts) && userImpliesGenerate(opts.userText || '')); let effective = patch; - if (!effective && !fromVisionHop && !fromAutoCritique && !fromDebug) { + if (!effective && !fromVisionHop && !fromAutoCritique) { const aspect = parseAspectFromUserText(opts.userText || ''); if (aspect && (replyMissingJsonPatch(reply) || isSameButAspectRequest(opts.userText || ''))) { effective = { aspect, actions: ['generate'] }; @@ -8090,16 +7879,17 @@ appendSystemNote(`Патч пустой — применил aspect ${aspect} сам.`); } } - if (!effective && !fromVisionHop && !fromAutoCritique && !fromDebug) { + if (!effective && !fromVisionHop && !fromAutoCritique) { const synthesized = synthesizePatchAfterEmptyFence(reply, opts.userText || '', opts); if (synthesized) { effective = synthesized; - appendSystemNote('Патч пустой — собрал prompt из ответа и запустил Generate.'); + appendSystemNote(synthesized.actions + ? 'Патч пустой — собрал prompt из ответа и запустил Generate.' + : 'Патч пустой — собрал prompt из ответа.'); } } - if (!effective && !fromVisionHop && !fromAutoCritique && !fromDebug - && !opts.fromEmptyPatchRetry - && userImpliesGenerate(opts.userText || '')) { + if (!effective && !fromVisionHop && !fromAutoCritique && commanded + && claimTurnHop('empty_patch')) { appendSystemNote('Нужен кадр — прошу JSON с prompt + generate.'); await sendChat({ skipSlash: true, @@ -8130,44 +7920,33 @@ downloadCivitaiLoRA(pick, null); } } - const suppressGen = !fromVisionHop && !fromAutoCritique && userAsksNoGenerate(opts.userText || ''); - if (suppressGen && effective) { - effective = stripLookAt(stripGenerateAction(effective)); - state.pendingSilentGen = false; - if (effective) { - rememberLastPatch(effective); + const intent = resolveTurnIntent(effective, opts.userText || '', opts); + if (effective) { + if (intent.generate) { + const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : []; + if (!acts.includes('generate')) { + effective = { ...effective, actions: acts.concat('generate') }; + } + } else { + effective = stripGenerateAction(effective); + } + if (!intent.look) { + effective = stripLookAt(effective); } - } - if (effective && !fromDebug && !suppressGen) { - effective = ensureGenerateAction(effective, opts.userText || ''); rememberLastPatch(effective); } - if (effective && !fromVisionHop && !fromAutoCritique && !fromDebug && !shouldHonorLookAt(effective, opts)) { - effective = stripLookAt(effective); - if (effective) { - rememberLastPatch(effective); - } + if (intent.vetoed) { + state.pendingSilentGen = false; } - if (effective && !fromVisionHop && !fromAutoCritique && !suppressGen && shouldHonorLookAt(effective, opts)) { + if (effective && intent.look && !fromVisionHop && !fromAutoCritique) { const hopped = await maybeVisionHop(effective, opts.attachedSlotIds || []); if (hopped) { return; } } - if (fromDebug) { - // Q&A only — never apply patches / generate from a debug explanation turn. - state.pendingSilentGen = false; - return; - } - const hasGenAction = Array.isArray(effective?.actions) && effective.actions.map(String).includes('generate'); - const implied = userImpliesGenerate(opts.userText || ''); - const wantsGen = !suppressGen && !!(opts.userWantsGenerate || state.pendingSilentGen || implied - || (hasGenAction && !userIsChatNotFrame(opts.userText || ''))); - // Auto-Generate = skip Apply buttons when this turn is a frame — not "every patch". - const willGen = !!(effective && !fromAutoCritique && !suppressGen && wantsGen); // Maximize chat-model prep: structure + EN for Krea before Swarm Generate runs. - if (willGen && effective?.prompt && promptNeedsKreaPrep(effective.prompt) - && !opts.fromPromptEnRetry && !fromVisionHop && !fromDebug) { + if (intent.generate && effective?.prompt && promptNeedsKreaPrep(effective.prompt) + && !fromVisionHop && claimTurnHop('krea_prep')) { state.pendingPromptEnMerge = { ...effective }; appendSystemNote('Готовлю промпт для Krea 2 чат-моделью (EN + структура)…'); setBusyPhase('refining'); @@ -8180,32 +7959,29 @@ }); return; } - const doApply = !!(effective && (wantsGen || $('sa_auto_apply')?.checked)); + const doApply = !!(effective && (intent.generate || $('sa_auto_apply')?.checked)); if (doApply) { - if (wantsGen) { + if (intent.generate) { startBusyUi('silent_gen'); } else { setBusyPhase('applying'); } await applyPatch(effective, 'all'); syncLiveParamsBar(); - updateTasteFromPatch(effective, opts.userText || ''); - // Auto-Generate must not fire on «запомни / шаблон» turns — even if the model - // echoed a prompt patch or sneaked actions:["generate"]. - if (!fromAutoCritique && !suppressGen && wantsGen) { + if (intent.generate) { if (effective?.prompt && promptNeedsKreaPrep(effective.prompt)) { setStatus('Промпт всё ещё не EN/Krea-ready — Generate с тем что есть'); } const src = await runGenerateFromPatch( { ...effective, actions: Array.isArray(effective.actions) ? effective.actions : ['generate'] }, - { force: wantsGen }, + { force: true }, ); if (src) { await maybeAutoCritique(src); await maybeAutoVisionLook(src); } } else if (!state.generating) { - stopBusyUi(suppressGen ? 'Запомнил · без Generate' : (wantsGen ? 'Применено' : '')); + stopBusyUi(intent.vetoed ? 'Запомнил · без Generate' : ''); } } else if (replyMissingJsonPatch(reply) && !fromAutoCritique && !fromVisionHop) { setStatus('Ответ без JSON-патча — ничего не применено'); @@ -8522,7 +8298,7 @@ } if (cmd === 'pack') { if (!setPackValue(arg, { flash: true, user: true })) { - setStatus('Pack: write|critique|compose|params|inpaint|describe'); + setStatus('Pack: write|ordinary|critique|compose|params|inpaint|describe|card|persona'); } else { setStatus(`Pack → ${$('sa_pack')?.value}`); } @@ -8582,7 +8358,7 @@ async function maybeVisionHop(patch, attachedSlotIds) { const ids = lookAtIdsFromPatch(patch); - if (!ids.length || state.visionHopUsed) { + if (!ids.length || turnHopUsed('vision')) { return false; } scrubPreviewFromGenerateSlot(); @@ -8606,7 +8382,9 @@ if (!need.length) { return false; } - state.visionHopUsed = true; + if (!claimTurnHop('vision')) { + return false; + } for (const s of need) { s.attach = true; } @@ -8620,7 +8398,9 @@ } async function sendChat(opts = {}) { - if ((state.busy || state.generating) && !opts.fromVisionHop && !opts.fromAutoCritique) { + // Continuations run inside the parent turn, which still holds `busy` + // (finishOk only clears it after handleReplySideEffects returns). + if ((state.busy || state.generating) && !isContinuationTurn(opts)) { return; } const rawInput = ($('sa_input')?.value || '').trim(); @@ -8628,16 +8408,13 @@ if (!text) { return; } - if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.fromDebug) { + if (!isMachineTurn(opts)) { state.lastUserParamIntent = userTextMentionsParams(text); state.lastUserControlIntent = userTextMentionsControls(text); state.pendingSilentGen = userImpliesGenerate(text); - if (userAsksNoGenerate(text)) { - state.pendingSilentGen = false; - } } - if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.skipSlash) { + if (!isMachineTurn(opts) && !opts.skipSlash) { if (rawInput.startsWith('/')) { if ($('sa_input')) { $('sa_input').value = ''; @@ -8650,8 +8427,7 @@ } // «такую же, только 9 на 16» — apply aspect + Generate without waiting for an empty LLM critique. - if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.fromDebug - && !opts.fromCards && isSameButAspectRequest(text)) { + if (!isMachineTurn(opts) && isSameButAspectRequest(text)) { const aspect = parseAspectFromUserText(text); if (aspect) { if ($('sa_input')) { @@ -8679,7 +8455,7 @@ return; } - if (!opts.skipAutoPack && !opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.fromCards && !opts.fromDebug) { + if (!opts.skipAutoPack && !isMachineTurn(opts)) { const guessed = autoSelectPack(text); if (guessed) { setPackValue(guessed, { flash: true }); @@ -8705,7 +8481,7 @@ // If Krea just ran, expectColdLoad stays true until warm / first token — don't clear it here. state.llmParked = false; setInterruptVisible(true); - if (state.expectColdLoad && !opts.fromVisionHop && !opts.fromAutoCritique) { + if (state.expectColdLoad && !isContinuationTurn(opts)) { startBusyUi('warming'); setStatus('Возвращаю LLM в GPU…'); try { @@ -8733,9 +8509,8 @@ return; } - if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop) { - state.critiqueHopUsed = false; - state.visionHopUsed = false; + if (!isContinuationTurn(opts) && !opts.fromDownload) { + resetTurnHops(); } let wantedIds = (opts.forceSlotIds || []).map(normalizeSlotId).filter(Boolean); @@ -8783,6 +8558,12 @@ $('sa_input').value = ''; } persistHistory(); + } else { + const marker = opts.historyUserText || (opts.fromDebug ? '/debug ask' : ''); + if (marker) { + state.history.push({ role: 'user', content: marker }); + persistHistory(); + } } const context = collectLiveContext(); @@ -8855,12 +8636,13 @@ const prose = extractPatch(reply).prose || reply; state.history.push({ role: 'assistant', content: prose, persona, pack }); persistHistory(); - setBusyPhase(state.pendingSilentGen || userImpliesGenerate(text) ? 'silent_gen' : 'thinking'); + setBusyPhase(state.pendingSilentGen ? 'silent_gen' : 'thinking'); try { await handleReplySideEffects(reply, civitaiResults, { ...opts, userText: text, - userWantsGenerate: !!state.pendingSilentGen || userImpliesGenerate(text), + userWantsGenerate: !!opts.userWantsGenerate + || (!isMachineTurn(opts) && state.pendingSilentGen), attachedSlotIds: visionSlots.map((s) => s.id), }); } finally { @@ -9184,8 +8966,6 @@ } window.__swarmAssistentWired = true; loadSettings(); - loadTaste(); - loadTasteFromServer(); setView(state.view || 'chat'); updateGate(); ensureBoard(); @@ -9423,7 +9203,9 @@ setPackValue('inpaint_edit', { flash: true }); }); $('sa_btn_clear_init')?.addEventListener('click', () => { - clearInitAndMask(); + if (window.confirm('Сбросить Init и Mask?')) { + clearInitAndMask(); + } closeAllMoreMenus(); }); $('sa_btn_clear_image')?.addEventListener('click', () => clearSlot(state.selectedSlotId)); diff --git a/src/intent.js b/src/intent.js new file mode 100644 index 0000000..6b280db --- /dev/null +++ b/src/intent.js @@ -0,0 +1,191 @@ +/** Turn intent heuristics — pure functions testable with node --test. */ + +export function cyrTokenRe(alts) { + const boundary = '(^|[^0-9A-Za-z_А-Яа-яЁё])'; + const end = '(?=$|[^0-9A-Za-z_А-Яа-яЁё])'; + return new RegExp(`${boundary}(?:${alts})${end}`, 'i'); +} + +export function userAsksGenerate(text) { + const t = String(text || '').trim(); + if (!t) { + return false; + } + if (/^(gen|generate|go|рисуй|нарисуй)([!.…\s]|$)/i.test(t)) { + return true; + } + if (/^(ещё|еще)(\s+раз)?([!.…\s]|$)/i.test(t)) { + return true; + } + const letter = '[0-9A-Za-z_А-Яа-яЁё]'; + const stem = `${letter}*`; + return cyrTokenRe( + 'сгенерируй|сгенерировать|генерируй|generate|нарисуй|перегенерируй|перерисуй|' + + `сделай\\s+(картинк${stem}|изображен${stem}|фото${stem})|` + + `хочу\\s+(картинк${stem}|изображен${stem}|фото${stem})|` + + 'run\\s+generat|/gen', + ).test(t); +} + +export function userAsksContinue(text) { + const t = String(text || '').trim(); + if (!t) { + return false; + } + if (/^(давай\s+дальше|продолжай|продолжим|go\s+on|continue|keep\s+going|next(\s+one)?|next\s+frame)([!.…\s]|$)/i.test(t)) { + return true; + } + return cyrTokenRe( + 'давай\\s+дальше|следующ(ий|ая|ее|ую)\\s+кадр|ещё\\s+кадр|еще\\s+кадр|' + + 'кадр\\s*№?\\s*\\d+|сделай\\s+следующ', + ).test(t); +} + +export function isSameButAspectRequest(text) { + const t = String(text || '').trim(); + if (!t) { + return false; + } + return cyrTokenRe( + 'тот\\s+же\\s+(кадр|сцена|промпт|prompt)|так\\s+же\\s+но\\s+(друг|иной)\\s+(формат|размер|aspect|соотношен)|' + + 'same\\s+but\\s+(wider|taller|16:9|4:3|portrait|landscape)', + ).test(t); +} + +export function userAsksNoGenerate(text) { + const t = String(text || '').trim(); + if (!t || userAsksGenerate(t)) { + return false; + } + if (/\b(remember|save\s+(this\s+)?(as\s+)?(the\s+)?(base\s+)?(prompt|template)|don'?t\s+generat|do\s+not\s+generat|no\s+generat|without\s+generat)\b/i.test(t)) { + return true; + } + return cyrTokenRe( + 'запомн|запомни|запомним|сохрани|сохраним|шаблон|' + + 'базов(ый|ого|ому|ым|ая|ую|ое)?\\s+промпт|' + + 'не\\s+генерир|без\\s+генерац|не\\s+надо\\s+генер|только\\s+запомн|пока\\s+запомн|' + + 'не\\s+рисуй|не\\s+запускай\\s+генер', + ).test(t); +} + +export function userCommandsGenerate(text) { + const t = String(text || '').trim(); + if (!t || userAsksNoGenerate(t)) { + return false; + } + return userAsksGenerate(t) || userAsksContinue(t) || isSameButAspectRequest(t); +} + +export function userAsksLook(text) { + const t = String(text || '').trim(); + if (!t) { + return false; + } + if (/\b(look\s+at|critique|criticize|describe\s+(this|the|ref|image)|what\s+do\s+you\s+see)\b/i.test(t)) { + return true; + } + if (cyrTokenRe('критик[а-яё]*|что\\s+не\\s+так|разбери').test(t)) { + return true; + } + if (cyrTokenRe('опиши\\s+(это|эту|реф|изображ[а-яё]*|картинк[а-яё]*|кадр|результат|референс)').test(t)) { + return true; + } + return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t); +} + +export function userIsChatNotFrame(text) { + const t = String(text || '').trim(); + if (!t) { + return false; + } + if (/^(ок|окей|ok|okay|ладно|хорошо|понял|ясно|спасибо|thanks)([!.…\s]*)$/i.test(t)) { + return true; + } + if (cyrTokenRe( + 'что\\s+такое|как\\s+работает|зачем\\s+|какие\\s+(лор|модел|чекпоинт)|' + + 'список\\s+лор|где\\s+настрой|что\\s+значит|' + + 'нравит|спасибо|благодар|почему\\s+так|что\\s+ты\\s+(сделал|изменил)|' + + 'только\\s+(ответь|скажи|объясни)|без\\s+(кадр|генерац)|не\\s+надо\\s+кадр', + ).test(t) && !userAsksGenerate(t) && !userAsksContinue(t)) { + return true; + } + return false; +} + +export function userImpliesGenerate(text) { + const t = String(text || '').trim(); + if (!t || userAsksNoGenerate(t) || userIsChatNotFrame(t)) { + return false; + } + if (userCommandsGenerate(t)) { + return true; + } + if (t.length < 8) { + return false; + } + const wantsLook = userAsksLook(t); + const wantsRedraw = cyrTokenRe('поправь|исправь|перегенерир|перерисуй|улучши|переделай').test(t) + || /\b(fix|redo|redraw|improve)\b/i.test(t); + if (wantsLook && !wantsRedraw) { + return false; + } + if (cyrTokenRe( + 'нарису|сгенер|перерису|' + + 'сделай\\s+(картинк|изображен|фото|кадр)|' + + 'хочу\\s+(картинк|изображен|фото|увидеть|видеть)|' + + 'покажи\\s+как\\s+(она|он|это)|' + + 'сделай\\s+(её|ее|его|мне)\\s|' + + 'пусть\\s+будет|' + + 'другой\\s+(ракурс|свет|наряд|поза)|' + + 'поменяй\\s+(позу|свет|одежд|фон)|добавь\\s+(свет|детал)|' + + 'ещё\\s+одн|еще\\s+одн', + ).test(t)) { + return true; + } + if (/\b(draw|paint|render|make her|make him|another one|new frame)\b/i.test(t)) { + return true; + } + const isQuestion = /[??]\s*$/.test(t); + if (isQuestion) { + return cyrTokenRe('нарису|сгенер|можешь\\s+(сделать|нарисовать)|можно\\s+(картинк|сгенер)').test(t); + } + return false; +} + +export function packBlocksAutoGenerate(pack) { + const p = String(pack || ''); + return p === 'describe_ref' || p === 'catalog_card' || p === 'author_persona' || p === 'debug_explain'; +} + +export function packWantsVision(pack) { + const p = String(pack || ''); + return p === 'critique_image' || p === 'describe_ref' || p === 'compose_scene' || p === 'inpaint_edit'; +} + +export function resolveTurnIntent(patch, userText, opts = {}, packId = '') { + const machine = !!opts.machineTurn; + const vetoed = !machine && userAsksNoGenerate(userText); + const commanded = !!opts.userWantsGenerate || (!machine && userCommandsGenerate(userText)); + const implied = !machine && userImpliesGenerate(userText); + const modelAsked = Array.isArray(patch?.actions) && patch.actions.map(String).includes('generate'); + + let generate; + if (vetoed || opts.fromAutoCritique) { + generate = false; + } else if (commanded) { + generate = true; + } else if (packBlocksAutoGenerate(packId)) { + generate = false; + } else { + generate = modelAsked || implied; + } + + const hasLook = !!patch + && (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null); + const honorLook = opts.fromAutoCritique || opts.fromVisionHop + || (!machine && userAsksLook(userText)) + || packWantsVision(packId); + const look = !!(hasLook && !vetoed && !generate && honorLook); + + return { generate, look, vetoed }; +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..0e86082 --- /dev/null +++ b/src/main.js @@ -0,0 +1,19 @@ +import { attachApi } from './api.js'; +import { attachPatch, setPatchKeys } from './patch.js'; +import { attachPersist } from './persist.js'; + +window.SA = window.SA || {}; +attachApi(window.SA); +attachPatch(window.SA); +attachPersist(window.SA); + +/** Called from app after AssistentGetConfig — single source: Config/_base/patch-keys.json */ +window.SA.applyConfigPatchKeys = function (config) { + const keys = config?.patch_keys; + if (Array.isArray(keys) && keys.length) { + setPatchKeys(keys); + window.SA.PATCH_KEYS = keys; + } +}; + +import './app.js'; diff --git a/src/patch.js b/src/patch.js new file mode 100644 index 0000000..82da9ab --- /dev/null +++ b/src/patch.js @@ -0,0 +1,145 @@ +/** Patch detection / extraction — mirrors AssistentPatch.cs. */ + +const DEFAULT_PATCH_KEYS = [ + 'prompt', 'negative', 'loras', 'width', 'height', 'steps', 'cfg', 'seed', 'sigma_shift', 'sampler', 'scheduler', + 'actions', 'search_query', 'civitai_query', + 'use_init_image', 'clear_init_image', 'init_creativity', 'denoise', + 'use_mask_image', 'clear_mask_image', 'mask_blur', 'mask_grow', + 'look_at', 'vision_from', 'vision_slots', 'slot_to_init', 'slot_to_mask', + 'snapshot_generate', 'select_slot', 'aspect', 'images', 'batch', 'vary', 'lock_seed', + 'creativity', 'intensity', 'complexity', 'movement', + 'clear_prompt_images', 'slot_to_prompt_image', 'pack', 'memories', 'memory', + 'memory_query', 'memory_kind', 'tag_query', 'user_prefs', + 'inventory_query', 'skills', 'persona_shelves', 'persona_clone', 'persona', 'controls', + 'variants', +]; + +let PATCH_KEYS = DEFAULT_PATCH_KEYS.slice(); + +export function setPatchKeys(keys) { + if (Array.isArray(keys) && keys.length) { + PATCH_KEYS = keys.map(String); + } +} + +export function getPatchKeys() { + return PATCH_KEYS.slice(); +} + +const FENCE_RE = /```(?:json)?\s*([\s\S]*?)```/gi; + +function has(obj, key) { + return obj[key] !== undefined && obj[key] !== null; +} + +export function isCardObject(obj) { + if (!obj || typeof obj !== 'object') { + return false; + } + const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint); + const genish = !!(obj.prompt != null || obj.negative != null || obj.loras || obj.actions + || obj.width || obj.height || obj.steps != null || obj.cfg != null || obj.aspect || obj.seed != null + || obj.search_query || obj.civitai_query || obj.look_at || obj.controls); + 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)); +} + +export function isPatchObject(obj) { + if (!obj || typeof obj !== 'object') { + return false; + } + if (isCardObject(obj)) { + return false; + } + return PATCH_KEYS.some((k) => has(obj, k)); +} + +export function normalizePatch(patch) { + if (!patch || typeof patch !== 'object') { + return patch; + } + if (!has(patch, 'search_query') && has(patch, 'civitai_query')) { + patch.search_query = patch.civitai_query; + } + if (!has(patch, 'init_creativity') && has(patch, 'denoise')) { + patch.init_creativity = patch.denoise; + } + if (!has(patch, 'look_at')) { + if (has(patch, 'vision_from')) { + patch.look_at = patch.vision_from; + } else if (has(patch, 'vision_slots')) { + patch.look_at = patch.vision_slots; + } + } + return patch; +} + +export function extractPatch(text) { + if (!text) { + return { prose: text || '', patch: null }; + } + const re = new RegExp(FENCE_RE.source, 'gi'); + let match; + let lastPatch = null; + let prose = text; + while ((match = re.exec(text)) !== null) { + try { + const obj = JSON.parse(match[1].trim()); + if (isPatchObject(obj)) { + lastPatch = normalizePatch(obj); + prose = (text.slice(0, match.index) + text.slice(match.index + match[0].length)).trim(); + } + } catch (e) { /* not json */ } + } + return { prose, patch: lastPatch }; +} + +export function isTerminalStreamPatch(obj) { + if (!obj || typeof obj !== 'object') { + return false; + } + if (isCardObject(obj)) { + return true; + } + if (Array.isArray(obj.variants) && obj.variants.length) { + return true; + } + if (obj.look_at != null || obj.vision_from != null || obj.vision_slots != null) { + return true; + } + if (obj.search_query != null || obj.civitai_query != null + || obj.memory_query != null || obj.tag_query != null || obj.inventory_query != null) { + return true; + } + const acts = Array.isArray(obj.actions) ? obj.actions.map(String) : []; + const hopOrGen = [ + 'skill_load', 'persona_read', 'memory_get', 'memory_search', 'lookup_tags', + 'list_inventory', 'search_civitai', 'interrupt', 'generate', + 'memory_upsert', 'user_pref_upsert', + ]; + if (acts.some((a) => hopOrGen.includes(a))) { + return true; + } + if (String(obj.prompt || '').trim().length >= 48) { + return true; + } + if (obj.loras != null || obj.aspect != null || obj.steps != null + || obj.width != null || obj.height != null || obj.cfg != null + || obj.seed != null || obj.controls != null + || obj.memories != null || obj.user_prefs != null) { + return true; + } + return false; +} + +export function attachPatch(SA) { + SA.PATCH_KEYS = PATCH_KEYS; + SA.setPatchKeys = setPatchKeys; + SA.isCardObject = isCardObject; + SA.isPatchObject = isPatchObject; + SA.isTerminalStreamPatch = isTerminalStreamPatch; + SA.normalizePatch = normalizePatch; + SA.extractPatch = extractPatch; +} diff --git a/src/persist.js b/src/persist.js new file mode 100644 index 0000000..88efae1 --- /dev/null +++ b/src/persist.js @@ -0,0 +1,139 @@ +/** Sqlite persistence for chats + UI state. */ + +const LS_CHATS = 'swarm_assistent_chats_v1'; +const SAVE_DEBOUNCE_MS = 700; + +const timers = { chats: new Map(), ui: null }; + +function normalizeChat(raw) { + if (!raw || !raw.id) { + return null; + } + return { + id: String(raw.id), + title: String(raw.title || 'Новый чат'), + createdAt: Number(raw.createdAt) || Date.now(), + updatedAt: Number(raw.updatedAt) || Date.now(), + messages: Array.isArray(raw.messages) ? raw.messages : [], + messages_count: Number(raw.messages_count) || (Array.isArray(raw.messages) ? raw.messages.length : 0), + params: raw.params && typeof raw.params === 'object' ? raw.params : null, + }; +} + +export function attachPersist(SA, request = SA.request) { + async function loadChats() { + let chats = []; + try { + const data = await request('AssistentListChats', { with_messages: true }); + chats = (data?.chats || []).map(normalizeChat).filter(Boolean); + } catch (e) { + console.warn('Assistent: disk chats unavailable', e); + return null; + } + return chats.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)); + } + + async function getChat(id) { + if (!id) { + return null; + } + const data = await request('AssistentGetChat', { id }); + return normalizeChat(data?.chat); + } + + async function searchChats(q) { + const query = String(q || '').trim(); + if (query.length < 2) { + return []; + } + const data = await request('AssistentListChats', { q: query, with_messages: false, limit: 40 }); + return (data?.chats || []).map(normalizeChat).filter(Boolean); + } + + function saveChat(chat, { immediate = false } = {}) { + const clean = normalizeChat(chat); + if (!clean) { + return Promise.resolve(null); + } + const send = () => { + timers.chats.delete(clean.id); + return request('AssistentSaveChat', { + id: clean.id, + title: clean.title, + messages: clean.messages, + params: clean.params, + createdAt: clean.createdAt, + updatedAt: clean.updatedAt, + }); + }; + if (immediate) { + const pending = timers.chats.get(clean.id); + if (pending) { + clearTimeout(pending); + } + return send(); + } + const pending = timers.chats.get(clean.id); + if (pending) { + clearTimeout(pending); + } + timers.chats.set(clean.id, setTimeout(() => { + send().catch((e) => console.warn('Assistent: chat save failed', clean.id, e)); + }, SAVE_DEBOUNCE_MS)); + return Promise.resolve(null); + } + + function deleteChat(id) { + if (!id) { + return Promise.resolve(null); + } + const pending = timers.chats.get(id); + if (pending) { + clearTimeout(pending); + timers.chats.delete(id); + } + return request('AssistentDeleteChat', { id }); + } + + async function loadUiState() { + try { + const data = await request('AssistentGetUiState', {}); + const ui = data?.ui_state; + return ui && typeof ui === 'object' ? ui : null; + } catch (e) { + return null; + } + } + + function saveUiState(uiState, { immediate = false } = {}) { + if (!uiState || typeof uiState !== 'object') { + return Promise.resolve(null); + } + const send = () => { + timers.ui = null; + return request('AssistentSaveUiState', { ui_state: uiState }); + }; + if (timers.ui) { + clearTimeout(timers.ui); + timers.ui = null; + } + if (immediate) { + return send(); + } + timers.ui = setTimeout(() => { + send().catch((e) => console.warn('Assistent: ui-state save failed', e)); + }, SAVE_DEBOUNCE_MS); + return Promise.resolve(null); + } + + SA.persist = { + LS_CHATS, + loadChats, + getChat, + searchChats, + saveChat, + deleteChat, + loadUiState, + saveUiState, + }; +} diff --git a/test/intent.test.js b/test/intent.test.js new file mode 100644 index 0000000..353f3aa --- /dev/null +++ b/test/intent.test.js @@ -0,0 +1,34 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { + userAsksLook, + userCommandsGenerate, + userAsksNoGenerate, + resolveTurnIntent, + packWantsVision, +} from '../src/intent.js'; + +describe('intent.js', () => { + it('userAsksLook detects critique RU', () => { + assert.equal(userAsksLook('посмотри на результат'), true); + assert.equal(userAsksLook('нарисуй лису'), false); + }); + + it('userCommandsGenerate respects veto', () => { + assert.equal(userCommandsGenerate('сгенерируй кадр'), true); + assert.equal(userCommandsGenerate('только запомни промпт'), false); + assert.equal(userAsksNoGenerate('только запомни промпт'), true); + }); + + it('resolveTurnIntent honors look without generate', () => { + const patch = { look_at: ['generate'] }; + const intent = resolveTurnIntent(patch, 'что не так с кадром?', {}, 'ordinary'); + assert.equal(intent.generate, false); + assert.equal(intent.look, true); + }); + + it('packWantsVision for critique pack', () => { + assert.equal(packWantsVision('critique_image'), true); + assert.equal(packWantsVision('ordinary'), false); + }); +}); diff --git a/test/patch.test.js b/test/patch.test.js new file mode 100644 index 0000000..857923c --- /dev/null +++ b/test/patch.test.js @@ -0,0 +1,37 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { + extractPatch, + isPatchObject, + isCardObject, + normalizePatch, + setPatchKeys, +} from '../src/patch.js'; + +describe('patch.js', () => { + it('extractPatch finds generation patch in fence', () => { + const text = 'Here you go\n```json\n{"prompt":"A red fox in snow","actions":["generate"]}\n```'; + const { prose, patch } = extractPatch(text); + assert.ok(patch); + assert.equal(patch.prompt, 'A red fox in snow'); + assert.ok(!prose.includes('```')); + }); + + it('isCardObject vs isPatchObject', () => { + const card = { kind: 'lora', name: 'Foo', triggers: ['bar'] }; + const gen = { prompt: 'test', actions: ['generate'] }; + assert.equal(isCardObject(card), true); + assert.equal(isPatchObject(card), false); + assert.equal(isPatchObject(gen), true); + }); + + it('normalizePatch aliases civitai_query', () => { + const p = normalizePatch({ civitai_query: 'anime style' }); + assert.equal(p.search_query, 'anime style'); + }); + + it('scheduler-only patch is detected with full key list', () => { + setPatchKeys(['prompt', 'scheduler']); + assert.equal(isPatchObject({ scheduler: 'euler' }), true); + }); +});