diff --git a/Assets/assistent.bundle.js b/Assets/assistent.bundle.js index 72ae7e6..7af94e4 100644 --- a/Assets/assistent.bundle.js +++ b/Assets/assistent.bundle.js @@ -43,6 +43,7 @@ "actions", "generate", "ask", + "checkpoint", "use_init_image", "clear_init_image", "init_creativity", @@ -85,6 +86,20 @@ function has(obj, key) { return obj[key] !== void 0 && obj[key] !== null; } + function generateFlagOn(obj) { + if (!obj || typeof obj !== "object") { + return false; + } + const g = obj.generate; + if (g === true || g === 1) { + return true; + } + if (typeof g === "string" && /^(true|1|yes|on)$/i.test(g.trim())) { + return true; + } + const acts = Array.isArray(obj.actions) ? obj.actions.map(String) : []; + return acts.includes("generate"); + } function isPatchObject2(obj) { if (!obj || typeof obj !== "object") { return false; @@ -96,7 +111,7 @@ return patch; } const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : []; - if (patch.generate === true || acts.includes("generate")) { + if (generateFlagOn(patch) || acts.includes("generate")) { patch.generate = true; } if (typeof patch.ask === "string") { @@ -114,31 +129,63 @@ } return patch; } + function tryParsePatchJson(raw) { + try { + const obj = JSON.parse(String(raw || "").trim()); + if (isPatchObject2(obj)) { + return normalizePatch(obj); + } + } catch (e) { + } + return null; + } 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; + let lastAny = null; + let lastAnyIndex = -1; + let lastAnyLen = 0; + let lastTerminal = null; + let lastTermIndex = -1; + let lastTermLen = 0; 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) { + const parsed = tryParsePatchJson(match[1]); + if (!parsed) { + continue; + } + lastAny = parsed; + lastAnyIndex = match.index; + lastAnyLen = match[0].length; + if (isTerminalStreamPatch(parsed)) { + lastTerminal = parsed; + lastTermIndex = match.index; + lastTermLen = match[0].length; } } - return { prose, patch: lastPatch }; + const chosen = lastTerminal || lastAny; + if (chosen) { + const idx = lastTerminal ? lastTermIndex : lastAnyIndex; + const len = lastTerminal ? lastTermLen : lastAnyLen; + const prose = (text.slice(0, idx) + text.slice(idx + len)).trim(); + return { prose, patch: chosen }; + } + const brace = text.lastIndexOf("{"); + if (brace >= 0) { + const parsed = tryParsePatchJson(text.slice(brace)); + if (parsed) { + return { prose: text.slice(0, brace).trim(), patch: parsed }; + } + } + return { prose: text, patch: null }; } function isTerminalStreamPatch(obj) { if (!obj || typeof obj !== "object") { return false; } - if (obj.generate === true) { + if (generateFlagOn(obj)) { return true; } if (Array.isArray(obj.ask) && obj.ask.length) { @@ -153,14 +200,10 @@ if (obj.look_at != null || obj.vision_from != null || obj.vision_slots != null) { return true; } - const acts = Array.isArray(obj.actions) ? obj.actions.map(String) : []; - if (acts.includes("generate")) { - 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) { + 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.checkpoint != null || obj.negative != null) { return true; } return false; @@ -172,6 +215,7 @@ SA2.isTerminalStreamPatch = isTerminalStreamPatch; SA2.normalizePatch = normalizePatch; SA2.extractPatch = extractPatch; + SA2.generateFlagOn = generateFlagOn; } // src/persist.js @@ -375,7 +419,9 @@ } const delta = { ...raw }; const acts = Array.isArray(delta.actions) ? delta.actions.map(String) : []; - if (delta.generate === true || acts.includes("generate")) { + const g = delta.generate; + const generateOn = g === true || g === 1 || typeof g === "string" && /^(true|1|yes|on)$/i.test(g.trim()) || acts.includes("generate"); + if (generateOn) { delta.generate = true; } if (typeof delta.ask === "string") { @@ -392,7 +438,8 @@ if (!patch || typeof patch !== "object") { return false; } - if (patch.generate === true) { + const n = normalizeDelta(patch); + if (n?.generate === true) { return true; } const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : []; @@ -631,10 +678,12 @@ session_exact: extras.sessionExact || null }; } - function resolveTurnIntent(patch, userText, { vetoFn } = {}) { + function resolveTurnIntent(patch, userText, { vetoFn, askGenerateFn, fromAutoCritique } = {}) { const delta = normalizeDelta(patch) || {}; const vetoed = typeof vetoFn === "function" ? !!vetoFn(userText) : false; - const generate = !vetoed && patchWantsGenerate(delta); + const modelAsked = patchWantsGenerate(delta); + const userAsked = typeof askGenerateFn === "function" && !!askGenerateFn(userText) && !!(modelAsked || String(delta.prompt || "").trim()); + const generate = !vetoed && !fromAutoCritique && (modelAsked || userAsked); const hasLook = delta.look_at != null || delta.vision_from != null || delta.vision_slots != null; const look = !!(hasLook && !generate && !vetoed); const ask = patchAskList(delta); @@ -884,7 +933,7 @@ scrollToBottom, hideEmpty } = opts; - let card2 = null; + let card = null; let listEl = null; let titleEl = null; let steps = []; @@ -894,16 +943,16 @@ if (!box) { return null; } - if (card2 && card2.isConnected) { - return card2; + if (card && card.isConnected) { + return card; } if (typeof hideEmpty === "function") { hideEmpty(); } - card2 = document.createElement("div"); - card2.className = "sa-activity sa-activity-live"; - card2.setAttribute("role", "status"); - card2.setAttribute("aria-live", "polite"); + card = document.createElement("div"); + card.className = "sa-activity sa-activity-live"; + card.setAttribute("role", "status"); + card.setAttribute("aria-live", "polite"); const head = document.createElement("button"); head.type = "button"; head.className = "sa-activity-head"; @@ -923,18 +972,18 @@ head.appendChild(chev); head.addEventListener("click", () => { open = !open; - card2.classList.toggle("sa-activity-collapsed", !open); + card.classList.toggle("sa-activity-collapsed", !open); head.setAttribute("aria-expanded", open ? "true" : "false"); }); listEl = document.createElement("div"); listEl.className = "sa-activity-steps"; - card2.appendChild(head); - card2.appendChild(listEl); - box.appendChild(card2); + card.appendChild(head); + card.appendChild(listEl); + box.appendChild(card); if (typeof scrollToBottom === "function") { scrollToBottom(); } - return card2; + return card; } function renderStep(step) { const row = document.createElement("div"); @@ -970,15 +1019,15 @@ if (titleEl) { titleEl.textContent = running ? running.label : last?.label || "Assistent"; } - card2.classList.toggle("sa-activity-live", steps.some((s) => s.status === "running")); - card2.classList.toggle("sa-activity-done", steps.length > 0 && steps.every((s) => s.status === "done" || s.status === "skip")); + card.classList.toggle("sa-activity-live", steps.some((s) => s.status === "running")); + card.classList.toggle("sa-activity-done", steps.length > 0 && steps.every((s) => s.status === "done" || s.status === "skip")); if (typeof scrollToBottom === "function") { scrollToBottom(); } } function begin(title) { steps = []; - card2 = null; + card = null; listEl = null; titleEl = null; open = true; @@ -1021,9 +1070,9 @@ titleEl.textContent = summary; } paint(); - if (card2) { - card2.classList.remove("sa-activity-live"); - card2.classList.add("sa-activity-done"); + if (card) { + card.classList.remove("sa-activity-live"); + card.classList.add("sa-activity-done"); } } function noteModelCommands(patch) { @@ -1242,8 +1291,8 @@ \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 +/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 \u0441\u043F\u0438\u0441\u043A\u0435) +/history \u2014 \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u0438\u043B\u0438 \u0441\u043A\u0440\u044B\u0442\u044C \u043F\u0430\u043D\u0435\u043B\u044C \u0447\u0430\u0442\u043E\u0432 /compress \u2014 \u0441\u0436\u0430\u0442\u044C \u0441\u0442\u0430\u0440\u044B\u0435 \u0445\u043E\u0434\u044B \u0432 \u0441\u0430\u043C\u043C\u0430\u0440\u0438 /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 @@ -1259,11 +1308,11 @@ /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.`; +\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 \u043F\u0430\u043D\u0435\u043B\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: "/history", hint: "\u043F\u0430\u043D\u0435\u043B\u044C \u0447\u0430\u0442\u043E\u0432" }, { cmd: "/compress", hint: "\u0441\u0436\u0430\u0442\u044C \u0441\u0442\u0430\u0440\u044B\u0435 \u0445\u043E\u0434\u044B" }, { 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" }, @@ -2143,9 +2192,16 @@ ${patch.prompt}`; function mergePromptEnRewrite(effective) { const base = state.pendingPromptEnMerge; state.pendingPromptEnMerge = null; - if (!base || !effective) { + if (!base) { return effective; } + if (!effective) { + return { + ...base, + generate: true, + actions: Array.isArray(base.actions) && base.actions.length ? base.actions : ["generate"] + }; + } return { ...base, ...effective, @@ -2184,6 +2240,21 @@ ${patch.prompt}`; } return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t); } + function userAsksGenerate(text) { + if (window.SA && typeof SA.userAsksGenerate === "function") { + return SA.userAsksGenerate(text); + } + const t = String(text || "").trim(); + if (!t || userAsksNoGenerate(t)) { + return false; + } + if (/\b(generat(e|ion)|draw|render|make\s+(an?\s+)?image|run\s+generate)\b/i.test(t)) { + return true; + } + return cyrTokenRe( + "\u0441\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u043D\u0430\u0440\u0438\u0441\u0443\u0439|\u043D\u0430\u0440\u0438\u0441\u0443\u0439\u0442\u0435|\u0437\u0430\u043F\u0443\u0441\u0442\u0438\\s+\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u0441\u0434\u0435\u043B\u0430\u0439\\s+(\u043A\u0430\u0434\u0440|\u043A\u0430\u0440\u0442\u0438\u043D\u043A[\u0430-\u044F\u0451]*|\u0438\u0437\u043E\u0431\u0440\u0430\u0436[\u0430-\u044F\u0451]*)" + ).test(t); + } function packWantsVision(pack) { const p = String(pack || ""); return p === "critique_image" || p === "describe_ref" || p === "compose_scene" || p === "inpaint_edit"; @@ -2191,11 +2262,16 @@ ${patch.prompt}`; function resolveTurnIntent2(patch, userText, opts = {}) { const S = window.SA && window.SA.session; if (S && typeof S.resolveTurnIntent === "function") { - return S.resolveTurnIntent(patch, userText, { vetoFn: userAsksNoGenerate }); + return S.resolveTurnIntent(patch, userText, { + vetoFn: userAsksNoGenerate, + askGenerateFn: userAsksGenerate, + fromAutoCritique: !!opts.fromAutoCritique + }); } const vetoed = !isMachineTurn(opts) && userAsksNoGenerate(userText); - const modelAsked = !!(patch && (patch.generate === true || Array.isArray(patch.actions) && patch.actions.map(String).includes("generate"))); - const generate = !vetoed && !opts.fromAutoCritique && modelAsked; + const modelAsked = !!(patch && (patch.generate === true || patch.generate === 1 || typeof patch.generate === "string" && /^(true|1|yes|on)$/i.test(patch.generate) || Array.isArray(patch.actions) && patch.actions.map(String).includes("generate"))); + const userAsked = !isMachineTurn(opts) && userAsksGenerate(userText) && !!(modelAsked || String(patch?.prompt || "").trim()); + const generate = !vetoed && !opts.fromAutoCritique && (modelAsked || userAsked); const hasLook = !!patch && (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null); const look = !!(hasLook && !vetoed && !generate); const ask = Array.isArray(patch?.ask) ? patch.ask.map(String) : typeof patch?.ask === "string" && patch.ask ? [patch.ask] : []; @@ -3364,6 +3440,7 @@ ${patch.prompt}`; genBtn.textContent = "\u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C"; genBtn.addEventListener("click", async () => { if (isGenerateUnavailable()) { + setStatus("Generate \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u2014 \u043F\u043E\u0434\u043E\u0436\u0434\u0438 \u0438\u043B\u0438 \u043D\u0430\u0436\u043C\u0438 \u0421\u0442\u043E\u043F"); return; } startBusyUi("silent_gen"); @@ -4161,7 +4238,7 @@ ${patch.prompt}`; 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).
'; + 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 \u043A\u043D\u043E\u043F\u043A\u0430 \u043F\u0430\u043D\u0435\u043B\u0438 \u0441\u043B\u0435\u0432\u0430 \u2014 \u043F\u0440\u043E\u0448\u043B\u044B\u0435 \u0434\u0438\u0430\u043B\u043E\u0433\u0438.
'; box.appendChild(empty); } function renderHistoryIntoUi(messages) { @@ -4222,19 +4299,34 @@ ${patch.prompt}`; } 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"; + el.title = chat?.title || "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442"; + } + function chatHasTranscript(c) { + return (c?.messages || []).length > 0 || (c?.messages_count || 0) > 0; } function savedChatsCount() { - return (state.chats || []).filter((c) => (c.messages || []).length > 0).length; + return (state.chats || []).filter(chatHasTranscript).length; + } + function isBlankActiveChat() { + if ((state.history || []).length) { + return false; + } + const chat = findChat(state.activeChatId); + return !chat || !chatHasTranscript(chat); } function syncHistoryBadge() { const btn = $2("sa_btn_chats"); - if (!btn) { - return; + const countEl = $2("sa_chats_count"); + const n = (state.chats || []).filter(chatHasTranscript).length; + const open = !!state.chatsPanelOpen; + if (btn) { + btn.title = open ? n ? `\u0421\u043A\u0440\u044B\u0442\u044C \u0447\u0430\u0442\u044B (${n})` : "\u0421\u043A\u0440\u044B\u0442\u044C \u0447\u0430\u0442\u044B" : n ? `\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0447\u0430\u0442\u044B (${n})` : "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0447\u0430\u0442\u044B"; + btn.setAttribute("aria-label", btn.title); + } + if (countEl) { + countEl.hidden = n < 1; + countEl.textContent = n > 99 ? "99+" : String(n); } - 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) { @@ -4304,7 +4396,7 @@ ${patch.prompt}`; 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); + let chats = (state.chats || []).slice().sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)).filter((c) => c.id === state.activeChatId || chatHasTranscript(c)); if (q) { const local = chats.filter((c) => chatMatchesQuery(c, q)); const seen = new Set(local.map((c) => c.id)); @@ -4342,6 +4434,7 @@ ${patch.prompt}`; btn?.classList.toggle("sa-sessions-toggle-active", state.chatsPanelOpen); root?.classList.toggle("sa-drawer-open", state.chatsPanelOpen); localStorage.setItem(LS_CHATS_DRAWER, state.chatsPanelOpen ? "1" : "0"); + syncHistoryBadge(); if (state.chatsPanelOpen) { saveActiveChatToStore(); const search = $2("sa_chats_search"); @@ -4353,13 +4446,17 @@ ${patch.prompt}`; } saveUiStateToDisk(); } - 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"); + async function startNewChat({ saveCurrent = true, force = false, openDrawer = false } = {}) { + if (!force && isBlankActiveChat()) { + setStatus("\u0423\u0436\u0435 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442"); + if (openDrawer) { + setChatsPanelOpen(true); + } + $2("sa_input")?.focus(); return; } - if (force) { - abortInFlightWork({ status: "" }); + if (state.busy || state.generating) { + abortInFlightWork({ status: "", interruptSwarm: !!state.generating }); } if (saveCurrent) { saveActiveChatToStore({ dropEmpty: true }); @@ -4401,6 +4498,12 @@ ${patch.prompt}`; updateCtxChip(); 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(); + if (openDrawer) { + setChatsPanelOpen(true); + } + if (openDrawer || !force) { + $2("sa_input")?.focus(); + } } async function switchToChat(id) { if (!id || id === state.activeChatId) { @@ -4540,7 +4643,7 @@ ${patch.prompt}`; syncBuildGenButton(); clearPersistedHistory(); resetContextMemory({ persist: false }); - 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.
'); + 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, \u043A\u043D\u043E\u043F\u043A\u0430 \u043F\u0430\u043D\u0435\u043B\u0438 \u0441\u043B\u0435\u0432\u0430 \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(); @@ -5577,7 +5680,7 @@ ${patch.prompt}`; if (!state.gotDelta || !state.busy || state.generating || state.turnSettled) { return; } - const waitMs = state.streamFenceDone ? 4e3 : 15e3; + const waitMs = state.streamFenceDone ? 800 : state.gotDelta ? 2200 : 15e3; if (Date.now() - (state.lastDeltaAt || 0) < waitMs) { return; } @@ -6159,23 +6262,30 @@ ${patch.prompt}`; state.streamFenceDone = false; return div; } + function parseTerminalPatchObject(raw) { + try { + const obj = JSON.parse(String(raw || "").trim()); + const terminal = window.SA && typeof SA.isTerminalStreamPatch === "function" ? SA.isTerminalStreamPatch(obj) : typeof isPatchObject === "function" && isPatchObject(obj); + return terminal ? obj : null; + } catch (e) { + return null; + } + } 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) : typeof isPatchObject === "function" && isPatchObject(obj); - if (terminal) { + if (/```[\s\S]*```/.test(t)) { + const re = /```(?:json)?\s*([\s\S]*?)```/gi; + let match; + while ((match = re.exec(t)) !== null) { + if (parseTerminalPatchObject(match[1])) { return true; } - } catch (e) { } } + const brace = t.lastIndexOf("{"); + if (brace >= 0 && parseTerminalPatchObject(t.slice(brace))) { + return true; + } return false; } function trimToClosedPatchFence(text) { @@ -6184,16 +6294,18 @@ ${patch.prompt}`; 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) : typeof isPatchObject === "function" && isPatchObject(obj); - if (terminal) { - lastEnd = match.index + match[0].length; - } - } catch (e) { + if (parseTerminalPatchObject(match[1])) { + lastEnd = match.index + match[0].length; } } - return lastEnd > 0 ? t.slice(0, lastEnd).trimEnd() : t; + if (lastEnd > 0) { + return t.slice(0, lastEnd).trimEnd(); + } + const brace = t.lastIndexOf("{"); + if (brace >= 0 && parseTerminalPatchObject(t.slice(brace))) { + return t.trimEnd(); + } + return t; } function appendStreamDelta(delta) { if (state.streamFenceDone) { @@ -6218,6 +6330,18 @@ ${patch.prompt}`; if (streamHasClosedPatchFence(state.streamText)) { state.streamText = trimToClosedPatchFence(state.streamText); state.streamFenceDone = true; + setAssistantBody(state.streamEl, state.streamText, { live: true }); + scrollMessagesToBottom(); + const fn = state.onClosedTerminalFence; + state.onClosedTerminalFence = null; + if (typeof fn === "function") { + try { + fn(state.streamText); + } catch (e) { + console.warn("Assistent closed-fence finalize", e); + } + } + return; } setAssistantBody(state.streamEl, state.streamText, { live: true }); scrollMessagesToBottom(); @@ -6242,16 +6366,9 @@ ${patch.prompt}`; const { prose, patch } = extractPatch2(fullReply); setAssistantBody(el, prose || fullReply || ""); el.querySelectorAll(".sa-patch, .sa-civitai-list").forEach((n) => n.remove()); - if (patch && !(card && !patch.prompt && !patch.actions && !patch.loras)) { + if (patch) { 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 (!(meta && meta.historical)) { mountCurateButtons(el, meta); @@ -7387,8 +7504,6 @@ ${data.ui.help_extra}`.trim(); const n = state.inventory.loras.length; const ck = state.inventory.checkpoints.length; setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts${rescan ? " (rescanned)" : ""}`); - if (state.view === "cards") { - } if (done) { done(state.inventory); } @@ -8314,7 +8429,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; } const extracted = typeof extractPatch2 === "function" ? extractPatch2(reply) : { patch: null }; let effective = extracted && extracted.patch ? extracted.patch : null; - if (opts.fromPromptEnRetry && effective && typeof mergePromptEnRewrite === "function") { + if (opts.fromPromptEnRetry && typeof mergePromptEnRewrite === "function" && (effective || state.pendingPromptEnMerge)) { effective = mergePromptEnRewrite(effective); } const S = window.SA && window.SA.session; @@ -8322,11 +8437,12 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; if (effective && act && typeof act.noteModelCommands === "function") { act.noteModelCommands(effective); } + const promptChanged = !!(effective && String(effective.prompt || "").trim()); if (effective && S) { state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective); activityDone("delta", { kind: "delta", - label: "\u041E\u0431\u043D\u043E\u0432\u0438\u043B \u0441\u0435\u0441\u0441\u0438\u044E", + label: promptChanged ? "\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D" : "\u041E\u0431\u043D\u043E\u0432\u0438\u043B \u0441\u0435\u0441\u0441\u0438\u044E", detail: Object.keys(effective).filter((k) => effective[k] != null && !["actions", "notes"].includes(k)).slice(0, 10).join(", ") }); try { @@ -8342,7 +8458,10 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; if (Array.isArray(effective?.actions) && effective.actions.map(String).includes("interrupt")) { if (typeof doInterruptNow === "function") doInterruptNow(); } - const intent = resolveTurnIntent2(effective, opts.userText || "", opts); + let intent = resolveTurnIntent2(effective, opts.userText || "", opts); + if (opts.userWantsGenerate && effective && !intent.vetoed && !fromAutoCritique) { + intent = { ...intent, generate: true }; + } if (effective) { if (intent.generate) { const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : []; @@ -8450,6 +8569,10 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective); await pushSessionToSwarm(state.chatSession); if (typeof syncLiveParamsBar === "function") syncLiveParamsBar(); + if (typeof appendSystemNote === "function") { + appendSystemNote(promptChanged ? "\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D \xB7 Generate" : "\u0417\u0430\u043F\u0443\u0441\u043A\u0430\u044E Generate"); + } + setStatus(promptChanged ? "\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D \xB7 Generate" : "Generate"); const srcOut = await runGenerateFromPatch( { ...effective || {}, actions: ["generate"], generate: true }, { force: true, fromSession: true } @@ -8459,12 +8582,20 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; if (typeof maybeAutoCritique === "function") await maybeAutoCritique(srcOut); if (typeof maybeAutoVisionLook === "function") await maybeAutoVisionLook(srcOut); } - } else if (effective && $2("sa_auto_apply")?.checked) { + } else if (effective) { if (S) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective); - await pushSessionToSwarm(state.chatSession); - if (typeof syncLiveParamsBar === "function") syncLiveParamsBar(); + if (promptChanged || $2("sa_auto_apply")?.checked) { + await pushSessionToSwarm(state.chatSession); + if (typeof syncLiveParamsBar === "function") syncLiveParamsBar(); + if (promptChanged && typeof appendSystemNote === "function") { + appendSystemNote("\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D"); + } + if (promptChanged) { + setStatus("\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D"); + } + } if (!state.generating && typeof stopBusyUi === "function") { - stopBusyUi(intent.vetoed ? "\u0417\u0430\u043F\u043E\u043C\u043D\u0438\u043B \xB7 \u0431\u0435\u0437 Generate" : ""); + stopBusyUi(intent.vetoed ? "\u0417\u0430\u043F\u043E\u043C\u043D\u0438\u043B \xB7 \u0431\u0435\u0437 Generate" : promptChanged ? "\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D" : ""); } } state.pendingSilentGen = false; @@ -8628,12 +8759,12 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; return true; } if (cmd === "new" || cmd === "newchat") { - await startNewChat({ saveCurrent: true }); + await startNewChat({ saveCurrent: true, openDrawer: true }); return true; } if (cmd === "history" || cmd === "chats" || cmd === "sessions") { - setChatsPanelOpen(true); - setStatus("/history"); + setChatsPanelOpen(!state.chatsPanelOpen); + setStatus(state.chatsPanelOpen ? "/history" : "\u0427\u0430\u0442\u044B \u0441\u043A\u0440\u044B\u0442\u044B"); return true; } if (cmd === "compress" || cmd === "compact" || cmd === "\u0441\u0436\u0430\u0442\u044C") { @@ -9036,6 +9167,8 @@ ${HELP_TEXT}`); startBusyUi(state.expectColdLoad ? "loading" : "thinking"); } const context = collectLiveContext(); + context.prior_assistant_turns = (state.history || []).filter((m) => m && m.role === "assistant" && !m.systemish).length; + context.do_not_greet = context.prior_assistant_turns > 0; context.has_vision_image = visionReadySlots().length > 0; context.images_in_request = !!(images && images.length); context.attached_slot_ids = attachableSlots().map((s) => s.id); @@ -9074,6 +9207,7 @@ ${HELP_TEXT}`); return; } state.turnSettled = true; + state.onClosedTerminalFence = null; clearStreamStall(); if (meta.system_chars != null) { state.lastSystemChars = Number(meta.system_chars) || 0; @@ -9128,6 +9262,7 @@ ${HELP_TEXT}`); return; } state.turnSettled = true; + state.onClosedTerminalFence = null; clearStreamStall(); state.busy = false; setInterruptVisible(state.generating); @@ -9144,14 +9279,19 @@ ${HELP_TEXT}`); }; if (typeof makeWSRequest === "function") { beginStreamMessage(msgMeta); - armStreamStall(chatEpoch, (reply) => { + const settleStreamReply = (reply) => { if (state.turnSettled || chatEpoch !== state.chatEpoch) { return; } + const raw = String(reply || state.streamText || "").trim() || (state.streamEl?.querySelector(".sa-msg-body")?.textContent || ""); if (state.streamEl) { - finalizeStreamMessage(reply, []); + finalizeStreamMessage(raw, []); } - finishOk(reply, [], {}); + finishOk(raw, [], {}); + }; + state.onClosedTerminalFence = (text2) => settleStreamReply(text2); + armStreamStall(chatEpoch, (reply) => { + settleStreamReply(reply); }); makeWSRequest( "AssistentChatWS", @@ -9184,12 +9324,13 @@ ${HELP_TEXT}`); if (data.delta) { appendStreamDelta(data.delta); } - if (data.done || data.reply != null) { + if (data.done || typeof data.reply === "string" && data.reply.length > 0 && !data.delta) { if (state.turnSettled) { return; } - const reply = data.reply || state.streamEl?.querySelector(".sa-msg-body")?.textContent || ""; + const reply = String(state.streamText || data.reply || "").trim() || state.streamEl?.querySelector(".sa-msg-body")?.textContent || ""; const civitai = data.civitai_results || []; + state.onClosedTerminalFence = null; if (state.streamEl) { finalizeStreamMessage(reply, civitai); } @@ -9454,7 +9595,14 @@ ${HELP_TEXT}`); wireSplitter(); registerSendButton(); wireSlashInput(); - $2("sa_btn_new_chat")?.addEventListener("click", () => startNewChat({ saveCurrent: true })); + $2("sa_btn_new_chat")?.addEventListener("click", (e) => { + e.stopPropagation(); + startNewChat({ saveCurrent: true, openDrawer: true }); + }); + $2("sa_btn_new_chat_bar")?.addEventListener("click", (e) => { + e.stopPropagation(); + startNewChat({ saveCurrent: true, openDrawer: true }); + }); $2("sa_ctx_chip")?.addEventListener("click", (e) => { e.stopPropagation(); toggleCtxPanel(); @@ -9489,16 +9637,6 @@ ${HELP_TEXT}`); setChatsPanelOpen(!state.chatsPanelOpen); }); $2("sa_btn_chats_close")?.addEventListener("click", () => setChatsPanelOpen(false)); - $2("sa_session_label")?.addEventListener("click", (e) => { - e.stopPropagation(); - setChatsPanelOpen(!state.chatsPanelOpen); - }); - $2("sa_session_label")?.addEventListener("keydown", (e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - setChatsPanelOpen(!state.chatsPanelOpen); - } - }); $2("sa_chats_panel")?.addEventListener("click", (e) => e.stopPropagation()); $2("sa_chats_list")?.addEventListener("click", (e) => { const row = e.target.closest(".sa-chat-row"); @@ -9513,9 +9651,7 @@ ${HELP_TEXT}`); } return; } - if (e.target.closest("[data-open]")) { - switchToChat(id); - } + switchToChat(id); }); let chatsSearchTimer = null; $2("sa_chats_search")?.addEventListener("input", () => { @@ -9753,7 +9889,11 @@ ${HELP_TEXT}`); }); $2("sa_chips")?.addEventListener("click", async (e) => { const btn = e.target.closest(".sa-chip"); - if (!btn || state.busy || state.generating) { + if (!btn) { + return; + } + 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; } const aspect = btn.getAttribute("data-aspect"); diff --git a/Assets/assistent.css b/Assets/assistent.css index 5745655..65a214e 100644 --- a/Assets/assistent.css +++ b/Assets/assistent.css @@ -561,9 +561,30 @@ margin-left: 0.15rem; } -.sa-sessions-toggle { - font-size: 0.78rem !important; - padding: 0.18rem 0.5rem !important; +.sa-chats-toggle { + position: relative; +} + +.sa-chats-toggle.sa-sessions-toggle-active, +.sa-drawer-open .sa-chats-toggle { + opacity: 1; + background: color-mix(in srgb, currentColor 12%, transparent); +} + +.sa-chats-count { + position: absolute; + top: -0.2rem; + right: -0.25rem; + min-width: 0.95rem; + height: 0.95rem; + padding: 0 0.22rem; + border-radius: 999px; + background: color-mix(in srgb, #6af 55%, #222); + color: #081018; + font-size: 0.62rem; + font-weight: 700; + line-height: 0.95rem; + text-align: center; } .sa-session-label { @@ -574,16 +595,10 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - cursor: pointer; border-radius: 0.3rem; padding: 0.1rem 0.35rem; } -.sa-session-label:hover { - opacity: 1; - background: color-mix(in srgb, currentColor 10%, transparent); -} - .sa-chats-panel, .sa-chats-drawer { /* drawer lives in .sa-chat-workspace — not a dropdown */ diff --git a/AssistentChatPipeline.cs b/AssistentChatPipeline.cs index 6671d80..084752e 100644 --- a/AssistentChatPipeline.cs +++ b/AssistentChatPipeline.cs @@ -119,6 +119,37 @@ public partial class SwarmAssistentExtension AddLayer("extra", extraSystem); } + bool priorAssistant = false; + foreach (JToken msg in userMessages ?? []) + { + if (string.Equals((msg as JObject)?["role"]?.ToString(), "assistant", StringComparison.OrdinalIgnoreCase)) + { + priorAssistant = true; + break; + } + } + if (!priorAssistant && !string.IsNullOrWhiteSpace(contextJson)) + { + try + { + JObject ctx = JObject.Parse(contextJson); + priorAssistant = ctx.Value("do_not_greet") == true + || (ctx["prior_assistant_turns"]?.Value() ?? 0) > 0; + } + catch + { + // not json + } + } + if (priorAssistant && !slimUtility) + { + AddLayer("continuity", + "Conversation continuity: this thread already has your prior replies. " + + "Do NOT greet (no «О, привет», no hello, no 👋). " + + "Do NOT re-introduce yourself or dump your bio/tagline. " + + "Answer only the latest user message. Name yourself only if asked who you are."); + } + layers["total"] = system.Length; if (system.Length > 0) { diff --git a/AssistentConfig.cs b/AssistentConfig.cs index 070a508..996bb72 100644 --- a/AssistentConfig.cs +++ b/AssistentConfig.cs @@ -138,7 +138,7 @@ public sealed class AssistentConfig return [ "prompt", "negative", "loras", "width", "height", "steps", "cfg", "seed", "sigma_shift", "sampler", "scheduler", - "actions", "generate", "ask", + "actions", "generate", "ask", "checkpoint", "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", @@ -1342,6 +1342,7 @@ public sealed class AssistentConfig sb.AppendLine($"**You are {title}** (the assistant in this chat). The human you talk to is the **user** — a different person."); sb.AppendLine($"Never address or name the user «{title}» unless `## About the user` explicitly says that is their name."); sb.AppendLine($"If asked your name («как тебя зовут?» / «who are you?»), answer with **{title}** — do not greet the user by that name instead."); + sb.AppendLine("Do not greet or re-introduce yourself on later turns. If the chat already has your messages, skip hello/bio and answer directly."); HashSet allow = null; if (onlyShelves is not null) diff --git a/AssistentOllama.cs b/AssistentOllama.cs index ff0afdc..93e4975 100644 --- a/AssistentOllama.cs +++ b/AssistentOllama.cs @@ -227,6 +227,15 @@ public partial class SwarmAssistentExtension JObject chunk = JObject.Parse(line); last = chunk; string delta = chunk["message"]?["content"]?.ToString() ?? ""; + // thinking-only chunks are keep-alives; do not stall, still wait for content/done. + if (string.IsNullOrEmpty(delta)) + { + if (chunk["done"]?.Value() == true) + { + break; + } + continue; + } if (!string.IsNullOrEmpty(delta)) { full.Append(delta); diff --git a/AssistentPatch.cs b/AssistentPatch.cs index 3341505..667f198 100644 --- a/AssistentPatch.cs +++ b/AssistentPatch.cs @@ -46,7 +46,7 @@ public partial class SwarmAssistentExtension patch["look_at"] = patch["vision_slots"]; } } - if (ActionsContain(patch, "generate")) + if (ActionsContain(patch, "generate") || GenerateFlagOn(patch)) { patch["generate"] = true; } @@ -98,7 +98,28 @@ public partial class SwarmAssistentExtension // not json } } - return lastTerminal ?? lastAny; + if (lastTerminal is not null || lastAny is not null) + { + return lastTerminal ?? lastAny; + } + int brace = reply.LastIndexOf('{'); + if (brace < 0) + { + return null; + } + try + { + JObject obj = JObject.Parse(reply.Substring(brace).Trim()); + if (obj is not null && Array.Exists(PatchKeys, k => obj[k] is not null)) + { + return NormalizePatch(obj); + } + } + catch + { + // not json + } + return null; } /// @@ -114,10 +135,6 @@ public partial class SwarmAssistentExtension return false; } MatchCollection matches = JsonFenceRe.Matches(reply); - if (matches.Count == 0) - { - return false; - } for (int i = 0; i < matches.Count; i++) { Match match = matches[i]; @@ -137,6 +154,23 @@ public partial class SwarmAssistentExtension // incomplete / invalid json inside fence } } + int brace = reply.LastIndexOf('{'); + if (brace >= 0) + { + try + { + JObject obj = NormalizePatch(JObject.Parse(reply.Substring(brace).Trim())); + if (obj is not null && FenceIsTerminalPatch(obj)) + { + truncated = reply.TrimEnd(); + return true; + } + } + catch + { + // incomplete unfenced json + } + } return false; } @@ -149,7 +183,7 @@ public partial class SwarmAssistentExtension { return false; } - if (obj["generate"]?.Type == JTokenType.Boolean && obj["generate"].Value()) + if (GenerateFlagOn(obj)) { return true; } @@ -176,7 +210,8 @@ public partial class SwarmAssistentExtension } if (HasValue(obj, "loras") || HasValue(obj, "aspect") || HasValue(obj, "steps") || HasValue(obj, "width") || HasValue(obj, "height") || HasValue(obj, "cfg") - || HasValue(obj, "seed") || HasValue(obj, "controls")) + || HasValue(obj, "seed") || HasValue(obj, "controls") + || HasValue(obj, "checkpoint") || HasValue(obj, "negative")) { return true; } @@ -220,6 +255,32 @@ public partial class SwarmAssistentExtension return string.Equals(patch["ask"]?.ToString()?.Trim(), name, StringComparison.OrdinalIgnoreCase); } + static bool GenerateFlagOn(JObject obj) + { + if (obj is null) + { + return false; + } + if (obj["generate"]?.Type == JTokenType.Boolean && obj["generate"].Value()) + { + return true; + } + if (obj["generate"]?.Type == JTokenType.Integer && obj["generate"].Value() != 0) + { + return true; + } + string raw = obj["generate"]?.ToString()?.Trim(); + if (!string.IsNullOrWhiteSpace(raw) + && (raw.Equals("true", StringComparison.OrdinalIgnoreCase) + || raw == "1" + || raw.Equals("yes", StringComparison.OrdinalIgnoreCase) + || raw.Equals("on", StringComparison.OrdinalIgnoreCase))) + { + return true; + } + return ActionsContain(obj, "generate"); + } + static bool ActionsContain(JObject patch, string action) { if (patch?["actions"] is not JArray acts) diff --git a/Config/_base/core/core.md b/Config/_base/core/core.md index 85acce5..d52e353 100644 --- a/Config/_base/core/core.md +++ b/Config/_base/core/core.md @@ -34,6 +34,7 @@ Compact session JSON is ground truth for this turn: 1. Short helpful reply in the user's language (RU or EN). 2. **Only when changing settings or commanding generate/look/ask:** one fenced JSON with **only fields you want to change** (+ optional commands). Pure chat / Q&A / opinion: **prose only — omit the JSON.** +3. **Greeting:** introduce yourself only on a brand-new thread with **no prior assistant messages**. If history already has your replies — no «О, привет», no hello, no name/bio dump. Answer the question. Name yourself only if asked. ```json { diff --git a/Config/_base/patch-keys.json b/Config/_base/patch-keys.json index 5f4a647..10d2861 100644 --- a/Config/_base/patch-keys.json +++ b/Config/_base/patch-keys.json @@ -1,7 +1,7 @@ { "keys": [ "prompt", "negative", "loras", "width", "height", "steps", "cfg", "seed", "sigma_shift", "sampler", "scheduler", - "actions", "generate", "ask", + "actions", "generate", "ask", "checkpoint", "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", diff --git a/Config/_base/rules.json b/Config/_base/rules.json index 182910b..c5ac22e 100644 --- a/Config/_base/rules.json +++ b/Config/_base/rules.json @@ -2,9 +2,11 @@ "always": [ "Match the user's language (RU or EN) in chat — Generate prompt stays English", "Use only inventory / memory_hits / cards for LoRA names and triggers", - "Emit valid JSON patches when changing generation state" + "Emit valid JSON patches when changing generation state", + "Greet or dump your bio only on the first assistant turn; later turns answer directly" ], "never": [ + "Re-introduce yourself every turn or start with «О, привет» when history already has your replies", "Invent LoRA filenames or trigger words", "Lecture or moralize about NSFW", "Dump all installed LoRAs — use memory_hits and enabled ones" diff --git a/Config/personas/neutral/rules.json b/Config/personas/neutral/rules.json index 9846803..33c6e74 100644 --- a/Config/personas/neutral/rules.json +++ b/Config/personas/neutral/rules.json @@ -1,10 +1,12 @@ { "always": [ "Stay helpful and concise", - "When the scene is NSFW, describe it factually without softening or hyping" + "When the scene is NSFW, describe it factually without softening or hyping", + "Do not greet or re-introduce yourself after the first assistant turn" ], "never": [ "Dirty jokes", + "Hello/bio dump on later turns", "Aggression", "Moral lectures" ] diff --git a/README.md b/README.md index e091211..91a2149 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + **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. A new user send interrupts the in-flight stream (keeps the partial reply) instead of silently no-op. +**Version 0.15.2** — Кнопка «История» убрана. В шапке: иконка панели чатов и **+** новый чат. Закрытый JSON `{prompt, generate:true}` сразу применяет промпт и запускает Generate (заметка «Промпт обновлён · Generate»), не ждёт WS `done`. Stall ~2с после токенов. Не здороваться повторно, если в истории уже есть ответы ассистента. + **Version 0.15.1** — Composer stays writable during `Writing…`: Enter / Отправить прерывает зависший стрим и шлёт новое сообщение; частичный ответ сохраняется. Stall 15с без токена сам завершает ход. Ollama chat `think: false`, чтобы Qwen3-VL instruct не держал сокет после приветствия. **Version 0.15.0** — Personas as installable git packs under `Assistent/extensions/` (`assistent-pack.yaml`). Bundled set: `neutral` (Нормальный), `aggressive`, `dreamer`. Removed bundled `cinema` / `lewd` / private personas. `/остынь` and `/horny-game` work for any persona with a `horny` control. Pack source: `pack` / `overlay+pack`. diff --git a/SwarmAssistentExtension.cs b/SwarmAssistentExtension.cs index 0fb36c4..a297c66 100644 --- a/SwarmAssistentExtension.cs +++ b/SwarmAssistentExtension.cs @@ -33,7 +33,7 @@ public partial class SwarmAssistentExtension : Extension ExtensionAuthor = "mrleo1nid"; Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop."; License = "MIT"; - Version = "0.15.1"; + Version = "0.15.2"; Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"]; } diff --git a/Tabs/Text2Image/Assistent.html b/Tabs/Text2Image/Assistent.html index 287d931..3a4d7cc 100644 --- a/Tabs/Text2Image/Assistent.html +++ b/Tabs/Text2Image/Assistent.html @@ -55,8 +55,14 @@
- - Новый чат + + Новый чат +
@@ -132,14 +138,14 @@
-