Ship Assistent 0.15.1: keep composer writable during hung Writing streams.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+101
-6
@@ -1316,6 +1316,10 @@
|
|||||||
busyStarted: 0,
|
busyStarted: 0,
|
||||||
gotDelta: false,
|
gotDelta: false,
|
||||||
busyTimer: null,
|
busyTimer: null,
|
||||||
|
lastDeltaAt: 0,
|
||||||
|
streamStallTimer: null,
|
||||||
|
turnSettled: false,
|
||||||
|
lastBusyPhaseShown: "",
|
||||||
slots: [],
|
slots: [],
|
||||||
selectedSlotId: "ref1",
|
selectedSlotId: "ref1",
|
||||||
refSeq: 1,
|
refSeq: 1,
|
||||||
@@ -1511,6 +1515,11 @@
|
|||||||
refining: "prep",
|
refining: "prep",
|
||||||
compressing: "compress"
|
compressing: "compress"
|
||||||
};
|
};
|
||||||
|
if (state.lastBusyPhaseShown !== state.busyPhase) {
|
||||||
|
state.lastBusyPhaseShown = state.busyPhase;
|
||||||
|
if (state.busyPhase === "streaming" || state.busyPhase === "refining") {
|
||||||
|
activityDone("think");
|
||||||
|
}
|
||||||
activityStep(`phase:${state.busyPhase}`, {
|
activityStep(`phase:${state.busyPhase}`, {
|
||||||
kind: phaseKind[state.busyPhase] || "think",
|
kind: phaseKind[state.busyPhase] || "think",
|
||||||
label: text,
|
label: text,
|
||||||
@@ -1524,6 +1533,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const barText = $2("sa_livebar_text");
|
const barText = $2("sa_livebar_text");
|
||||||
if (barText) {
|
if (barText) {
|
||||||
barText.textContent = text;
|
barText.textContent = text;
|
||||||
@@ -1544,12 +1554,15 @@
|
|||||||
state.busyPhase = phase || "thinking";
|
state.busyPhase = phase || "thinking";
|
||||||
$2("swarm_assistent_root")?.classList.add("sa-is-busy");
|
$2("swarm_assistent_root")?.classList.add("sa-is-busy");
|
||||||
$2("sa_composer")?.classList.add("sa-composer-busy");
|
$2("sa_composer")?.classList.add("sa-composer-busy");
|
||||||
|
state.lastBusyPhaseShown = "";
|
||||||
const send = $2("sa_btn_send");
|
const send = $2("sa_btn_send");
|
||||||
if (send) {
|
if (send) {
|
||||||
send.disabled = true;
|
send.disabled = false;
|
||||||
}
|
}
|
||||||
const input = $2("sa_input");
|
const input = $2("sa_input");
|
||||||
if (input) {
|
if (input) {
|
||||||
|
input.readOnly = false;
|
||||||
|
input.disabled = false;
|
||||||
input.classList.add("sa-input-busy");
|
input.classList.add("sa-input-busy");
|
||||||
}
|
}
|
||||||
const bar = $2("sa_livebar");
|
const bar = $2("sa_livebar");
|
||||||
@@ -1573,8 +1586,10 @@
|
|||||||
clearInterval(state.busyTimer);
|
clearInterval(state.busyTimer);
|
||||||
state.busyTimer = null;
|
state.busyTimer = null;
|
||||||
}
|
}
|
||||||
|
clearStreamStall();
|
||||||
const elapsed = Date.now() - (state.busyStarted || Date.now());
|
const elapsed = Date.now() - (state.busyStarted || Date.now());
|
||||||
state.busyPhase = "idle";
|
state.busyPhase = "idle";
|
||||||
|
state.lastBusyPhaseShown = "";
|
||||||
activityFinish(finalStatus || "\u0413\u043E\u0442\u043E\u0432\u043E");
|
activityFinish(finalStatus || "\u0413\u043E\u0442\u043E\u0432\u043E");
|
||||||
$2("swarm_assistent_root")?.classList.remove("sa-is-busy");
|
$2("swarm_assistent_root")?.classList.remove("sa-is-busy");
|
||||||
$2("sa_composer")?.classList.remove("sa-composer-busy");
|
$2("sa_composer")?.classList.remove("sa-composer-busy");
|
||||||
@@ -5545,6 +5560,53 @@ ${patch.prompt}`;
|
|||||||
state.chatEpoch = (state.chatEpoch || 0) + 1;
|
state.chatEpoch = (state.chatEpoch || 0) + 1;
|
||||||
return state.chatEpoch;
|
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 } = {}) {
|
function clearInFlightUi({ status } = {}) {
|
||||||
state.busy = false;
|
state.busy = false;
|
||||||
state.generating = false;
|
state.generating = false;
|
||||||
@@ -5567,6 +5629,7 @@ ${patch.prompt}`;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function abortInFlightWork({ status, interruptSwarm = false } = {}) {
|
function abortInFlightWork({ status, interruptSwarm = false } = {}) {
|
||||||
|
settlePartialStream();
|
||||||
bumpChatEpoch();
|
bumpChatEpoch();
|
||||||
cancelWaitForNewImage();
|
cancelWaitForNewImage();
|
||||||
if (interruptSwarm) {
|
if (interruptSwarm) {
|
||||||
@@ -5584,6 +5647,7 @@ ${patch.prompt}`;
|
|||||||
clearInFlightUi({ status: status != null ? status : "" });
|
clearInFlightUi({ status: status != null ? status : "" });
|
||||||
}
|
}
|
||||||
function doInterruptNow() {
|
function doInterruptNow() {
|
||||||
|
settlePartialStream();
|
||||||
bumpChatEpoch();
|
bumpChatEpoch();
|
||||||
cancelWaitForNewImage();
|
cancelWaitForNewImage();
|
||||||
try {
|
try {
|
||||||
@@ -6145,6 +6209,7 @@ ${patch.prompt}`;
|
|||||||
setAssistantBody(state.streamEl, "", { live: true });
|
setAssistantBody(state.streamEl, "", { live: true });
|
||||||
}
|
}
|
||||||
state.gotDelta = true;
|
state.gotDelta = true;
|
||||||
|
state.lastDeltaAt = Date.now();
|
||||||
state.expectColdLoad = false;
|
state.expectColdLoad = false;
|
||||||
if (state.busyPhase !== "refining") {
|
if (state.busyPhase !== "refining") {
|
||||||
setBusyPhase("streaming");
|
setBusyPhase("streaming");
|
||||||
@@ -6166,6 +6231,9 @@ ${patch.prompt}`;
|
|||||||
state.streamText = "";
|
state.streamText = "";
|
||||||
state.streamFenceDone = false;
|
state.streamFenceDone = false;
|
||||||
if (!el) {
|
if (!el) {
|
||||||
|
if (state.turnSettled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
appendMessage("assistant", fullReply, null, civitaiResults, meta || void 0);
|
appendMessage("assistant", fullReply, null, civitaiResults, meta || void 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -8790,9 +8858,6 @@ ${HELP_TEXT}`);
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async function sendChat(opts = {}) {
|
async function sendChat(opts = {}) {
|
||||||
if ((state.busy || state.generating) && !isContinuationTurn(opts)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isTrainingLocked() && !isContinuationTurn(opts)) {
|
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");
|
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;
|
return;
|
||||||
@@ -8802,6 +8867,13 @@ ${HELP_TEXT}`);
|
|||||||
if (!text) {
|
if (!text) {
|
||||||
return;
|
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)) {
|
if (!isMachineTurn(opts)) {
|
||||||
state.lastUserParamIntent = userTextMentionsParams(text);
|
state.lastUserParamIntent = userTextMentionsParams(text);
|
||||||
state.lastUserControlIntent = userTextMentionsControls(text);
|
state.lastUserControlIntent = userTextMentionsControls(text);
|
||||||
@@ -8863,6 +8935,7 @@ ${HELP_TEXT}`);
|
|||||||
}
|
}
|
||||||
const chatEpoch = bumpChatEpoch();
|
const chatEpoch = bumpChatEpoch();
|
||||||
state.busy = true;
|
state.busy = true;
|
||||||
|
state.turnSettled = false;
|
||||||
state.llmParked = false;
|
state.llmParked = false;
|
||||||
setInterruptVisible(true);
|
setInterruptVisible(true);
|
||||||
if (!isContinuationTurn(opts) && !opts.fromAskHop) {
|
if (!isContinuationTurn(opts) && !opts.fromAskHop) {
|
||||||
@@ -8997,6 +9070,11 @@ ${HELP_TEXT}`);
|
|||||||
if (chatEpoch !== state.chatEpoch) {
|
if (chatEpoch !== state.chatEpoch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (state.turnSettled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.turnSettled = true;
|
||||||
|
clearStreamStall();
|
||||||
if (meta.system_chars != null) {
|
if (meta.system_chars != null) {
|
||||||
state.lastSystemChars = Number(meta.system_chars) || 0;
|
state.lastSystemChars = Number(meta.system_chars) || 0;
|
||||||
}
|
}
|
||||||
@@ -9046,6 +9124,11 @@ ${HELP_TEXT}`);
|
|||||||
if (chatEpoch !== state.chatEpoch) {
|
if (chatEpoch !== state.chatEpoch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (state.turnSettled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.turnSettled = true;
|
||||||
|
clearStreamStall();
|
||||||
state.busy = false;
|
state.busy = false;
|
||||||
setInterruptVisible(state.generating);
|
setInterruptVisible(state.generating);
|
||||||
stopBusyUi(msg);
|
stopBusyUi(msg);
|
||||||
@@ -9061,6 +9144,15 @@ ${HELP_TEXT}`);
|
|||||||
};
|
};
|
||||||
if (typeof makeWSRequest === "function") {
|
if (typeof makeWSRequest === "function") {
|
||||||
beginStreamMessage(msgMeta);
|
beginStreamMessage(msgMeta);
|
||||||
|
armStreamStall(chatEpoch, (reply) => {
|
||||||
|
if (state.turnSettled || chatEpoch !== state.chatEpoch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.streamEl) {
|
||||||
|
finalizeStreamMessage(reply, []);
|
||||||
|
}
|
||||||
|
finishOk(reply, [], {});
|
||||||
|
});
|
||||||
makeWSRequest(
|
makeWSRequest(
|
||||||
"AssistentChatWS",
|
"AssistentChatWS",
|
||||||
payload,
|
payload,
|
||||||
@@ -9091,12 +9183,16 @@ ${HELP_TEXT}`);
|
|||||||
}
|
}
|
||||||
if (data.delta) {
|
if (data.delta) {
|
||||||
appendStreamDelta(data.delta);
|
appendStreamDelta(data.delta);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (data.done || data.reply != null) {
|
if (data.done || data.reply != null) {
|
||||||
|
if (state.turnSettled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const reply = data.reply || state.streamEl?.querySelector(".sa-msg-body")?.textContent || "";
|
const reply = data.reply || state.streamEl?.querySelector(".sa-msg-body")?.textContent || "";
|
||||||
const civitai = data.civitai_results || [];
|
const civitai = data.civitai_results || [];
|
||||||
|
if (state.streamEl) {
|
||||||
finalizeStreamMessage(reply, civitai);
|
finalizeStreamMessage(reply, civitai);
|
||||||
|
}
|
||||||
finishOk(reply, civitai, {
|
finishOk(reply, civitai, {
|
||||||
system_chars: data.system_chars,
|
system_chars: data.system_chars,
|
||||||
system_layers: data.system_layers,
|
system_layers: data.system_layers,
|
||||||
@@ -9607,7 +9703,6 @@ ${HELP_TEXT}`);
|
|||||||
$2("sa_btn_build_gen")?.addEventListener("click", () => buildCurrentAndGenerate());
|
$2("sa_btn_build_gen")?.addEventListener("click", () => buildCurrentAndGenerate());
|
||||||
$2("sa_btn_interrupt")?.addEventListener("click", () => {
|
$2("sa_btn_interrupt")?.addEventListener("click", () => {
|
||||||
doInterruptNow();
|
doInterruptNow();
|
||||||
clearInFlightUi({ status: "\u041F\u0440\u0435\u0440\u0432\u0430\u043D\u043E" });
|
|
||||||
});
|
});
|
||||||
$2("sa_btn_clear")?.addEventListener("click", () => {
|
$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?")) {
|
if (window.confirm("\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0435\u0441\u044C \u0447\u0430\u0442 Assistent?")) {
|
||||||
|
|||||||
@@ -870,13 +870,15 @@
|
|||||||
.sa-dots i:nth-child(2) { animation-delay: 0.15s; }
|
.sa-dots i:nth-child(2) { animation-delay: 0.15s; }
|
||||||
.sa-dots i:nth-child(3) { animation-delay: 0.3s; }
|
.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;
|
opacity: 0.55;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sa-is-busy #sa_input,
|
||||||
.sa-input-busy {
|
.sa-input-busy {
|
||||||
opacity: 0.7;
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sa-composer-busy {
|
.sa-composer-busy {
|
||||||
|
|||||||
@@ -184,6 +184,9 @@ public partial class SwarmAssistentExtension
|
|||||||
["num_predict"] = numPredict,
|
["num_predict"] = numPredict,
|
||||||
},
|
},
|
||||||
["keep_alive"] = "15m",
|
["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 StringContent content = new(payload.ToString(Newtonsoft.Json.Formatting.None), Encoding.UTF8, "application/json");
|
||||||
using HttpRequestMessage req = new(HttpMethod.Post, $"{root}/api/chat") { Content = content };
|
using HttpRequestMessage req = new(HttpMethod.Post, $"{root}/api/chat") { Content = content };
|
||||||
|
|||||||
@@ -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.
|
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`.
|
**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`.
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public partial class SwarmAssistentExtension : Extension
|
|||||||
ExtensionAuthor = "mrleo1nid";
|
ExtensionAuthor = "mrleo1nid";
|
||||||
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
|
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
|
||||||
License = "MIT";
|
License = "MIT";
|
||||||
Version = "0.15.0";
|
Version = "0.15.1";
|
||||||
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"];
|
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@
|
|||||||
<div class="sa-chips" id="sa_chips" role="toolbar" aria-label="Быстрые параметры"></div>
|
<div class="sa-chips" id="sa_chips" role="toolbar" aria-label="Быстрые параметры"></div>
|
||||||
<div class="sa-lora-chips" id="sa_lora_chips" role="toolbar" aria-label="Активные LoRA"></div>
|
<div class="sa-lora-chips" id="sa_lora_chips" role="toolbar" aria-label="Активные LoRA"></div>
|
||||||
<div class="sa-slash-wrap">
|
<div class="sa-slash-wrap">
|
||||||
<textarea id="sa_input" rows="3" placeholder="Промпт, img2img, критика… Enter = отправить · /help = команды" aria-label="Сообщение Assistent"></textarea>
|
<textarea id="sa_input" rows="3" placeholder="Промпт, img2img, критика… Enter = отправить (прервёт Writing…) · /help = команды" aria-label="Сообщение Assistent"></textarea>
|
||||||
<div class="sa-slash-menu" id="sa_slash_menu" hidden role="listbox"></div>
|
<div class="sa-slash-menu" id="sa_slash_menu" hidden role="listbox"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sa-composer-actions">
|
<div class="sa-composer-actions">
|
||||||
|
|||||||
+109
-7
@@ -155,6 +155,10 @@
|
|||||||
busyStarted: 0,
|
busyStarted: 0,
|
||||||
gotDelta: false,
|
gotDelta: false,
|
||||||
busyTimer: null,
|
busyTimer: null,
|
||||||
|
lastDeltaAt: 0,
|
||||||
|
streamStallTimer: null,
|
||||||
|
turnSettled: false,
|
||||||
|
lastBusyPhaseShown: '',
|
||||||
slots: [],
|
slots: [],
|
||||||
selectedSlotId: 'ref1',
|
selectedSlotId: 'ref1',
|
||||||
refSeq: 1,
|
refSeq: 1,
|
||||||
@@ -379,12 +383,16 @@
|
|||||||
warming: 'warm', parking: 'park', encoding: 'look', generating: 'generate',
|
warming: 'warm', parking: 'park', encoding: 'look', generating: 'generate',
|
||||||
applying: 'merge', silent_gen: 'generate', refining: 'prep', compressing: 'compress',
|
applying: 'merge', silent_gen: 'generate', refining: 'prep', compressing: 'compress',
|
||||||
};
|
};
|
||||||
|
if (state.lastBusyPhaseShown !== state.busyPhase) {
|
||||||
|
state.lastBusyPhaseShown = state.busyPhase;
|
||||||
|
if (state.busyPhase === 'streaming' || state.busyPhase === 'refining') {
|
||||||
|
activityDone('think');
|
||||||
|
}
|
||||||
activityStep(`phase:${state.busyPhase}`, {
|
activityStep(`phase:${state.busyPhase}`, {
|
||||||
kind: phaseKind[state.busyPhase] || 'think',
|
kind: phaseKind[state.busyPhase] || 'think',
|
||||||
label: text,
|
label: text,
|
||||||
status: 'running',
|
status: 'running',
|
||||||
});
|
});
|
||||||
// Mark prior phase:* steps done when switching phase
|
|
||||||
const a = getActivity();
|
const a = getActivity();
|
||||||
if (a && Array.isArray(a.steps)) {
|
if (a && Array.isArray(a.steps)) {
|
||||||
for (const s of a.steps) {
|
for (const s of a.steps) {
|
||||||
@@ -393,6 +401,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const barText = $('sa_livebar_text');
|
const barText = $('sa_livebar_text');
|
||||||
if (barText) {
|
if (barText) {
|
||||||
barText.textContent = text;
|
barText.textContent = text;
|
||||||
@@ -414,12 +423,16 @@
|
|||||||
state.busyPhase = phase || 'thinking';
|
state.busyPhase = phase || 'thinking';
|
||||||
$('swarm_assistent_root')?.classList.add('sa-is-busy');
|
$('swarm_assistent_root')?.classList.add('sa-is-busy');
|
||||||
$('sa_composer')?.classList.add('sa-composer-busy');
|
$('sa_composer')?.classList.add('sa-composer-busy');
|
||||||
|
state.lastBusyPhaseShown = '';
|
||||||
|
// Keep composer typed/sendable: Enter or Отправить interrupts the hung stream.
|
||||||
const send = $('sa_btn_send');
|
const send = $('sa_btn_send');
|
||||||
if (send) {
|
if (send) {
|
||||||
send.disabled = true;
|
send.disabled = false;
|
||||||
}
|
}
|
||||||
const input = $('sa_input');
|
const input = $('sa_input');
|
||||||
if (input) {
|
if (input) {
|
||||||
|
input.readOnly = false;
|
||||||
|
input.disabled = false;
|
||||||
input.classList.add('sa-input-busy');
|
input.classList.add('sa-input-busy');
|
||||||
}
|
}
|
||||||
const bar = $('sa_livebar');
|
const bar = $('sa_livebar');
|
||||||
@@ -444,8 +457,10 @@
|
|||||||
clearInterval(state.busyTimer);
|
clearInterval(state.busyTimer);
|
||||||
state.busyTimer = null;
|
state.busyTimer = null;
|
||||||
}
|
}
|
||||||
|
clearStreamStall();
|
||||||
const elapsed = Date.now() - (state.busyStarted || Date.now());
|
const elapsed = Date.now() - (state.busyStarted || Date.now());
|
||||||
state.busyPhase = 'idle';
|
state.busyPhase = 'idle';
|
||||||
|
state.lastBusyPhaseShown = '';
|
||||||
activityFinish(finalStatus || 'Готово');
|
activityFinish(finalStatus || 'Готово');
|
||||||
$('swarm_assistent_root')?.classList.remove('sa-is-busy');
|
$('swarm_assistent_root')?.classList.remove('sa-is-busy');
|
||||||
$('sa_composer')?.classList.remove('sa-composer-busy');
|
$('sa_composer')?.classList.remove('sa-composer-busy');
|
||||||
@@ -4807,6 +4822,60 @@
|
|||||||
return state.chatEpoch;
|
return state.chatEpoch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearStreamStall() {
|
||||||
|
if (state.streamStallTimer) {
|
||||||
|
clearInterval(state.streamStallTimer);
|
||||||
|
state.streamStallTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** If Ollama never sends `done` after the last token, unlock the composer. */
|
||||||
|
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 ? 4000 : 15000;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keep a visible partial reply when Stop / new send cuts the stream. */
|
||||||
|
function settlePartialStream() {
|
||||||
|
const text = String(state.streamText || '').trim();
|
||||||
|
if (!text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const persona = $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral';
|
||||||
|
const pack = $('sa_pack')?.value || defaultPackId();
|
||||||
|
const prose = (typeof extractPatch === 'function' ? (extractPatch(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 } = {}) {
|
function clearInFlightUi({ status } = {}) {
|
||||||
state.busy = false;
|
state.busy = false;
|
||||||
state.generating = false;
|
state.generating = false;
|
||||||
@@ -4828,6 +4897,7 @@
|
|||||||
|
|
||||||
/** Invalidate in-flight Assistent WS/wait; optionally also interrupt Swarm Generate. */
|
/** Invalidate in-flight Assistent WS/wait; optionally also interrupt Swarm Generate. */
|
||||||
function abortInFlightWork({ status, interruptSwarm = false } = {}) {
|
function abortInFlightWork({ status, interruptSwarm = false } = {}) {
|
||||||
|
settlePartialStream();
|
||||||
bumpChatEpoch();
|
bumpChatEpoch();
|
||||||
cancelWaitForNewImage();
|
cancelWaitForNewImage();
|
||||||
if (interruptSwarm) {
|
if (interruptSwarm) {
|
||||||
@@ -4843,6 +4913,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function doInterruptNow() {
|
function doInterruptNow() {
|
||||||
|
settlePartialStream();
|
||||||
bumpChatEpoch();
|
bumpChatEpoch();
|
||||||
cancelWaitForNewImage();
|
cancelWaitForNewImage();
|
||||||
try {
|
try {
|
||||||
@@ -5460,6 +5531,7 @@ if (role === 'assistant' && !(meta && meta.historical)) {
|
|||||||
setAssistantBody(state.streamEl, '', { live: true });
|
setAssistantBody(state.streamEl, '', { live: true });
|
||||||
}
|
}
|
||||||
state.gotDelta = true;
|
state.gotDelta = true;
|
||||||
|
state.lastDeltaAt = Date.now();
|
||||||
state.expectColdLoad = false;
|
state.expectColdLoad = false;
|
||||||
if (state.busyPhase !== 'refining') {
|
if (state.busyPhase !== 'refining') {
|
||||||
setBusyPhase('streaming');
|
setBusyPhase('streaming');
|
||||||
@@ -5482,6 +5554,9 @@ if (role === 'assistant' && !(meta && meta.historical)) {
|
|||||||
state.streamText = '';
|
state.streamText = '';
|
||||||
state.streamFenceDone = false;
|
state.streamFenceDone = false;
|
||||||
if (!el) {
|
if (!el) {
|
||||||
|
if (state.turnSettled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
appendMessage('assistant', fullReply, null, civitaiResults, meta || undefined);
|
appendMessage('assistant', fullReply, null, civitaiResults, meta || undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -8226,9 +8301,6 @@ if (!(meta && meta.historical)) {
|
|||||||
async function sendChat(opts = {}) {
|
async function sendChat(opts = {}) {
|
||||||
// Continuations run inside the parent turn, which still holds `busy`
|
// Continuations run inside the parent turn, which still holds `busy`
|
||||||
// (finishOk only clears it after handleReplySideEffects returns).
|
// (finishOk only clears it after handleReplySideEffects returns).
|
||||||
if ((state.busy || state.generating) && !isContinuationTurn(opts)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isTrainingLocked() && !isContinuationTurn(opts)) {
|
if (isTrainingLocked() && !isContinuationTurn(opts)) {
|
||||||
setStatus('Идёт тренировка — чат заблокирован');
|
setStatus('Идёт тренировка — чат заблокирован');
|
||||||
return;
|
return;
|
||||||
@@ -8238,6 +8310,13 @@ if (!(meta && meta.historical)) {
|
|||||||
if (!text) {
|
if (!text) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (state.generating && !isContinuationTurn(opts)) {
|
||||||
|
setStatus('Идёт Generate — нажми Стоп, потом отправь');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.busy && !isContinuationTurn(opts)) {
|
||||||
|
abortInFlightWork({ status: 'Новое сообщение', interruptSwarm: false });
|
||||||
|
}
|
||||||
if (!isMachineTurn(opts)) {
|
if (!isMachineTurn(opts)) {
|
||||||
state.lastUserParamIntent = userTextMentionsParams(text);
|
state.lastUserParamIntent = userTextMentionsParams(text);
|
||||||
state.lastUserControlIntent = userTextMentionsControls(text);
|
state.lastUserControlIntent = userTextMentionsControls(text);
|
||||||
@@ -8306,6 +8385,7 @@ if (!(meta && meta.historical)) {
|
|||||||
|
|
||||||
const chatEpoch = bumpChatEpoch();
|
const chatEpoch = bumpChatEpoch();
|
||||||
state.busy = true;
|
state.busy = true;
|
||||||
|
state.turnSettled = false;
|
||||||
// If Krea just ran, expectColdLoad stays true until warm / first token — don't clear it here.
|
// If Krea just ran, expectColdLoad stays true until warm / first token — don't clear it here.
|
||||||
state.llmParked = false;
|
state.llmParked = false;
|
||||||
setInterruptVisible(true);
|
setInterruptVisible(true);
|
||||||
@@ -8460,6 +8540,11 @@ if (!(meta && meta.historical)) {
|
|||||||
if (chatEpoch !== state.chatEpoch) {
|
if (chatEpoch !== state.chatEpoch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (state.turnSettled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.turnSettled = true;
|
||||||
|
clearStreamStall();
|
||||||
if (meta.system_chars != null) {
|
if (meta.system_chars != null) {
|
||||||
state.lastSystemChars = Number(meta.system_chars) || 0;
|
state.lastSystemChars = Number(meta.system_chars) || 0;
|
||||||
}
|
}
|
||||||
@@ -8512,6 +8597,11 @@ if (!(meta && meta.historical)) {
|
|||||||
if (chatEpoch !== state.chatEpoch) {
|
if (chatEpoch !== state.chatEpoch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (state.turnSettled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.turnSettled = true;
|
||||||
|
clearStreamStall();
|
||||||
state.busy = false;
|
state.busy = false;
|
||||||
setInterruptVisible(state.generating);
|
setInterruptVisible(state.generating);
|
||||||
stopBusyUi(msg);
|
stopBusyUi(msg);
|
||||||
@@ -8528,6 +8618,15 @@ if (!(meta && meta.historical)) {
|
|||||||
|
|
||||||
if (typeof makeWSRequest === 'function') {
|
if (typeof makeWSRequest === 'function') {
|
||||||
beginStreamMessage(msgMeta);
|
beginStreamMessage(msgMeta);
|
||||||
|
armStreamStall(chatEpoch, (reply) => {
|
||||||
|
if (state.turnSettled || chatEpoch !== state.chatEpoch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.streamEl) {
|
||||||
|
finalizeStreamMessage(reply, []);
|
||||||
|
}
|
||||||
|
finishOk(reply, [], {});
|
||||||
|
});
|
||||||
makeWSRequest(
|
makeWSRequest(
|
||||||
'AssistentChatWS',
|
'AssistentChatWS',
|
||||||
payload,
|
payload,
|
||||||
@@ -8561,12 +8660,16 @@ if (!(meta && meta.historical)) {
|
|||||||
}
|
}
|
||||||
if (data.delta) {
|
if (data.delta) {
|
||||||
appendStreamDelta(data.delta);
|
appendStreamDelta(data.delta);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (data.done || data.reply != null) {
|
if (data.done || data.reply != null) {
|
||||||
|
if (state.turnSettled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const reply = data.reply || (state.streamEl?.querySelector('.sa-msg-body')?.textContent) || '';
|
const reply = data.reply || (state.streamEl?.querySelector('.sa-msg-body')?.textContent) || '';
|
||||||
const civitai = data.civitai_results || [];
|
const civitai = data.civitai_results || [];
|
||||||
|
if (state.streamEl) {
|
||||||
finalizeStreamMessage(reply, civitai);
|
finalizeStreamMessage(reply, civitai);
|
||||||
|
}
|
||||||
finishOk(reply, civitai, {
|
finishOk(reply, civitai, {
|
||||||
system_chars: data.system_chars,
|
system_chars: data.system_chars,
|
||||||
system_layers: data.system_layers,
|
system_layers: data.system_layers,
|
||||||
@@ -9095,7 +9198,6 @@ if (!(meta && meta.historical)) {
|
|||||||
$('sa_btn_build_gen')?.addEventListener('click', () => buildCurrentAndGenerate());
|
$('sa_btn_build_gen')?.addEventListener('click', () => buildCurrentAndGenerate());
|
||||||
$('sa_btn_interrupt')?.addEventListener('click', () => {
|
$('sa_btn_interrupt')?.addEventListener('click', () => {
|
||||||
doInterruptNow();
|
doInterruptNow();
|
||||||
clearInFlightUi({ status: 'Прервано' });
|
|
||||||
});
|
});
|
||||||
$('sa_btn_clear')?.addEventListener('click', () => {
|
$('sa_btn_clear')?.addEventListener('click', () => {
|
||||||
if (window.confirm('Очистить весь чат Assistent?')) {
|
if (window.confirm('Очистить весь чат Assistent?')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user