Ship Assistent 0.15.2: apply closed generate patches immediately, recover stalled streams, and skip repeat greetings when a chat already has replies.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+262
-122
@@ -43,6 +43,7 @@
|
|||||||
"actions",
|
"actions",
|
||||||
"generate",
|
"generate",
|
||||||
"ask",
|
"ask",
|
||||||
|
"checkpoint",
|
||||||
"use_init_image",
|
"use_init_image",
|
||||||
"clear_init_image",
|
"clear_init_image",
|
||||||
"init_creativity",
|
"init_creativity",
|
||||||
@@ -85,6 +86,20 @@
|
|||||||
function has(obj, key) {
|
function has(obj, key) {
|
||||||
return obj[key] !== void 0 && obj[key] !== null;
|
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) {
|
function isPatchObject2(obj) {
|
||||||
if (!obj || typeof obj !== "object") {
|
if (!obj || typeof obj !== "object") {
|
||||||
return false;
|
return false;
|
||||||
@@ -96,7 +111,7 @@
|
|||||||
return patch;
|
return patch;
|
||||||
}
|
}
|
||||||
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
|
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;
|
patch.generate = true;
|
||||||
}
|
}
|
||||||
if (typeof patch.ask === "string") {
|
if (typeof patch.ask === "string") {
|
||||||
@@ -114,31 +129,63 @@
|
|||||||
}
|
}
|
||||||
return patch;
|
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) {
|
function extractPatch(text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return { prose: text || "", patch: null };
|
return { prose: text || "", patch: null };
|
||||||
}
|
}
|
||||||
const re = new RegExp(FENCE_RE.source, "gi");
|
const re = new RegExp(FENCE_RE.source, "gi");
|
||||||
let match;
|
let match;
|
||||||
let lastPatch = null;
|
let lastAny = null;
|
||||||
let prose = text;
|
let lastAnyIndex = -1;
|
||||||
|
let lastAnyLen = 0;
|
||||||
|
let lastTerminal = null;
|
||||||
|
let lastTermIndex = -1;
|
||||||
|
let lastTermLen = 0;
|
||||||
while ((match = re.exec(text)) !== null) {
|
while ((match = re.exec(text)) !== null) {
|
||||||
try {
|
const parsed = tryParsePatchJson(match[1]);
|
||||||
const obj = JSON.parse(match[1].trim());
|
if (!parsed) {
|
||||||
if (isPatchObject2(obj)) {
|
continue;
|
||||||
lastPatch = normalizePatch(obj);
|
}
|
||||||
prose = (text.slice(0, match.index) + text.slice(match.index + match[0].length)).trim();
|
lastAny = parsed;
|
||||||
}
|
lastAnyIndex = match.index;
|
||||||
} catch (e) {
|
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) {
|
function isTerminalStreamPatch(obj) {
|
||||||
if (!obj || typeof obj !== "object") {
|
if (!obj || typeof obj !== "object") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (obj.generate === true) {
|
if (generateFlagOn(obj)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (Array.isArray(obj.ask) && obj.ask.length) {
|
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) {
|
if (obj.look_at != null || obj.vision_from != null || obj.vision_slots != null) {
|
||||||
return true;
|
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) {
|
if (String(obj.prompt || "").trim().length >= 48) {
|
||||||
return true;
|
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 true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -172,6 +215,7 @@
|
|||||||
SA2.isTerminalStreamPatch = isTerminalStreamPatch;
|
SA2.isTerminalStreamPatch = isTerminalStreamPatch;
|
||||||
SA2.normalizePatch = normalizePatch;
|
SA2.normalizePatch = normalizePatch;
|
||||||
SA2.extractPatch = extractPatch;
|
SA2.extractPatch = extractPatch;
|
||||||
|
SA2.generateFlagOn = generateFlagOn;
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/persist.js
|
// src/persist.js
|
||||||
@@ -375,7 +419,9 @@
|
|||||||
}
|
}
|
||||||
const delta = { ...raw };
|
const delta = { ...raw };
|
||||||
const acts = Array.isArray(delta.actions) ? delta.actions.map(String) : [];
|
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;
|
delta.generate = true;
|
||||||
}
|
}
|
||||||
if (typeof delta.ask === "string") {
|
if (typeof delta.ask === "string") {
|
||||||
@@ -392,7 +438,8 @@
|
|||||||
if (!patch || typeof patch !== "object") {
|
if (!patch || typeof patch !== "object") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (patch.generate === true) {
|
const n = normalizeDelta(patch);
|
||||||
|
if (n?.generate === true) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
|
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
|
||||||
@@ -631,10 +678,12 @@
|
|||||||
session_exact: extras.sessionExact || null
|
session_exact: extras.sessionExact || null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function resolveTurnIntent(patch, userText, { vetoFn } = {}) {
|
function resolveTurnIntent(patch, userText, { vetoFn, askGenerateFn, fromAutoCritique } = {}) {
|
||||||
const delta = normalizeDelta(patch) || {};
|
const delta = normalizeDelta(patch) || {};
|
||||||
const vetoed = typeof vetoFn === "function" ? !!vetoFn(userText) : false;
|
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 hasLook = delta.look_at != null || delta.vision_from != null || delta.vision_slots != null;
|
||||||
const look = !!(hasLook && !generate && !vetoed);
|
const look = !!(hasLook && !generate && !vetoed);
|
||||||
const ask = patchAskList(delta);
|
const ask = patchAskList(delta);
|
||||||
@@ -884,7 +933,7 @@
|
|||||||
scrollToBottom,
|
scrollToBottom,
|
||||||
hideEmpty
|
hideEmpty
|
||||||
} = opts;
|
} = opts;
|
||||||
let card2 = null;
|
let card = null;
|
||||||
let listEl = null;
|
let listEl = null;
|
||||||
let titleEl = null;
|
let titleEl = null;
|
||||||
let steps = [];
|
let steps = [];
|
||||||
@@ -894,16 +943,16 @@
|
|||||||
if (!box) {
|
if (!box) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (card2 && card2.isConnected) {
|
if (card && card.isConnected) {
|
||||||
return card2;
|
return card;
|
||||||
}
|
}
|
||||||
if (typeof hideEmpty === "function") {
|
if (typeof hideEmpty === "function") {
|
||||||
hideEmpty();
|
hideEmpty();
|
||||||
}
|
}
|
||||||
card2 = document.createElement("div");
|
card = document.createElement("div");
|
||||||
card2.className = "sa-activity sa-activity-live";
|
card.className = "sa-activity sa-activity-live";
|
||||||
card2.setAttribute("role", "status");
|
card.setAttribute("role", "status");
|
||||||
card2.setAttribute("aria-live", "polite");
|
card.setAttribute("aria-live", "polite");
|
||||||
const head = document.createElement("button");
|
const head = document.createElement("button");
|
||||||
head.type = "button";
|
head.type = "button";
|
||||||
head.className = "sa-activity-head";
|
head.className = "sa-activity-head";
|
||||||
@@ -923,18 +972,18 @@
|
|||||||
head.appendChild(chev);
|
head.appendChild(chev);
|
||||||
head.addEventListener("click", () => {
|
head.addEventListener("click", () => {
|
||||||
open = !open;
|
open = !open;
|
||||||
card2.classList.toggle("sa-activity-collapsed", !open);
|
card.classList.toggle("sa-activity-collapsed", !open);
|
||||||
head.setAttribute("aria-expanded", open ? "true" : "false");
|
head.setAttribute("aria-expanded", open ? "true" : "false");
|
||||||
});
|
});
|
||||||
listEl = document.createElement("div");
|
listEl = document.createElement("div");
|
||||||
listEl.className = "sa-activity-steps";
|
listEl.className = "sa-activity-steps";
|
||||||
card2.appendChild(head);
|
card.appendChild(head);
|
||||||
card2.appendChild(listEl);
|
card.appendChild(listEl);
|
||||||
box.appendChild(card2);
|
box.appendChild(card);
|
||||||
if (typeof scrollToBottom === "function") {
|
if (typeof scrollToBottom === "function") {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
return card2;
|
return card;
|
||||||
}
|
}
|
||||||
function renderStep(step) {
|
function renderStep(step) {
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
@@ -970,15 +1019,15 @@
|
|||||||
if (titleEl) {
|
if (titleEl) {
|
||||||
titleEl.textContent = running ? running.label : last?.label || "Assistent";
|
titleEl.textContent = running ? running.label : last?.label || "Assistent";
|
||||||
}
|
}
|
||||||
card2.classList.toggle("sa-activity-live", steps.some((s) => s.status === "running"));
|
card.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-done", steps.length > 0 && steps.every((s) => s.status === "done" || s.status === "skip"));
|
||||||
if (typeof scrollToBottom === "function") {
|
if (typeof scrollToBottom === "function") {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function begin(title) {
|
function begin(title) {
|
||||||
steps = [];
|
steps = [];
|
||||||
card2 = null;
|
card = null;
|
||||||
listEl = null;
|
listEl = null;
|
||||||
titleEl = null;
|
titleEl = null;
|
||||||
open = true;
|
open = true;
|
||||||
@@ -1021,9 +1070,9 @@
|
|||||||
titleEl.textContent = summary;
|
titleEl.textContent = summary;
|
||||||
}
|
}
|
||||||
paint();
|
paint();
|
||||||
if (card2) {
|
if (card) {
|
||||||
card2.classList.remove("sa-activity-live");
|
card.classList.remove("sa-activity-live");
|
||||||
card2.classList.add("sa-activity-done");
|
card.classList.add("sa-activity-done");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function noteModelCommands(patch) {
|
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.`;
|
\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):
|
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
|
/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)
|
/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 \u0441\u043F\u0438\u0441\u043E\u043A \u0447\u0430\u0442\u043E\u0432
|
/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
|
/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 \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
|
/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
|
/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.
|
\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 = [
|
let SLASH_COMMANDS = [
|
||||||
{ cmd: "/help", hint: "\u0441\u043F\u0438\u0441\u043E\u043A \u043A\u043E\u043C\u0430\u043D\u0434" },
|
{ 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: "/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: "/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: "/debug", hint: "\u0441\u0432\u043E\u0434\u043A\u0430 \xB7 ask = \u0441 LLM" },
|
||||||
{ cmd: "/why", hint: "debug + \u043F\u043E\u044F\u0441\u043D\u0435\u043D\u0438\u0435 LLM" },
|
{ cmd: "/why", hint: "debug + \u043F\u043E\u044F\u0441\u043D\u0435\u043D\u0438\u0435 LLM" },
|
||||||
@@ -2143,9 +2192,16 @@ ${patch.prompt}`;
|
|||||||
function mergePromptEnRewrite(effective) {
|
function mergePromptEnRewrite(effective) {
|
||||||
const base = state.pendingPromptEnMerge;
|
const base = state.pendingPromptEnMerge;
|
||||||
state.pendingPromptEnMerge = null;
|
state.pendingPromptEnMerge = null;
|
||||||
if (!base || !effective) {
|
if (!base) {
|
||||||
return effective;
|
return effective;
|
||||||
}
|
}
|
||||||
|
if (!effective) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
generate: true,
|
||||||
|
actions: Array.isArray(base.actions) && base.actions.length ? base.actions : ["generate"]
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
...effective,
|
...effective,
|
||||||
@@ -2184,6 +2240,21 @@ ${patch.prompt}`;
|
|||||||
}
|
}
|
||||||
return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t);
|
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) {
|
function packWantsVision(pack) {
|
||||||
const p = String(pack || "");
|
const p = String(pack || "");
|
||||||
return p === "critique_image" || p === "describe_ref" || p === "compose_scene" || p === "inpaint_edit";
|
return p === "critique_image" || p === "describe_ref" || p === "compose_scene" || p === "inpaint_edit";
|
||||||
@@ -2191,11 +2262,16 @@ ${patch.prompt}`;
|
|||||||
function resolveTurnIntent2(patch, userText, opts = {}) {
|
function resolveTurnIntent2(patch, userText, opts = {}) {
|
||||||
const S = window.SA && window.SA.session;
|
const S = window.SA && window.SA.session;
|
||||||
if (S && typeof S.resolveTurnIntent === "function") {
|
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 vetoed = !isMachineTurn(opts) && userAsksNoGenerate(userText);
|
||||||
const modelAsked = !!(patch && (patch.generate === true || Array.isArray(patch.actions) && patch.actions.map(String).includes("generate")));
|
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 generate = !vetoed && !opts.fromAutoCritique && modelAsked;
|
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 hasLook = !!patch && (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
|
||||||
const look = !!(hasLook && !vetoed && !generate);
|
const look = !!(hasLook && !vetoed && !generate);
|
||||||
const ask = Array.isArray(patch?.ask) ? patch.ask.map(String) : typeof patch?.ask === "string" && patch.ask ? [patch.ask] : [];
|
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.textContent = "\u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C";
|
||||||
genBtn.addEventListener("click", async () => {
|
genBtn.addEventListener("click", async () => {
|
||||||
if (isGenerateUnavailable()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
startBusyUi("silent_gen");
|
startBusyUi("silent_gen");
|
||||||
@@ -4161,7 +4238,7 @@ ${patch.prompt}`;
|
|||||||
const empty = document.createElement("div");
|
const empty = document.createElement("div");
|
||||||
empty.className = "sa-chat-empty";
|
empty.className = "sa-chat-empty";
|
||||||
empty.id = "sa_chat_empty";
|
empty.id = "sa_chat_empty";
|
||||||
empty.innerHTML = emptyHint || '<div class="sa-chat-empty-title">\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442</div><div class="sa-chat-empty-hint">\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.<br><strong>+</strong> \u2014 \u0435\u0449\u0451 \u043E\u0434\u0438\u043D \u0447\u0430\u0442 \xB7 <strong>\u0418\u0441\u0442\u043E\u0440\u0438\u044F</strong> \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).</div>';
|
empty.innerHTML = emptyHint || '<div class="sa-chat-empty-title">\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442</div><div class="sa-chat-empty-hint">\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.<br><strong>+</strong> \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.</div>';
|
||||||
box.appendChild(empty);
|
box.appendChild(empty);
|
||||||
}
|
}
|
||||||
function renderHistoryIntoUi(messages) {
|
function renderHistoryIntoUi(messages) {
|
||||||
@@ -4222,19 +4299,34 @@ ${patch.prompt}`;
|
|||||||
}
|
}
|
||||||
const chat = findChat(state.activeChatId);
|
const chat = findChat(state.activeChatId);
|
||||||
el.textContent = chat?.title || "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442";
|
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() {
|
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() {
|
function syncHistoryBadge() {
|
||||||
const btn = $2("sa_btn_chats");
|
const btn = $2("sa_btn_chats");
|
||||||
if (!btn) {
|
const countEl = $2("sa_chats_count");
|
||||||
return;
|
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) {
|
function formatChatWhen(ts) {
|
||||||
if (!ts) {
|
if (!ts) {
|
||||||
@@ -4304,7 +4396,7 @@ ${patch.prompt}`;
|
|||||||
root.innerHTML = "";
|
root.innerHTML = "";
|
||||||
syncHistoryBadge();
|
syncHistoryBadge();
|
||||||
const q = (state.chatsQuery || "").trim().toLowerCase();
|
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) {
|
if (q) {
|
||||||
const local = chats.filter((c) => chatMatchesQuery(c, q));
|
const local = chats.filter((c) => chatMatchesQuery(c, q));
|
||||||
const seen = new Set(local.map((c) => c.id));
|
const seen = new Set(local.map((c) => c.id));
|
||||||
@@ -4342,6 +4434,7 @@ ${patch.prompt}`;
|
|||||||
btn?.classList.toggle("sa-sessions-toggle-active", state.chatsPanelOpen);
|
btn?.classList.toggle("sa-sessions-toggle-active", state.chatsPanelOpen);
|
||||||
root?.classList.toggle("sa-drawer-open", state.chatsPanelOpen);
|
root?.classList.toggle("sa-drawer-open", state.chatsPanelOpen);
|
||||||
localStorage.setItem(LS_CHATS_DRAWER, state.chatsPanelOpen ? "1" : "0");
|
localStorage.setItem(LS_CHATS_DRAWER, state.chatsPanelOpen ? "1" : "0");
|
||||||
|
syncHistoryBadge();
|
||||||
if (state.chatsPanelOpen) {
|
if (state.chatsPanelOpen) {
|
||||||
saveActiveChatToStore();
|
saveActiveChatToStore();
|
||||||
const search = $2("sa_chats_search");
|
const search = $2("sa_chats_search");
|
||||||
@@ -4353,13 +4446,17 @@ ${patch.prompt}`;
|
|||||||
}
|
}
|
||||||
saveUiStateToDisk();
|
saveUiStateToDisk();
|
||||||
}
|
}
|
||||||
async function startNewChat({ saveCurrent = true, force = false } = {}) {
|
async function startNewChat({ saveCurrent = true, force = false, openDrawer = false } = {}) {
|
||||||
if (!force && (state.busy || state.generating)) {
|
if (!force && isBlankActiveChat()) {
|
||||||
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");
|
setStatus("\u0423\u0436\u0435 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442");
|
||||||
|
if (openDrawer) {
|
||||||
|
setChatsPanelOpen(true);
|
||||||
|
}
|
||||||
|
$2("sa_input")?.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (force) {
|
if (state.busy || state.generating) {
|
||||||
abortInFlightWork({ status: "" });
|
abortInFlightWork({ status: "", interruptSwarm: !!state.generating });
|
||||||
}
|
}
|
||||||
if (saveCurrent) {
|
if (saveCurrent) {
|
||||||
saveActiveChatToStore({ dropEmpty: true });
|
saveActiveChatToStore({ dropEmpty: true });
|
||||||
@@ -4401,6 +4498,12 @@ ${patch.prompt}`;
|
|||||||
updateCtxChip();
|
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");
|
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();
|
maybeWelcome();
|
||||||
|
if (openDrawer) {
|
||||||
|
setChatsPanelOpen(true);
|
||||||
|
}
|
||||||
|
if (openDrawer || !force) {
|
||||||
|
$2("sa_input")?.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function switchToChat(id) {
|
async function switchToChat(id) {
|
||||||
if (!id || id === state.activeChatId) {
|
if (!id || id === state.activeChatId) {
|
||||||
@@ -4540,7 +4643,7 @@ ${patch.prompt}`;
|
|||||||
syncBuildGenButton();
|
syncBuildGenButton();
|
||||||
clearPersistedHistory();
|
clearPersistedHistory();
|
||||||
resetContextMemory({ persist: false });
|
resetContextMemory({ persist: false });
|
||||||
resetMessagesUi('<div class="sa-chat-empty-title">\u0427\u0430\u0442 \u043E\u0447\u0438\u0449\u0435\u043D</div><div class="sa-chat-empty-hint">\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. <strong>+</strong> \u2014 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442 \u0432 \u0418\u0441\u0442\u043E\u0440\u0438\u044E, <strong>\u0418\u0441\u0442\u043E\u0440\u0438\u044F</strong> \u2014 \u043F\u0440\u043E\u0448\u043B\u044B\u0435 \u0434\u0438\u0430\u043B\u043E\u0433\u0438.</div>');
|
resetMessagesUi('<div class="sa-chat-empty-title">\u0427\u0430\u0442 \u043E\u0447\u0438\u0449\u0435\u043D</div><div class="sa-chat-empty-hint">\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. <strong>+</strong> \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.</div>');
|
||||||
setStatus("\u0427\u0430\u0442 \u043E\u0447\u0438\u0449\u0435\u043D");
|
setStatus("\u0427\u0430\u0442 \u043E\u0447\u0438\u0449\u0435\u043D");
|
||||||
updateSessionLabel();
|
updateSessionLabel();
|
||||||
syncHistoryBadge();
|
syncHistoryBadge();
|
||||||
@@ -5577,7 +5680,7 @@ ${patch.prompt}`;
|
|||||||
if (!state.gotDelta || !state.busy || state.generating || state.turnSettled) {
|
if (!state.gotDelta || !state.busy || state.generating || state.turnSettled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const waitMs = state.streamFenceDone ? 4e3 : 15e3;
|
const waitMs = state.streamFenceDone ? 800 : state.gotDelta ? 2200 : 15e3;
|
||||||
if (Date.now() - (state.lastDeltaAt || 0) < waitMs) {
|
if (Date.now() - (state.lastDeltaAt || 0) < waitMs) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -6159,23 +6262,30 @@ ${patch.prompt}`;
|
|||||||
state.streamFenceDone = false;
|
state.streamFenceDone = false;
|
||||||
return div;
|
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) {
|
function streamHasClosedPatchFence(text) {
|
||||||
const t = String(text || "");
|
const t = String(text || "");
|
||||||
if (!/```[\s\S]*```/.test(t)) {
|
if (/```[\s\S]*```/.test(t)) {
|
||||||
return false;
|
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||||||
}
|
let match;
|
||||||
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
|
while ((match = re.exec(t)) !== null) {
|
||||||
let match;
|
if (parseTerminalPatchObject(match[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) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const brace = t.lastIndexOf("{");
|
||||||
|
if (brace >= 0 && parseTerminalPatchObject(t.slice(brace))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
function trimToClosedPatchFence(text) {
|
function trimToClosedPatchFence(text) {
|
||||||
@@ -6184,16 +6294,18 @@ ${patch.prompt}`;
|
|||||||
let match;
|
let match;
|
||||||
let lastEnd = -1;
|
let lastEnd = -1;
|
||||||
while ((match = re.exec(t)) !== null) {
|
while ((match = re.exec(t)) !== null) {
|
||||||
try {
|
if (parseTerminalPatchObject(match[1])) {
|
||||||
const obj = JSON.parse(match[1].trim());
|
lastEnd = match.index + match[0].length;
|
||||||
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) {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) {
|
function appendStreamDelta(delta) {
|
||||||
if (state.streamFenceDone) {
|
if (state.streamFenceDone) {
|
||||||
@@ -6218,6 +6330,18 @@ ${patch.prompt}`;
|
|||||||
if (streamHasClosedPatchFence(state.streamText)) {
|
if (streamHasClosedPatchFence(state.streamText)) {
|
||||||
state.streamText = trimToClosedPatchFence(state.streamText);
|
state.streamText = trimToClosedPatchFence(state.streamText);
|
||||||
state.streamFenceDone = true;
|
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 });
|
setAssistantBody(state.streamEl, state.streamText, { live: true });
|
||||||
scrollMessagesToBottom();
|
scrollMessagesToBottom();
|
||||||
@@ -6242,16 +6366,9 @@ ${patch.prompt}`;
|
|||||||
const { prose, patch } = extractPatch2(fullReply);
|
const { prose, patch } = extractPatch2(fullReply);
|
||||||
setAssistantBody(el, prose || fullReply || "");
|
setAssistantBody(el, prose || fullReply || "");
|
||||||
el.querySelectorAll(".sa-patch, .sa-civitai-list").forEach((n) => n.remove());
|
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;
|
const silent = !!(meta && meta.silentPatch) || !!state.pendingSilentGen;
|
||||||
mountPatchBlock(el, patch, { silent });
|
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)) {
|
if (!(meta && meta.historical)) {
|
||||||
mountCurateButtons(el, meta);
|
mountCurateButtons(el, meta);
|
||||||
@@ -7387,8 +7504,6 @@ ${data.ui.help_extra}`.trim();
|
|||||||
const n = state.inventory.loras.length;
|
const n = state.inventory.loras.length;
|
||||||
const ck = state.inventory.checkpoints.length;
|
const ck = state.inventory.checkpoints.length;
|
||||||
setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts${rescan ? " (rescanned)" : ""}`);
|
setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts${rescan ? " (rescanned)" : ""}`);
|
||||||
if (state.view === "cards") {
|
|
||||||
}
|
|
||||||
if (done) {
|
if (done) {
|
||||||
done(state.inventory);
|
done(state.inventory);
|
||||||
}
|
}
|
||||||
@@ -8314,7 +8429,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
|
|||||||
}
|
}
|
||||||
const extracted = typeof extractPatch2 === "function" ? extractPatch2(reply) : { patch: null };
|
const extracted = typeof extractPatch2 === "function" ? extractPatch2(reply) : { patch: null };
|
||||||
let effective = extracted && extracted.patch ? extracted.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);
|
effective = mergePromptEnRewrite(effective);
|
||||||
}
|
}
|
||||||
const S = window.SA && window.SA.session;
|
const S = window.SA && window.SA.session;
|
||||||
@@ -8322,11 +8437,12 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
|
|||||||
if (effective && act && typeof act.noteModelCommands === "function") {
|
if (effective && act && typeof act.noteModelCommands === "function") {
|
||||||
act.noteModelCommands(effective);
|
act.noteModelCommands(effective);
|
||||||
}
|
}
|
||||||
|
const promptChanged = !!(effective && String(effective.prompt || "").trim());
|
||||||
if (effective && S) {
|
if (effective && S) {
|
||||||
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
||||||
activityDone("delta", {
|
activityDone("delta", {
|
||||||
kind: "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(", ")
|
detail: Object.keys(effective).filter((k) => effective[k] != null && !["actions", "notes"].includes(k)).slice(0, 10).join(", ")
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
@@ -8342,7 +8458,10 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
|
|||||||
if (Array.isArray(effective?.actions) && effective.actions.map(String).includes("interrupt")) {
|
if (Array.isArray(effective?.actions) && effective.actions.map(String).includes("interrupt")) {
|
||||||
if (typeof doInterruptNow === "function") doInterruptNow();
|
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 (effective) {
|
||||||
if (intent.generate) {
|
if (intent.generate) {
|
||||||
const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : [];
|
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);
|
if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
||||||
await pushSessionToSwarm(state.chatSession);
|
await pushSessionToSwarm(state.chatSession);
|
||||||
if (typeof syncLiveParamsBar === "function") syncLiveParamsBar();
|
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(
|
const srcOut = await runGenerateFromPatch(
|
||||||
{ ...effective || {}, actions: ["generate"], generate: true },
|
{ ...effective || {}, actions: ["generate"], generate: true },
|
||||||
{ force: true, fromSession: true }
|
{ force: true, fromSession: true }
|
||||||
@@ -8459,12 +8582,20 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
|
|||||||
if (typeof maybeAutoCritique === "function") await maybeAutoCritique(srcOut);
|
if (typeof maybeAutoCritique === "function") await maybeAutoCritique(srcOut);
|
||||||
if (typeof maybeAutoVisionLook === "function") await maybeAutoVisionLook(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);
|
if (S) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
||||||
await pushSessionToSwarm(state.chatSession);
|
if (promptChanged || $2("sa_auto_apply")?.checked) {
|
||||||
if (typeof syncLiveParamsBar === "function") syncLiveParamsBar();
|
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") {
|
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;
|
state.pendingSilentGen = false;
|
||||||
@@ -8628,12 +8759,12 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (cmd === "new" || cmd === "newchat") {
|
if (cmd === "new" || cmd === "newchat") {
|
||||||
await startNewChat({ saveCurrent: true });
|
await startNewChat({ saveCurrent: true, openDrawer: true });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (cmd === "history" || cmd === "chats" || cmd === "sessions") {
|
if (cmd === "history" || cmd === "chats" || cmd === "sessions") {
|
||||||
setChatsPanelOpen(true);
|
setChatsPanelOpen(!state.chatsPanelOpen);
|
||||||
setStatus("/history");
|
setStatus(state.chatsPanelOpen ? "/history" : "\u0427\u0430\u0442\u044B \u0441\u043A\u0440\u044B\u0442\u044B");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (cmd === "compress" || cmd === "compact" || cmd === "\u0441\u0436\u0430\u0442\u044C") {
|
if (cmd === "compress" || cmd === "compact" || cmd === "\u0441\u0436\u0430\u0442\u044C") {
|
||||||
@@ -9036,6 +9167,8 @@ ${HELP_TEXT}`);
|
|||||||
startBusyUi(state.expectColdLoad ? "loading" : "thinking");
|
startBusyUi(state.expectColdLoad ? "loading" : "thinking");
|
||||||
}
|
}
|
||||||
const context = collectLiveContext();
|
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.has_vision_image = visionReadySlots().length > 0;
|
||||||
context.images_in_request = !!(images && images.length);
|
context.images_in_request = !!(images && images.length);
|
||||||
context.attached_slot_ids = attachableSlots().map((s) => s.id);
|
context.attached_slot_ids = attachableSlots().map((s) => s.id);
|
||||||
@@ -9074,6 +9207,7 @@ ${HELP_TEXT}`);
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.turnSettled = true;
|
state.turnSettled = true;
|
||||||
|
state.onClosedTerminalFence = null;
|
||||||
clearStreamStall();
|
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;
|
||||||
@@ -9128,6 +9262,7 @@ ${HELP_TEXT}`);
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.turnSettled = true;
|
state.turnSettled = true;
|
||||||
|
state.onClosedTerminalFence = null;
|
||||||
clearStreamStall();
|
clearStreamStall();
|
||||||
state.busy = false;
|
state.busy = false;
|
||||||
setInterruptVisible(state.generating);
|
setInterruptVisible(state.generating);
|
||||||
@@ -9144,14 +9279,19 @@ ${HELP_TEXT}`);
|
|||||||
};
|
};
|
||||||
if (typeof makeWSRequest === "function") {
|
if (typeof makeWSRequest === "function") {
|
||||||
beginStreamMessage(msgMeta);
|
beginStreamMessage(msgMeta);
|
||||||
armStreamStall(chatEpoch, (reply) => {
|
const settleStreamReply = (reply) => {
|
||||||
if (state.turnSettled || chatEpoch !== state.chatEpoch) {
|
if (state.turnSettled || chatEpoch !== state.chatEpoch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const raw = String(reply || state.streamText || "").trim() || (state.streamEl?.querySelector(".sa-msg-body")?.textContent || "");
|
||||||
if (state.streamEl) {
|
if (state.streamEl) {
|
||||||
finalizeStreamMessage(reply, []);
|
finalizeStreamMessage(raw, []);
|
||||||
}
|
}
|
||||||
finishOk(reply, [], {});
|
finishOk(raw, [], {});
|
||||||
|
};
|
||||||
|
state.onClosedTerminalFence = (text2) => settleStreamReply(text2);
|
||||||
|
armStreamStall(chatEpoch, (reply) => {
|
||||||
|
settleStreamReply(reply);
|
||||||
});
|
});
|
||||||
makeWSRequest(
|
makeWSRequest(
|
||||||
"AssistentChatWS",
|
"AssistentChatWS",
|
||||||
@@ -9184,12 +9324,13 @@ ${HELP_TEXT}`);
|
|||||||
if (data.delta) {
|
if (data.delta) {
|
||||||
appendStreamDelta(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) {
|
if (state.turnSettled) {
|
||||||
return;
|
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 || [];
|
const civitai = data.civitai_results || [];
|
||||||
|
state.onClosedTerminalFence = null;
|
||||||
if (state.streamEl) {
|
if (state.streamEl) {
|
||||||
finalizeStreamMessage(reply, civitai);
|
finalizeStreamMessage(reply, civitai);
|
||||||
}
|
}
|
||||||
@@ -9454,7 +9595,14 @@ ${HELP_TEXT}`);
|
|||||||
wireSplitter();
|
wireSplitter();
|
||||||
registerSendButton();
|
registerSendButton();
|
||||||
wireSlashInput();
|
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) => {
|
$2("sa_ctx_chip")?.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleCtxPanel();
|
toggleCtxPanel();
|
||||||
@@ -9489,16 +9637,6 @@ ${HELP_TEXT}`);
|
|||||||
setChatsPanelOpen(!state.chatsPanelOpen);
|
setChatsPanelOpen(!state.chatsPanelOpen);
|
||||||
});
|
});
|
||||||
$2("sa_btn_chats_close")?.addEventListener("click", () => setChatsPanelOpen(false));
|
$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_panel")?.addEventListener("click", (e) => e.stopPropagation());
|
||||||
$2("sa_chats_list")?.addEventListener("click", (e) => {
|
$2("sa_chats_list")?.addEventListener("click", (e) => {
|
||||||
const row = e.target.closest(".sa-chat-row");
|
const row = e.target.closest(".sa-chat-row");
|
||||||
@@ -9513,9 +9651,7 @@ ${HELP_TEXT}`);
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.target.closest("[data-open]")) {
|
switchToChat(id);
|
||||||
switchToChat(id);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
let chatsSearchTimer = null;
|
let chatsSearchTimer = null;
|
||||||
$2("sa_chats_search")?.addEventListener("input", () => {
|
$2("sa_chats_search")?.addEventListener("input", () => {
|
||||||
@@ -9753,7 +9889,11 @@ ${HELP_TEXT}`);
|
|||||||
});
|
});
|
||||||
$2("sa_chips")?.addEventListener("click", async (e) => {
|
$2("sa_chips")?.addEventListener("click", async (e) => {
|
||||||
const btn = e.target.closest(".sa-chip");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const aspect = btn.getAttribute("data-aspect");
|
const aspect = btn.getAttribute("data-aspect");
|
||||||
|
|||||||
+24
-9
@@ -561,9 +561,30 @@
|
|||||||
margin-left: 0.15rem;
|
margin-left: 0.15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sa-sessions-toggle {
|
.sa-chats-toggle {
|
||||||
font-size: 0.78rem !important;
|
position: relative;
|
||||||
padding: 0.18rem 0.5rem !important;
|
}
|
||||||
|
|
||||||
|
.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 {
|
.sa-session-label {
|
||||||
@@ -574,16 +595,10 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 0.3rem;
|
border-radius: 0.3rem;
|
||||||
padding: 0.1rem 0.35rem;
|
padding: 0.1rem 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sa-session-label:hover {
|
|
||||||
opacity: 1;
|
|
||||||
background: color-mix(in srgb, currentColor 10%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sa-chats-panel,
|
.sa-chats-panel,
|
||||||
.sa-chats-drawer {
|
.sa-chats-drawer {
|
||||||
/* drawer lives in .sa-chat-workspace — not a dropdown */
|
/* drawer lives in .sa-chat-workspace — not a dropdown */
|
||||||
|
|||||||
@@ -119,6 +119,37 @@ public partial class SwarmAssistentExtension
|
|||||||
AddLayer("extra", extraSystem);
|
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<bool?>("do_not_greet") == true
|
||||||
|
|| (ctx["prior_assistant_turns"]?.Value<int?>() ?? 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;
|
layers["total"] = system.Length;
|
||||||
if (system.Length > 0)
|
if (system.Length > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-1
@@ -138,7 +138,7 @@ public sealed class AssistentConfig
|
|||||||
return
|
return
|
||||||
[
|
[
|
||||||
"prompt", "negative", "loras", "width", "height", "steps", "cfg", "seed", "sigma_shift", "sampler", "scheduler",
|
"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_init_image", "clear_init_image", "init_creativity", "denoise",
|
||||||
"use_mask_image", "clear_mask_image", "mask_blur", "mask_grow",
|
"use_mask_image", "clear_mask_image", "mask_blur", "mask_grow",
|
||||||
"look_at", "vision_from", "vision_slots", "slot_to_init", "slot_to_mask",
|
"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($"**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($"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($"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<string> allow = null;
|
HashSet<string> allow = null;
|
||||||
if (onlyShelves is not null)
|
if (onlyShelves is not null)
|
||||||
|
|||||||
@@ -227,6 +227,15 @@ public partial class SwarmAssistentExtension
|
|||||||
JObject chunk = JObject.Parse(line);
|
JObject chunk = JObject.Parse(line);
|
||||||
last = chunk;
|
last = chunk;
|
||||||
string delta = chunk["message"]?["content"]?.ToString() ?? "";
|
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<bool>() == true)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!string.IsNullOrEmpty(delta))
|
if (!string.IsNullOrEmpty(delta))
|
||||||
{
|
{
|
||||||
full.Append(delta);
|
full.Append(delta);
|
||||||
|
|||||||
+69
-8
@@ -46,7 +46,7 @@ public partial class SwarmAssistentExtension
|
|||||||
patch["look_at"] = patch["vision_slots"];
|
patch["look_at"] = patch["vision_slots"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ActionsContain(patch, "generate"))
|
if (ActionsContain(patch, "generate") || GenerateFlagOn(patch))
|
||||||
{
|
{
|
||||||
patch["generate"] = true;
|
patch["generate"] = true;
|
||||||
}
|
}
|
||||||
@@ -98,7 +98,28 @@ public partial class SwarmAssistentExtension
|
|||||||
// not json
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -114,10 +135,6 @@ public partial class SwarmAssistentExtension
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
MatchCollection matches = JsonFenceRe.Matches(reply);
|
MatchCollection matches = JsonFenceRe.Matches(reply);
|
||||||
if (matches.Count == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < matches.Count; i++)
|
for (int i = 0; i < matches.Count; i++)
|
||||||
{
|
{
|
||||||
Match match = matches[i];
|
Match match = matches[i];
|
||||||
@@ -137,6 +154,23 @@ public partial class SwarmAssistentExtension
|
|||||||
// incomplete / invalid json inside fence
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +183,7 @@ public partial class SwarmAssistentExtension
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (obj["generate"]?.Type == JTokenType.Boolean && obj["generate"].Value<bool>())
|
if (GenerateFlagOn(obj))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -176,7 +210,8 @@ public partial class SwarmAssistentExtension
|
|||||||
}
|
}
|
||||||
if (HasValue(obj, "loras") || HasValue(obj, "aspect") || HasValue(obj, "steps")
|
if (HasValue(obj, "loras") || HasValue(obj, "aspect") || HasValue(obj, "steps")
|
||||||
|| HasValue(obj, "width") || HasValue(obj, "height") || HasValue(obj, "cfg")
|
|| 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;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -220,6 +255,32 @@ public partial class SwarmAssistentExtension
|
|||||||
return string.Equals(patch["ask"]?.ToString()?.Trim(), name, StringComparison.OrdinalIgnoreCase);
|
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<bool>())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj["generate"]?.Type == JTokenType.Integer && obj["generate"].Value<int>() != 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)
|
static bool ActionsContain(JObject patch, string action)
|
||||||
{
|
{
|
||||||
if (patch?["actions"] is not JArray acts)
|
if (patch?["actions"] is not JArray acts)
|
||||||
|
|||||||
@@ -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).
|
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.**
|
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
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"keys": [
|
"keys": [
|
||||||
"prompt", "negative", "loras", "width", "height", "steps", "cfg", "seed", "sigma_shift", "sampler", "scheduler",
|
"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_init_image", "clear_init_image", "init_creativity", "denoise",
|
||||||
"use_mask_image", "clear_mask_image", "mask_blur", "mask_grow",
|
"use_mask_image", "clear_mask_image", "mask_blur", "mask_grow",
|
||||||
"look_at", "vision_from", "vision_slots", "slot_to_init", "slot_to_mask",
|
"look_at", "vision_from", "vision_slots", "slot_to_init", "slot_to_mask",
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
"always": [
|
"always": [
|
||||||
"Match the user's language (RU or EN) in chat — Generate prompt stays English",
|
"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",
|
"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": [
|
"never": [
|
||||||
|
"Re-introduce yourself every turn or start with «О, привет» when history already has your replies",
|
||||||
"Invent LoRA filenames or trigger words",
|
"Invent LoRA filenames or trigger words",
|
||||||
"Lecture or moralize about NSFW",
|
"Lecture or moralize about NSFW",
|
||||||
"Dump all installed LoRAs — use memory_hits and enabled ones"
|
"Dump all installed LoRAs — use memory_hits and enabled ones"
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
"always": [
|
"always": [
|
||||||
"Stay helpful and concise",
|
"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": [
|
"never": [
|
||||||
"Dirty jokes",
|
"Dirty jokes",
|
||||||
|
"Hello/bio dump on later turns",
|
||||||
"Aggression",
|
"Aggression",
|
||||||
"Moral lectures"
|
"Moral lectures"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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.
|
**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.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.1";
|
Version = "0.15.2";
|
||||||
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"];
|
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,8 +55,14 @@
|
|||||||
<header class="sa-chat-header">
|
<header class="sa-chat-header">
|
||||||
<div class="sa-chat-title">
|
<div class="sa-chat-title">
|
||||||
<div class="sa-sessions-bar">
|
<div class="sa-sessions-bar">
|
||||||
<button type="button" class="basic-button sa-sessions-toggle" id="sa_btn_chats" title="История чатов" aria-expanded="false">История</button>
|
<button type="button" class="sa-chats-icon-btn sa-chats-toggle" id="sa_btn_chats" title="Показать чаты" aria-expanded="false" aria-controls="sa_chats_panel" aria-label="Показать чаты">
|
||||||
<span class="sa-session-label" id="sa_session_label" title="Текущий чат" role="button" tabindex="0">Новый чат</span>
|
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true"><rect x="2.2" y="3" width="11.6" height="10" rx="1.4" stroke="currentColor" stroke-width="1.4"/><path d="M10.2 3v10" stroke="currentColor" stroke-width="1.4"/></svg>
|
||||||
|
<span class="sa-chats-count" id="sa_chats_count" hidden></span>
|
||||||
|
</button>
|
||||||
|
<span class="sa-session-label" id="sa_session_label" title="Текущий чат">Новый чат</span>
|
||||||
|
<button type="button" class="sa-chats-icon-btn" id="sa_btn_new_chat_bar" title="Новый чат" aria-label="Новый чат">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sa-header-right">
|
<div class="sa-header-right">
|
||||||
@@ -132,14 +138,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<aside class="sa-chats-drawer" id="sa_chats_panel" aria-label="История чатов">
|
<aside class="sa-chats-drawer" id="sa_chats_panel" aria-label="Чаты">
|
||||||
<div class="sa-chats-drawer-head">
|
<div class="sa-chats-drawer-head">
|
||||||
<span class="sa-chats-drawer-label">Чаты</span>
|
<span class="sa-chats-drawer-label">Чаты</span>
|
||||||
<div class="sa-chats-drawer-actions">
|
<div class="sa-chats-drawer-actions">
|
||||||
<button type="button" class="sa-chats-icon-btn" id="sa_btn_new_chat" title="Новый чат" aria-label="Новый чат">
|
<button type="button" class="sa-chats-icon-btn" id="sa_btn_new_chat" title="Новый чат" aria-label="Новый чат">
|
||||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="sa-chats-icon-btn" id="sa_btn_chats_close" title="Скрыть панель" aria-label="Скрыть историю">
|
<button type="button" class="sa-chats-icon-btn" id="sa_btn_chats_close" title="Скрыть панель" aria-label="Скрыть чаты">
|
||||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+196
-95
@@ -79,8 +79,8 @@
|
|||||||
|
|
||||||
let HELP_TEXT = `Slash-команды (без LLM):
|
let HELP_TEXT = `Slash-команды (без LLM):
|
||||||
/help — этот список
|
/help — этот список
|
||||||
/new — новый чат (текущий сохранится в Историю)
|
/new — новый чат (текущий сохранится в списке)
|
||||||
/history — открыть список чатов
|
/history — открыть или скрыть панель чатов
|
||||||
/compress — сжать старые ходы в саммари
|
/compress — сжать старые ходы в саммари
|
||||||
/debug — сводка UI/Exact (без LLM)
|
/debug — сводка UI/Exact (без LLM)
|
||||||
/debug ask — то же + короткий ответ модели
|
/debug ask — то же + короткий ответ модели
|
||||||
@@ -96,12 +96,12 @@
|
|||||||
/inventory — rescan моделей + обновить список LoRA
|
/inventory — rescan моделей + обновить список LoRA
|
||||||
|
|
||||||
Чипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.
|
Чипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.
|
||||||
При старте всегда новый чат; смена чата в Истории восстанавливает параметры.`;
|
При старте всегда новый чат; смена чата в панели восстанавливает параметры.`;
|
||||||
|
|
||||||
let SLASH_COMMANDS = [
|
let SLASH_COMMANDS = [
|
||||||
{ cmd: '/help', hint: 'список команд' },
|
{ cmd: '/help', hint: 'список команд' },
|
||||||
{ cmd: '/new', hint: 'новый чат' },
|
{ cmd: '/new', hint: 'новый чат' },
|
||||||
{ cmd: '/history', hint: 'история чатов' },
|
{ cmd: '/history', hint: 'панель чатов' },
|
||||||
{ cmd: '/compress', hint: 'сжать старые ходы' },
|
{ cmd: '/compress', hint: 'сжать старые ходы' },
|
||||||
{ cmd: '/debug', hint: 'сводка · ask = с LLM' },
|
{ cmd: '/debug', hint: 'сводка · ask = с LLM' },
|
||||||
{ cmd: '/why', hint: 'debug + пояснение LLM' },
|
{ cmd: '/why', hint: 'debug + пояснение LLM' },
|
||||||
@@ -234,7 +234,7 @@
|
|||||||
|
|
||||||
// ---- Turn lifecycle --------------------------------------------------
|
// ---- Turn lifecycle --------------------------------------------------
|
||||||
// A turn is one user utterance. It can fan out into nested LLM hops: Krea
|
// A turn is one user utterance. It can fan out into nested LLM hops: Krea
|
||||||
// prompt prep, empty-patch retry, vision, auto-critique. They share one
|
// prompt prep, ask:settings/inventory, vision, auto-critique. They share one
|
||||||
// budget so a turn always terminates, and the text they carry is written by
|
// budget so a turn always terminates, and the text they carry is written by
|
||||||
// the client, not the user — intent heuristics must never read it.
|
// the client, not the user — intent heuristics must never read it.
|
||||||
|
|
||||||
@@ -1091,9 +1091,18 @@
|
|||||||
function mergePromptEnRewrite(effective) {
|
function mergePromptEnRewrite(effective) {
|
||||||
const base = state.pendingPromptEnMerge;
|
const base = state.pendingPromptEnMerge;
|
||||||
state.pendingPromptEnMerge = null;
|
state.pendingPromptEnMerge = null;
|
||||||
if (!base || !effective) {
|
if (!base) {
|
||||||
return effective;
|
return effective;
|
||||||
}
|
}
|
||||||
|
if (!effective) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
generate: true,
|
||||||
|
actions: (Array.isArray(base.actions) && base.actions.length)
|
||||||
|
? base.actions
|
||||||
|
: ['generate'],
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
...effective,
|
...effective,
|
||||||
@@ -1141,6 +1150,22 @@
|
|||||||
}
|
}
|
||||||
return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t);
|
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(
|
||||||
|
'сгенер[а-яё]*|нарисуй|нарисуйте|'
|
||||||
|
+ 'запусти\\s+генер[а-яё]*|сделай\\s+(кадр|картинк[а-яё]*|изображ[а-яё]*)',
|
||||||
|
).test(t);
|
||||||
|
}
|
||||||
|
|
||||||
function packWantsVision(pack) {
|
function packWantsVision(pack) {
|
||||||
const p = String(pack || '');
|
const p = String(pack || '');
|
||||||
@@ -1149,12 +1174,20 @@
|
|||||||
function resolveTurnIntent(patch, userText, opts = {}) {
|
function resolveTurnIntent(patch, userText, opts = {}) {
|
||||||
const S = window.SA && window.SA.session;
|
const S = window.SA && window.SA.session;
|
||||||
if (S && typeof S.resolveTurnIntent === 'function') {
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
// generate:true / actions / user «сгенерируй» when a prompt already exists.
|
||||||
const vetoed = !isMachineTurn(opts) && userAsksNoGenerate(userText);
|
const vetoed = !isMachineTurn(opts) && userAsksNoGenerate(userText);
|
||||||
const modelAsked = !!(patch && (patch.generate === true
|
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'))));
|
|| (Array.isArray(patch.actions) && patch.actions.map(String).includes('generate'))));
|
||||||
const generate = !vetoed && !opts.fromAutoCritique && modelAsked;
|
const userAsked = !isMachineTurn(opts) && userAsksGenerate(userText)
|
||||||
|
&& !!(modelAsked || String(patch?.prompt || '').trim());
|
||||||
|
const generate = !vetoed && !opts.fromAutoCritique && (modelAsked || userAsked);
|
||||||
const hasLook = !!patch
|
const hasLook = !!patch
|
||||||
&& (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
|
&& (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
|
||||||
const look = !!(hasLook && !vetoed && !generate);
|
const look = !!(hasLook && !vetoed && !generate);
|
||||||
@@ -2435,6 +2468,7 @@
|
|||||||
genBtn.textContent = 'Сгенерировать';
|
genBtn.textContent = 'Сгенерировать';
|
||||||
genBtn.addEventListener('click', async () => {
|
genBtn.addEventListener('click', async () => {
|
||||||
if (isGenerateUnavailable()) {
|
if (isGenerateUnavailable()) {
|
||||||
|
setStatus('Generate недоступен — подожди или нажми Стоп');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
startBusyUi('silent_gen');
|
startBusyUi('silent_gen');
|
||||||
@@ -3294,7 +3328,7 @@
|
|||||||
empty.className = 'sa-chat-empty';
|
empty.className = 'sa-chat-empty';
|
||||||
empty.id = 'sa_chat_empty';
|
empty.id = 'sa_chat_empty';
|
||||||
empty.innerHTML = emptyHint
|
empty.innerHTML = emptyHint
|
||||||
|| '<div class="sa-chat-empty-title">Новый чат</div><div class="sa-chat-empty-hint">Параметры Generate остаются как сейчас.<br><strong>+</strong> — ещё один чат · <strong>История</strong> — вернуться к прошлому (с его параметрами).</div>';
|
|| '<div class="sa-chat-empty-title">Новый чат</div><div class="sa-chat-empty-hint">Параметры Generate остаются как сейчас.<br><strong>+</strong> — ещё один чат · кнопка панели слева — прошлые диалоги.</div>';
|
||||||
box.appendChild(empty);
|
box.appendChild(empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3357,23 +3391,40 @@
|
|||||||
}
|
}
|
||||||
const chat = findChat(state.activeChatId);
|
const chat = findChat(state.activeChatId);
|
||||||
el.textContent = chat?.title || 'Новый чат';
|
el.textContent = chat?.title || 'Новый чат';
|
||||||
el.title = (chat?.title || 'Новый чат') + ' — клик: История';
|
el.title = chat?.title || 'Новый чат';
|
||||||
|
}
|
||||||
|
|
||||||
|
function chatHasTranscript(c) {
|
||||||
|
return ((c?.messages || []).length > 0) || ((c?.messages_count || 0) > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function savedChatsCount() {
|
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() {
|
function syncHistoryBadge() {
|
||||||
const btn = $('sa_btn_chats');
|
const btn = $('sa_btn_chats');
|
||||||
if (!btn) {
|
const countEl = $('sa_chats_count');
|
||||||
return;
|
const n = (state.chats || []).filter(chatHasTranscript).length;
|
||||||
|
const open = !!state.chatsPanelOpen;
|
||||||
|
if (btn) {
|
||||||
|
btn.title = open
|
||||||
|
? (n ? `Скрыть чаты (${n})` : 'Скрыть чаты')
|
||||||
|
: (n ? `Показать чаты (${n})` : 'Показать чаты');
|
||||||
|
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 ? `История (${n})` : 'История';
|
|
||||||
btn.title = n > 0
|
|
||||||
? `Сохранённых чатов: ${n}. Переключение восстанавливает параметры.`
|
|
||||||
: 'История чатов (пока пусто)';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatChatWhen(ts) {
|
function formatChatWhen(ts) {
|
||||||
@@ -3450,7 +3501,7 @@
|
|||||||
let chats = (state.chats || [])
|
let chats = (state.chats || [])
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
||||||
.filter((c) => (c.messages || []).length > 0 || (c.messages_count || 0) > 0);
|
.filter((c) => c.id === state.activeChatId || chatHasTranscript(c));
|
||||||
if (q) {
|
if (q) {
|
||||||
const local = chats.filter((c) => chatMatchesQuery(c, q));
|
const local = chats.filter((c) => chatMatchesQuery(c, q));
|
||||||
const seen = new Set(local.map((c) => c.id));
|
const seen = new Set(local.map((c) => c.id));
|
||||||
@@ -3491,6 +3542,7 @@
|
|||||||
btn?.classList.toggle('sa-sessions-toggle-active', state.chatsPanelOpen);
|
btn?.classList.toggle('sa-sessions-toggle-active', state.chatsPanelOpen);
|
||||||
root?.classList.toggle('sa-drawer-open', state.chatsPanelOpen);
|
root?.classList.toggle('sa-drawer-open', state.chatsPanelOpen);
|
||||||
localStorage.setItem(LS_CHATS_DRAWER, state.chatsPanelOpen ? '1' : '0');
|
localStorage.setItem(LS_CHATS_DRAWER, state.chatsPanelOpen ? '1' : '0');
|
||||||
|
syncHistoryBadge();
|
||||||
if (state.chatsPanelOpen) {
|
if (state.chatsPanelOpen) {
|
||||||
saveActiveChatToStore();
|
saveActiveChatToStore();
|
||||||
const search = $('sa_chats_search');
|
const search = $('sa_chats_search');
|
||||||
@@ -3503,14 +3555,17 @@
|
|||||||
saveUiStateToDisk();
|
saveUiStateToDisk();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startNewChat({ saveCurrent = true, force = false } = {}) {
|
async function startNewChat({ saveCurrent = true, force = false, openDrawer = false } = {}) {
|
||||||
if (!force && (state.busy || state.generating)) {
|
if (!force && isBlankActiveChat()) {
|
||||||
setStatus('Занято — дождись конца ответа или Стоп');
|
setStatus('Уже новый чат');
|
||||||
|
if (openDrawer) {
|
||||||
|
setChatsPanelOpen(true);
|
||||||
|
}
|
||||||
|
$('sa_input')?.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (force) {
|
if (state.busy || state.generating) {
|
||||||
// Drop in-flight reply so it cannot land in the new chat.
|
abortInFlightWork({ status: '', interruptSwarm: !!state.generating });
|
||||||
abortInFlightWork({ status: '' });
|
|
||||||
}
|
}
|
||||||
if (saveCurrent) {
|
if (saveCurrent) {
|
||||||
saveActiveChatToStore({ dropEmpty: true });
|
saveActiveChatToStore({ dropEmpty: true });
|
||||||
@@ -3549,6 +3604,12 @@
|
|||||||
updateCtxChip();
|
updateCtxChip();
|
||||||
setStatus('Новый чат — параметры Generate как сейчас');
|
setStatus('Новый чат — параметры Generate как сейчас');
|
||||||
maybeWelcome();
|
maybeWelcome();
|
||||||
|
if (openDrawer) {
|
||||||
|
setChatsPanelOpen(true);
|
||||||
|
}
|
||||||
|
if (openDrawer || !force) {
|
||||||
|
$('sa_input')?.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function switchToChat(id) {
|
async function switchToChat(id) {
|
||||||
@@ -3692,7 +3753,7 @@
|
|||||||
syncBuildGenButton();
|
syncBuildGenButton();
|
||||||
clearPersistedHistory();
|
clearPersistedHistory();
|
||||||
resetContextMemory({ persist: false });
|
resetContextMemory({ persist: false });
|
||||||
resetMessagesUi('<div class="sa-chat-empty-title">Чат очищен</div><div class="sa-chat-empty-hint">Сообщения сброшены. Параметры Generate на месте. <strong>+</strong> — новый чат в Историю, <strong>История</strong> — прошлые диалоги.</div>');
|
resetMessagesUi('<div class="sa-chat-empty-title">Чат очищен</div><div class="sa-chat-empty-hint">Сообщения сброшены. Параметры Generate на месте. <strong>+</strong> — новый чат, кнопка панели слева — прошлые диалоги.</div>');
|
||||||
setStatus('Чат очищен');
|
setStatus('Чат очищен');
|
||||||
updateSessionLabel();
|
updateSessionLabel();
|
||||||
syncHistoryBadge();
|
syncHistoryBadge();
|
||||||
@@ -4841,7 +4902,7 @@
|
|||||||
if (!state.gotDelta || !state.busy || state.generating || state.turnSettled) {
|
if (!state.gotDelta || !state.busy || state.generating || state.turnSettled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const waitMs = state.streamFenceDone ? 4000 : 15000;
|
const waitMs = state.streamFenceDone ? 800 : (state.gotDelta ? 2200 : 15000);
|
||||||
if (Date.now() - (state.lastDeltaAt || 0) < waitMs) {
|
if (Date.now() - (state.lastDeltaAt || 0) < waitMs) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -5477,23 +5538,32 @@ if (role === 'assistant' && !(meta && meta.historical)) {
|
|||||||
return div;
|
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) {
|
function streamHasClosedPatchFence(text) {
|
||||||
const t = String(text || '');
|
const t = String(text || '');
|
||||||
if (!/```[\s\S]*```/.test(t)) {
|
if (/```[\s\S]*```/.test(t)) {
|
||||||
return false;
|
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||||||
}
|
let match;
|
||||||
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
|
while ((match = re.exec(t)) !== null) {
|
||||||
let match;
|
if (parseTerminalPatchObject(match[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) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (e) { /* ignore */ }
|
}
|
||||||
|
}
|
||||||
|
const brace = t.lastIndexOf('{');
|
||||||
|
if (brace >= 0 && parseTerminalPatchObject(t.slice(brace))) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -5504,17 +5574,18 @@ if (role === 'assistant' && !(meta && meta.historical)) {
|
|||||||
let match;
|
let match;
|
||||||
let lastEnd = -1;
|
let lastEnd = -1;
|
||||||
while ((match = re.exec(t)) !== null) {
|
while ((match = re.exec(t)) !== null) {
|
||||||
try {
|
if (parseTerminalPatchObject(match[1])) {
|
||||||
const obj = JSON.parse(match[1].trim());
|
lastEnd = match.index + match[0].length;
|
||||||
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) { /* ignore */ }
|
|
||||||
}
|
}
|
||||||
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) {
|
function appendStreamDelta(delta) {
|
||||||
@@ -5540,6 +5611,18 @@ if (role === 'assistant' && !(meta && meta.historical)) {
|
|||||||
if (streamHasClosedPatchFence(state.streamText)) {
|
if (streamHasClosedPatchFence(state.streamText)) {
|
||||||
state.streamText = trimToClosedPatchFence(state.streamText);
|
state.streamText = trimToClosedPatchFence(state.streamText);
|
||||||
state.streamFenceDone = true;
|
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 });
|
setAssistantBody(state.streamEl, state.streamText, { live: true });
|
||||||
scrollMessagesToBottom();
|
scrollMessagesToBottom();
|
||||||
@@ -5562,19 +5645,12 @@ if (role === 'assistant' && !(meta && meta.historical)) {
|
|||||||
}
|
}
|
||||||
el.classList.remove('sa-streaming', 'sa-typing');
|
el.classList.remove('sa-streaming', 'sa-typing');
|
||||||
mountAssistantMeta(el, meta || undefined);
|
mountAssistantMeta(el, meta || undefined);
|
||||||
const { prose, patch } = extractPatch(fullReply);
|
const { prose, patch } = extractPatch(fullReply);
|
||||||
setAssistantBody(el, prose || fullReply || '');
|
setAssistantBody(el, prose || fullReply || '');
|
||||||
el.querySelectorAll('.sa-patch, .sa-civitai-list').forEach((n) => n.remove());
|
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;
|
const silent = !!(meta && meta.silentPatch) || !!state.pendingSilentGen;
|
||||||
mountPatchBlock(el, patch, { silent });
|
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)) {
|
if (!(meta && meta.historical)) {
|
||||||
mountCurateButtons(el, meta);
|
mountCurateButtons(el, meta);
|
||||||
@@ -6754,10 +6830,6 @@ if (!(meta && meta.historical)) {
|
|||||||
const n = state.inventory.loras.length;
|
const n = state.inventory.loras.length;
|
||||||
const ck = state.inventory.checkpoints.length;
|
const ck = state.inventory.checkpoints.length;
|
||||||
setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts${rescan ? ' (rescanned)' : ''}`);
|
setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts${rescan ? ' (rescanned)' : ''}`);
|
||||||
|
|
||||||
if (state.view === 'cards') {
|
|
||||||
|
|
||||||
}
|
|
||||||
if (done) {
|
if (done) {
|
||||||
done(state.inventory);
|
done(state.inventory);
|
||||||
}
|
}
|
||||||
@@ -7731,7 +7803,8 @@ if (!(meta && meta.historical)) {
|
|||||||
}
|
}
|
||||||
const extracted = typeof extractPatch === 'function' ? extractPatch(reply) : { patch: null };
|
const extracted = typeof extractPatch === 'function' ? extractPatch(reply) : { patch: null };
|
||||||
let effective = extracted && extracted.patch ? extracted.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);
|
effective = mergePromptEnRewrite(effective);
|
||||||
}
|
}
|
||||||
const S = window.SA && window.SA.session;
|
const S = window.SA && window.SA.session;
|
||||||
@@ -7739,11 +7812,12 @@ if (!(meta && meta.historical)) {
|
|||||||
if (effective && act && typeof act.noteModelCommands === 'function') {
|
if (effective && act && typeof act.noteModelCommands === 'function') {
|
||||||
act.noteModelCommands(effective);
|
act.noteModelCommands(effective);
|
||||||
}
|
}
|
||||||
|
const promptChanged = !!(effective && String(effective.prompt || '').trim());
|
||||||
if (effective && S) {
|
if (effective && S) {
|
||||||
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
||||||
activityDone('delta', {
|
activityDone('delta', {
|
||||||
kind: 'delta',
|
kind: 'delta',
|
||||||
label: 'Обновил сессию',
|
label: promptChanged ? 'Промпт обновлён' : 'Обновил сессию',
|
||||||
detail: Object.keys(effective).filter((k) => effective[k] != null
|
detail: Object.keys(effective).filter((k) => effective[k] != null
|
||||||
&& !['actions', 'notes'].includes(k)).slice(0, 10).join(', '),
|
&& !['actions', 'notes'].includes(k)).slice(0, 10).join(', '),
|
||||||
});
|
});
|
||||||
@@ -7759,7 +7833,10 @@ if (!(meta && meta.historical)) {
|
|||||||
if (Array.isArray(effective?.actions) && effective.actions.map(String).includes('interrupt')) {
|
if (Array.isArray(effective?.actions) && effective.actions.map(String).includes('interrupt')) {
|
||||||
if (typeof doInterruptNow === 'function') doInterruptNow();
|
if (typeof doInterruptNow === 'function') doInterruptNow();
|
||||||
}
|
}
|
||||||
const intent = resolveTurnIntent(effective, opts.userText || '', opts);
|
let intent = resolveTurnIntent(effective, opts.userText || '', opts);
|
||||||
|
if (opts.userWantsGenerate && effective && !intent.vetoed && !fromAutoCritique) {
|
||||||
|
intent = { ...intent, generate: true };
|
||||||
|
}
|
||||||
if (effective) {
|
if (effective) {
|
||||||
if (intent.generate) {
|
if (intent.generate) {
|
||||||
const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : [];
|
const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : [];
|
||||||
@@ -7835,7 +7912,8 @@ if (!(meta && meta.historical)) {
|
|||||||
}
|
}
|
||||||
if (intent.generate && effective?.prompt
|
if (intent.generate && effective?.prompt
|
||||||
&& typeof promptNeedsKreaPrep === 'function' && promptNeedsKreaPrep(effective.prompt)
|
&& typeof promptNeedsKreaPrep === 'function' && promptNeedsKreaPrep(effective.prompt)
|
||||||
&& !fromVisionHop && typeof claimTurnHop === 'function' && claimTurnHop('krea_prep')) {
|
&& !fromVisionHop
|
||||||
|
&& typeof claimTurnHop === 'function' && claimTurnHop('krea_prep')) {
|
||||||
state.pendingPromptEnMerge = { ...effective };
|
state.pendingPromptEnMerge = { ...effective };
|
||||||
activityStep('prep', {
|
activityStep('prep', {
|
||||||
kind: 'prep',
|
kind: 'prep',
|
||||||
@@ -7870,6 +7948,10 @@ if (!(meta && meta.historical)) {
|
|||||||
if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
||||||
await pushSessionToSwarm(state.chatSession);
|
await pushSessionToSwarm(state.chatSession);
|
||||||
if (typeof syncLiveParamsBar === 'function') syncLiveParamsBar();
|
if (typeof syncLiveParamsBar === 'function') syncLiveParamsBar();
|
||||||
|
if (typeof appendSystemNote === 'function') {
|
||||||
|
appendSystemNote(promptChanged ? 'Промпт обновлён · Generate' : 'Запускаю Generate');
|
||||||
|
}
|
||||||
|
setStatus(promptChanged ? 'Промпт обновлён · Generate' : 'Generate');
|
||||||
const srcOut = await runGenerateFromPatch(
|
const srcOut = await runGenerateFromPatch(
|
||||||
{ ...(effective || {}), actions: ['generate'], generate: true },
|
{ ...(effective || {}), actions: ['generate'], generate: true },
|
||||||
{ force: true, fromSession: true },
|
{ force: true, fromSession: true },
|
||||||
@@ -7879,12 +7961,20 @@ if (!(meta && meta.historical)) {
|
|||||||
if (typeof maybeAutoCritique === 'function') await maybeAutoCritique(srcOut);
|
if (typeof maybeAutoCritique === 'function') await maybeAutoCritique(srcOut);
|
||||||
if (typeof maybeAutoVisionLook === 'function') await maybeAutoVisionLook(srcOut);
|
if (typeof maybeAutoVisionLook === 'function') await maybeAutoVisionLook(srcOut);
|
||||||
}
|
}
|
||||||
} else if (effective && $('sa_auto_apply')?.checked) {
|
} else if (effective) {
|
||||||
if (S) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
if (S) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
||||||
await pushSessionToSwarm(state.chatSession);
|
if (promptChanged || $('sa_auto_apply')?.checked) {
|
||||||
if (typeof syncLiveParamsBar === 'function') syncLiveParamsBar();
|
await pushSessionToSwarm(state.chatSession);
|
||||||
|
if (typeof syncLiveParamsBar === 'function') syncLiveParamsBar();
|
||||||
|
if (promptChanged && typeof appendSystemNote === 'function') {
|
||||||
|
appendSystemNote('Промпт обновлён');
|
||||||
|
}
|
||||||
|
if (promptChanged) {
|
||||||
|
setStatus('Промпт обновлён');
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!state.generating && typeof stopBusyUi === 'function') {
|
if (!state.generating && typeof stopBusyUi === 'function') {
|
||||||
stopBusyUi(intent.vetoed ? 'Запомнил · без Generate' : '');
|
stopBusyUi(intent.vetoed ? 'Запомнил · без Generate' : (promptChanged ? 'Промпт обновлён' : ''));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.pendingSilentGen = false;
|
state.pendingSilentGen = false;
|
||||||
@@ -8058,12 +8148,12 @@ if (!(meta && meta.historical)) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (cmd === 'new' || cmd === 'newchat') {
|
if (cmd === 'new' || cmd === 'newchat') {
|
||||||
await startNewChat({ saveCurrent: true });
|
await startNewChat({ saveCurrent: true, openDrawer: true });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (cmd === 'history' || cmd === 'chats' || cmd === 'sessions') {
|
if (cmd === 'history' || cmd === 'chats' || cmd === 'sessions') {
|
||||||
setChatsPanelOpen(true);
|
setChatsPanelOpen(!state.chatsPanelOpen);
|
||||||
setStatus('/history');
|
setStatus(state.chatsPanelOpen ? '/history' : 'Чаты скрыты');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (cmd === 'compress' || cmd === 'compact' || cmd === 'сжать') {
|
if (cmd === 'compress' || cmd === 'compact' || cmd === 'сжать') {
|
||||||
@@ -8501,6 +8591,8 @@ if (!(meta && meta.historical)) {
|
|||||||
const context = collectLiveContext();
|
const context = collectLiveContext();
|
||||||
// has_vision_image = board has a real frame (even when JPEG is not in this request).
|
// has_vision_image = board has a real frame (even when JPEG is not in this request).
|
||||||
// images_in_request = JPEG bytes are attached to the last user message this turn.
|
// images_in_request = JPEG bytes are attached to the last user message this turn.
|
||||||
|
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.has_vision_image = visionReadySlots().length > 0;
|
||||||
context.images_in_request = !!(images && images.length);
|
context.images_in_request = !!(images && images.length);
|
||||||
context.attached_slot_ids = attachableSlots().map((s) => s.id);
|
context.attached_slot_ids = attachableSlots().map((s) => s.id);
|
||||||
@@ -8544,6 +8636,7 @@ if (!(meta && meta.historical)) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.turnSettled = true;
|
state.turnSettled = true;
|
||||||
|
state.onClosedTerminalFence = null;
|
||||||
clearStreamStall();
|
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;
|
||||||
@@ -8601,6 +8694,7 @@ if (!(meta && meta.historical)) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.turnSettled = true;
|
state.turnSettled = true;
|
||||||
|
state.onClosedTerminalFence = null;
|
||||||
clearStreamStall();
|
clearStreamStall();
|
||||||
state.busy = false;
|
state.busy = false;
|
||||||
setInterruptVisible(state.generating);
|
setInterruptVisible(state.generating);
|
||||||
@@ -8618,14 +8712,20 @@ if (!(meta && meta.historical)) {
|
|||||||
|
|
||||||
if (typeof makeWSRequest === 'function') {
|
if (typeof makeWSRequest === 'function') {
|
||||||
beginStreamMessage(msgMeta);
|
beginStreamMessage(msgMeta);
|
||||||
armStreamStall(chatEpoch, (reply) => {
|
const settleStreamReply = (reply) => {
|
||||||
if (state.turnSettled || chatEpoch !== state.chatEpoch) {
|
if (state.turnSettled || chatEpoch !== state.chatEpoch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const raw = String(reply || state.streamText || '').trim()
|
||||||
|
|| (state.streamEl?.querySelector('.sa-msg-body')?.textContent || '');
|
||||||
if (state.streamEl) {
|
if (state.streamEl) {
|
||||||
finalizeStreamMessage(reply, []);
|
finalizeStreamMessage(raw, []);
|
||||||
}
|
}
|
||||||
finishOk(reply, [], {});
|
finishOk(raw, [], {});
|
||||||
|
};
|
||||||
|
state.onClosedTerminalFence = (text) => settleStreamReply(text);
|
||||||
|
armStreamStall(chatEpoch, (reply) => {
|
||||||
|
settleStreamReply(reply);
|
||||||
});
|
});
|
||||||
makeWSRequest(
|
makeWSRequest(
|
||||||
'AssistentChatWS',
|
'AssistentChatWS',
|
||||||
@@ -8661,12 +8761,14 @@ if (!(meta && meta.historical)) {
|
|||||||
if (data.delta) {
|
if (data.delta) {
|
||||||
appendStreamDelta(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) {
|
if (state.turnSettled) {
|
||||||
return;
|
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 || [];
|
const civitai = data.civitai_results || [];
|
||||||
|
state.onClosedTerminalFence = null;
|
||||||
if (state.streamEl) {
|
if (state.streamEl) {
|
||||||
finalizeStreamMessage(reply, civitai);
|
finalizeStreamMessage(reply, civitai);
|
||||||
}
|
}
|
||||||
@@ -8944,7 +9046,14 @@ if (!(meta && meta.historical)) {
|
|||||||
wireSlashInput();
|
wireSlashInput();
|
||||||
|
|
||||||
|
|
||||||
$('sa_btn_new_chat')?.addEventListener('click', () => startNewChat({ saveCurrent: true }));
|
$('sa_btn_new_chat')?.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
startNewChat({ saveCurrent: true, openDrawer: true });
|
||||||
|
});
|
||||||
|
$('sa_btn_new_chat_bar')?.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
startNewChat({ saveCurrent: true, openDrawer: true });
|
||||||
|
});
|
||||||
$('sa_ctx_chip')?.addEventListener('click', (e) => {
|
$('sa_ctx_chip')?.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleCtxPanel();
|
toggleCtxPanel();
|
||||||
@@ -8979,16 +9088,6 @@ if (!(meta && meta.historical)) {
|
|||||||
setChatsPanelOpen(!state.chatsPanelOpen);
|
setChatsPanelOpen(!state.chatsPanelOpen);
|
||||||
});
|
});
|
||||||
$('sa_btn_chats_close')?.addEventListener('click', () => setChatsPanelOpen(false));
|
$('sa_btn_chats_close')?.addEventListener('click', () => setChatsPanelOpen(false));
|
||||||
$('sa_session_label')?.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setChatsPanelOpen(!state.chatsPanelOpen);
|
|
||||||
});
|
|
||||||
$('sa_session_label')?.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
setChatsPanelOpen(!state.chatsPanelOpen);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('sa_chats_panel')?.addEventListener('click', (e) => e.stopPropagation());
|
$('sa_chats_panel')?.addEventListener('click', (e) => e.stopPropagation());
|
||||||
$('sa_chats_list')?.addEventListener('click', (e) => {
|
$('sa_chats_list')?.addEventListener('click', (e) => {
|
||||||
const row = e.target.closest('.sa-chat-row');
|
const row = e.target.closest('.sa-chat-row');
|
||||||
@@ -9003,9 +9102,7 @@ if (!(meta && meta.historical)) {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.target.closest('[data-open]')) {
|
switchToChat(id);
|
||||||
switchToChat(id);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
let chatsSearchTimer = null;
|
let chatsSearchTimer = null;
|
||||||
$('sa_chats_search')?.addEventListener('input', () => {
|
$('sa_chats_search')?.addEventListener('input', () => {
|
||||||
@@ -9248,7 +9345,11 @@ if (!(meta && meta.historical)) {
|
|||||||
});
|
});
|
||||||
$('sa_chips')?.addEventListener('click', async (e) => {
|
$('sa_chips')?.addEventListener('click', async (e) => {
|
||||||
const btn = e.target.closest('.sa-chip');
|
const btn = e.target.closest('.sa-chip');
|
||||||
if (!btn || state.busy || state.generating) {
|
if (!btn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.busy || state.generating) {
|
||||||
|
setStatus('Занято — подожди или нажми Стоп');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const aspect = btn.getAttribute('data-aspect');
|
const aspect = btn.getAttribute('data-aspect');
|
||||||
|
|||||||
+34
-5
@@ -1,4 +1,4 @@
|
|||||||
/** Turn intent — veto only; generate comes from model `generate: true` (or legacy actions). */
|
/** Turn intent — veto + model generate; user «сгенерируй» also counts when a prompt/delta exists. */
|
||||||
|
|
||||||
export function cyrTokenRe(alts) {
|
export function cyrTokenRe(alts) {
|
||||||
const boundary = '(^|[^0-9A-Za-z_А-Яа-яЁё])';
|
const boundary = '(^|[^0-9A-Za-z_А-Яа-яЁё])';
|
||||||
@@ -23,6 +23,20 @@ export function userAsksNoGenerate(text) {
|
|||||||
).test(t);
|
).test(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function 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(
|
||||||
|
'сгенер[а-яё]*|нарисуй|нарисуйте|'
|
||||||
|
+ 'запусти\\s+генер[а-яё]*|сделай\\s+(кадр|картинк[а-яё]*|изображ[а-яё]*)',
|
||||||
|
).test(t);
|
||||||
|
}
|
||||||
|
|
||||||
export function userAsksLook(text) {
|
export function userAsksLook(text) {
|
||||||
const t = String(text || '').trim();
|
const t = String(text || '').trim();
|
||||||
if (!t) {
|
if (!t) {
|
||||||
@@ -56,12 +70,27 @@ export function packWantsVision(pack) {
|
|||||||
return p === 'critique_image' || p === 'describe_ref' || p === 'compose_scene' || p === 'inpaint_edit';
|
return p === 'critique_image' || p === 'describe_ref' || p === 'compose_scene' || p === 'inpaint_edit';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Model generate + explicit veto. No RU imply/command heuristics. */
|
function generateFlagOn(patch) {
|
||||||
|
if (!patch || typeof patch !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const g = patch.generate;
|
||||||
|
if (g === true || g === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof g === 'string' && /^(true|1|yes|on)$/i.test(g.trim())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Array.isArray(patch.actions) && patch.actions.map(String).includes('generate');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Model generate flag, or user «сгенерируй» when a prompt/delta already exists. */
|
||||||
export function resolveTurnIntent(patch, userText, opts = {}) {
|
export function resolveTurnIntent(patch, userText, opts = {}) {
|
||||||
const vetoed = !opts.machineTurn && userAsksNoGenerate(userText);
|
const vetoed = !opts.machineTurn && userAsksNoGenerate(userText);
|
||||||
const modelAsked = patch?.generate === true
|
const modelAsked = generateFlagOn(patch);
|
||||||
|| (Array.isArray(patch?.actions) && patch.actions.map(String).includes('generate'));
|
const userAsked = !opts.machineTurn && userAsksGenerate(userText)
|
||||||
const generate = !vetoed && !opts.fromAutoCritique && !!modelAsked;
|
&& !!(modelAsked || String(patch?.prompt || '').trim());
|
||||||
|
const generate = !vetoed && !opts.fromAutoCritique && (modelAsked || userAsked);
|
||||||
const hasLook = !!patch
|
const hasLook = !!patch
|
||||||
&& (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
|
&& (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
|
||||||
const look = !!(hasLook && !vetoed && !generate);
|
const look = !!(hasLook && !vetoed && !generate);
|
||||||
|
|||||||
+66
-18
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const DEFAULT_PATCH_KEYS = [
|
const DEFAULT_PATCH_KEYS = [
|
||||||
'prompt', 'negative', 'loras', 'width', 'height', 'steps', 'cfg', 'seed', 'sigma_shift', 'sampler', 'scheduler',
|
'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_init_image', 'clear_init_image', 'init_creativity', 'denoise',
|
||||||
'use_mask_image', 'clear_mask_image', 'mask_blur', 'mask_grow',
|
'use_mask_image', 'clear_mask_image', 'mask_blur', 'mask_grow',
|
||||||
'look_at', 'vision_from', 'vision_slots', 'slot_to_init', 'slot_to_mask',
|
'look_at', 'vision_from', 'vision_slots', 'slot_to_init', 'slot_to_mask',
|
||||||
@@ -30,6 +30,22 @@ function has(obj, key) {
|
|||||||
return obj[key] !== undefined && obj[key] !== null;
|
return obj[key] !== undefined && obj[key] !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** true / "true" / 1 / actions:["generate"] — models sometimes stringify the flag. */
|
||||||
|
export 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');
|
||||||
|
}
|
||||||
|
|
||||||
export function isPatchObject(obj) {
|
export function isPatchObject(obj) {
|
||||||
if (!obj || typeof obj !== 'object') {
|
if (!obj || typeof obj !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
@@ -42,7 +58,7 @@ export function normalizePatch(patch) {
|
|||||||
return patch;
|
return patch;
|
||||||
}
|
}
|
||||||
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
|
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;
|
patch.generate = true;
|
||||||
}
|
}
|
||||||
if (typeof patch.ask === 'string') {
|
if (typeof patch.ask === 'string') {
|
||||||
@@ -61,31 +77,65 @@ export function normalizePatch(patch) {
|
|||||||
return patch;
|
return patch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tryParsePatchJson(raw) {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(String(raw || '').trim());
|
||||||
|
if (isPatchObject(obj)) {
|
||||||
|
return normalizePatch(obj);
|
||||||
|
}
|
||||||
|
} catch (e) { /* not json */ }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function extractPatch(text) {
|
export function extractPatch(text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return { prose: text || '', patch: null };
|
return { prose: text || '', patch: null };
|
||||||
}
|
}
|
||||||
const re = new RegExp(FENCE_RE.source, 'gi');
|
const re = new RegExp(FENCE_RE.source, 'gi');
|
||||||
let match;
|
let match;
|
||||||
let lastPatch = null;
|
let lastAny = null;
|
||||||
let prose = text;
|
let lastAnyIndex = -1;
|
||||||
|
let lastAnyLen = 0;
|
||||||
|
let lastTerminal = null;
|
||||||
|
let lastTermIndex = -1;
|
||||||
|
let lastTermLen = 0;
|
||||||
while ((match = re.exec(text)) !== null) {
|
while ((match = re.exec(text)) !== null) {
|
||||||
try {
|
const parsed = tryParsePatchJson(match[1]);
|
||||||
const obj = JSON.parse(match[1].trim());
|
if (!parsed) {
|
||||||
if (isPatchObject(obj)) {
|
continue;
|
||||||
lastPatch = normalizePatch(obj);
|
}
|
||||||
prose = (text.slice(0, match.index) + text.slice(match.index + match[0].length)).trim();
|
lastAny = parsed;
|
||||||
}
|
lastAnyIndex = match.index;
|
||||||
} catch (e) { /* not json */ }
|
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 };
|
||||||
|
}
|
||||||
|
// Unfenced trailing object — some turns emit raw {prompt, generate:true}.
|
||||||
|
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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTerminalStreamPatch(obj) {
|
export function isTerminalStreamPatch(obj) {
|
||||||
if (!obj || typeof obj !== 'object') {
|
if (!obj || typeof obj !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (obj.generate === true) {
|
if (generateFlagOn(obj)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (Array.isArray(obj.ask) && obj.ask.length) {
|
if (Array.isArray(obj.ask) && obj.ask.length) {
|
||||||
@@ -100,16 +150,13 @@ export function isTerminalStreamPatch(obj) {
|
|||||||
if (obj.look_at != null || obj.vision_from != null || obj.vision_slots != null) {
|
if (obj.look_at != null || obj.vision_from != null || obj.vision_slots != null) {
|
||||||
return true;
|
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) {
|
if (String(obj.prompt || '').trim().length >= 48) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (obj.loras != null || obj.aspect != null || obj.steps != null
|
if (obj.loras != null || obj.aspect != null || obj.steps != null
|
||||||
|| obj.width != null || obj.height != null || obj.cfg != null
|
|| obj.width != null || obj.height != null || obj.cfg != null
|
||||||
|| obj.seed != null || obj.controls != null) {
|
|| obj.seed != null || obj.controls != null || obj.checkpoint != null
|
||||||
|
|| obj.negative != null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -122,4 +169,5 @@ export function attachPatch(SA) {
|
|||||||
SA.isTerminalStreamPatch = isTerminalStreamPatch;
|
SA.isTerminalStreamPatch = isTerminalStreamPatch;
|
||||||
SA.normalizePatch = normalizePatch;
|
SA.normalizePatch = normalizePatch;
|
||||||
SA.extractPatch = extractPatch;
|
SA.extractPatch = extractPatch;
|
||||||
|
SA.generateFlagOn = generateFlagOn;
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-4
@@ -59,7 +59,11 @@ export function normalizeDelta(raw) {
|
|||||||
}
|
}
|
||||||
const delta = { ...raw };
|
const delta = { ...raw };
|
||||||
const acts = Array.isArray(delta.actions) ? delta.actions.map(String) : [];
|
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;
|
delta.generate = true;
|
||||||
}
|
}
|
||||||
if (typeof delta.ask === 'string') {
|
if (typeof delta.ask === 'string') {
|
||||||
@@ -77,7 +81,8 @@ export function patchWantsGenerate(patch) {
|
|||||||
if (!patch || typeof patch !== 'object') {
|
if (!patch || typeof patch !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (patch.generate === true) {
|
const n = normalizeDelta(patch);
|
||||||
|
if (n?.generate === true) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
|
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
|
||||||
@@ -340,10 +345,14 @@ export function fullSettingsDump(session, extras = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveTurnIntent(patch, userText, { vetoFn } = {}) {
|
/** Model generate/actions, or askGenerateFn when a prompt/delta exists. vetoFn / fromAutoCritique cancel. */
|
||||||
|
export function resolveTurnIntent(patch, userText, { vetoFn, askGenerateFn, fromAutoCritique } = {}) {
|
||||||
const delta = normalizeDelta(patch) || {};
|
const delta = normalizeDelta(patch) || {};
|
||||||
const vetoed = typeof vetoFn === 'function' ? !!vetoFn(userText) : false;
|
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 hasLook = delta.look_at != null || delta.vision_from != null || delta.vision_slots != null;
|
||||||
const look = !!(hasLook && !generate && !vetoed);
|
const look = !!(hasLook && !generate && !vetoed);
|
||||||
const ask = patchAskList(delta);
|
const ask = patchAskList(delta);
|
||||||
|
|||||||
+14
-3
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
|||||||
import {
|
import {
|
||||||
userAsksLook,
|
userAsksLook,
|
||||||
userAsksNoGenerate,
|
userAsksNoGenerate,
|
||||||
|
userAsksGenerate,
|
||||||
resolveTurnIntent,
|
resolveTurnIntent,
|
||||||
packWantsVision,
|
packWantsVision,
|
||||||
} from '../src/intent.js';
|
} from '../src/intent.js';
|
||||||
@@ -19,7 +20,14 @@ describe('intent.js', () => {
|
|||||||
assert.equal(userAsksNoGenerate('нарисуй лису'), false);
|
assert.equal(userAsksNoGenerate('нарисуй лису'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolveTurnIntent uses model generate only + veto', () => {
|
it('userAsksGenerate detects сгенерируй', () => {
|
||||||
|
assert.equal(userAsksGenerate('сгенерируй'), true);
|
||||||
|
assert.equal(userAsksGenerate('нарисуй лису'), true);
|
||||||
|
assert.equal(userAsksGenerate('что ты умеешь'), false);
|
||||||
|
assert.equal(userAsksGenerate('не генерируй'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolveTurnIntent uses model generate + user generate ask + veto', () => {
|
||||||
const lookOnly = resolveTurnIntent({ look_at: ['generate'] }, 'что не так с кадром?', {});
|
const lookOnly = resolveTurnIntent({ look_at: ['generate'] }, 'что не так с кадром?', {});
|
||||||
assert.equal(lookOnly.generate, false);
|
assert.equal(lookOnly.generate, false);
|
||||||
assert.equal(lookOnly.look, true);
|
assert.equal(lookOnly.look, true);
|
||||||
@@ -27,12 +35,15 @@ describe('intent.js', () => {
|
|||||||
const gen = resolveTurnIntent({ prompt: 'fox', generate: true }, 'нарисуй лису', {});
|
const gen = resolveTurnIntent({ prompt: 'fox', generate: true }, 'нарисуй лису', {});
|
||||||
assert.equal(gen.generate, true);
|
assert.equal(gen.generate, true);
|
||||||
|
|
||||||
|
const userGen = resolveTurnIntent({ prompt: 'A slender redhead in leather' }, 'сгенерируй', {});
|
||||||
|
assert.equal(userGen.generate, true);
|
||||||
|
|
||||||
const vetoed = resolveTurnIntent({ prompt: 'fox', generate: true }, 'только запомни промпт', {});
|
const vetoed = resolveTurnIntent({ prompt: 'fox', generate: true }, 'только запомни промпт', {});
|
||||||
assert.equal(vetoed.generate, false);
|
assert.equal(vetoed.generate, false);
|
||||||
assert.equal(vetoed.vetoed, true);
|
assert.equal(vetoed.vetoed, true);
|
||||||
|
|
||||||
const noHeuristic = resolveTurnIntent({ prompt: 'fox' }, 'нарисуй красивую лису в снегу', {});
|
const chatOnly = resolveTurnIntent({ prompt: 'fox' }, 'что ты умеешь про генерацию?', {});
|
||||||
assert.equal(noHeuristic.generate, false);
|
assert.equal(chatOnly.generate, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('packWantsVision for critique pack', () => {
|
it('packWantsVision for critique pack', () => {
|
||||||
|
|||||||
@@ -32,6 +32,30 @@ describe('patch.js', () => {
|
|||||||
assert.equal(p.generate, true);
|
assert.equal(p.generate, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('extractPatch prefers last terminal fence over a later weak pack fence', () => {
|
||||||
|
const text = [
|
||||||
|
'ok',
|
||||||
|
'```json',
|
||||||
|
'{"prompt":"A slender redhead in a leather jacket, 85mm, street light","generate":true}',
|
||||||
|
'```',
|
||||||
|
'```json',
|
||||||
|
'{"pack":"ordinary"}',
|
||||||
|
'```',
|
||||||
|
].join('\n');
|
||||||
|
const { patch } = extractPatch(text);
|
||||||
|
assert.equal(patch.generate, true);
|
||||||
|
assert.match(patch.prompt, /redhead/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extractPatch accepts generate true string and unfenced object', () => {
|
||||||
|
const fenced = extractPatch('ok\n```json\n{"prompt":"A slender redhead in a leather jacket, 85mm","generate":"true"}\n```');
|
||||||
|
assert.equal(fenced.patch.generate, true);
|
||||||
|
assert.match(fenced.patch.prompt, /redhead/);
|
||||||
|
const raw = extractPatch('done\n{"prompt":"A fiery redhead in leather, street light","generate":true}');
|
||||||
|
assert.equal(raw.patch.generate, true);
|
||||||
|
assert.equal(raw.prose, 'done');
|
||||||
|
});
|
||||||
|
|
||||||
it('scheduler-only patch is detected with full key list', () => {
|
it('scheduler-only patch is detected with full key list', () => {
|
||||||
setPatchKeys(['prompt', 'scheduler', 'generate', 'ask']);
|
setPatchKeys(['prompt', 'scheduler', 'generate', 'ask']);
|
||||||
assert.equal(isPatchObject({ scheduler: 'euler' }), true);
|
assert.equal(isPatchObject({ scheduler: 'euler' }), true);
|
||||||
@@ -77,6 +101,17 @@ describe('session.js', () => {
|
|||||||
assert.equal(intent.vetoed, true);
|
assert.equal(intent.vetoed, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resolveTurnIntent honors сгенерируй when prompt is present', () => {
|
||||||
|
const intent = resolveTurnIntent(
|
||||||
|
{ prompt: 'A slender redhead in a leather jacket' },
|
||||||
|
'сгенерируй',
|
||||||
|
{ askGenerateFn: (t) => /сгенер/i.test(t) },
|
||||||
|
);
|
||||||
|
assert.equal(intent.generate, true);
|
||||||
|
assert.equal(patchWantsGenerate({ generate: 'true' }), true);
|
||||||
|
assert.equal(patchWantsGenerate({ generate: true }), true);
|
||||||
|
});
|
||||||
|
|
||||||
it('mergeExactParamsForGenerate fills turbo profile when LLM omits steps/cfg', () => {
|
it('mergeExactParamsForGenerate fills turbo profile when LLM omits steps/cfg', () => {
|
||||||
const exact = {
|
const exact = {
|
||||||
generation: { profile: 'turbo', steps: 8, cfg: 1, sigma_shift: 1.15 },
|
generation: { profile: 'turbo', steps: 8, cfg: 1, sigma_shift: 1.15 },
|
||||||
|
|||||||
Reference in New Issue
Block a user