diff --git a/Assets/assistent.bundle.js b/Assets/assistent.bundle.js index 5260ad0..72ae7e6 100644 --- a/Assets/assistent.bundle.js +++ b/Assets/assistent.bundle.js @@ -1316,6 +1316,10 @@ busyStarted: 0, gotDelta: false, busyTimer: null, + lastDeltaAt: 0, + streamStallTimer: null, + turnSettled: false, + lastBusyPhaseShown: "", slots: [], selectedSlotId: "ref1", refSeq: 1, @@ -1511,16 +1515,22 @@ refining: "prep", compressing: "compress" }; - activityStep(`phase:${state.busyPhase}`, { - kind: phaseKind[state.busyPhase] || "think", - label: text, - status: "running" - }); - const a = getActivity(); - if (a && Array.isArray(a.steps)) { - for (const s of a.steps) { - if (s.id.startsWith("phase:") && s.id !== `phase:${state.busyPhase}` && s.status === "running") { - a.done(s.id); + if (state.lastBusyPhaseShown !== state.busyPhase) { + state.lastBusyPhaseShown = state.busyPhase; + if (state.busyPhase === "streaming" || state.busyPhase === "refining") { + activityDone("think"); + } + activityStep(`phase:${state.busyPhase}`, { + kind: phaseKind[state.busyPhase] || "think", + label: text, + status: "running" + }); + const a = getActivity(); + if (a && Array.isArray(a.steps)) { + for (const s of a.steps) { + if (s.id.startsWith("phase:") && s.id !== `phase:${state.busyPhase}` && s.status === "running") { + a.done(s.id); + } } } } @@ -1544,12 +1554,15 @@ state.busyPhase = phase || "thinking"; $2("swarm_assistent_root")?.classList.add("sa-is-busy"); $2("sa_composer")?.classList.add("sa-composer-busy"); + state.lastBusyPhaseShown = ""; const send = $2("sa_btn_send"); if (send) { - send.disabled = true; + send.disabled = false; } const input = $2("sa_input"); if (input) { + input.readOnly = false; + input.disabled = false; input.classList.add("sa-input-busy"); } const bar = $2("sa_livebar"); @@ -1573,8 +1586,10 @@ clearInterval(state.busyTimer); state.busyTimer = null; } + clearStreamStall(); const elapsed = Date.now() - (state.busyStarted || Date.now()); state.busyPhase = "idle"; + state.lastBusyPhaseShown = ""; activityFinish(finalStatus || "\u0413\u043E\u0442\u043E\u0432\u043E"); $2("swarm_assistent_root")?.classList.remove("sa-is-busy"); $2("sa_composer")?.classList.remove("sa-composer-busy"); @@ -5545,6 +5560,53 @@ ${patch.prompt}`; state.chatEpoch = (state.chatEpoch || 0) + 1; return state.chatEpoch; } + function clearStreamStall() { + if (state.streamStallTimer) { + clearInterval(state.streamStallTimer); + state.streamStallTimer = null; + } + } + function armStreamStall(chatEpoch, onStall) { + clearStreamStall(); + state.lastDeltaAt = Date.now(); + state.streamStallTimer = setInterval(() => { + if (chatEpoch !== state.chatEpoch) { + clearStreamStall(); + return; + } + if (!state.gotDelta || !state.busy || state.generating || state.turnSettled) { + return; + } + const waitMs = state.streamFenceDone ? 4e3 : 15e3; + if (Date.now() - (state.lastDeltaAt || 0) < waitMs) { + return; + } + clearStreamStall(); + const reply = state.streamText || state.streamEl?.querySelector(".sa-msg-body")?.textContent || ""; + try { + onStall(String(reply || "")); + } catch (e) { + console.warn("Assistent stream stall", e); + } + }, 800); + } + function settlePartialStream() { + const text = String(state.streamText || "").trim(); + if (!text) { + return; + } + const persona = $2("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral"; + const pack = $2("sa_pack")?.value || defaultPackId(); + const prose = typeof extractPatch2 === "function" ? extractPatch2(text).prose || text : text; + if (state.streamEl) { + finalizeStreamMessage(text, []); + } else { + appendMessage("assistant", prose); + } + state.history.push({ role: "assistant", content: prose, persona, pack }); + persistHistory(); + state.turnSettled = true; + } function clearInFlightUi({ status } = {}) { state.busy = false; state.generating = false; @@ -5567,6 +5629,7 @@ ${patch.prompt}`; } } function abortInFlightWork({ status, interruptSwarm = false } = {}) { + settlePartialStream(); bumpChatEpoch(); cancelWaitForNewImage(); if (interruptSwarm) { @@ -5584,6 +5647,7 @@ ${patch.prompt}`; clearInFlightUi({ status: status != null ? status : "" }); } function doInterruptNow() { + settlePartialStream(); bumpChatEpoch(); cancelWaitForNewImage(); try { @@ -6145,6 +6209,7 @@ ${patch.prompt}`; setAssistantBody(state.streamEl, "", { live: true }); } state.gotDelta = true; + state.lastDeltaAt = Date.now(); state.expectColdLoad = false; if (state.busyPhase !== "refining") { setBusyPhase("streaming"); @@ -6166,6 +6231,9 @@ ${patch.prompt}`; state.streamText = ""; state.streamFenceDone = false; if (!el) { + if (state.turnSettled) { + return; + } appendMessage("assistant", fullReply, null, civitaiResults, meta || void 0); return; } @@ -8790,9 +8858,6 @@ ${HELP_TEXT}`); return true; } async function sendChat(opts = {}) { - if ((state.busy || state.generating) && !isContinuationTurn(opts)) { - return; - } if (isTrainingLocked() && !isContinuationTurn(opts)) { setStatus("\u0418\u0434\u0451\u0442 \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430 \u2014 \u0447\u0430\u0442 \u0437\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043D"); return; @@ -8802,6 +8867,13 @@ ${HELP_TEXT}`); if (!text) { return; } + if (state.generating && !isContinuationTurn(opts)) { + setStatus("\u0418\u0434\u0451\u0442 Generate \u2014 \u043D\u0430\u0436\u043C\u0438 \u0421\u0442\u043E\u043F, \u043F\u043E\u0442\u043E\u043C \u043E\u0442\u043F\u0440\u0430\u0432\u044C"); + return; + } + if (state.busy && !isContinuationTurn(opts)) { + abortInFlightWork({ status: "\u041D\u043E\u0432\u043E\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435", interruptSwarm: false }); + } if (!isMachineTurn(opts)) { state.lastUserParamIntent = userTextMentionsParams(text); state.lastUserControlIntent = userTextMentionsControls(text); @@ -8863,6 +8935,7 @@ ${HELP_TEXT}`); } const chatEpoch = bumpChatEpoch(); state.busy = true; + state.turnSettled = false; state.llmParked = false; setInterruptVisible(true); if (!isContinuationTurn(opts) && !opts.fromAskHop) { @@ -8997,6 +9070,11 @@ ${HELP_TEXT}`); if (chatEpoch !== state.chatEpoch) { return; } + if (state.turnSettled) { + return; + } + state.turnSettled = true; + clearStreamStall(); if (meta.system_chars != null) { state.lastSystemChars = Number(meta.system_chars) || 0; } @@ -9046,6 +9124,11 @@ ${HELP_TEXT}`); if (chatEpoch !== state.chatEpoch) { return; } + if (state.turnSettled) { + return; + } + state.turnSettled = true; + clearStreamStall(); state.busy = false; setInterruptVisible(state.generating); stopBusyUi(msg); @@ -9061,6 +9144,15 @@ ${HELP_TEXT}`); }; if (typeof makeWSRequest === "function") { beginStreamMessage(msgMeta); + armStreamStall(chatEpoch, (reply) => { + if (state.turnSettled || chatEpoch !== state.chatEpoch) { + return; + } + if (state.streamEl) { + finalizeStreamMessage(reply, []); + } + finishOk(reply, [], {}); + }); makeWSRequest( "AssistentChatWS", payload, @@ -9091,12 +9183,16 @@ ${HELP_TEXT}`); } if (data.delta) { appendStreamDelta(data.delta); - return; } if (data.done || data.reply != null) { + if (state.turnSettled) { + return; + } const reply = data.reply || state.streamEl?.querySelector(".sa-msg-body")?.textContent || ""; const civitai = data.civitai_results || []; - finalizeStreamMessage(reply, civitai); + if (state.streamEl) { + finalizeStreamMessage(reply, civitai); + } finishOk(reply, civitai, { system_chars: data.system_chars, system_layers: data.system_layers, @@ -9607,7 +9703,6 @@ ${HELP_TEXT}`); $2("sa_btn_build_gen")?.addEventListener("click", () => buildCurrentAndGenerate()); $2("sa_btn_interrupt")?.addEventListener("click", () => { doInterruptNow(); - clearInFlightUi({ status: "\u041F\u0440\u0435\u0440\u0432\u0430\u043D\u043E" }); }); $2("sa_btn_clear")?.addEventListener("click", () => { if (window.confirm("\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0435\u0441\u044C \u0447\u0430\u0442 Assistent?")) { diff --git a/Assets/assistent.css b/Assets/assistent.css index 8c6f635..5745655 100644 --- a/Assets/assistent.css +++ b/Assets/assistent.css @@ -870,13 +870,15 @@ .sa-dots i:nth-child(2) { animation-delay: 0.15s; } .sa-dots i:nth-child(3) { animation-delay: 0.3s; } -.sa-is-busy .sa-primary { +.sa-is-busy .sa-primary:not(#sa_btn_send) { opacity: 0.55; pointer-events: none; } +.sa-is-busy #sa_input, .sa-input-busy { - opacity: 0.7; + opacity: 1; + pointer-events: auto; } .sa-composer-busy { diff --git a/AssistentOllama.cs b/AssistentOllama.cs index 35f4f4c..ff0afdc 100644 --- a/AssistentOllama.cs +++ b/AssistentOllama.cs @@ -184,6 +184,9 @@ public partial class SwarmAssistentExtension ["num_predict"] = numPredict, }, ["keep_alive"] = "15m", + // Qwen3-VL instruct still thinks by default; that holds the WS open after + // the visible greeting and leaves the composer looking locked. + ["think"] = false, }; using StringContent content = new(payload.ToString(Newtonsoft.Json.Formatting.None), Encoding.UTF8, "application/json"); using HttpRequestMessage req = new(HttpMethod.Post, $"{root}/api/chat") { Content = content }; diff --git a/README.md b/README.md index 05eee16..e091211 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ 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. +**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.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 2f10396..0fb36c4 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.0"; + Version = "0.15.1"; Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"]; } diff --git a/Tabs/Text2Image/Assistent.html b/Tabs/Text2Image/Assistent.html index 9b8dff8..287d931 100644 --- a/Tabs/Text2Image/Assistent.html +++ b/Tabs/Text2Image/Assistent.html @@ -112,7 +112,7 @@