10690 lines
418 KiB
JavaScript
10690 lines
418 KiB
JavaScript
(() => {
|
||
// src/api.js
|
||
function createRequest() {
|
||
return function request(name, body) {
|
||
return new Promise((resolve, reject) => {
|
||
if (typeof genericRequest !== "function") {
|
||
reject(new Error("genericRequest unavailable"));
|
||
return;
|
||
}
|
||
genericRequest(
|
||
name,
|
||
body || {},
|
||
(data) => {
|
||
if (data && data.error) {
|
||
reject(new Error(String(data.error)));
|
||
} else {
|
||
resolve(data);
|
||
}
|
||
},
|
||
0,
|
||
(err) => reject(err instanceof Error ? err : new Error(String(err || "request failed")))
|
||
);
|
||
});
|
||
};
|
||
}
|
||
function attachApi(SA2) {
|
||
SA2.request = createRequest();
|
||
}
|
||
|
||
// src/patch.js
|
||
var DEFAULT_PATCH_KEYS = [
|
||
"prompt",
|
||
"negative",
|
||
"loras",
|
||
"width",
|
||
"height",
|
||
"steps",
|
||
"cfg",
|
||
"seed",
|
||
"sigma_shift",
|
||
"sampler",
|
||
"scheduler",
|
||
"actions",
|
||
"generate",
|
||
"ask",
|
||
"checkpoint",
|
||
"use_init_image",
|
||
"clear_init_image",
|
||
"init_creativity",
|
||
"denoise",
|
||
"use_mask_image",
|
||
"clear_mask_image",
|
||
"mask_blur",
|
||
"mask_grow",
|
||
"look_at",
|
||
"vision_from",
|
||
"vision_slots",
|
||
"slot_to_init",
|
||
"slot_to_mask",
|
||
"snapshot_generate",
|
||
"select_slot",
|
||
"aspect",
|
||
"images",
|
||
"batch",
|
||
"vary",
|
||
"lock_seed",
|
||
"creativity",
|
||
"intensity",
|
||
"complexity",
|
||
"movement",
|
||
"clear_prompt_images",
|
||
"slot_to_prompt_image",
|
||
"pack",
|
||
"persona",
|
||
"controls",
|
||
"inventory_query",
|
||
"variants"
|
||
];
|
||
var PATCH_KEYS = DEFAULT_PATCH_KEYS.slice();
|
||
function setPatchKeys(keys) {
|
||
if (Array.isArray(keys) && keys.length) {
|
||
PATCH_KEYS = keys.map(String);
|
||
}
|
||
}
|
||
var FENCE_RE = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||
function has(obj, key) {
|
||
return obj[key] !== void 0 && obj[key] !== null;
|
||
}
|
||
function generateFlagOn(obj) {
|
||
if (!obj || typeof obj !== "object") {
|
||
return false;
|
||
}
|
||
const g = obj.generate;
|
||
if (g === true || g === 1) {
|
||
return true;
|
||
}
|
||
if (typeof g === "string" && /^(true|1|yes|on)$/i.test(g.trim())) {
|
||
return true;
|
||
}
|
||
const acts = Array.isArray(obj.actions) ? obj.actions.map(String) : [];
|
||
return acts.includes("generate");
|
||
}
|
||
function isPatchObject2(obj) {
|
||
if (!obj || typeof obj !== "object") {
|
||
return false;
|
||
}
|
||
return PATCH_KEYS.some((k) => has(obj, k));
|
||
}
|
||
function normalizePatch(patch) {
|
||
if (!patch || typeof patch !== "object") {
|
||
return patch;
|
||
}
|
||
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
|
||
if (generateFlagOn(patch) || acts.includes("generate")) {
|
||
patch.generate = true;
|
||
}
|
||
if (typeof patch.ask === "string") {
|
||
patch.ask = [patch.ask];
|
||
}
|
||
if (!has(patch, "init_creativity") && has(patch, "denoise")) {
|
||
patch.init_creativity = patch.denoise;
|
||
}
|
||
if (!has(patch, "look_at")) {
|
||
if (has(patch, "vision_from")) {
|
||
patch.look_at = patch.vision_from;
|
||
} else if (has(patch, "vision_slots")) {
|
||
patch.look_at = patch.vision_slots;
|
||
}
|
||
}
|
||
return patch;
|
||
}
|
||
function tryParsePatchJson(raw) {
|
||
try {
|
||
const obj = JSON.parse(String(raw || "").trim());
|
||
if (isPatchObject2(obj)) {
|
||
return normalizePatch(obj);
|
||
}
|
||
} catch (e) {
|
||
}
|
||
return null;
|
||
}
|
||
function extractPatch(text) {
|
||
if (!text) {
|
||
return { prose: text || "", patch: null };
|
||
}
|
||
const re = new RegExp(FENCE_RE.source, "gi");
|
||
let match;
|
||
let lastAny = null;
|
||
let lastAnyIndex = -1;
|
||
let lastAnyLen = 0;
|
||
let lastTerminal = null;
|
||
let lastTermIndex = -1;
|
||
let lastTermLen = 0;
|
||
while ((match = re.exec(text)) !== null) {
|
||
const parsed = tryParsePatchJson(match[1]);
|
||
if (!parsed) {
|
||
continue;
|
||
}
|
||
lastAny = parsed;
|
||
lastAnyIndex = match.index;
|
||
lastAnyLen = match[0].length;
|
||
if (isTerminalStreamPatch(parsed)) {
|
||
lastTerminal = parsed;
|
||
lastTermIndex = match.index;
|
||
lastTermLen = match[0].length;
|
||
}
|
||
}
|
||
const chosen = lastTerminal || lastAny;
|
||
if (chosen) {
|
||
const idx = lastTerminal ? lastTermIndex : lastAnyIndex;
|
||
const len = lastTerminal ? lastTermLen : lastAnyLen;
|
||
const prose = (text.slice(0, idx) + text.slice(idx + len)).trim();
|
||
return { prose, patch: chosen };
|
||
}
|
||
const brace = text.lastIndexOf("{");
|
||
if (brace >= 0) {
|
||
const parsed = tryParsePatchJson(text.slice(brace));
|
||
if (parsed) {
|
||
return { prose: text.slice(0, brace).trim(), patch: parsed };
|
||
}
|
||
}
|
||
return { prose: text, patch: null };
|
||
}
|
||
function isTerminalStreamPatch(obj) {
|
||
if (!obj || typeof obj !== "object") {
|
||
return false;
|
||
}
|
||
if (generateFlagOn(obj)) {
|
||
return true;
|
||
}
|
||
if (Array.isArray(obj.ask) && obj.ask.length) {
|
||
return true;
|
||
}
|
||
if (typeof obj.ask === "string" && obj.ask) {
|
||
return true;
|
||
}
|
||
if (Array.isArray(obj.variants) && obj.variants.length) {
|
||
return true;
|
||
}
|
||
if (obj.look_at != null || obj.vision_from != null || obj.vision_slots != null) {
|
||
return true;
|
||
}
|
||
if (String(obj.prompt || "").trim().length >= 48) {
|
||
return true;
|
||
}
|
||
if (obj.loras != null || obj.aspect != null || obj.steps != null || obj.width != null || obj.height != null || obj.cfg != null || obj.seed != null || obj.controls != null || obj.checkpoint != null || obj.negative != null) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function attachPatch(SA2) {
|
||
SA2.PATCH_KEYS = PATCH_KEYS;
|
||
SA2.setPatchKeys = setPatchKeys;
|
||
SA2.isPatchObject = isPatchObject2;
|
||
SA2.isTerminalStreamPatch = isTerminalStreamPatch;
|
||
SA2.normalizePatch = normalizePatch;
|
||
SA2.extractPatch = extractPatch;
|
||
SA2.generateFlagOn = generateFlagOn;
|
||
}
|
||
|
||
// src/persist.js
|
||
var LS_CHATS = "swarm_assistent_chats_v1";
|
||
var SAVE_DEBOUNCE_MS = 700;
|
||
var timers = { chats: /* @__PURE__ */ new Map(), ui: null };
|
||
function normalizeChat(raw) {
|
||
if (!raw || !raw.id) {
|
||
return null;
|
||
}
|
||
return {
|
||
id: String(raw.id),
|
||
title: String(raw.title || "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442"),
|
||
createdAt: Number(raw.createdAt) || Date.now(),
|
||
updatedAt: Number(raw.updatedAt) || Date.now(),
|
||
messages: Array.isArray(raw.messages) ? raw.messages : [],
|
||
messages_count: Number(raw.messages_count) || (Array.isArray(raw.messages) ? raw.messages.length : 0),
|
||
params: raw.params && typeof raw.params === "object" ? raw.params : null
|
||
};
|
||
}
|
||
function attachPersist(SA2, request = SA2.request) {
|
||
async function loadChats() {
|
||
let chats = [];
|
||
try {
|
||
const data = await request("AssistentListChats", { with_messages: true });
|
||
chats = (data?.chats || []).map(normalizeChat).filter(Boolean);
|
||
} catch (e) {
|
||
console.warn("Assistent: disk chats unavailable", e);
|
||
return null;
|
||
}
|
||
return chats.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
||
}
|
||
async function getChat(id) {
|
||
if (!id) {
|
||
return null;
|
||
}
|
||
const data = await request("AssistentGetChat", { id });
|
||
return normalizeChat(data?.chat);
|
||
}
|
||
async function searchChats(q) {
|
||
const query = String(q || "").trim();
|
||
if (query.length < 2) {
|
||
return [];
|
||
}
|
||
const data = await request("AssistentListChats", { q: query, with_messages: false, limit: 40 });
|
||
return (data?.chats || []).map(normalizeChat).filter(Boolean);
|
||
}
|
||
function saveChat(chat, { immediate = false } = {}) {
|
||
const clean = normalizeChat(chat);
|
||
if (!clean) {
|
||
return Promise.resolve(null);
|
||
}
|
||
const send = () => {
|
||
timers.chats.delete(clean.id);
|
||
return request("AssistentSaveChat", {
|
||
id: clean.id,
|
||
title: clean.title,
|
||
messages: clean.messages,
|
||
params: clean.params,
|
||
createdAt: clean.createdAt,
|
||
updatedAt: clean.updatedAt
|
||
});
|
||
};
|
||
if (immediate) {
|
||
const pending2 = timers.chats.get(clean.id);
|
||
if (pending2) {
|
||
clearTimeout(pending2);
|
||
}
|
||
return send();
|
||
}
|
||
const pending = timers.chats.get(clean.id);
|
||
if (pending) {
|
||
clearTimeout(pending);
|
||
}
|
||
timers.chats.set(clean.id, setTimeout(() => {
|
||
send().catch((e) => console.warn("Assistent: chat save failed", clean.id, e));
|
||
}, SAVE_DEBOUNCE_MS));
|
||
return Promise.resolve(null);
|
||
}
|
||
function deleteChat(id) {
|
||
if (!id) {
|
||
return Promise.resolve(null);
|
||
}
|
||
const pending = timers.chats.get(id);
|
||
if (pending) {
|
||
clearTimeout(pending);
|
||
timers.chats.delete(id);
|
||
}
|
||
return request("AssistentDeleteChat", { id });
|
||
}
|
||
async function loadUiState() {
|
||
try {
|
||
const data = await request("AssistentGetUiState", {});
|
||
const ui = data?.ui_state;
|
||
return ui && typeof ui === "object" ? ui : null;
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
function saveUiState(uiState, { immediate = false } = {}) {
|
||
if (!uiState || typeof uiState !== "object") {
|
||
return Promise.resolve(null);
|
||
}
|
||
const send = () => {
|
||
timers.ui = null;
|
||
return request("AssistentSaveUiState", { ui_state: uiState });
|
||
};
|
||
if (timers.ui) {
|
||
clearTimeout(timers.ui);
|
||
timers.ui = null;
|
||
}
|
||
if (immediate) {
|
||
return send();
|
||
}
|
||
timers.ui = setTimeout(() => {
|
||
send().catch((e) => console.warn("Assistent: ui-state save failed", e));
|
||
}, SAVE_DEBOUNCE_MS);
|
||
return Promise.resolve(null);
|
||
}
|
||
SA2.persist = {
|
||
LS_CHATS,
|
||
loadChats,
|
||
getChat,
|
||
searchChats,
|
||
saveChat,
|
||
deleteChat,
|
||
loadUiState,
|
||
saveUiState
|
||
};
|
||
}
|
||
|
||
// src/session.js
|
||
var MAX_DATA_URL_CHARS = 35e4;
|
||
var GEN_KEYS = [
|
||
"prompt",
|
||
"negative",
|
||
"width",
|
||
"height",
|
||
"aspect",
|
||
"steps",
|
||
"cfg",
|
||
"sigma_shift",
|
||
"seed",
|
||
"sampler",
|
||
"scheduler",
|
||
"batch",
|
||
"checkpoint",
|
||
"loras",
|
||
"controls",
|
||
"use_init_image",
|
||
"clear_init_image",
|
||
"init_creativity",
|
||
"denoise",
|
||
"use_mask_image",
|
||
"clear_mask_image",
|
||
"mask_blur",
|
||
"mask_grow"
|
||
];
|
||
function emptySession() {
|
||
return {
|
||
gen: {
|
||
prompt: "",
|
||
negative: "",
|
||
width: null,
|
||
height: null,
|
||
aspect: null,
|
||
steps: null,
|
||
cfg: null,
|
||
sigma_shift: null,
|
||
seed: null,
|
||
sampler: null,
|
||
scheduler: null,
|
||
batch: null,
|
||
checkpoint: null,
|
||
loras: [],
|
||
controls: {},
|
||
use_init_image: false,
|
||
clear_init_image: false,
|
||
init_creativity: null,
|
||
denoise: null,
|
||
use_mask_image: false,
|
||
clear_mask_image: false,
|
||
mask_blur: null,
|
||
mask_grow: null
|
||
},
|
||
board: {
|
||
slots: [],
|
||
selectedSlotId: "ref1",
|
||
genResults: [],
|
||
selectedGenResultId: null,
|
||
refSeq: 1
|
||
},
|
||
persona: "neutral",
|
||
pack: "ordinary",
|
||
context_memory: null
|
||
};
|
||
}
|
||
function normalizeDelta(raw) {
|
||
if (!raw || typeof raw !== "object") {
|
||
return null;
|
||
}
|
||
const delta = { ...raw };
|
||
const acts = Array.isArray(delta.actions) ? delta.actions.map(String) : [];
|
||
const g = delta.generate;
|
||
const generateOn = g === true || g === 1 || typeof g === "string" && /^(true|1|yes|on)$/i.test(g.trim()) || acts.includes("generate");
|
||
if (generateOn) {
|
||
delta.generate = true;
|
||
}
|
||
if (typeof delta.ask === "string") {
|
||
delta.ask = [delta.ask];
|
||
}
|
||
if (!Array.isArray(delta.ask)) {
|
||
delete delta.ask;
|
||
} else {
|
||
delta.ask = delta.ask.map(String).filter(Boolean);
|
||
}
|
||
return delta;
|
||
}
|
||
function patchWantsGenerate(patch) {
|
||
if (!patch || typeof patch !== "object") {
|
||
return false;
|
||
}
|
||
const n = normalizeDelta(patch);
|
||
if (n?.generate === true) {
|
||
return true;
|
||
}
|
||
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
|
||
return acts.includes("generate");
|
||
}
|
||
function patchAskList(patch) {
|
||
const n = normalizeDelta(patch);
|
||
return Array.isArray(n?.ask) ? n.ask : [];
|
||
}
|
||
function mergeDelta(session, rawDelta) {
|
||
const base = session && typeof session === "object" ? structuredCloneSession(session) : emptySession();
|
||
const delta = normalizeDelta(rawDelta);
|
||
if (!delta) {
|
||
return base;
|
||
}
|
||
if (!base.gen) {
|
||
base.gen = emptySession().gen;
|
||
}
|
||
for (const key of GEN_KEYS) {
|
||
if (delta[key] === void 0 || delta[key] === null) {
|
||
continue;
|
||
}
|
||
if (key === "loras" && Array.isArray(delta.loras)) {
|
||
base.gen.loras = delta.loras.map((l) => ({
|
||
name: l?.name || l,
|
||
weight: l?.weight != null ? Number(l.weight) : 1,
|
||
triggers: Array.isArray(l?.triggers) ? l.triggers : void 0,
|
||
trigger_phrase: l?.trigger_phrase || void 0
|
||
})).filter((l) => l.name);
|
||
continue;
|
||
}
|
||
if (key === "controls" && typeof delta.controls === "object") {
|
||
base.gen.controls = { ...base.gen.controls || {}, ...delta.controls };
|
||
continue;
|
||
}
|
||
if (key === "checkpoint") {
|
||
base.gen.checkpoint = typeof delta.checkpoint === "object" ? { ...delta.checkpoint } : { name: String(delta.checkpoint) };
|
||
continue;
|
||
}
|
||
base.gen[key] = delta[key];
|
||
}
|
||
if (delta.images != null && delta.batch == null) {
|
||
base.gen.batch = delta.images;
|
||
}
|
||
if (delta.pack) {
|
||
base.pack = String(delta.pack);
|
||
}
|
||
if (delta.persona) {
|
||
base.persona = String(delta.persona);
|
||
}
|
||
return base;
|
||
}
|
||
function structuredCloneSession(session) {
|
||
try {
|
||
return JSON.parse(JSON.stringify(session));
|
||
} catch {
|
||
return emptySession();
|
||
}
|
||
}
|
||
function slimSrc(src) {
|
||
if (!src || typeof src !== "string") {
|
||
return null;
|
||
}
|
||
const s = src.trim();
|
||
if (!s || s.startsWith("#")) {
|
||
return null;
|
||
}
|
||
if (s.startsWith("data:") && s.length > MAX_DATA_URL_CHARS) {
|
||
return null;
|
||
}
|
||
return s;
|
||
}
|
||
function snapshotFromLive({
|
||
genFields,
|
||
board,
|
||
persona,
|
||
pack,
|
||
context_memory
|
||
}) {
|
||
const session = emptySession();
|
||
if (genFields && typeof genFields === "object") {
|
||
for (const key of GEN_KEYS) {
|
||
if (genFields[key] !== void 0) {
|
||
session.gen[key] = genFields[key];
|
||
}
|
||
}
|
||
}
|
||
session.persona = persona || "neutral";
|
||
session.pack = pack || "ordinary";
|
||
if (context_memory && typeof context_memory === "object") {
|
||
session.context_memory = context_memory;
|
||
}
|
||
if (board && typeof board === "object") {
|
||
session.board = {
|
||
slots: (board.slots || []).map((s) => ({
|
||
id: s.id,
|
||
type: s.type,
|
||
label: s.label,
|
||
src: slimSrc(s.src),
|
||
attach: !!s.attach,
|
||
note: s.note || null
|
||
})),
|
||
selectedSlotId: board.selectedSlotId || "ref1",
|
||
genResults: (board.genResults || []).map((r) => ({
|
||
id: r.id,
|
||
label: r.label,
|
||
src: slimSrc(r.src),
|
||
patch: r.patch || null
|
||
})),
|
||
selectedGenResultId: board.selectedGenResultId || null,
|
||
refSeq: board.refSeq || 1
|
||
};
|
||
}
|
||
return session;
|
||
}
|
||
function sessionFromLegacyParams(params) {
|
||
if (!params || typeof params !== "object") {
|
||
return emptySession();
|
||
}
|
||
if (params.gen && typeof params.gen === "object") {
|
||
const s2 = emptySession();
|
||
s2.gen = { ...s2.gen, ...params.gen };
|
||
if (params.board && typeof params.board === "object") {
|
||
s2.board = { ...s2.board, ...params.board };
|
||
}
|
||
s2.persona = params.persona || s2.persona;
|
||
s2.pack = params.pack || s2.pack;
|
||
if (params.context_memory && typeof params.context_memory === "object") {
|
||
s2.context_memory = params.context_memory;
|
||
}
|
||
return s2;
|
||
}
|
||
const s = emptySession();
|
||
for (const key of GEN_KEYS) {
|
||
if (params[key] !== void 0 && params[key] !== null) {
|
||
s.gen[key] = params[key];
|
||
}
|
||
}
|
||
if (Array.isArray(params.loras)) {
|
||
s.gen.loras = params.loras;
|
||
}
|
||
if (params.checkpoint) {
|
||
s.gen.checkpoint = typeof params.checkpoint === "object" ? params.checkpoint : { name: String(params.checkpoint) };
|
||
}
|
||
s.persona = params.persona || "neutral";
|
||
s.pack = params.pack || "ordinary";
|
||
if (params.context_memory && typeof params.context_memory === "object") {
|
||
s.context_memory = params.context_memory;
|
||
}
|
||
s.board.genResults = Array.isArray(params.genResults) ? params.genResults : [];
|
||
s.board.selectedGenResultId = params.selectedGenResultId || null;
|
||
if (Array.isArray(params.slots)) {
|
||
s.board.slots = params.slots;
|
||
}
|
||
if (params.selectedSlotId) {
|
||
s.board.selectedSlotId = params.selectedSlotId;
|
||
}
|
||
if (params.refSeq) {
|
||
s.board.refSeq = params.refSeq;
|
||
}
|
||
return s;
|
||
}
|
||
function toPersistParams(session) {
|
||
const s = session && typeof session === "object" ? session : emptySession();
|
||
const out = {
|
||
gen: s.gen || emptySession().gen,
|
||
board: s.board || emptySession().board,
|
||
persona: s.persona || "neutral",
|
||
pack: s.pack || "ordinary"
|
||
};
|
||
if (s.context_memory && typeof s.context_memory === "object") {
|
||
out.context_memory = s.context_memory;
|
||
}
|
||
return out;
|
||
}
|
||
function slimText(t, max) {
|
||
const s = String(t || "");
|
||
if (s.length <= max) {
|
||
return s;
|
||
}
|
||
return `${s.slice(0, max)}\u2026`;
|
||
}
|
||
function compactContext(session, extras = {}) {
|
||
const s = session && typeof session === "object" ? session : emptySession();
|
||
const g = s.gen || {};
|
||
const board = s.board || {};
|
||
const slots = board.slots || [];
|
||
const genSlot = slots.find((x) => x.type === "generate" || x.id === "generate");
|
||
const refs = slots.filter((x) => x.type === "ref" || String(x.id || "").startsWith("ref"));
|
||
return {
|
||
session: true,
|
||
prompt: slimText(g.prompt, extras.promptMax || 2e3),
|
||
negative: slimText(g.negative, 500),
|
||
aspect: g.aspect || null,
|
||
width: g.width ?? null,
|
||
height: g.height ?? null,
|
||
steps: g.steps ?? null,
|
||
cfg: g.cfg ?? null,
|
||
seed: g.seed ?? null,
|
||
sigma_shift: g.sigma_shift ?? null,
|
||
sampler: g.sampler || null,
|
||
scheduler: g.scheduler || null,
|
||
batch: g.batch ?? null,
|
||
checkpoint: g.checkpoint?.name || g.checkpoint || null,
|
||
selected_loras: (g.loras || []).map((l) => ({
|
||
name: l.name || l,
|
||
weight: l.weight != null ? l.weight : 1
|
||
})),
|
||
persona: s.persona || "neutral",
|
||
pack: s.pack || "ordinary",
|
||
board: {
|
||
has_generate: !!(genSlot?.src || (board.genResults || []).some((r) => r.src)),
|
||
refs: refs.map((r) => ({ id: r.id, has_image: !!r.src, attach: !!r.attach })),
|
||
gen_results: (board.genResults || []).map((r) => ({
|
||
id: r.id,
|
||
label: r.label,
|
||
has_image: !!r.src,
|
||
selected: r.id === board.selectedGenResultId
|
||
})),
|
||
selected_slot: board.selectedSlotId || null
|
||
},
|
||
architecture_ok: extras.architecture_ok !== false,
|
||
...extras.extra
|
||
};
|
||
}
|
||
function fullSettingsDump(session, extras = {}) {
|
||
const compact = compactContext(session, extras);
|
||
const s = session && typeof session === "object" ? session : emptySession();
|
||
return {
|
||
...compact,
|
||
detail: "settings",
|
||
gen: { ...s.gen || {} },
|
||
controls: s.gen?.controls || {},
|
||
exact: extras.exact || null,
|
||
krea_profiles: extras.kreaProfiles || null,
|
||
session_exact: extras.sessionExact || null
|
||
};
|
||
}
|
||
function resolveTurnIntent(patch, userText, { vetoFn, askGenerateFn, fromAutoCritique } = {}) {
|
||
const delta = normalizeDelta(patch) || {};
|
||
const vetoed = typeof vetoFn === "function" ? !!vetoFn(userText) : false;
|
||
const modelAsked = patchWantsGenerate(delta);
|
||
const userAsked = typeof askGenerateFn === "function" && !!askGenerateFn(userText) && !!(modelAsked || String(delta.prompt || "").trim());
|
||
const generate = !vetoed && !fromAutoCritique && (modelAsked || userAsked);
|
||
const hasLook = delta.look_at != null || delta.vision_from != null || delta.vision_slots != null;
|
||
const look = !!(hasLook && !generate && !vetoed);
|
||
const ask = patchAskList(delta);
|
||
return { generate, look, vetoed, ask };
|
||
}
|
||
var EXACT_GENERATE_PARAM_KEYS = ["steps", "cfg", "sigma_shift"];
|
||
function resolveExactProfileDefaults({ exact, profiles, profileName } = {}) {
|
||
const gen = exact?.generation && typeof exact.generation === "object" ? exact.generation : {};
|
||
const profile = profileName || gen.profile || "turbo";
|
||
const fromProfile = profiles?.[profile] && typeof profiles[profile] === "object" ? profiles[profile] : {};
|
||
return {
|
||
profile,
|
||
steps: fromProfile.steps ?? gen.steps ?? null,
|
||
cfg: fromProfile.cfg ?? gen.cfg ?? null,
|
||
sigma_shift: fromProfile.sigma_shift ?? gen.sigma_shift ?? null
|
||
};
|
||
}
|
||
function mergeExactParamsForGenerate(patch, {
|
||
exact,
|
||
profiles,
|
||
profileName,
|
||
sessionExact,
|
||
userParamIntent
|
||
} = {}) {
|
||
if (!patchWantsGenerate(patch)) {
|
||
return { patch, clearSessionKeys: [], profile: null };
|
||
}
|
||
const defaults = resolveExactProfileDefaults({ exact, profiles, profileName });
|
||
const out = { ...patch };
|
||
const clearSessionKeys = [];
|
||
for (const key of EXACT_GENERATE_PARAM_KEYS) {
|
||
if (out[key] != null) {
|
||
if (!userParamIntent && defaults[key] != null && String(out[key]) !== String(defaults[key])) {
|
||
out[key] = defaults[key];
|
||
if (sessionExact?.[key] != null && String(sessionExact[key]) !== String(defaults[key])) {
|
||
clearSessionKeys.push(key);
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
if (userParamIntent && sessionExact?.[key] != null) {
|
||
out[key] = sessionExact[key];
|
||
continue;
|
||
}
|
||
if (defaults[key] != null) {
|
||
out[key] = defaults[key];
|
||
if (sessionExact?.[key] != null && String(sessionExact[key]) !== String(defaults[key])) {
|
||
clearSessionKeys.push(key);
|
||
}
|
||
}
|
||
}
|
||
return { patch: out, clearSessionKeys, profile: defaults.profile };
|
||
}
|
||
function attachSession(SA2) {
|
||
SA2.session = {
|
||
emptySession,
|
||
normalizeDelta,
|
||
mergeDelta,
|
||
patchWantsGenerate,
|
||
patchAskList,
|
||
snapshotFromLive,
|
||
sessionFromLegacyParams,
|
||
toPersistParams,
|
||
compactContext,
|
||
fullSettingsDump,
|
||
resolveTurnIntent,
|
||
resolveExactProfileDefaults,
|
||
mergeExactParamsForGenerate,
|
||
EXACT_GENERATE_PARAM_KEYS,
|
||
GEN_KEYS
|
||
};
|
||
}
|
||
|
||
// src/context.js
|
||
function emptyContextMemory() {
|
||
return {
|
||
summary: "",
|
||
untilCount: 0,
|
||
foldedTurns: 0,
|
||
at: 0,
|
||
uiCollapsed: false,
|
||
promptEvalCount: null
|
||
};
|
||
}
|
||
function normalizeContextMemory(raw) {
|
||
if (!raw || typeof raw !== "object") {
|
||
return emptyContextMemory();
|
||
}
|
||
const summary = String(raw.summary || "").trim();
|
||
return {
|
||
summary,
|
||
untilCount: Math.max(0, Number(raw.untilCount) || 0),
|
||
foldedTurns: Math.max(0, Number(raw.foldedTurns) || 0),
|
||
at: Number(raw.at) || 0,
|
||
uiCollapsed: !!raw.uiCollapsed && !!summary,
|
||
promptEvalCount: raw.promptEvalCount != null ? Number(raw.promptEvalCount) || null : null
|
||
};
|
||
}
|
||
function charsToTokens(chars, charsPerToken = 3.2) {
|
||
const cpt = Math.max(1.5, Number(charsPerToken) || 3.2);
|
||
const n = Math.max(0, Number(chars) || 0);
|
||
return Math.ceil(n / cpt);
|
||
}
|
||
function estimateBudget(opts = {}) {
|
||
const numCtx = Math.max(1024, Number(opts.numCtx) || 16384);
|
||
const numPredict = Math.max(256, Number(opts.numPredict) || 3072);
|
||
const charsPerToken = Math.max(1.5, Number(opts.charsPerToken) || 3.2);
|
||
const compressAt = Math.min(0.95, Math.max(0.4, Number(opts.compressAt) || 0.7));
|
||
const systemChars = Math.max(0, Number(opts.systemChars) || 0);
|
||
const historyChars = Math.max(0, Number(opts.historyChars) || 0);
|
||
const memoryChars = Math.max(0, Number(opts.memoryChars) || 0);
|
||
const inputChars = systemChars + historyChars + memoryChars;
|
||
const estimated = charsToTokens(inputChars, charsPerToken);
|
||
const used = opts.promptEvalCount != null && Number(opts.promptEvalCount) > 0 ? Number(opts.promptEvalCount) : estimated;
|
||
const headroom = Math.max(1024, numCtx - numPredict);
|
||
const threshold = Math.floor(headroom * compressAt);
|
||
const ratio = numCtx > 0 ? used / numCtx : 0;
|
||
let level = "ok";
|
||
if (ratio >= 0.85 || used >= threshold) {
|
||
level = "hot";
|
||
} else if (ratio >= 0.65 || used >= threshold * 0.85) {
|
||
level = "warn";
|
||
}
|
||
return {
|
||
numCtx,
|
||
numPredict,
|
||
headroom,
|
||
threshold,
|
||
systemChars,
|
||
historyChars,
|
||
memoryChars,
|
||
inputChars,
|
||
estimated,
|
||
used,
|
||
fromEval: opts.promptEvalCount != null && Number(opts.promptEvalCount) > 0,
|
||
ratio,
|
||
level,
|
||
charsPerToken,
|
||
compressAt
|
||
};
|
||
}
|
||
function shouldCompress(budget, memory, historyLen, opts = {}) {
|
||
const keep = Math.max(2, Number(opts.keepMessages) || 8);
|
||
const len = Math.max(0, Number(historyLen) || 0);
|
||
const until = Math.max(0, Number(memory?.untilCount) || 0);
|
||
const uncovered = Math.max(0, len - until);
|
||
if (uncovered <= keep) {
|
||
return false;
|
||
}
|
||
const used = budget?.used ?? 0;
|
||
const threshold = budget?.threshold ?? Infinity;
|
||
return used >= threshold;
|
||
}
|
||
function assembleModelMessages(history, memory, keepTurns) {
|
||
const keep = Math.max(1, Number(keepTurns) || 4) * 2;
|
||
const until = Math.max(0, Number(memory?.untilCount) || 0);
|
||
const list = (history || []).filter((m) => m && (m.role === "user" || m.role === "assistant") && !m.systemish);
|
||
const afterSummary = until > 0 ? list.slice(until) : list;
|
||
const window2 = afterSummary.length > keep ? afterSummary.slice(-keep) : afterSummary;
|
||
return window2.map((m) => ({
|
||
role: m.role,
|
||
content: String(m.content || "").slice(0, 4e3)
|
||
}));
|
||
}
|
||
function messagesToFold(history, memory, keepTurns) {
|
||
const keep = Math.max(1, Number(keepTurns) || 4) * 2;
|
||
const list = (history || []).filter((m) => m && (m.role === "user" || m.role === "assistant") && !m.systemish);
|
||
const until = Math.max(0, Number(memory?.untilCount) || 0);
|
||
const foldEnd = Math.max(until, list.length - keep);
|
||
if (foldEnd <= until) {
|
||
return [];
|
||
}
|
||
return list.slice(until, foldEnd);
|
||
}
|
||
function mergeSummary(oldSummary, incoming) {
|
||
const next = String(incoming || "").trim();
|
||
if (!next) {
|
||
return String(oldSummary || "").trim();
|
||
}
|
||
const prev = String(oldSummary || "").trim();
|
||
if (!prev) {
|
||
return next;
|
||
}
|
||
return next;
|
||
}
|
||
function formatTokenShort(n) {
|
||
const v = Math.max(0, Number(n) || 0);
|
||
if (v >= 1e4) {
|
||
return `${(v / 1e3).toFixed(1)}k`;
|
||
}
|
||
if (v >= 1e3) {
|
||
return `${(v / 1e3).toFixed(1)}k`;
|
||
}
|
||
return String(Math.round(v));
|
||
}
|
||
function conversationMemoryBlock(memory, maxChars = 2400) {
|
||
const m = normalizeContextMemory(memory);
|
||
if (!m.summary) {
|
||
return null;
|
||
}
|
||
let text = m.summary;
|
||
if (text.length > maxChars) {
|
||
text = `${text.slice(0, maxChars)}\u2026`;
|
||
}
|
||
return {
|
||
summary: text,
|
||
until_count: m.untilCount,
|
||
folded_turns: m.foldedTurns
|
||
};
|
||
}
|
||
function attachContext(SA2) {
|
||
SA2.context = {
|
||
emptyContextMemory,
|
||
normalizeContextMemory,
|
||
charsToTokens,
|
||
estimateBudget,
|
||
shouldCompress,
|
||
assembleModelMessages,
|
||
messagesToFold,
|
||
mergeSummary,
|
||
formatTokenShort,
|
||
conversationMemoryBlock
|
||
};
|
||
}
|
||
|
||
// src/activity.js
|
||
var STEP_ICONS = {
|
||
think: "\u25C7",
|
||
stream: "\u270E",
|
||
delta: "\u21E2",
|
||
ask: "?",
|
||
look: "\u25CE",
|
||
prep: "\u21BB",
|
||
generate: "\u25B7",
|
||
merge: "\u2295",
|
||
warm: "\u25B2",
|
||
park: "\u25BC",
|
||
inventory: "\u25A4",
|
||
compress: "\u25A4",
|
||
done: "\u2713",
|
||
skip: "\u2013",
|
||
error: "!"
|
||
};
|
||
function createActivityController(opts = {}) {
|
||
const {
|
||
getMessagesEl,
|
||
scrollToBottom,
|
||
hideEmpty
|
||
} = opts;
|
||
let card = null;
|
||
let listEl = null;
|
||
let titleEl = null;
|
||
let steps = [];
|
||
let open = true;
|
||
function ensureCard() {
|
||
const box = typeof getMessagesEl === "function" ? getMessagesEl() : null;
|
||
if (!box) {
|
||
return null;
|
||
}
|
||
if (card && card.isConnected) {
|
||
return card;
|
||
}
|
||
if (typeof hideEmpty === "function") {
|
||
hideEmpty();
|
||
}
|
||
card = document.createElement("div");
|
||
card.className = "sa-activity sa-activity-live";
|
||
card.setAttribute("role", "status");
|
||
card.setAttribute("aria-live", "polite");
|
||
const head = document.createElement("button");
|
||
head.type = "button";
|
||
head.className = "sa-activity-head";
|
||
head.setAttribute("aria-expanded", "true");
|
||
const spin = document.createElement("span");
|
||
spin.className = "sa-activity-spin";
|
||
spin.setAttribute("aria-hidden", "true");
|
||
titleEl = document.createElement("span");
|
||
titleEl.className = "sa-activity-title";
|
||
titleEl.textContent = "Assistent";
|
||
const chev = document.createElement("span");
|
||
chev.className = "sa-activity-chev";
|
||
chev.setAttribute("aria-hidden", "true");
|
||
chev.textContent = "\u25BE";
|
||
head.appendChild(spin);
|
||
head.appendChild(titleEl);
|
||
head.appendChild(chev);
|
||
head.addEventListener("click", () => {
|
||
open = !open;
|
||
card.classList.toggle("sa-activity-collapsed", !open);
|
||
head.setAttribute("aria-expanded", open ? "true" : "false");
|
||
});
|
||
listEl = document.createElement("div");
|
||
listEl.className = "sa-activity-steps";
|
||
card.appendChild(head);
|
||
card.appendChild(listEl);
|
||
box.appendChild(card);
|
||
if (typeof scrollToBottom === "function") {
|
||
scrollToBottom();
|
||
}
|
||
return card;
|
||
}
|
||
function renderStep(step) {
|
||
const row = document.createElement("div");
|
||
row.className = `sa-activity-step sa-activity-${step.status || "running"}`;
|
||
row.dataset.id = step.id;
|
||
const icon = document.createElement("span");
|
||
icon.className = "sa-activity-icon";
|
||
icon.setAttribute("aria-hidden", "true");
|
||
icon.textContent = STEP_ICONS[step.kind] || STEP_ICONS.think;
|
||
const body = document.createElement("div");
|
||
body.className = "sa-activity-body";
|
||
const label = document.createElement("div");
|
||
label.className = "sa-activity-label";
|
||
label.textContent = step.label || step.id;
|
||
body.appendChild(label);
|
||
if (step.detail) {
|
||
const detail = document.createElement("div");
|
||
detail.className = "sa-activity-detail";
|
||
detail.textContent = step.detail;
|
||
body.appendChild(detail);
|
||
}
|
||
row.appendChild(icon);
|
||
row.appendChild(body);
|
||
return row;
|
||
}
|
||
function paint() {
|
||
if (!ensureCard() || !listEl) {
|
||
return;
|
||
}
|
||
listEl.replaceChildren(...steps.map(renderStep));
|
||
const running = steps.find((s) => s.status === "running");
|
||
const last = steps[steps.length - 1];
|
||
if (titleEl) {
|
||
titleEl.textContent = running ? running.label : last?.label || "Assistent";
|
||
}
|
||
card.classList.toggle("sa-activity-live", steps.some((s) => s.status === "running"));
|
||
card.classList.toggle("sa-activity-done", steps.length > 0 && steps.every((s) => s.status === "done" || s.status === "skip"));
|
||
if (typeof scrollToBottom === "function") {
|
||
scrollToBottom();
|
||
}
|
||
}
|
||
function begin(title) {
|
||
steps = [];
|
||
card = null;
|
||
listEl = null;
|
||
titleEl = null;
|
||
open = true;
|
||
ensureCard();
|
||
if (titleEl && title) {
|
||
titleEl.textContent = title;
|
||
}
|
||
paint();
|
||
}
|
||
function upsert(id, patch) {
|
||
ensureCard();
|
||
let step = steps.find((s) => s.id === id);
|
||
if (!step) {
|
||
step = { id, kind: "think", label: id, status: "running", detail: "" };
|
||
steps.push(step);
|
||
}
|
||
Object.assign(step, patch);
|
||
if (!step.status) {
|
||
step.status = "running";
|
||
}
|
||
paint();
|
||
return step;
|
||
}
|
||
function done(id, patch = {}) {
|
||
return upsert(id, { ...patch, status: "done" });
|
||
}
|
||
function skip(id, patch = {}) {
|
||
return upsert(id, { ...patch, status: "skip" });
|
||
}
|
||
function fail(id, patch = {}) {
|
||
return upsert(id, { ...patch, status: "error" });
|
||
}
|
||
function finish(summary) {
|
||
steps.forEach((s) => {
|
||
if (s.status === "running") {
|
||
s.status = "done";
|
||
}
|
||
});
|
||
if (summary && titleEl) {
|
||
titleEl.textContent = summary;
|
||
}
|
||
paint();
|
||
if (card) {
|
||
card.classList.remove("sa-activity-live");
|
||
card.classList.add("sa-activity-done");
|
||
}
|
||
}
|
||
function noteModelCommands(patch) {
|
||
if (!patch || typeof patch !== "object") {
|
||
return;
|
||
}
|
||
const keys = Object.keys(patch).filter((k) => patch[k] != null && !["actions", "generate", "ask", "look_at", "vision_from", "vision_slots", "notes", "variants"].includes(k));
|
||
if (keys.length) {
|
||
done("delta", {
|
||
kind: "delta",
|
||
label: "\u041E\u0431\u043D\u043E\u0432\u0438\u043B \u0441\u0435\u0441\u0441\u0438\u044E",
|
||
detail: keys.slice(0, 10).join(", ")
|
||
});
|
||
}
|
||
const ask = Array.isArray(patch.ask) ? patch.ask.map(String) : patch.ask ? [String(patch.ask)] : [];
|
||
if (ask.length) {
|
||
upsert("ask", {
|
||
kind: "ask",
|
||
label: `\u0417\u0430\u043F\u0440\u043E\u0441\u0438\u043B ${ask.join(", ")}`,
|
||
detail: "\u043F\u043E\u0434\u0433\u0440\u0443\u0436\u0430\u044E \u0434\u0435\u0442\u0430\u043B\u0438\u2026",
|
||
status: "running"
|
||
});
|
||
}
|
||
if (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null) {
|
||
const slots = [].concat(patch.look_at || patch.vision_from || patch.vision_slots || []);
|
||
upsert("look", {
|
||
kind: "look",
|
||
label: "\u0421\u043C\u043E\u0442\u0440\u0438\u0442 \u043D\u0430 \u043A\u0430\u0434\u0440",
|
||
detail: slots.map(String).slice(0, 4).join(", "),
|
||
status: "running"
|
||
});
|
||
}
|
||
if (patch.generate === true || Array.isArray(patch.actions) && patch.actions.map(String).includes("generate")) {
|
||
upsert("generate", {
|
||
kind: "generate",
|
||
label: "Generate",
|
||
detail: "\u0436\u0434\u0451\u0442 \u043F\u0430\u0439\u043F\u043B\u0430\u0439\u043D\u2026",
|
||
status: "running"
|
||
});
|
||
}
|
||
if (Array.isArray(patch.variants) && patch.variants.length) {
|
||
upsert("variants", {
|
||
kind: "generate",
|
||
label: `\u0412\u0430\u0440\u0438\u0430\u043D\u0442\u044B \xD7${patch.variants.length}`,
|
||
status: "running"
|
||
});
|
||
}
|
||
}
|
||
return {
|
||
begin,
|
||
upsert,
|
||
done,
|
||
skip,
|
||
fail,
|
||
finish,
|
||
noteModelCommands,
|
||
get steps() {
|
||
return steps.slice();
|
||
}
|
||
};
|
||
}
|
||
function attachActivity(SA2) {
|
||
SA2.createActivityController = createActivityController;
|
||
}
|
||
|
||
// src/kreaProfile.js
|
||
function numClose(a, b, eps = 0.051) {
|
||
const x = Number(a);
|
||
const y = Number(b);
|
||
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||
return false;
|
||
}
|
||
return Math.abs(x - y) <= eps;
|
||
}
|
||
function detectKreaProfileName(modelBlob, exactProfile) {
|
||
const blob = String(modelBlob || "").toLowerCase();
|
||
const hasTurbo = /turbo/.test(blob);
|
||
const hasRaw = /\braw\b|_raw\b|-raw\b|\/raw\b/.test(blob);
|
||
if (hasTurbo) {
|
||
return "turbo";
|
||
}
|
||
if (hasRaw) {
|
||
return "raw";
|
||
}
|
||
void exactProfile;
|
||
return "raw";
|
||
}
|
||
function profileParamDefaults(profiles, profileName, generation = {}) {
|
||
const profile = String(profileName || "raw");
|
||
const fromProfile = profiles && typeof profiles[profile] === "object" ? profiles[profile] : {};
|
||
const gen = generation && typeof generation === "object" ? generation : {};
|
||
return {
|
||
profile,
|
||
steps: fromProfile.steps ?? gen.steps ?? null,
|
||
cfg: fromProfile.cfg ?? gen.cfg ?? null,
|
||
sigma_shift: fromProfile.sigma_shift ?? gen.sigma_shift ?? null
|
||
};
|
||
}
|
||
function liveMatchesExactProfile(live, defaults) {
|
||
if (!defaults || defaults.steps == null || defaults.cfg == null) {
|
||
return true;
|
||
}
|
||
const stepsOk = live?.steps == null || numClose(live.steps, defaults.steps, 0.5);
|
||
const cfgOk = live?.cfg == null || numClose(live.cfg, defaults.cfg);
|
||
const sigmaOk = defaults.sigma_shift == null || live?.sigma_shift == null || numClose(live.sigma_shift, defaults.sigma_shift);
|
||
return stepsOk && cfgOk && sigmaOk;
|
||
}
|
||
function exactKeysToForce(live, defaults, {
|
||
patch = null,
|
||
sessionExact = null,
|
||
userParamIntent = false
|
||
} = {}) {
|
||
if (!defaults) {
|
||
return [];
|
||
}
|
||
const out = [];
|
||
for (const key of ["steps", "cfg", "sigma_shift"]) {
|
||
const want = defaults[key];
|
||
if (want == null) {
|
||
continue;
|
||
}
|
||
const eps = key === "steps" ? 0.5 : 0.051;
|
||
if (userParamIntent && patch && patch[key] != null && !numClose(patch[key], want, eps)) {
|
||
continue;
|
||
}
|
||
if (userParamIntent && sessionExact && sessionExact[key] != null) {
|
||
continue;
|
||
}
|
||
const have = live?.[key];
|
||
if (have == null || !numClose(have, want, eps)) {
|
||
out.push(key);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
function attachKreaProfile(SA2) {
|
||
SA2.kreaProfile = {
|
||
numClose,
|
||
detectKreaProfileName,
|
||
profileParamDefaults,
|
||
liveMatchesExactProfile,
|
||
exactKeysToForce
|
||
};
|
||
}
|
||
|
||
// src/app.js
|
||
(function() {
|
||
const LS_BASE = "swarm_assistent_base_url";
|
||
const LS_MODEL = "swarm_assistent_model";
|
||
const LS_EMBED = "swarm_assistent_embed_model";
|
||
const LS_PACK = "swarm_assistent_pack";
|
||
const LS_PERSONA = "swarm_assistent_persona";
|
||
const LS_VIEW = "swarm_assistent_view";
|
||
const LS_AUTO_VISION = "swarm_assistent_auto_vision";
|
||
const LS_AUTO_APPLY = "swarm_assistent_auto_apply";
|
||
const LS_AUTO_GENERATE = "swarm_assistent_auto_generate";
|
||
const LS_AUTO_CRITIQUE = "swarm_assistent_auto_critique";
|
||
const LS_AUTO_DOWNLOAD = "swarm_assistent_auto_download";
|
||
const LS_PARK_LLM = "swarm_assistent_park_llm";
|
||
const LS_PANE_WIDTH = "swarm_assistent_pane_width";
|
||
const LS_WELCOMED = "swarm_assistent_welcomed";
|
||
const LS_CHATS2 = "swarm_assistent_chats_v1";
|
||
const LS_BOARD_TAB = "swarm_assistent_board_tab";
|
||
const LS_CHATS_DRAWER = "swarm_assistent_chats_drawer";
|
||
const MAX_CHATS = 40;
|
||
const MAX_CHAT_MSGS = 24;
|
||
const TAB_BUTTON_ID = "maintab_assistent";
|
||
const GEN_ID = "generate";
|
||
let MAX_REF_SLOTS = 4;
|
||
let MAX_GEN_VARIANTS = 4;
|
||
let CONTEXT_PROMPT_MAX = 2e3;
|
||
let HISTORY_KEEP_TURNS = 4;
|
||
let INVENTORY_PROMPT_RICH = 12;
|
||
let INVENTORY_PROMPT_NAMES = 24;
|
||
let COMPRESS_AT = 0.7;
|
||
let CHARS_PER_TOKEN = 3.2;
|
||
let COMPRESS_AUTO = true;
|
||
let ASPECT_TABLE = {
|
||
"1:1": [1024, 1024],
|
||
"4:3": [1184, 896],
|
||
"3:2": [1248, 832],
|
||
"16:9": [1376, 768],
|
||
"2.35:1": [1568, 672],
|
||
"4:5": [928, 1152],
|
||
"2:3": [832, 1248],
|
||
"9:16": [768, 1376]
|
||
};
|
||
let PACK_ALIASES = {
|
||
ordinary: "ordinary",
|
||
combine: "ordinary",
|
||
normal: "ordinary",
|
||
general: "ordinary",
|
||
default: "ordinary",
|
||
write: "write_prompt",
|
||
write_prompt: "write_prompt",
|
||
critique: "critique_image",
|
||
critique_image: "critique_image",
|
||
compose: "compose_scene",
|
||
compose_scene: "compose_scene",
|
||
params: "fix_params",
|
||
fix_params: "fix_params",
|
||
inpaint: "inpaint_edit",
|
||
inpaint_edit: "inpaint_edit",
|
||
describe: "describe_ref",
|
||
describe_ref: "describe_ref"
|
||
};
|
||
let WELCOME_HTML = `
|
||
<div class="sa-welcome-title">Assistent \xB7 Krea 2</div>
|
||
<ul>
|
||
<li><strong>Generate</strong> \u0441\u043B\u0435\u0432\u0430 \u2014 \u0436\u0438\u0432\u043E\u0439 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440. \u0412 \u0447\u0430\u0442 \u0441\u0430\u043C \u043D\u0435 \u0443\u0445\u043E\u0434\u0438\u0442.</li>
|
||
<li><strong>Refs</strong> \u2014 \u0440\u0435\u0444\u0435\u0440\u0435\u043D\u0441\u044B \u043D\u0430 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\u0439 \u0432\u043A\u043B\u0430\u0434\u043A\u0435: drop / paste / \u0421\u043D\u0438\u043C\u043E\u043A gen.</li>
|
||
<li>\u0413\u0430\u043B\u043E\u0447\u043A\u0430 vision \u043D\u0430 \u043E\u043A\u043D\u0435 \u2014 \u043E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u043A\u0430\u0434\u0440 \u043C\u043E\u0434\u0435\u043B\u0438.</li>
|
||
<li>\u0427\u0438\u043F\u0441\u044B aspect / seed / Vary / Turbo\xB7RAW. \u0412 \u0447\u0430\u0442\u0435: <code>/help</code>.</li>
|
||
<li>\u041A\u043D\u043E\u043F\u043A\u0438 \u043F\u0430\u0442\u0447\u0430 \u0442\u043E\u043B\u044C\u043A\u043E \u0443 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F.</li>
|
||
</ul>
|
||
\u041D\u0430\u043F\u0438\u0448\u0438, \u0447\u0442\u043E \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u2014 \u0438\u043B\u0438 \u043A\u0438\u043D\u044C \u0440\u0435\u0444\u0435\u0440\u0435\u043D\u0441 \u0438 \u043F\u043E\u043F\u0440\u043E\u0441\u0438 \u043F\u0440\u0430\u0432\u043A\u0443.`;
|
||
let HELP_TEXT = `Slash-\u043A\u043E\u043C\u0430\u043D\u0434\u044B (\u0431\u0435\u0437 LLM):
|
||
/help \u2014 \u044D\u0442\u043E\u0442 \u0441\u043F\u0438\u0441\u043E\u043A
|
||
/new \u2014 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442 (\u0442\u0435\u043A\u0443\u0449\u0438\u0439 \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u0441\u044F \u0432 \u0441\u043F\u0438\u0441\u043A\u0435)
|
||
/history \u2014 \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u0438\u043B\u0438 \u0441\u043A\u0440\u044B\u0442\u044C \u043F\u0430\u043D\u0435\u043B\u044C \u0447\u0430\u0442\u043E\u0432
|
||
/compress \u2014 \u0441\u0436\u0430\u0442\u044C \u0441\u0442\u0430\u0440\u044B\u0435 \u0445\u043E\u0434\u044B \u0432 \u0441\u0430\u043C\u043C\u0430\u0440\u0438
|
||
/debug \u2014 \u0441\u0432\u043E\u0434\u043A\u0430 UI/Exact (\u0431\u0435\u0437 LLM)
|
||
/debug ask \u2014 \u0442\u043E \u0436\u0435 + \u043A\u043E\u0440\u043E\u0442\u043A\u0438\u0439 \u043E\u0442\u0432\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0438
|
||
/why \u2014 \u0441\u0440\u0430\u0437\u0443 /debug ask
|
||
/gen \u2014 Generate \u0441\u0435\u0439\u0447\u0430\u0441
|
||
/look generate|refN \u2014 \u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043A\u0430\u0434\u0440 \u043C\u043E\u0434\u0435\u043B\u0438 (vision)
|
||
/init /mask /clear \u2014 Init / Mask / Clear Init
|
||
/interrupt \u2014 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044E
|
||
/aspect 16:9 \u2014 \u0440\u0430\u0437\u043C\u0435\u0440 \u0438\u0437 \u0442\u0430\u0431\u043B\u0438\u0446\u044B 1K
|
||
/seed lock|random \u2014 \u0437\u0430\u0444\u0438\u043A\u0441\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0438\u043B\u0438 \u0440\u0430\u043D\u0434\u043E\u043C\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C seed
|
||
/vary \u2014 \u043D\u043E\u0432\u044B\u0439 seed, \u0442\u043E\u0442 \u0436\u0435 \u043F\u0440\u043E\u043C\u043F\u0442
|
||
/pack write|critique|compose|params|inpaint|describe
|
||
/inventory \u2014 rescan \u043C\u043E\u0434\u0435\u043B\u0435\u0439 + \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u043F\u0438\u0441\u043E\u043A LoRA
|
||
|
||
\u0427\u0438\u043F\u0441\u044B \u043D\u0430\u0434 \u043F\u043E\u043B\u0435\u043C \u0432\u0432\u043E\u0434\u0430 \u0434\u0435\u043B\u0430\u044E\u0442 \u0442\u043E \u0436\u0435 \u0434\u043B\u044F aspect / seed / vary / Turbo\xB7RAW.
|
||
\u041F\u0440\u0438 \u0441\u0442\u0430\u0440\u0442\u0435 \u0432\u0441\u0435\u0433\u0434\u0430 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442; \u0441\u043C\u0435\u043D\u0430 \u0447\u0430\u0442\u0430 \u0432 \u043F\u0430\u043D\u0435\u043B\u0438 \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u0435\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B.`;
|
||
let SLASH_COMMANDS = [
|
||
{ cmd: "/help", hint: "\u0441\u043F\u0438\u0441\u043E\u043A \u043A\u043E\u043C\u0430\u043D\u0434" },
|
||
{ cmd: "/new", hint: "\u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442" },
|
||
{ cmd: "/history", hint: "\u043F\u0430\u043D\u0435\u043B\u044C \u0447\u0430\u0442\u043E\u0432" },
|
||
{ cmd: "/compress", hint: "\u0441\u0436\u0430\u0442\u044C \u0441\u0442\u0430\u0440\u044B\u0435 \u0445\u043E\u0434\u044B" },
|
||
{ cmd: "/debug", hint: "\u0441\u0432\u043E\u0434\u043A\u0430 \xB7 ask = \u0441 LLM" },
|
||
{ cmd: "/why", hint: "debug + \u043F\u043E\u044F\u0441\u043D\u0435\u043D\u0438\u0435 LLM" },
|
||
{ cmd: "/gen", hint: "Generate \u0441\u0435\u0439\u0447\u0430\u0441" },
|
||
{ cmd: "/look ", hint: "generate|refN" },
|
||
{ cmd: "/init", hint: "\u043A\u0430\u043A Init" },
|
||
{ cmd: "/mask", hint: "\u043A\u0430\u043A Mask" },
|
||
{ cmd: "/clear", hint: "\u0441\u0431\u0440\u043E\u0441 Init/Mask" },
|
||
{ cmd: "/interrupt", hint: "\u0441\u0442\u043E\u043F" },
|
||
{ cmd: "/aspect ", hint: "16:9" },
|
||
{ cmd: "/seed ", hint: "lock|random" },
|
||
{ cmd: "/vary", hint: "\u043D\u043E\u0432\u044B\u0439 seed" },
|
||
{ cmd: "/pack ", hint: "write|critique|\u2026" },
|
||
{ cmd: "/inventory", hint: "rescan \u043C\u043E\u0434\u0435\u043B\u0435\u0439" }
|
||
];
|
||
const state = {
|
||
history: [],
|
||
config: null,
|
||
exact: null,
|
||
sessionExact: {},
|
||
lastUserParamIntent: false,
|
||
lastUserControlIntent: false,
|
||
lastPatch: null,
|
||
pendingSilentGen: false,
|
||
pendingPromptEnMerge: null,
|
||
enabledSkills: [],
|
||
kreaProfiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 }, raw: { steps: 28, cfg: 4.5 } },
|
||
preferredEmbed: null,
|
||
busy: false,
|
||
generating: false,
|
||
chatEpoch: 0,
|
||
waitImageTimer: null,
|
||
lastImageDataUrl: null,
|
||
preferredModel: null,
|
||
inventory: { loras: [], checkpoints: [], wildcards: [], has_civitai_key: false },
|
||
inventoryFetchedAt: 0,
|
||
streamEl: null,
|
||
streamMeta: null,
|
||
streamText: "",
|
||
streamFenceDone: false,
|
||
turnHops: [],
|
||
lastSystemChars: 0,
|
||
lastSystemLayers: null,
|
||
lastContextChars: 0,
|
||
lastPromptEvalCount: null,
|
||
contextMemory: null,
|
||
ctxPanelOpen: false,
|
||
compressing: false,
|
||
busyPhase: "idle",
|
||
busyStarted: 0,
|
||
gotDelta: false,
|
||
busyTimer: null,
|
||
lastDeltaAt: 0,
|
||
streamStallTimer: null,
|
||
turnSettled: false,
|
||
lastBusyPhaseShown: "",
|
||
slots: [],
|
||
selectedSlotId: "ref1",
|
||
refSeq: 1,
|
||
genResults: [],
|
||
selectedGenResultId: null,
|
||
lightboxIndex: -1,
|
||
packUserTouched: false,
|
||
view: "chat",
|
||
boardTab: "generate",
|
||
personas: [],
|
||
chatSession: null,
|
||
pendingPersonaNote: null,
|
||
chats: [],
|
||
activeChatId: null,
|
||
restoringChat: false,
|
||
chatsPanelOpen: false,
|
||
chatsDrawerOpen: false,
|
||
chatsQuery: "",
|
||
chatsSearchHits: null,
|
||
slashIndex: 0,
|
||
llmParked: false,
|
||
expectColdLoad: false,
|
||
memoryRows: [],
|
||
userPrefs: [],
|
||
settingsTab: "behavior",
|
||
settingsPersonaId: null,
|
||
ollamaHealth: "unknown",
|
||
trainingLock: false,
|
||
activity: null
|
||
};
|
||
function getActivity() {
|
||
if (state.activity) {
|
||
return state.activity;
|
||
}
|
||
if (window.SA && typeof SA.createActivityController === "function") {
|
||
state.activity = SA.createActivityController({
|
||
getMessagesEl: () => $2("sa_messages"),
|
||
scrollToBottom: () => scrollMessagesToBottom(),
|
||
hideEmpty: () => hideChatEmpty()
|
||
});
|
||
}
|
||
return state.activity;
|
||
}
|
||
function activityBegin(title) {
|
||
const a = getActivity();
|
||
if (a) {
|
||
a.begin(title || "Assistent");
|
||
}
|
||
}
|
||
function activityStep(id, patch) {
|
||
const a = getActivity();
|
||
if (a) {
|
||
a.upsert(id, patch);
|
||
}
|
||
}
|
||
function activityDone(id, patch) {
|
||
const a = getActivity();
|
||
if (a) {
|
||
a.done(id, patch);
|
||
}
|
||
}
|
||
function activityFinish(summary) {
|
||
const a = getActivity();
|
||
if (a) {
|
||
a.finish(summary);
|
||
}
|
||
}
|
||
const HOP_BUDGET = 4;
|
||
function isContinuationTurn(opts) {
|
||
return !!(opts && (opts.fromVisionHop || opts.fromAutoCritique || opts.fromPromptEnRetry || opts.fromAskHop));
|
||
}
|
||
function isMachineTurn(opts) {
|
||
return isContinuationTurn(opts) || !!(opts && opts.fromDebug);
|
||
}
|
||
function resetTurnHops() {
|
||
state.turnHops = [];
|
||
state.pendingPromptEnMerge = null;
|
||
}
|
||
function turnHopUsed(kind) {
|
||
return (state.turnHops || []).includes(kind);
|
||
}
|
||
function claimTurnHop(kind) {
|
||
if (!Array.isArray(state.turnHops)) {
|
||
state.turnHops = [];
|
||
}
|
||
if (state.turnHops.includes(kind) || state.turnHops.length >= HOP_BUDGET) {
|
||
return false;
|
||
}
|
||
state.turnHops.push(kind);
|
||
return true;
|
||
}
|
||
function diskPersist() {
|
||
return window.SA && window.SA.persist || null;
|
||
}
|
||
function $2(id) {
|
||
return document.getElementById(id);
|
||
}
|
||
function modelShort(name) {
|
||
const s = String(name || "");
|
||
const slash = s.lastIndexOf("/");
|
||
return (slash >= 0 ? s.slice(slash + 1) : s) || "model";
|
||
}
|
||
function fmtElapsed(ms) {
|
||
const s = Math.max(0, Math.floor(ms / 1e3));
|
||
if (s < 60) {
|
||
return `${s}s`;
|
||
}
|
||
return `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, "0")}s`;
|
||
}
|
||
function hideChatEmpty() {
|
||
const empty = $2("sa_chat_empty");
|
||
if (empty) {
|
||
empty.hidden = true;
|
||
}
|
||
}
|
||
let scrollMessagesRaf = 0;
|
||
function messagesNearBottom(thresholdPx = 96) {
|
||
const box = $2("sa_messages");
|
||
if (!box) {
|
||
return true;
|
||
}
|
||
return box.scrollHeight - box.scrollTop - box.clientHeight <= thresholdPx;
|
||
}
|
||
function scrollMessagesToBottom({ force = false } = {}) {
|
||
const box = $2("sa_messages");
|
||
if (!box) {
|
||
return;
|
||
}
|
||
if (!force && !messagesNearBottom()) {
|
||
return;
|
||
}
|
||
if (scrollMessagesRaf) {
|
||
return;
|
||
}
|
||
scrollMessagesRaf = requestAnimationFrame(() => {
|
||
scrollMessagesRaf = 0;
|
||
const el = $2("sa_messages");
|
||
if (el && (force || messagesNearBottom(120))) {
|
||
el.scrollTop = el.scrollHeight;
|
||
}
|
||
});
|
||
}
|
||
function showChatEmptyIfIdle() {
|
||
const box = $2("sa_messages");
|
||
const empty = $2("sa_chat_empty");
|
||
if (!box || !empty) {
|
||
return;
|
||
}
|
||
const hasMsg = [...box.children].some((el) => el.id !== "sa_chat_empty");
|
||
empty.hidden = hasMsg;
|
||
}
|
||
function setBusyPhase(phase) {
|
||
state.busyPhase = phase || "thinking";
|
||
tickBusyUi();
|
||
syncPatchActionAvailability();
|
||
syncGenerateBusy();
|
||
}
|
||
function tickBusyUi() {
|
||
if (state.busyPhase === "idle") {
|
||
return;
|
||
}
|
||
const elapsed = Date.now() - (state.busyStarted || Date.now());
|
||
if (!state.gotDelta && (state.busyPhase === "thinking" || state.busyPhase === "waiting") && elapsed > 1600) {
|
||
state.busyPhase = state.llmParked || state.expectColdLoad ? "loading" : "waiting";
|
||
}
|
||
const model = modelShort($2("sa_model")?.value);
|
||
const labels = {
|
||
encoding: "Encoding image\u2026",
|
||
waiting: "\u0416\u0434\u0443 Ollama / \u043F\u0435\u0440\u0432\u044B\u0439 \u0442\u043E\u043A\u0435\u043D\u2026",
|
||
loading: `\u0417\u0430\u0433\u0440\u0443\u0436\u0430\u044E ${model} \u0432 GPU\u2026 \u043E\u0431\u044B\u0447\u043D\u043E 30\u2013120 \u0441 \u043F\u043E\u0441\u043B\u0435 park`,
|
||
warming: `\u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E ${model} \u0432 GPU\u2026`,
|
||
thinking: "Thinking\u2026",
|
||
streaming: "Writing\u2026",
|
||
generating: "Generating image\u2026",
|
||
parking: "\u041E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0430\u044E VRAM (park LLM)\u2026",
|
||
applying: "Applying patch\u2026",
|
||
silent_gen: "\u041F\u0440\u0438\u043C\u0435\u043D\u044F\u044E \u043F\u0430\u0442\u0447 \u2192 Generate\u2026",
|
||
refining: "\u0423\u0442\u043E\u0447\u043D\u044F\u044E \u043E\u0442\u0432\u0435\u0442\u2026",
|
||
compressing: "\u0421\u0436\u0438\u043C\u0430\u044E \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u2026"
|
||
};
|
||
const text = labels[state.busyPhase] || "Working\u2026";
|
||
const phaseKind = {
|
||
thinking: "think",
|
||
streaming: "stream",
|
||
waiting: "think",
|
||
loading: "warm",
|
||
warming: "warm",
|
||
parking: "park",
|
||
encoding: "look",
|
||
generating: "generate",
|
||
applying: "merge",
|
||
silent_gen: "generate",
|
||
refining: "prep",
|
||
compressing: "compress"
|
||
};
|
||
if (state.lastBusyPhaseShown !== state.busyPhase) {
|
||
state.lastBusyPhaseShown = state.busyPhase;
|
||
if (state.busyPhase === "streaming" || state.busyPhase === "refining") {
|
||
activityDone("think");
|
||
}
|
||
activityStep(`phase:${state.busyPhase}`, {
|
||
kind: phaseKind[state.busyPhase] || "think",
|
||
label: text,
|
||
status: "running"
|
||
});
|
||
const a = getActivity();
|
||
if (a && Array.isArray(a.steps)) {
|
||
for (const s of a.steps) {
|
||
if (s.id.startsWith("phase:") && s.id !== `phase:${state.busyPhase}` && s.status === "running") {
|
||
a.done(s.id);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
const barText = $2("sa_livebar_text");
|
||
if (barText) {
|
||
barText.textContent = text;
|
||
}
|
||
const elapsedEl = $2("sa_elapsed");
|
||
if (elapsedEl) {
|
||
elapsedEl.textContent = fmtElapsed(elapsed);
|
||
}
|
||
const status = $2("sa_status");
|
||
if (status) {
|
||
status.textContent = text;
|
||
status.classList.add("sa-status-busy");
|
||
}
|
||
}
|
||
function startBusyUi(phase) {
|
||
state.busyStarted = Date.now();
|
||
state.gotDelta = false;
|
||
state.busyPhase = phase || "thinking";
|
||
$2("swarm_assistent_root")?.classList.add("sa-is-busy");
|
||
$2("sa_composer")?.classList.add("sa-composer-busy");
|
||
state.lastBusyPhaseShown = "";
|
||
const send = $2("sa_btn_send");
|
||
if (send) {
|
||
send.disabled = false;
|
||
}
|
||
const input = $2("sa_input");
|
||
if (input) {
|
||
input.readOnly = false;
|
||
input.disabled = false;
|
||
input.classList.add("sa-input-busy");
|
||
}
|
||
const bar = $2("sa_livebar");
|
||
if (bar) {
|
||
bar.hidden = false;
|
||
}
|
||
const dot = $2("sa_live_dot");
|
||
if (dot) {
|
||
dot.hidden = false;
|
||
}
|
||
tickBusyUi();
|
||
syncPatchActionAvailability();
|
||
syncGenerateBusy();
|
||
if (state.busyTimer) {
|
||
clearInterval(state.busyTimer);
|
||
}
|
||
state.busyTimer = setInterval(tickBusyUi, 400);
|
||
}
|
||
function stopBusyUi(finalStatus) {
|
||
if (state.busyTimer) {
|
||
clearInterval(state.busyTimer);
|
||
state.busyTimer = null;
|
||
}
|
||
clearStreamStall();
|
||
const elapsed = Date.now() - (state.busyStarted || Date.now());
|
||
state.busyPhase = "idle";
|
||
state.lastBusyPhaseShown = "";
|
||
activityFinish(finalStatus || "\u0413\u043E\u0442\u043E\u0432\u043E");
|
||
$2("swarm_assistent_root")?.classList.remove("sa-is-busy");
|
||
$2("sa_composer")?.classList.remove("sa-composer-busy");
|
||
const send = $2("sa_btn_send");
|
||
if (send) {
|
||
send.disabled = false;
|
||
}
|
||
const input = $2("sa_input");
|
||
if (input) {
|
||
input.classList.remove("sa-input-busy");
|
||
}
|
||
const bar = $2("sa_livebar");
|
||
if (bar) {
|
||
bar.hidden = true;
|
||
}
|
||
const dot = $2("sa_live_dot");
|
||
if (dot) {
|
||
dot.hidden = true;
|
||
}
|
||
const status = $2("sa_status");
|
||
if (status) {
|
||
status.classList.remove("sa-status-busy");
|
||
}
|
||
if (finalStatus != null) {
|
||
const suffix = elapsed >= 1e3 ? ` \xB7 ${fmtElapsed(elapsed)}` : "";
|
||
setStatus(finalStatus + suffix);
|
||
}
|
||
syncPatchActionAvailability();
|
||
syncGenerateBusy();
|
||
}
|
||
function setStatus(text) {
|
||
const el = $2("sa_status");
|
||
if (el) {
|
||
el.textContent = text || "";
|
||
}
|
||
}
|
||
function setInterruptVisible(on) {
|
||
const btn = $2("sa_btn_interrupt");
|
||
if (btn) {
|
||
btn.hidden = !on;
|
||
btn.classList.toggle("sa-interrupt-active", !!on);
|
||
}
|
||
}
|
||
function looksLikeKrea(text) {
|
||
const s = String(text || "");
|
||
return /krea\s*2|krea2|krea-2/i.test(s) || /krea/i.test(s);
|
||
}
|
||
function resolveCurrentCheckpoint() {
|
||
const out = {
|
||
name: null,
|
||
architecture: null,
|
||
compat_class: null,
|
||
title: null,
|
||
class: null,
|
||
source: null
|
||
};
|
||
try {
|
||
if (typeof currentModelHelper !== "undefined" && currentModelHelper) {
|
||
out.name = currentModelHelper.curModel || null;
|
||
out.architecture = currentModelHelper.curArch || null;
|
||
out.compat_class = currentModelHelper.curCompatClass || null;
|
||
out.source = "currentModelHelper";
|
||
}
|
||
} catch (e) {
|
||
}
|
||
try {
|
||
if (typeof getCurrentModel === "function") {
|
||
const model = getCurrentModel();
|
||
if (model) {
|
||
out.name = out.name || model.name || null;
|
||
out.title = model.title || null;
|
||
out.architecture = out.architecture || model.architecture || null;
|
||
out.class = model.class || null;
|
||
out.compat_class = out.compat_class || model.compat_class || null;
|
||
out.source = out.source || "getCurrentModel";
|
||
}
|
||
}
|
||
} catch (e) {
|
||
}
|
||
try {
|
||
const sel = document.getElementById("current_model") || document.getElementById("input_model");
|
||
if (sel) {
|
||
const opt = sel.selectedOptions && sel.selectedOptions[0];
|
||
const hint = [
|
||
sel.value,
|
||
opt && opt.text,
|
||
opt && opt.dataset && opt.dataset.cleanname
|
||
].filter(Boolean).join(" ");
|
||
if (!out.name && sel.value) {
|
||
out.name = sel.value;
|
||
out.source = out.source || "dropdown";
|
||
}
|
||
if (hint && !out.architecture) {
|
||
out.title = out.title || hint;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
}
|
||
return out;
|
||
}
|
||
function isKreaSelected() {
|
||
try {
|
||
const m = resolveCurrentCheckpoint();
|
||
const blob = [
|
||
m.architecture,
|
||
m.compat_class,
|
||
m.title,
|
||
m.name,
|
||
m.class
|
||
].join(" ");
|
||
return looksLikeKrea(blob);
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
let lastExactForceCkpt = null;
|
||
function updateGate() {
|
||
const ok = isKreaSelected();
|
||
const gate = $2("sa_gate");
|
||
const layout = $2("sa_layout");
|
||
if (gate) {
|
||
gate.hidden = ok;
|
||
if (!ok) {
|
||
const m = resolveCurrentCheckpoint();
|
||
const seen = [m.architecture, m.compat_class, m.name].filter(Boolean).join(" \xB7 ");
|
||
const p = gate.querySelector("p");
|
||
if (p) {
|
||
p.innerHTML = seen ? `Swarm Assistent is for <strong>Krea 2</strong> models only. Current: <code>${escapeHtml2(
|
||
seen
|
||
)}</code> \u2014 pick a checkpoint with architecture <code>krea-2</code>.` : "Swarm Assistent is for <strong>Krea 2</strong> models only. Select a Krea 2 checkpoint on Generate to enable the chat.";
|
||
}
|
||
}
|
||
}
|
||
if (layout) {
|
||
layout.classList.toggle("sa-disabled", !ok);
|
||
}
|
||
try {
|
||
const ckptName = resolveCurrentCheckpoint()?.name || null;
|
||
if (ok && ckptName && ckptName !== lastExactForceCkpt) {
|
||
const defs = typeof exactProfileDefaults === "function" ? exactProfileDefaults(detectKreaProfileName2()) : null;
|
||
if (defs && defs.steps != null && defs.cfg != null) {
|
||
lastExactForceCkpt = ckptName;
|
||
forceExactParamsForGenerate({});
|
||
}
|
||
} else if (!ok) {
|
||
lastExactForceCkpt = null;
|
||
}
|
||
} catch (e) {
|
||
}
|
||
return ok;
|
||
}
|
||
function escapeHtml2(s) {
|
||
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||
}
|
||
const PROSE_SECTION_TITLES = {
|
||
critique: "\u041A\u0440\u0438\u0442\u0438\u043A\u0430",
|
||
\u043A\u0440\u0438\u0442\u0438\u043A\u0430: "\u041A\u0440\u0438\u0442\u0438\u043A\u0430",
|
||
analysis: "\u0420\u0430\u0437\u0431\u043E\u0440",
|
||
\u0440\u0430\u0437\u0431\u043E\u0440: "\u0420\u0430\u0437\u0431\u043E\u0440",
|
||
notes: "\u0417\u0430\u043C\u0435\u0442\u043A\u0438",
|
||
\u0437\u0430\u043C\u0435\u0442\u043A\u0438: "\u0417\u0430\u043C\u0435\u0442\u043A\u0438",
|
||
summary: "\u041A\u0440\u0430\u0442\u043A\u043E",
|
||
\u043A\u0440\u0430\u0442\u043A\u043E: "\u041A\u0440\u0430\u0442\u043A\u043E",
|
||
prompt: "\u041F\u0440\u043E\u043C\u043F\u0442",
|
||
\u043F\u0440\u043E\u043C\u043F\u0442: "\u041F\u0440\u043E\u043C\u043F\u0442",
|
||
"improved prompt": "\u041F\u0440\u043E\u043C\u043F\u0442",
|
||
"next prompt": "\u041F\u0440\u043E\u043C\u043F\u0442",
|
||
deliverable: "\u0418\u0442\u043E\u0433",
|
||
\u0438\u0442\u043E\u0433: "\u0418\u0442\u043E\u0433",
|
||
verdict: "\u0412\u0435\u0440\u0434\u0438\u043A\u0442",
|
||
\u0432\u0435\u0440\u0434\u0438\u043A\u0442: "\u0412\u0435\u0440\u0434\u0438\u043A\u0442",
|
||
issues: "\u041F\u0440\u043E\u0431\u043B\u0435\u043C\u044B",
|
||
\u043F\u0440\u043E\u0431\u043B\u0435\u043C\u044B: "\u041F\u0440\u043E\u0431\u043B\u0435\u043C\u044B",
|
||
fixes: "\u041F\u0440\u0430\u0432\u043A\u0438",
|
||
\u043F\u0440\u0430\u0432\u043A\u0438: "\u041F\u0440\u0430\u0432\u043A\u0438",
|
||
suggestion: "\u041F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435",
|
||
suggestions: "\u041F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F",
|
||
\u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F: "\u041F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F"
|
||
};
|
||
function localizeProseHeading(raw) {
|
||
const cleaned = String(raw || "").replace(/[*_`#]/g, "").trim();
|
||
if (!cleaned) {
|
||
return null;
|
||
}
|
||
const key = cleaned.toLowerCase().replace(/\s+/g, " ");
|
||
if (/^json\s*patch$/.test(key) || /^патч$/.test(key) || /^json\s*патч$/.test(key)) {
|
||
return null;
|
||
}
|
||
if (PROSE_SECTION_TITLES[key]) {
|
||
return PROSE_SECTION_TITLES[key];
|
||
}
|
||
const head = key.split(/[—:\-|]/)[0].trim();
|
||
if (PROSE_SECTION_TITLES[head]) {
|
||
return PROSE_SECTION_TITLES[head];
|
||
}
|
||
return cleaned;
|
||
}
|
||
function formatProseInline(escapedLine) {
|
||
let t = escapedLine;
|
||
t = t.replace(/`([^`]+)`/g, '<code class="sa-prose-code">$1</code>');
|
||
t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
||
t = t.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, "$1<em>$2</em>");
|
||
return t;
|
||
}
|
||
function formatAssistantProseHtml(raw) {
|
||
let text = String(raw || "").replace(/\r\n/g, "\n");
|
||
text = text.replace(/(?:^|\n)#{1,6}\s*JSON\s*Patch\s*(?=\n|$)/gi, "\n");
|
||
text = text.replace(/(?:^|\n)\s*JSON\s*Patch\s*:?\s*(?=\n|$)/gi, "\n");
|
||
text = text.replace(/\n{3,}/g, "\n\n").trim();
|
||
if (!text) {
|
||
return "";
|
||
}
|
||
const lines = text.split("\n");
|
||
const parts = [];
|
||
let listItems = [];
|
||
const flushList = () => {
|
||
if (!listItems.length) {
|
||
return;
|
||
}
|
||
parts.push(
|
||
`<ul class="sa-prose-list">${listItems.map((li) => `<li>${formatProseInline(escapeHtml2(li))}</li>`).join("")}</ul>`
|
||
);
|
||
listItems = [];
|
||
};
|
||
for (const line of lines) {
|
||
const heading = line.match(/^#{1,3}\s+(.+?)\s*$/);
|
||
if (heading) {
|
||
flushList();
|
||
const title = localizeProseHeading(heading[1]);
|
||
if (!title) {
|
||
continue;
|
||
}
|
||
const level = Math.min((line.match(/^#+/) || ["###"])[0].length, 3);
|
||
parts.push(
|
||
`<div class="sa-prose-h sa-prose-h${level}" role="heading" aria-level="${level}">${escapeHtml2(title)}</div>`
|
||
);
|
||
continue;
|
||
}
|
||
const bullet = line.match(/^\s*[-*•]\s+(.+)$/);
|
||
if (bullet) {
|
||
listItems.push(bullet[1]);
|
||
continue;
|
||
}
|
||
flushList();
|
||
if (!line.trim()) {
|
||
parts.push('<div class="sa-prose-gap" aria-hidden="true"></div>');
|
||
continue;
|
||
}
|
||
parts.push(`<p class="sa-prose-p">${formatProseInline(escapeHtml2(line))}</p>`);
|
||
}
|
||
flushList();
|
||
return parts.join("");
|
||
}
|
||
function setAssistantBody(div, text, { live = false } = {}) {
|
||
if (!div) {
|
||
return;
|
||
}
|
||
let body = div.querySelector(".sa-msg-body");
|
||
if (!body) {
|
||
body = document.createElement("div");
|
||
body.className = "sa-msg-body";
|
||
div.appendChild(body);
|
||
}
|
||
const raw = text || "";
|
||
if (live) {
|
||
body.classList.add("sa-prose", "sa-prose-live");
|
||
body.classList.remove("sa-prose-rich");
|
||
body.textContent = raw;
|
||
return;
|
||
}
|
||
body.classList.add("sa-prose", "sa-prose-rich");
|
||
body.classList.remove("sa-prose-live");
|
||
const html = formatAssistantProseHtml(raw);
|
||
if (html) {
|
||
body.innerHTML = html;
|
||
} else {
|
||
body.textContent = "";
|
||
}
|
||
}
|
||
function val(id) {
|
||
const el = document.getElementById(id);
|
||
return el ? el.value : "";
|
||
}
|
||
function setVal(id, value) {
|
||
const el = document.getElementById(id);
|
||
if (!el) {
|
||
return;
|
||
}
|
||
el.value = value;
|
||
el.dispatchEvent(new Event("input", { bubbles: true }));
|
||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||
}
|
||
function liveNegativePrompt() {
|
||
return String(val("input_negativeprompt") || val("alt_negativeprompt_textbox") || "").trim();
|
||
}
|
||
function exactDefaultNegative() {
|
||
const { exact } = resolveExactBundle();
|
||
const n = exact?.generation?.negative ?? exact?.negative;
|
||
return n != null ? String(n).trim() : "";
|
||
}
|
||
function setNegativePrompt(text) {
|
||
const s = text != null ? String(text) : "";
|
||
if (document.getElementById("input_negativeprompt")) {
|
||
setVal("input_negativeprompt", s);
|
||
}
|
||
if (document.getElementById("alt_negativeprompt_textbox")) {
|
||
setVal("alt_negativeprompt_textbox", s);
|
||
}
|
||
}
|
||
function ensureNegativeForGenerate(patch) {
|
||
let neg = "";
|
||
if (patch && patch.negative != null && String(patch.negative).trim() !== "") {
|
||
neg = String(patch.negative).trim();
|
||
} else {
|
||
neg = liveNegativePrompt() || exactDefaultNegative();
|
||
}
|
||
if (neg) {
|
||
setNegativePrompt(neg);
|
||
if (patch && (patch.negative == null || String(patch.negative).trim() === "")) {
|
||
patch.negative = neg;
|
||
}
|
||
}
|
||
return neg;
|
||
}
|
||
function isEmptyParamField(raw, { treatZeroEmpty = false } = {}) {
|
||
if (raw == null) {
|
||
return true;
|
||
}
|
||
const s = String(raw).trim();
|
||
if (s === "") {
|
||
return true;
|
||
}
|
||
if (treatZeroEmpty && (s === "0" || Number(s) === 0)) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function cyrTokenRe(alts) {
|
||
const boundary = "(^|[^0-9A-Za-z_\u0410-\u042F\u0430-\u044F\u0401\u0451])";
|
||
const end = "(?=$|[^0-9A-Za-z_\u0410-\u042F\u0430-\u044F\u0401\u0451])";
|
||
return new RegExp(`${boundary}(?:${alts})${end}`, "i");
|
||
}
|
||
function parseAspectFromUserText(text) {
|
||
const t = String(text || "");
|
||
if (!t.trim()) {
|
||
return null;
|
||
}
|
||
const ratio = t.match(/(?:^|[^0-9])(\d+(?:\.\d+)?)\s*[:x×хX]\s*(\d+(?:\.\d+)?)(?=$|[^0-9])/);
|
||
if (ratio) {
|
||
const key = normalizeAspect(`${ratio[1]}:${ratio[2]}`);
|
||
if (key) {
|
||
return key;
|
||
}
|
||
}
|
||
const na = t.match(/(?:^|[^0-9])(\d+(?:\.\d+)?)\s*(?:на|к|to)\s*(\d+(?:\.\d+)?)(?=$|[^0-9])/i);
|
||
if (na) {
|
||
const key = normalizeAspect(`${na[1]}:${na[2]}`);
|
||
if (key) {
|
||
return key;
|
||
}
|
||
}
|
||
const named = t.match(/\b(16:9|9:16|1:1|4:5|2:3|3:2|4:3|2\.35:1)\b/i);
|
||
if (named) {
|
||
return normalizeAspect(named[1]);
|
||
}
|
||
if (cyrTokenRe("\u043F\u043E\u0440\u0442\u0440\u0435\u0442|\u0432\u0435\u0440\u0442\u0438\u043A\u0430\u043B[\u0430-\u044F\u0451]*").test(t) || /\b(portrait|vertical)\b/i.test(t)) {
|
||
return normalizeAspect("9:16") || normalizeAspect("2:3");
|
||
}
|
||
if (cyrTokenRe("\u0430\u043B\u044C\u0431\u043E\u043C|\u0433\u043E\u0440\u0438\u0437\u043E\u043D\u0442\u0430\u043B[\u0430-\u044F\u0451]*").test(t) || /\b(landscape|horizontal|widescreen)\b/i.test(t)) {
|
||
return normalizeAspect("16:9");
|
||
}
|
||
return null;
|
||
}
|
||
function isSameButAspectRequest(text) {
|
||
const t = String(text || "");
|
||
if (!parseAspectFromUserText(t)) {
|
||
return false;
|
||
}
|
||
return /такую\s+же|тот\s+же\s+промпт|same\s+(one|prompt|thing|again)|только\s+(поменя|смени|поставь)|поменяй\s+на|смени\s+на|only\s+change|just\s+change/i.test(t) || /поменяй\s+(размер|aspect|соотношен)/i.test(t) || /смени\s+(размер|aspect|соотношен)/i.test(t);
|
||
}
|
||
function userTextMentionsControls(text) {
|
||
const t = String(text || "");
|
||
if (/\b(horny|controls?|preference[_\s-]?bias)\b/i.test(t)) {
|
||
return true;
|
||
}
|
||
return cyrTokenRe("\u0445\u043E\u0440\u043D\u0438|\u043E\u0441\u0442\u044B\u043D\u044C|\u0441\u043B\u0430\u0439\u0434\u0435\u0440").test(t) || /\/\s*(остынь|ostyn|horny-game)/i.test(t) || /слайдер\s*вкус|вкус\s*(на|в)\s*\d|поставь\s*вкус|крутани\s*вкус/i.test(t);
|
||
}
|
||
function patchLooksLikeGeneration(patch) {
|
||
if (!patch || typeof patch !== "object") {
|
||
return false;
|
||
}
|
||
if (patch.prompt != null || patch.loras || patch.aspect != null || patch.width != null || patch.height != null || patch.steps != null || patch.cfg != null || patch.seed != null) {
|
||
return true;
|
||
}
|
||
return Array.isArray(patch.actions) && patch.actions.map(String).includes("generate");
|
||
}
|
||
function filterControlPatch(incoming, patch) {
|
||
const schema = state.config?.controls || {};
|
||
const out = {};
|
||
if (!incoming || typeof incoming !== "object") {
|
||
return out;
|
||
}
|
||
if (patchLooksLikeGeneration(patch) && !state.lastUserControlIntent) {
|
||
return out;
|
||
}
|
||
for (const [id, raw] of Object.entries(incoming)) {
|
||
if (!schema[id]) {
|
||
continue;
|
||
}
|
||
const n = Number(raw);
|
||
if (!Number.isFinite(n)) {
|
||
continue;
|
||
}
|
||
const def = Number(schema[id]?.default);
|
||
const cur = getControlValue(id, Number.isFinite(def) ? def : n);
|
||
if (Math.abs(n - cur) < 5e-4) {
|
||
continue;
|
||
}
|
||
if (!state.lastUserControlIntent && Number.isFinite(def) && Math.abs(n - def) < 5e-4 && Math.abs(cur - def) > 5e-4) {
|
||
continue;
|
||
}
|
||
out[id] = n;
|
||
}
|
||
return out;
|
||
}
|
||
function userTextMentionsParams(text) {
|
||
const t = String(text || "");
|
||
if (parseAspectFromUserText(t)) {
|
||
return true;
|
||
}
|
||
if (/\b(steps?|cfg|seed|sigma|aspect|resolution|batch|turbo|raw)\b/i.test(t)) {
|
||
return true;
|
||
}
|
||
return cyrTokenRe(
|
||
"\u0440\u0430\u0437\u043C\u0435\u0440|\u0448\u0438\u0440\u0438\u043D[\u0430-\u044F\u0451]*|\u0432\u044B\u0441\u043E\u0442[\u0430-\u044F\u0451]*|\u0441\u043E\u043E\u0442\u043D\u043E\u0448\u0435\u043D[\u0430-\u044F\u0451]*|\u0442\u0443\u0440\u0431\u043E|\u043F\u043E\u0440\u0442\u0440\u0435\u0442|\u0432\u0435\u0440\u0442\u0438\u043A\u0430\u043B[\u0430-\u044F\u0451]*|\u0448\u0430\u0433|\u0448\u0430\u0433\u043E\u043C|\u0448\u0430\u0433\u0430\u043C\u0438|\u0448\u0430\u0433\u043E\u0432|\u0448\u0430\u0433\u0430"
|
||
).test(t);
|
||
}
|
||
function replyMissingJsonPatch(reply) {
|
||
const t = String(reply || "");
|
||
if (!t.trim()) {
|
||
return false;
|
||
}
|
||
if (/```(?:json)?\s*\{[\s\S]*?\}```/i.test(t)) {
|
||
return false;
|
||
}
|
||
return /###\s*JSON\s*Patch\b/i.test(t) || /JSON\s*Patch\s*:?\s*$/im.test(t);
|
||
}
|
||
function userAsksContinue(text) {
|
||
const t = String(text || "").trim();
|
||
if (!t) {
|
||
return false;
|
||
}
|
||
if (/^(давай\s+дальше|продолжай|продолжим|go\s+on|continue|keep\s+going|next(\s+one)?|next\s+frame)([!.…\s]|$)/i.test(t)) {
|
||
return true;
|
||
}
|
||
return cyrTokenRe(
|
||
"\u0434\u0430\u0432\u0430\u0439\\s+\u0434\u0430\u043B\u044C\u0448\u0435|\u0441\u043B\u0435\u0434\u0443\u044E\u0449(\u0438\u0439|\u0430\u044F|\u0435\u0435|\u0443\u044E)\\s+\u043A\u0430\u0434\u0440|\u0435\u0449\u0451\\s+\u043A\u0430\u0434\u0440|\u0435\u0449\u0435\\s+\u043A\u0430\u0434\u0440|\u043A\u0430\u0434\u0440\\s*\u2116?\\s*\\d+|\u0441\u0434\u0435\u043B\u0430\u0439\\s+\u0441\u043B\u0435\u0434\u0443\u044E\u0449"
|
||
).test(t);
|
||
}
|
||
function extractPromptFromProse(reply) {
|
||
const t = String(reply || "").replace(/\r\n/g, "\n");
|
||
if (!t.trim()) {
|
||
return null;
|
||
}
|
||
const bq = [];
|
||
for (const line of t.split("\n")) {
|
||
const m = line.match(/^\s{0,3}>\s?(.*)$/);
|
||
if (m) {
|
||
bq.push(m[1]);
|
||
continue;
|
||
}
|
||
if (bq.length) {
|
||
break;
|
||
}
|
||
}
|
||
const fromBq = bq.join("\n").trim();
|
||
if (fromBq.length >= 48) {
|
||
return fromBq.slice(0, 4e3);
|
||
}
|
||
const section = t.match(
|
||
/(?:^|\n)#{1,6}\s*(?:📷\s*)?(?:prompt|промпт|improved\s+prompt|next\s+prompt|кадр[^\n]*)\s*\n+([\s\S]+?)(?=\n#{1,6}\s|\n```|$)/i
|
||
);
|
||
if (section) {
|
||
const body = section[1].replace(/^\s{0,3}>\s?/gm, "").trim();
|
||
if (body.length >= 48) {
|
||
return body.slice(0, 4e3);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function promptLooksKreaReady(prompt) {
|
||
const t = String(prompt || "").trim();
|
||
if (t.length < 80) {
|
||
return false;
|
||
}
|
||
const cyr = (t.match(/[\u0400-\u04FF]/g) || []).length;
|
||
const lat = (t.match(/[A-Za-z]/g) || []).length;
|
||
if (cyr >= 12) {
|
||
return false;
|
||
}
|
||
if (lat < 55) {
|
||
return false;
|
||
}
|
||
if (t.length < 120 && (t.match(/[,.;:]/g) || []).length < 2) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
function promptNeedsKreaPrep(prompt) {
|
||
return !promptLooksKreaReady(prompt);
|
||
}
|
||
function buildKreaPromptPrepRequest(patch) {
|
||
const keep = {
|
||
actions: Array.isArray(patch.actions) && patch.actions.length ? patch.actions : ["generate"]
|
||
};
|
||
if (patch.aspect) {
|
||
keep.aspect = patch.aspect;
|
||
}
|
||
if (patch.negative != null && String(patch.negative).trim() !== "") {
|
||
keep.negative = patch.negative;
|
||
} else {
|
||
const liveNeg = liveNegativePrompt() || exactDefaultNegative();
|
||
if (liveNeg) {
|
||
keep.negative = liveNeg;
|
||
}
|
||
}
|
||
if (Array.isArray(patch.loras)) {
|
||
keep.loras = patch.loras;
|
||
}
|
||
if (patch.width != null) {
|
||
keep.width = patch.width;
|
||
}
|
||
if (patch.height != null) {
|
||
keep.height = patch.height;
|
||
}
|
||
return `You are the Krea 2 prompt prep step (chat model). Rewrite SOURCE into the final Swarm Generate box text.
|
||
HARD RULES:
|
||
- JSON "prompt": English only (no Cyrillic) \u2014 translate if needed.
|
||
- Natural photographer/director prose for Qwen3-VL \u2014 not Danbooru tags, not (word:1.5), not masterpiece/best quality/8k.
|
||
- Structure & front-load: subject \u2192 pose/action \u2192 body/wardrobe \u2192 setting \u2192 materials/textures \u2192 camera/framing \u2192 lighting \u2192 medium/mood.
|
||
- Expand thin ideas; fix anti-patterns; one coherent scene.
|
||
- Keep LoRA trigger phrases in English near the subject they affect.
|
||
- Always include JSON "negative": keep/supplement SOURCE+live negative, or use Exact default if empty. Never drop it.
|
||
- Put \u201Cno blur / empty street\u201D ideas as positives in prompt, not as a huge negative dump.
|
||
- One short ack in the user language max, then ONE fenced JSON merging these keys: ${JSON.stringify(keep)} plus the new English "prompt" and "negative".
|
||
- Include actions:["generate"] when an image was requested.
|
||
|
||
SOURCE:
|
||
${patch.prompt}`;
|
||
}
|
||
function mergePromptEnRewrite(effective) {
|
||
const base = state.pendingPromptEnMerge;
|
||
state.pendingPromptEnMerge = null;
|
||
if (!base) {
|
||
return effective;
|
||
}
|
||
if (!effective) {
|
||
return {
|
||
...base,
|
||
generate: true,
|
||
actions: Array.isArray(base.actions) && base.actions.length ? base.actions : ["generate"]
|
||
};
|
||
}
|
||
return {
|
||
...base,
|
||
...effective,
|
||
prompt: effective.prompt || base.prompt,
|
||
negative: effective.negative != null && String(effective.negative).trim() !== "" ? effective.negative : base.negative || liveNegativePrompt() || exactDefaultNegative() || void 0,
|
||
actions: Array.isArray(effective.actions) && effective.actions.length ? effective.actions : base.actions || ["generate"],
|
||
loras: effective.loras || base.loras,
|
||
aspect: effective.aspect || base.aspect
|
||
};
|
||
}
|
||
function userAsksNoGenerate(text) {
|
||
const t = String(text || "").trim();
|
||
if (!t) {
|
||
return false;
|
||
}
|
||
if (/\b(remember|save\s+(this\s+)?(as\s+)?(the\s+)?(base\s+)?(prompt|template)|don'?t\s+generat|do\s+not\s+generat|no\s+generat|without\s+generat)\b/i.test(t)) {
|
||
return true;
|
||
}
|
||
return cyrTokenRe(
|
||
"\u0437\u0430\u043F\u043E\u043C\u043D|\u0437\u0430\u043F\u043E\u043C\u043D\u0438|\u0437\u0430\u043F\u043E\u043C\u043D\u0438\u043C|\u0441\u043E\u0445\u0440\u0430\u043D\u0438|\u0441\u043E\u0445\u0440\u0430\u043D\u0438\u043C|\u0448\u0430\u0431\u043B\u043E\u043D|\u0431\u0430\u0437\u043E\u0432(\u044B\u0439|\u043E\u0433\u043E|\u043E\u043C\u0443|\u044B\u043C|\u0430\u044F|\u0443\u044E|\u043E\u0435)?\\s+\u043F\u0440\u043E\u043C\u043F\u0442|\u043D\u0435\\s+\u0433\u0435\u043D\u0435\u0440\u0438\u0440[\u0430-\u044F\u0451]*|\u0431\u0435\u0437\\s+\u0433\u0435\u043D\u0435\u0440\u0430\u0446[\u0430-\u044F\u0451]*|\u043D\u0435\\s+\u043D\u0430\u0434\u043E\\s+\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u0442\u043E\u043B\u044C\u043A\u043E\\s+\u0437\u0430\u043F\u043E\u043C\u043D[\u0430-\u044F\u0451]*|\u043F\u043E\u043A\u0430\\s+\u0437\u0430\u043F\u043E\u043C\u043D[\u0430-\u044F\u0451]*|\u043D\u0435\\s+\u0440\u0438\u0441\u0443\u0439|\u043D\u0435\\s+\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439\\s+\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*"
|
||
).test(t);
|
||
}
|
||
function userAsksLook(text) {
|
||
const t = String(text || "").trim();
|
||
if (!t) {
|
||
return false;
|
||
}
|
||
if (/\b(look\s+at|critique|criticize|describe\s+(this|the|ref|image)|what\s+do\s+you\s+see)\b/i.test(t)) {
|
||
return true;
|
||
}
|
||
if (cyrTokenRe("\u043A\u0440\u0438\u0442\u0438\u043A[\u0430-\u044F\u0451]*|\u0447\u0442\u043E\\s+\u043D\u0435\\s+\u0442\u0430\u043A|\u0440\u0430\u0437\u0431\u0435\u0440\u0438").test(t)) {
|
||
return true;
|
||
}
|
||
if (cyrTokenRe("\u043E\u043F\u0438\u0448\u0438\\s+(\u044D\u0442\u043E|\u044D\u0442\u0443|\u0440\u0435\u0444|\u0438\u0437\u043E\u0431\u0440\u0430\u0436[\u0430-\u044F\u0451]*|\u043A\u0430\u0440\u0442\u0438\u043D\u043A[\u0430-\u044F\u0451]*|\u043A\u0430\u0434\u0440|\u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442|\u0440\u0435\u0444\u0435\u0440\u0435\u043D\u0441)").test(t)) {
|
||
return true;
|
||
}
|
||
return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t);
|
||
}
|
||
function userAsksGenerate(text) {
|
||
if (window.SA && typeof SA.userAsksGenerate === "function") {
|
||
return SA.userAsksGenerate(text);
|
||
}
|
||
const t = String(text || "").trim();
|
||
if (!t || userAsksNoGenerate(t)) {
|
||
return false;
|
||
}
|
||
if (/\b(generat(e|ion)|draw|render|make\s+(an?\s+)?image|run\s+generate)\b/i.test(t)) {
|
||
return true;
|
||
}
|
||
return cyrTokenRe(
|
||
"\u0441\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u043D\u0430\u0440\u0438\u0441\u0443\u0439|\u043D\u0430\u0440\u0438\u0441\u0443\u0439\u0442\u0435|\u0437\u0430\u043F\u0443\u0441\u0442\u0438\\s+\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u0441\u0434\u0435\u043B\u0430\u0439\\s+(\u043A\u0430\u0434\u0440|\u043A\u0430\u0440\u0442\u0438\u043D\u043A[\u0430-\u044F\u0451]*|\u0438\u0437\u043E\u0431\u0440\u0430\u0436[\u0430-\u044F\u0451]*)"
|
||
).test(t);
|
||
}
|
||
function packWantsVision(pack) {
|
||
const p = String(pack || "");
|
||
return p === "critique_image" || p === "describe_ref" || p === "compose_scene" || p === "inpaint_edit";
|
||
}
|
||
function resolveTurnIntent2(patch, userText, opts = {}) {
|
||
const S = window.SA && window.SA.session;
|
||
if (S && typeof S.resolveTurnIntent === "function") {
|
||
return S.resolveTurnIntent(patch, userText, {
|
||
vetoFn: userAsksNoGenerate,
|
||
askGenerateFn: userAsksGenerate,
|
||
fromAutoCritique: !!opts.fromAutoCritique
|
||
});
|
||
}
|
||
const vetoed = !isMachineTurn(opts) && userAsksNoGenerate(userText);
|
||
const modelAsked = !!(patch && (patch.generate === true || patch.generate === 1 || typeof patch.generate === "string" && /^(true|1|yes|on)$/i.test(patch.generate) || Array.isArray(patch.actions) && patch.actions.map(String).includes("generate")));
|
||
const userAsked = !isMachineTurn(opts) && userAsksGenerate(userText) && !!(modelAsked || String(patch?.prompt || "").trim());
|
||
const generate = !vetoed && !opts.fromAutoCritique && (modelAsked || userAsked);
|
||
const hasLook = !!patch && (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
|
||
const look = !!(hasLook && !vetoed && !generate);
|
||
const ask = Array.isArray(patch?.ask) ? patch.ask.map(String) : typeof patch?.ask === "string" && patch.ask ? [patch.ask] : [];
|
||
return { generate, look, vetoed, ask };
|
||
}
|
||
function stripGenerateAction(patch) {
|
||
if (!patch || typeof patch !== "object") {
|
||
return patch;
|
||
}
|
||
if (!Array.isArray(patch.actions)) {
|
||
return patch;
|
||
}
|
||
const next = patch.actions.map(String).filter((a) => a !== "generate");
|
||
if (next.length === patch.actions.length) {
|
||
return patch;
|
||
}
|
||
const out = { ...patch };
|
||
if (next.length) {
|
||
out.actions = next;
|
||
} else {
|
||
delete out.actions;
|
||
}
|
||
return out;
|
||
}
|
||
function stripLookAt(patch) {
|
||
if (!patch || typeof patch !== "object") {
|
||
return patch;
|
||
}
|
||
if (patch.look_at == null && patch.vision_from == null && patch.vision_slots == null) {
|
||
return patch;
|
||
}
|
||
const out = { ...patch };
|
||
delete out.look_at;
|
||
delete out.vision_from;
|
||
delete out.vision_slots;
|
||
return out;
|
||
}
|
||
function rememberLastPatch(patch) {
|
||
if (patch && typeof patch === "object") {
|
||
state.lastPatch = patch;
|
||
syncBuildGenButton();
|
||
}
|
||
}
|
||
function syncBuildGenButton() {
|
||
const btn = $2("sa_btn_build_gen");
|
||
if (!btn) {
|
||
return;
|
||
}
|
||
if (state.lastPatch) {
|
||
const keys = Object.keys(state.lastPatch).filter((k) => state.lastPatch[k] != null).slice(0, 6);
|
||
btn.title = `\u0415\u0441\u0442\u044C \u043F\u0430\u0442\u0447 Assistent (${keys.join(", ") || "\u2026"}) \u2192 Apply + Generate`;
|
||
btn.classList.add("sa-has-patch");
|
||
} else {
|
||
btn.title = "\u041D\u0435\u0442 \u043F\u0430\u0442\u0447\u0430 \u2014 Generate \u0441 \u0442\u0435\u043A\u0443\u0449\u0438\u043C \u043F\u0440\u043E\u043C\u043F\u0442\u043E\u043C SwarmUI";
|
||
btn.classList.remove("sa-has-patch");
|
||
}
|
||
}
|
||
function defaultPackId() {
|
||
return state.config?.assistant?.default_pack || $2("sa_pack")?.querySelector("option")?.value || "ordinary";
|
||
}
|
||
function syncModeBadge() {
|
||
const badge = $2("sa_mode_badge");
|
||
const pack = $2("sa_pack")?.value || defaultPackId();
|
||
if (!badge) {
|
||
return;
|
||
}
|
||
const shortMap = {
|
||
ordinary: "\u043E\u0431\u044B\u0447\u043D\u044B\u0439",
|
||
write_prompt: "write",
|
||
critique_image: "critique",
|
||
compose_scene: "compose",
|
||
fix_params: "params",
|
||
inpaint_edit: "inpaint",
|
||
describe_ref: "describe",
|
||
author_persona: "persona"
|
||
};
|
||
const short = shortMap[pack] || pack.replace(/_/g, " ").slice(0, 12);
|
||
badge.textContent = short;
|
||
badge.dataset.pack = pack;
|
||
badge.title = `\u0420\u0435\u0436\u0438\u043C: ${pack}`;
|
||
badge.classList.toggle("sa-mode-hot", pack === "critique_image" || pack === "inpaint_edit");
|
||
}
|
||
function syncLiveParamsBar() {
|
||
const el = $2("sa_live_params");
|
||
if (!el) {
|
||
return;
|
||
}
|
||
const w = parseInt(val("input_width") || "0", 10) || null;
|
||
const h = parseInt(val("input_height") || "0", 10) || null;
|
||
const aspect = guessAspectFromSize(w, h) || "\u2014";
|
||
const steps = val("input_steps") || "\u2014";
|
||
const cfg = val("input_cfgscale") || val("input_cfg") || "\u2014";
|
||
const seed = val("input_seed") || "\u2014";
|
||
const profile = detectKreaProfileName2();
|
||
el.textContent = `${aspect} \xB7 ${w || "?"}\xD7${h || "?"} \xB7 steps ${steps} \xB7 cfg ${cfg} \xB7 ${profile} \xB7 seed ${seed}`;
|
||
}
|
||
function applyAspectTableFrom(obj) {
|
||
if (!obj || typeof obj !== "object") {
|
||
return false;
|
||
}
|
||
const next = {};
|
||
for (const [k, v] of Object.entries(obj)) {
|
||
if (Array.isArray(v) && v.length >= 2) {
|
||
next[k] = [Number(v[0]), Number(v[1])];
|
||
}
|
||
}
|
||
if (!Object.keys(next).length) {
|
||
return false;
|
||
}
|
||
ASPECT_TABLE = next;
|
||
return true;
|
||
}
|
||
function resolveExactBundle() {
|
||
const exact = state.exact || state.config?.exact || {};
|
||
const profiles = exact.profiles || state.kreaProfiles || {};
|
||
return { exact, profiles };
|
||
}
|
||
function detectKreaProfileName2() {
|
||
const KP = window.SA?.kreaProfile;
|
||
try {
|
||
const model = resolveCurrentCheckpoint();
|
||
const blob = `${model?.name || ""} ${model?.title || ""}`;
|
||
const exactProfile = state.exact?.generation?.profile;
|
||
if (KP?.detectKreaProfileName) {
|
||
return KP.detectKreaProfileName(blob, exactProfile);
|
||
}
|
||
const lower = blob.toLowerCase();
|
||
if (/turbo/.test(lower)) {
|
||
return "turbo";
|
||
}
|
||
if (/\braw\b|_raw\b|-raw\b/.test(lower)) {
|
||
return "raw";
|
||
}
|
||
return "raw";
|
||
} catch (e) {
|
||
return state.exact?.generation?.profile || "raw";
|
||
}
|
||
}
|
||
function exactProfileDefaults(profileName) {
|
||
const { exact, profiles } = resolveExactBundle();
|
||
const profile = profileName || detectKreaProfileName2();
|
||
const KP = window.SA?.kreaProfile;
|
||
if (KP?.profileParamDefaults) {
|
||
return KP.profileParamDefaults(profiles, profile, exact.generation);
|
||
}
|
||
const gen = exact.generation && typeof exact.generation === "object" ? exact.generation : {};
|
||
const fromProfile = profiles[profile] && typeof profiles[profile] === "object" ? profiles[profile] : {};
|
||
return {
|
||
profile,
|
||
steps: fromProfile.steps ?? gen.steps ?? null,
|
||
cfg: fromProfile.cfg ?? gen.cfg ?? null,
|
||
sigma_shift: fromProfile.sigma_shift ?? gen.sigma_shift ?? null
|
||
};
|
||
}
|
||
function readLiveStepsCfgSigma() {
|
||
return {
|
||
steps: parseInt(val("input_steps") || "", 10) || null,
|
||
cfg: parseFloat(val("input_cfgscale") || val("input_cfg") || "") || null,
|
||
sigma_shift: parseFloat(val("input_sigmashift") || "") || null
|
||
};
|
||
}
|
||
function setCfgVal(n) {
|
||
if (document.getElementById("input_cfgscale")) {
|
||
setVal("input_cfgscale", String(n));
|
||
} else if (document.getElementById("input_cfg")) {
|
||
setVal("input_cfg", String(n));
|
||
}
|
||
}
|
||
function forceExactParamsForGenerate({ patch = null, profileName = null } = {}) {
|
||
if (typeof isKreaSelected === "function" && !isKreaSelected()) {
|
||
return false;
|
||
}
|
||
const profile = profileName || detectKreaProfileName2();
|
||
const defaults = exactProfileDefaults(profile);
|
||
const live = readLiveStepsCfgSigma();
|
||
const KP = window.SA?.kreaProfile;
|
||
const userIntent = !!state.lastUserParamIntent;
|
||
const keys = KP?.exactKeysToForce ? KP.exactKeysToForce(live, defaults, {
|
||
patch,
|
||
sessionExact: state.sessionExact,
|
||
userParamIntent: userIntent
|
||
}) : ["steps", "cfg", "sigma_shift"].filter((k) => {
|
||
const want = defaults[k];
|
||
if (want == null) {
|
||
return false;
|
||
}
|
||
if (userIntent && patch?.[k] != null && String(patch[k]) !== String(want)) {
|
||
return false;
|
||
}
|
||
if (userIntent && state.sessionExact?.[k] != null) {
|
||
return false;
|
||
}
|
||
return live[k] == null || String(live[k]) !== String(want);
|
||
});
|
||
if (!keys.length) {
|
||
return false;
|
||
}
|
||
for (const key of keys) {
|
||
const want = defaults[key];
|
||
if (key === "steps") {
|
||
setVal("input_steps", String(want));
|
||
} else if (key === "cfg") {
|
||
setCfgVal(want);
|
||
} else if (key === "sigma_shift") {
|
||
setVal("input_sigmashift", String(want));
|
||
}
|
||
if (state.sessionExact && state.sessionExact[key] != null && String(state.sessionExact[key]) !== String(want)) {
|
||
delete state.sessionExact[key];
|
||
}
|
||
if (state.chatSession?.gen) {
|
||
state.chatSession.gen[key] = want;
|
||
}
|
||
}
|
||
syncLiveParamsBar();
|
||
return true;
|
||
}
|
||
function mergedGenerationDefaults(profileName) {
|
||
const { exact, profiles } = resolveExactBundle();
|
||
const gen = exact.generation && typeof exact.generation === "object" ? { ...exact.generation } : {};
|
||
const profile = profileName || detectKreaProfileName2();
|
||
const fromProfile = profiles[profile] && typeof profiles[profile] === "object" ? { ...profiles[profile] } : {};
|
||
const session = state.sessionExact && typeof state.sessionExact === "object" ? { ...state.sessionExact } : {};
|
||
return { ...gen, ...fromProfile, profile, ...session };
|
||
}
|
||
function exactDefaultFor(key, profileName) {
|
||
const { exact, profiles } = resolveExactBundle();
|
||
const profile = profileName || exact.generation?.profile || detectKreaProfileName2();
|
||
const fromProfile = profiles[profile]?.[key];
|
||
if (fromProfile != null) {
|
||
return fromProfile;
|
||
}
|
||
return exact.generation?.[key];
|
||
}
|
||
function rememberSessionExact(partial) {
|
||
if (state.restoringChat || !partial || typeof partial !== "object") {
|
||
return;
|
||
}
|
||
const keys = ["steps", "cfg", "sigma_shift", "aspect", "width", "height", "images", "batch", "seed", "sampler", "scheduler"];
|
||
for (const k of keys) {
|
||
if (partial[k] != null) {
|
||
state.sessionExact[k] = partial[k];
|
||
}
|
||
}
|
||
if (partial.images == null && partial.batch != null) {
|
||
state.sessionExact.images = partial.batch;
|
||
}
|
||
}
|
||
function shouldRememberSessionParam(key, value) {
|
||
if (state.restoringChat || value == null) {
|
||
return false;
|
||
}
|
||
if (state.lastUserParamIntent) {
|
||
return true;
|
||
}
|
||
const exactVal = exactDefaultFor(key);
|
||
if (exactVal == null) {
|
||
return true;
|
||
}
|
||
return String(value) !== String(exactVal);
|
||
}
|
||
function ensureExactParamsForGenerate(patch) {
|
||
const S = window.SA && window.SA.session;
|
||
if (!patch || !S || typeof S.mergeExactParamsForGenerate !== "function") {
|
||
return patch;
|
||
}
|
||
if (!S.patchWantsGenerate(patch)) {
|
||
return patch;
|
||
}
|
||
const { exact, profiles } = resolveExactBundle();
|
||
const { patch: next, clearSessionKeys } = S.mergeExactParamsForGenerate(patch, {
|
||
exact,
|
||
profiles,
|
||
profileName: detectKreaProfileName2(),
|
||
sessionExact: state.sessionExact,
|
||
userParamIntent: !!state.lastUserParamIntent
|
||
});
|
||
for (const key of clearSessionKeys || []) {
|
||
if (state.sessionExact && state.sessionExact[key] != null) {
|
||
delete state.sessionExact[key];
|
||
}
|
||
}
|
||
if (state.chatSession && state.chatSession.gen) {
|
||
for (const key of S.EXACT_GENERATE_PARAM_KEYS || ["steps", "cfg", "sigma_shift"]) {
|
||
if (next[key] != null) {
|
||
state.chatSession.gen[key] = next[key];
|
||
}
|
||
}
|
||
}
|
||
return next;
|
||
}
|
||
function fillEmptyParamsFromExact() {
|
||
const defaults = mergedGenerationDefaults();
|
||
if (isEmptyParamField(val("input_steps"), { treatZeroEmpty: true }) && defaults.steps != null) {
|
||
setVal("input_steps", String(defaults.steps));
|
||
}
|
||
const cfgRaw = val("input_cfgscale") || val("input_cfg");
|
||
if (isEmptyParamField(cfgRaw, { treatZeroEmpty: true }) && defaults.cfg != null) {
|
||
if (document.getElementById("input_cfgscale")) {
|
||
setVal("input_cfgscale", String(defaults.cfg));
|
||
} else if (document.getElementById("input_cfg")) {
|
||
setVal("input_cfg", String(defaults.cfg));
|
||
}
|
||
}
|
||
if (isEmptyParamField(val("input_sigmashift")) && defaults.sigma_shift != null) {
|
||
setVal("input_sigmashift", String(defaults.sigma_shift));
|
||
}
|
||
const wEmpty = isEmptyParamField(val("input_width"), { treatZeroEmpty: true });
|
||
const hEmpty = isEmptyParamField(val("input_height"), { treatZeroEmpty: true });
|
||
if ((wEmpty || hEmpty) && defaults.aspect) {
|
||
const size = sizeFromAspect(defaults.aspect);
|
||
if (size) {
|
||
if (wEmpty) {
|
||
setVal("input_width", String(size[0]));
|
||
}
|
||
if (hEmpty) {
|
||
setVal("input_height", String(size[1]));
|
||
}
|
||
}
|
||
} else {
|
||
if (wEmpty && defaults.width != null) {
|
||
setVal("input_width", String(defaults.width));
|
||
}
|
||
if (hEmpty && defaults.height != null) {
|
||
setVal("input_height", String(defaults.height));
|
||
}
|
||
}
|
||
const batchId = document.getElementById("input_images") ? "input_images" : document.getElementById("input_batchsize") ? "input_batchsize" : null;
|
||
if (batchId && isEmptyParamField(val(batchId), { treatZeroEmpty: true })) {
|
||
const batch = defaults.images != null ? defaults.images : defaults.batch;
|
||
if (batch != null) {
|
||
setVal(batchId, String(batch));
|
||
}
|
||
}
|
||
if (!liveNegativePrompt()) {
|
||
const neg = exactDefaultNegative();
|
||
if (neg) {
|
||
setNegativePrompt(neg);
|
||
}
|
||
}
|
||
}
|
||
function shouldSkipSessionRollback(key, patchValue) {
|
||
if (state.restoringChat) {
|
||
return false;
|
||
}
|
||
if (state.lastUserParamIntent) {
|
||
return false;
|
||
}
|
||
if (state.sessionExact[key] == null) {
|
||
return false;
|
||
}
|
||
const sessionVal = state.sessionExact[key];
|
||
if (String(sessionVal) === String(patchValue)) {
|
||
return false;
|
||
}
|
||
const exactVal = exactDefaultFor(key);
|
||
if (exactVal == null) {
|
||
return false;
|
||
}
|
||
return String(patchValue) === String(exactVal);
|
||
}
|
||
function openAssistentTab() {
|
||
const tab = document.getElementById(TAB_BUTTON_ID);
|
||
if (tab) {
|
||
tab.click();
|
||
setTimeout(() => $2("sa_input")?.focus(), 50);
|
||
return true;
|
||
}
|
||
const pane = document.getElementById("assistent");
|
||
if (pane && typeof bootstrap !== "undefined" && bootstrap.Tab) {
|
||
try {
|
||
bootstrap.Tab.getOrCreateInstance(tab || pane).show();
|
||
} catch (e) {
|
||
}
|
||
}
|
||
setTimeout(() => $2("sa_input")?.focus(), 50);
|
||
return !!tab;
|
||
}
|
||
function historyMessageLimit() {
|
||
const turns = Math.max(1, Number(HISTORY_KEEP_TURNS) || 4);
|
||
return turns * 2;
|
||
}
|
||
function ctxApi() {
|
||
return window.SA?.context || null;
|
||
}
|
||
function getContextMemory() {
|
||
const C = ctxApi();
|
||
if (C?.normalizeContextMemory) {
|
||
return C.normalizeContextMemory(state.contextMemory);
|
||
}
|
||
return state.contextMemory && typeof state.contextMemory === "object" ? state.contextMemory : { summary: "", untilCount: 0, foldedTurns: 0, at: 0, uiCollapsed: false, promptEvalCount: null };
|
||
}
|
||
function setContextMemory(raw, { persist = true } = {}) {
|
||
const C = ctxApi();
|
||
state.contextMemory = C?.normalizeContextMemory ? C.normalizeContextMemory(raw) : raw && typeof raw === "object" ? raw : null;
|
||
if (state.chatSession && typeof state.chatSession === "object") {
|
||
state.chatSession.context_memory = state.contextMemory?.summary ? state.contextMemory : null;
|
||
}
|
||
if (persist && !state.restoringChat) {
|
||
persistHistory();
|
||
}
|
||
updateCtxChip();
|
||
if (state.ctxPanelOpen) {
|
||
renderCtxPanel();
|
||
}
|
||
}
|
||
function resetContextMemory({ persist = true } = {}) {
|
||
setContextMemory(ctxApi()?.emptyContextMemory?.() || {
|
||
summary: "",
|
||
untilCount: 0,
|
||
foldedTurns: 0,
|
||
at: 0,
|
||
uiCollapsed: false,
|
||
promptEvalCount: null
|
||
}, { persist });
|
||
}
|
||
function historyCharsForBudget(messages) {
|
||
return (messages || []).reduce((n, m) => n + String(m?.content || "").length, 0);
|
||
}
|
||
function currentBudgetEstimate(modelMessages) {
|
||
const C = ctxApi();
|
||
const mem = getContextMemory();
|
||
const memChars = mem.summary ? mem.summary.length : 0;
|
||
const hist = modelMessages || assembleOutgoingMessages();
|
||
const numCtx = Number($2("sa_num_ctx")?.value) || state.config?.assistant?.num_ctx || 16384;
|
||
const numPredict = Number(state.config?.assistant?.num_predict) || 3072;
|
||
if (!C?.estimateBudget) {
|
||
return {
|
||
used: 0,
|
||
numCtx,
|
||
level: "ok",
|
||
estimated: 0,
|
||
fromEval: false,
|
||
systemChars: state.lastSystemChars || 0,
|
||
historyChars: historyCharsForBudget(hist),
|
||
memoryChars: memChars,
|
||
threshold: Math.floor((numCtx - numPredict) * COMPRESS_AT)
|
||
};
|
||
}
|
||
return C.estimateBudget({
|
||
systemChars: state.lastSystemChars || 0,
|
||
historyChars: historyCharsForBudget(hist),
|
||
memoryChars: memChars,
|
||
numCtx,
|
||
numPredict,
|
||
charsPerToken: CHARS_PER_TOKEN,
|
||
compressAt: COMPRESS_AT,
|
||
promptEvalCount: state.lastPromptEvalCount ?? mem.promptEvalCount
|
||
});
|
||
}
|
||
function assembleOutgoingMessages({ includePendingUser } = {}) {
|
||
const C = ctxApi();
|
||
const mem = getContextMemory();
|
||
let msgs;
|
||
if (C?.assembleModelMessages) {
|
||
msgs = C.assembleModelMessages(state.history, mem, HISTORY_KEEP_TURNS).map((m) => {
|
||
let content = String(m.content || "");
|
||
if (m.role === "assistant") {
|
||
content = stripJsonFencesForHistory(content);
|
||
}
|
||
return { role: m.role, content: content.slice(0, 4e3) };
|
||
});
|
||
} else {
|
||
msgs = state.history.slice(-historyMessageLimit()).map((m) => {
|
||
let content = String(m.content || "");
|
||
if (m.role === "assistant") {
|
||
content = stripJsonFencesForHistory(content);
|
||
}
|
||
return { role: m.role, content: content.slice(0, 4e3) };
|
||
});
|
||
}
|
||
if (includePendingUser) {
|
||
msgs.push({ role: "user", content: String(includePendingUser) });
|
||
}
|
||
return msgs;
|
||
}
|
||
function buildCompressUserPrompt() {
|
||
const C = ctxApi();
|
||
const mem = getContextMemory();
|
||
const fold = C?.messagesToFold ? C.messagesToFold(state.history, mem, HISTORY_KEEP_TURNS) : [];
|
||
const lines = [];
|
||
if (mem.summary) {
|
||
lines.push("## Previous conversation memory");
|
||
lines.push(mem.summary);
|
||
lines.push("");
|
||
}
|
||
lines.push("## Dialogue chunk to fold");
|
||
for (const m of fold) {
|
||
const role = m.role === "assistant" ? "Assistant" : "User";
|
||
let content = String(m.content || "");
|
||
if (m.role === "assistant") {
|
||
content = stripJsonFencesForHistory(content);
|
||
}
|
||
content = content.slice(0, 1500);
|
||
lines.push(`### ${role}`);
|
||
lines.push(content || "(empty)");
|
||
lines.push("");
|
||
}
|
||
lines.push("Compress the chunk into the required heading format. Merge with previous memory when present.");
|
||
return { prompt: lines.join("\n"), foldCount: fold.length, fold };
|
||
}
|
||
function applyCompressResult(summaryText, foldCount, { uiCollapsed = false } = {}) {
|
||
const C = ctxApi();
|
||
const prev = getContextMemory();
|
||
const merged = C?.mergeSummary ? C.mergeSummary(prev.summary, summaryText) : String(summaryText || "").trim() || prev.summary;
|
||
const nextUntil = prev.untilCount + Math.max(0, foldCount);
|
||
setContextMemory({
|
||
summary: merged,
|
||
untilCount: nextUntil,
|
||
foldedTurns: Math.floor(nextUntil / 2),
|
||
at: Date.now(),
|
||
uiCollapsed: uiCollapsed || prev.uiCollapsed,
|
||
promptEvalCount: state.lastPromptEvalCount
|
||
});
|
||
}
|
||
function callOllamaOnce(payload) {
|
||
return new Promise((resolve, reject) => {
|
||
const fail = (err) => reject(new Error(String(err || "Chat failed")));
|
||
const ok = (data) => {
|
||
if (data?.error) {
|
||
fail(data.error);
|
||
return;
|
||
}
|
||
resolve(data || {});
|
||
};
|
||
if (typeof makeWSRequest === "function") {
|
||
let settled = false;
|
||
makeWSRequest(
|
||
"AssistentChatWS",
|
||
payload,
|
||
(data) => {
|
||
if (settled) {
|
||
return;
|
||
}
|
||
if (data?.error) {
|
||
settled = true;
|
||
fail(data.error);
|
||
return;
|
||
}
|
||
if (data?.done || data?.reply != null) {
|
||
settled = true;
|
||
ok(data);
|
||
}
|
||
},
|
||
0,
|
||
(err) => {
|
||
if (settled) {
|
||
return;
|
||
}
|
||
genericRequest("AssistentChat", payload, (data) => {
|
||
settled = true;
|
||
ok(data);
|
||
}, 0, (err2) => {
|
||
settled = true;
|
||
fail(err2 || err);
|
||
});
|
||
}
|
||
);
|
||
return;
|
||
}
|
||
genericRequest("AssistentChat", payload, ok, 0, fail);
|
||
});
|
||
}
|
||
async function runCompressTurn({ uiCollapsed = false, chatEpoch = state.chatEpoch } = {}) {
|
||
const C = ctxApi();
|
||
const mem = getContextMemory();
|
||
const fold = C?.messagesToFold ? C.messagesToFold(state.history, mem, HISTORY_KEEP_TURNS) : [];
|
||
if (!fold.length) {
|
||
return false;
|
||
}
|
||
const model = $2("sa_model")?.value;
|
||
if (!model) {
|
||
setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Ollama \u0432 \u2699");
|
||
return false;
|
||
}
|
||
const { prompt, foldCount } = buildCompressUserPrompt();
|
||
if (!foldCount) {
|
||
return false;
|
||
}
|
||
state.compressing = true;
|
||
updateCtxChip();
|
||
setBusyPhase("compressing");
|
||
setStatus("\u0421\u0436\u0438\u043C\u0430\u044E \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u2026");
|
||
const persona = $2("sa_persona")?.value || "neutral";
|
||
const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434";
|
||
const payload = {
|
||
baseUrl,
|
||
model,
|
||
pack: "compress_history",
|
||
persona,
|
||
includeBase: false,
|
||
messages: [{ role: "user", content: prompt }],
|
||
context_json: JSON.stringify({
|
||
compress: true,
|
||
previous_memory: mem.summary || null,
|
||
fold_count: foldCount
|
||
}),
|
||
skills: [],
|
||
embed_model: $2("sa_embed_model")?.value || state.preferredEmbed || ""
|
||
};
|
||
try {
|
||
const data = await callOllamaOnce(payload);
|
||
if (chatEpoch !== state.chatEpoch) {
|
||
return false;
|
||
}
|
||
if (data.prompt_eval_count != null) {
|
||
state.lastPromptEvalCount = Number(data.prompt_eval_count) || null;
|
||
} else if (data.raw?.prompt_eval_count != null) {
|
||
state.lastPromptEvalCount = Number(data.raw.prompt_eval_count) || null;
|
||
}
|
||
const reply = String(data.reply || "").trim();
|
||
if (!reply) {
|
||
setStatus("\u0421\u0436\u0430\u0442\u0438\u0435: \u043F\u0443\u0441\u0442\u043E\u0439 \u043E\u0442\u0432\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0438");
|
||
return false;
|
||
}
|
||
applyCompressResult(reply, foldCount, { uiCollapsed });
|
||
if (uiCollapsed) {
|
||
renderHistoryIntoUi(state.history);
|
||
}
|
||
setStatus("\u041A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 \u0441\u0436\u0430\u0442");
|
||
return true;
|
||
} catch (e) {
|
||
console.warn("Assistent compress failed", e);
|
||
setStatus(`\u0421\u0436\u0430\u0442\u0438\u0435 \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C: ${e.message || e}`);
|
||
return false;
|
||
} finally {
|
||
state.compressing = false;
|
||
updateCtxChip();
|
||
}
|
||
}
|
||
function maybeAutoCompressBeforeSend(chatEpoch) {
|
||
if (!COMPRESS_AUTO) {
|
||
return Promise.resolve(false);
|
||
}
|
||
const C = ctxApi();
|
||
const mem = getContextMemory();
|
||
const msgs = assembleOutgoingMessages();
|
||
const budget = currentBudgetEstimate(msgs);
|
||
const should = C?.shouldCompress ? C.shouldCompress(budget, mem, (state.history || []).filter((m) => m && !m.systemish).length, {
|
||
keepMessages: historyMessageLimit()
|
||
}) : false;
|
||
if (!should) {
|
||
return Promise.resolve(false);
|
||
}
|
||
return runCompressTurn({ uiCollapsed: false, chatEpoch });
|
||
}
|
||
function formatCtxChipLabel(budget) {
|
||
const C = ctxApi();
|
||
const used = C?.formatTokenShort ? C.formatTokenShort(budget.used) : String(budget.used || 0);
|
||
const cap = C?.formatTokenShort ? C.formatTokenShort(budget.numCtx) : String(budget.numCtx || 0);
|
||
return `${used} / ${cap}`;
|
||
}
|
||
function updateCtxChip() {
|
||
const chip = $2("sa_ctx_chip");
|
||
if (!chip) {
|
||
return;
|
||
}
|
||
const budget = currentBudgetEstimate();
|
||
const mem = getContextMemory();
|
||
const label = formatCtxChipLabel(budget);
|
||
const textEl = chip.querySelector(".sa-ctx-chip-text");
|
||
if (textEl) {
|
||
textEl.textContent = label;
|
||
} else {
|
||
chip.textContent = label;
|
||
}
|
||
chip.classList.remove("sa-ctx-ok", "sa-ctx-warn", "sa-ctx-hot", "sa-ctx-compressing", "sa-ctx-has-mem");
|
||
if (state.compressing) {
|
||
chip.classList.add("sa-ctx-compressing");
|
||
} else {
|
||
chip.classList.add(`sa-ctx-${budget.level || "ok"}`);
|
||
}
|
||
if (mem.summary) {
|
||
chip.classList.add("sa-ctx-has-mem");
|
||
}
|
||
const src = budget.fromEval ? "\u0444\u0430\u043A\u0442 Ollama" : "\u043E\u0446\u0435\u043D\u043A\u0430";
|
||
chip.title = `\u041A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 \u043C\u043E\u0434\u0435\u043B\u0438 \xB7 ${src}${mem.summary ? " \xB7 \u0435\u0441\u0442\u044C \u0441\u0430\u043C\u043C\u0430\u0440\u0438" : ""}`;
|
||
const dot = chip.querySelector(".sa-ctx-dot");
|
||
if (dot) {
|
||
dot.hidden = !mem.summary;
|
||
}
|
||
}
|
||
function toggleCtxPanel(force) {
|
||
const panel = $2("sa_ctx_panel");
|
||
const chip = $2("sa_ctx_chip");
|
||
if (!panel || !chip) {
|
||
return;
|
||
}
|
||
const open = force != null ? !!force : !state.ctxPanelOpen;
|
||
state.ctxPanelOpen = open;
|
||
panel.hidden = !open;
|
||
chip.setAttribute("aria-expanded", open ? "true" : "false");
|
||
if (open) {
|
||
renderCtxPanel();
|
||
}
|
||
}
|
||
function renderCtxPanel() {
|
||
const body = $2("sa_ctx_panel_body");
|
||
const bar = $2("sa_ctx_bar_fill");
|
||
const auto = $2("sa_ctx_auto");
|
||
if (!body) {
|
||
return;
|
||
}
|
||
const budget = currentBudgetEstimate();
|
||
const mem = getContextMemory();
|
||
const layers = state.lastSystemLayers || {};
|
||
const layerRows = Object.entries(layers).filter(([k]) => k !== "total").map(([k, v]) => `<div class="sa-ctx-layer"><span>${escapeHtml2(k)}</span><span>${Number(v) || 0}</span></div>`).join("");
|
||
const keep = HISTORY_KEEP_TURNS;
|
||
const uncovered = Math.max(0, (state.history || []).filter((m) => m && !m.systemish).length - (mem.untilCount || 0));
|
||
body.innerHTML = `
|
||
<div class="sa-ctx-meta">${budget.fromEval ? "\u0422\u043E\u043A\u0435\u043D\u044B (prompt_eval)" : "\u041E\u0446\u0435\u043D\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u043E\u0432"} \xB7 \u043F\u043E\u0440\u043E\u0433 ${budget.threshold || "\u2014"}</div>
|
||
<div class="sa-ctx-layer"><span>system</span><span>${budget.systemChars || 0}</span></div>
|
||
<div class="sa-ctx-layer"><span>history</span><span>${budget.historyChars || 0}</span></div>
|
||
<div class="sa-ctx-layer"><span>memory</span><span>${budget.memoryChars || 0}</span></div>
|
||
${layerRows ? `<div class="sa-ctx-layers-label">system_layers</div>${layerRows}` : ""}
|
||
<div class="sa-ctx-see">\u041C\u043E\u0434\u0435\u043B\u044C \u0432\u0438\u0434\u0438\u0442: ${mem.summary ? `\u0441\u0430\u043C\u043C\u0430\u0440\u0438 (${mem.foldedTurns || 0} \u0445\u043E\u0434\u043E\u0432) +` : ""} \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 ${keep} \u0445\u043E\u0434\u043E\u0432 \xB7 \u0441\u044B\u0440\u044B\u0445 \u0432 \u043E\u043A\u043D\u0435 \u2248 ${Math.min(uncovered, historyMessageLimit())}</div>
|
||
${mem.summary ? `<pre class="sa-ctx-summary">${escapeHtml2(mem.summary.slice(0, 800))}${mem.summary.length > 800 ? "\u2026" : ""}</pre>` : '<div class="sa-ctx-meta">\u0421\u0430\u043C\u043C\u0430\u0440\u0438 \u0435\u0449\u0451 \u043D\u0435\u0442 \u2014 \u0441\u0442\u0430\u0440\u044B\u0435 \u0445\u043E\u0434\u044B \u043F\u0440\u043E\u0441\u0442\u043E \u043E\u0442\u0431\u0440\u0430\u0441\u044B\u0432\u0430\u044E\u0442\u0441\u044F.</div>'}
|
||
`;
|
||
if (bar) {
|
||
const pct = Math.max(0, Math.min(100, budget.used / (budget.numCtx || 1) * 100));
|
||
bar.style.width = `${pct}%`;
|
||
bar.dataset.level = budget.level || "ok";
|
||
}
|
||
if (auto) {
|
||
auto.checked = !!COMPRESS_AUTO;
|
||
}
|
||
updateCtxChip();
|
||
}
|
||
async function compressNowFromUi() {
|
||
if (state.busy || state.generating || state.compressing) {
|
||
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");
|
||
return;
|
||
}
|
||
const model = $2("sa_model")?.value;
|
||
if (!model) {
|
||
setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Ollama \u0432 \u2699");
|
||
return;
|
||
}
|
||
state.busy = true;
|
||
setInterruptVisible(true);
|
||
startBusyUi("compressing");
|
||
const epoch = state.chatEpoch;
|
||
try {
|
||
const ok = await runCompressTurn({ uiCollapsed: true, chatEpoch: epoch });
|
||
if (!ok) {
|
||
setStatus("\u041D\u0435\u0447\u0435\u0433\u043E \u0441\u0436\u0438\u043C\u0430\u0442\u044C (\u0445\u0432\u043E\u0441\u0442 \u2264 keep)");
|
||
}
|
||
} finally {
|
||
if (epoch === state.chatEpoch) {
|
||
state.busy = false;
|
||
setInterruptVisible(state.generating);
|
||
stopBusyUi(getContextMemory().summary ? "\u041A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 \u0441\u0436\u0430\u0442" : "\u0413\u043E\u0442\u043E\u0432\u043E");
|
||
}
|
||
updateCtxChip();
|
||
if (state.ctxPanelOpen) {
|
||
renderCtxPanel();
|
||
}
|
||
}
|
||
}
|
||
function resetCompressionFromUi() {
|
||
resetContextMemory();
|
||
renderHistoryIntoUi(state.history);
|
||
setStatus("\u0421\u0436\u0430\u0442\u0438\u0435 \u0441\u0431\u0440\u043E\u0448\u0435\u043D\u043E \u2014 \u043C\u043E\u0434\u0435\u043B\u044C \u0441\u043D\u043E\u0432\u0430 \u0432\u0438\u0434\u0438\u0442 \u0442\u043E\u043B\u044C\u043A\u043E last-K");
|
||
if (state.ctxPanelOpen) {
|
||
renderCtxPanel();
|
||
}
|
||
}
|
||
function flashImagePane(slotId) {
|
||
const el = document.querySelector(`.sa-slot[data-id="${slotId || state.selectedSlotId}"]`);
|
||
if (!el) {
|
||
return;
|
||
}
|
||
el.classList.remove("sa-flash");
|
||
void el.offsetWidth;
|
||
el.classList.add("sa-flash");
|
||
}
|
||
function ensureBoard() {
|
||
if (state.slots.length) {
|
||
return;
|
||
}
|
||
state.slots = [
|
||
{ id: GEN_ID, type: "generate", label: "Generate", src: null, attach: false },
|
||
{ id: "ref1", type: "ref", label: "Ref 1", src: null, attach: true }
|
||
];
|
||
state.refSeq = 1;
|
||
state.selectedSlotId = "ref1";
|
||
}
|
||
function slotById(id) {
|
||
ensureBoard();
|
||
const key = normalizeSlotId(id);
|
||
return state.slots.find((s) => s.id === key) || null;
|
||
}
|
||
function generateSlot() {
|
||
return slotById(GEN_ID);
|
||
}
|
||
function refSlots() {
|
||
ensureBoard();
|
||
return state.slots.filter((s) => s.type === "ref");
|
||
}
|
||
function normalizeSlotId(id) {
|
||
const raw = String(id || "").trim().toLowerCase();
|
||
if (!raw) {
|
||
return "";
|
||
}
|
||
if (raw === "gen" || raw === "current" || raw === "live" || raw === "generation") {
|
||
return GEN_ID;
|
||
}
|
||
if (raw === "selected" || raw === "sel") {
|
||
return state.selectedSlotId;
|
||
}
|
||
const m = raw.match(/^ref\s*[_-]?\s*(\d+)$/);
|
||
if (m) {
|
||
return `ref${m[1]}`;
|
||
}
|
||
return raw;
|
||
}
|
||
function selectedSlot() {
|
||
return slotById(state.selectedSlotId) || generateSlot();
|
||
}
|
||
function selectedSrc() {
|
||
return selectedSlot()?.src || null;
|
||
}
|
||
function syncLastImageAlias() {
|
||
const attached = attachableSlots();
|
||
state.lastImageDataUrl = (attached[0] || selectedSlot() || generateSlot())?.src || null;
|
||
}
|
||
function attachableSlots() {
|
||
ensureBoard();
|
||
return state.slots.filter((s) => s.attach && s.src);
|
||
}
|
||
function visionReadySlots() {
|
||
ensureBoard();
|
||
return state.slots.filter((s) => s && s.src && !looksLikeModelPreview(s.src));
|
||
}
|
||
function setSlotSrc(id, src, { select = true, attach = null, note = null, switchTab = false, allowPreview = false } = {}) {
|
||
const slot = slotById(id);
|
||
if (!slot) {
|
||
return false;
|
||
}
|
||
const cleaned = src ? String(src).trim().split(/\s+/)[0] : null;
|
||
if (cleaned && cleaned.startsWith("#")) {
|
||
return false;
|
||
}
|
||
if (cleaned && !allowPreview && looksLikeModelPreview(cleaned)) {
|
||
setStatus("\u041F\u0440\u043E\u043F\u0443\u0441\u043A \u043F\u0440\u0435\u0432\u044C\u044E \u043C\u043E\u0434\u0435\u043B\u0438 (\u043D\u0443\u0436\u043D\u0430 \u0440\u0435\u0430\u043B\u044C\u043D\u0430\u044F \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044F)");
|
||
return false;
|
||
}
|
||
slot.src = cleaned || null;
|
||
if (attach != null) {
|
||
slot.attach = !!attach;
|
||
} else if (slot.type === "ref" && slot.src) {
|
||
slot.attach = true;
|
||
}
|
||
if (select) {
|
||
state.selectedSlotId = slot.id;
|
||
}
|
||
syncLastImageAlias();
|
||
renderBoard();
|
||
flashImagePane(slot.id);
|
||
if (switchTab) {
|
||
openAssistentTab();
|
||
}
|
||
if (note) {
|
||
setStatus(note);
|
||
}
|
||
return true;
|
||
}
|
||
function addRefSlot({ src = null, select = true } = {}) {
|
||
ensureBoard();
|
||
if (refSlots().length >= MAX_REF_SLOTS) {
|
||
setStatus(`Max ${MAX_REF_SLOTS} reference windows`);
|
||
const empty = refSlots().find((s) => !s.src);
|
||
if (empty && src) {
|
||
return setSlotSrc(empty.id, src, { select, note: `Loaded into ${empty.label}` });
|
||
}
|
||
return empty || null;
|
||
}
|
||
state.refSeq += 1;
|
||
const id = `ref${state.refSeq}`;
|
||
const slot = {
|
||
id,
|
||
type: "ref",
|
||
label: `Ref ${state.refSeq}`,
|
||
src: src || null,
|
||
attach: !!src
|
||
};
|
||
state.slots.push(slot);
|
||
if (select) {
|
||
state.selectedSlotId = id;
|
||
}
|
||
renderBoard();
|
||
return slot;
|
||
}
|
||
function clearSlot(id, { silent = false } = {}) {
|
||
const slot = slotById(id);
|
||
if (!slot) {
|
||
return;
|
||
}
|
||
if (slot.type === "generate") {
|
||
if (!silent) {
|
||
setStatus("Generate window is live \u2014 use Snapshot gen to copy it");
|
||
}
|
||
return;
|
||
}
|
||
slot.src = null;
|
||
slot.attach = true;
|
||
syncLastImageAlias();
|
||
renderBoard();
|
||
if (!silent) {
|
||
setStatus(`${slot.label} cleared`);
|
||
}
|
||
}
|
||
function snapshotGenerateToRef() {
|
||
const src = generateSlot()?.src && !looksLikeModelPreview(generateSlot().src) ? generateSlot().src : findCurrentGenerateSrc({ allowPreview: false });
|
||
if (!src) {
|
||
setStatus("\u041D\u0435\u0442 \u0442\u0435\u043A\u0443\u0449\u0435\u0433\u043E \u043A\u0430\u0434\u0440\u0430 Generate (\u043F\u0440\u0435\u0432\u044C\u044E \u043C\u043E\u0434\u0435\u043B\u0438 \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F)");
|
||
return false;
|
||
}
|
||
const empty = refSlots().find((s) => !s.src);
|
||
let ok = false;
|
||
if (empty) {
|
||
ok = setSlotSrc(empty.id, src, { note: `\u0421\u043D\u0438\u043C\u043E\u043A \u2192 ${empty.label}` });
|
||
} else {
|
||
const created = addRefSlot({ src, select: true });
|
||
if (created?.src) {
|
||
setStatus(`\u0421\u043D\u0438\u043C\u043E\u043A \u2192 ${created.label}`);
|
||
flashImagePane(created.id);
|
||
ok = true;
|
||
} else {
|
||
const last = refSlots()[refSlots().length - 1];
|
||
if (last) {
|
||
ok = setSlotSrc(last.id, src, { note: `\u0421\u043D\u0438\u043C\u043E\u043A \u2192 ${last.label} (\u0437\u0430\u043C\u0435\u043D\u0430)` });
|
||
}
|
||
}
|
||
}
|
||
if (ok) {
|
||
setBoardTab("refs");
|
||
}
|
||
return ok;
|
||
}
|
||
function putImageOnBoard(src, { note = null, switchTab = false, preferSelected = true } = {}) {
|
||
if (!src) {
|
||
return false;
|
||
}
|
||
ensureBoard();
|
||
const sel = selectedSlot();
|
||
if (preferSelected && sel && sel.type === "ref") {
|
||
return setSlotSrc(sel.id, src, { note: note || `Loaded into ${sel.label}`, switchTab });
|
||
}
|
||
const empty = refSlots().find((s) => !s.src);
|
||
if (empty) {
|
||
return setSlotSrc(empty.id, src, { note: note || `Loaded into ${empty.label}`, switchTab });
|
||
}
|
||
const created = addRefSlot({ src, select: true });
|
||
if (created) {
|
||
if (switchTab) {
|
||
openAssistentTab();
|
||
}
|
||
if (note) {
|
||
setStatus(note);
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function setImageFromSrc(src, opts = {}) {
|
||
return putImageOnBoard(src, opts);
|
||
}
|
||
function clearVisionImage(opts) {
|
||
clearSlot(state.selectedSlotId, opts);
|
||
}
|
||
function slotCatalog() {
|
||
ensureBoard();
|
||
return state.slots.map((s) => ({
|
||
id: s.id,
|
||
type: s.type,
|
||
label: s.label,
|
||
has_image: !!s.src,
|
||
attach: !!s.attach,
|
||
selected: s.id === state.selectedSlotId
|
||
}));
|
||
}
|
||
function lookAtIdsFromPatch(patch) {
|
||
if (!patch) {
|
||
return [];
|
||
}
|
||
const raw = patch.look_at || patch.vision_from || patch.vision_slots;
|
||
const list = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
||
if (Array.isArray(patch.actions)) {
|
||
for (const a of patch.actions.map(String)) {
|
||
const m = a.match(/^look_at[_:]?(generate|ref\d+|selected)$/i);
|
||
if (m) {
|
||
list.push(m[1]);
|
||
}
|
||
}
|
||
}
|
||
return [...new Set(list.map(normalizeSlotId).filter(Boolean))];
|
||
}
|
||
function resolveSlotSrc(id) {
|
||
if (!id) {
|
||
return selectedSrc() || generateSlot()?.src || findCurrentGenerateSrc();
|
||
}
|
||
const slot = slotById(id);
|
||
if (slot?.src) {
|
||
return slot.src;
|
||
}
|
||
if (normalizeSlotId(id) === GEN_ID) {
|
||
return findCurrentGenerateSrc();
|
||
}
|
||
return null;
|
||
}
|
||
function isSwarmGenerateRunning() {
|
||
try {
|
||
if (typeof num_live_gens === "number" && num_live_gens > 0) {
|
||
return true;
|
||
}
|
||
if (typeof num_waiting_gens === "number" && num_waiting_gens > 0) {
|
||
return true;
|
||
}
|
||
} catch (e) {
|
||
}
|
||
try {
|
||
if (typeof mainGenHandler !== "undefined" && mainGenHandler) {
|
||
if (mainGenHandler.isGenerating === true || mainGenHandler.running === true) {
|
||
return true;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
}
|
||
const interrupt = document.getElementById("interrupt_button") || document.getElementById("alt_interrupt_button");
|
||
if (interrupt && !interrupt.hidden && interrupt.offsetParent !== null) {
|
||
return true;
|
||
}
|
||
const genBtn = document.getElementById("generate_button") || document.getElementById("alt_generate_button");
|
||
if (genBtn && (genBtn.disabled || /interrupt/i.test(genBtn.textContent || ""))) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function isGenerateUnavailable() {
|
||
if (state.generating || state.busy) {
|
||
return true;
|
||
}
|
||
return isSwarmGenerateRunning();
|
||
}
|
||
function syncGenerateBusy() {
|
||
const overlay = document.querySelector(".sa-slot-gen .sa-slot-busy");
|
||
if (overlay) {
|
||
const stuck = state.generating && !isSwarmGenerateRunning();
|
||
overlay.hidden = !state.generating && state.busyPhase !== "generating" || stuck;
|
||
}
|
||
const running = state.generating || state.busyPhase === "generating";
|
||
document.querySelectorAll(".sa-slot-gen-result").forEach((el) => {
|
||
const busy = el.querySelector(".sa-slot-busy");
|
||
if (!busy) {
|
||
return;
|
||
}
|
||
const hasImg = el.classList.contains("sa-has-image");
|
||
busy.hidden = !running || hasImg;
|
||
});
|
||
}
|
||
function syncPatchActionAvailability() {
|
||
const bar = document.querySelector(".sa-patch-actions.sa-patch-current");
|
||
if (!bar) {
|
||
return;
|
||
}
|
||
const locked = isGenerateUnavailable();
|
||
bar.querySelectorAll(".sa-btn-gen").forEach((btn) => {
|
||
btn.disabled = locked;
|
||
let spin = btn.querySelector(".sa-spinner");
|
||
if (locked) {
|
||
if (!spin) {
|
||
spin = document.createElement("span");
|
||
spin.className = "sa-spinner sa-spinner-btn";
|
||
spin.setAttribute("aria-hidden", "true");
|
||
btn.prepend(spin);
|
||
}
|
||
} else if (spin) {
|
||
spin.remove();
|
||
}
|
||
});
|
||
}
|
||
function retireStalePatchActions() {
|
||
document.querySelectorAll(".sa-patch-actions").forEach((el) => {
|
||
const note = document.createElement("div");
|
||
note.className = "sa-patch-stale";
|
||
note.textContent = "Superseded \u2014 use the latest proposal";
|
||
el.replaceWith(note);
|
||
});
|
||
}
|
||
function mountPatchBlock(host, patch, { silent = false } = {}) {
|
||
if (!host || !patch) {
|
||
return;
|
||
}
|
||
rememberLastPatch(patch);
|
||
const wrap = document.createElement("div");
|
||
wrap.className = "sa-patch" + (silent ? " sa-patch-auto" : "");
|
||
const details = document.createElement("details");
|
||
details.className = "sa-patch-details";
|
||
const summary = document.createElement("summary");
|
||
const keys = Object.keys(patch).filter((k) => patch[k] != null && k !== "notes" && k !== "actions");
|
||
summary.textContent = silent ? `\u041F\u0430\u0442\u0447 \u043F\u0440\u0438\u043C\u0435\u043D\u0451\u043D \xB7 ${keys.slice(0, 6).join(", ") || "generate"}` : `JSON \u043F\u0430\u0442\u0447 \xB7 ${keys.slice(0, 8).join(", ") || "\u2026"}`;
|
||
const pre = document.createElement("pre");
|
||
pre.textContent = JSON.stringify(patch, null, 2);
|
||
details.appendChild(summary);
|
||
details.appendChild(pre);
|
||
wrap.appendChild(details);
|
||
mountPatchActions(wrap, patch, { silent });
|
||
host.appendChild(wrap);
|
||
}
|
||
function mountPatchActions(parent, patch, { silent = false } = {}) {
|
||
if (!parent || !patch) {
|
||
return;
|
||
}
|
||
rememberLastPatch(patch);
|
||
retireStalePatchActions();
|
||
const wrap = parent.classList.contains("sa-patch") ? parent : null;
|
||
const host = wrap || parent;
|
||
if (silent) {
|
||
const note = document.createElement("div");
|
||
note.className = "sa-patch-actions sa-patch-silent sa-patch-current";
|
||
const willGen = Array.isArray(patch.actions) && patch.actions.map(String).includes("generate") || !!patch.generate || !!state.pendingSilentGen;
|
||
note.textContent = willGen ? "\u0412 \u0441\u0435\u0441\u0441\u0438\u044E \xB7 Generate\u2026" : "\u0412 \u0441\u0435\u0441\u0441\u0438\u044E";
|
||
host.appendChild(note);
|
||
return;
|
||
}
|
||
const actions = document.createElement("div");
|
||
actions.className = "sa-patch-actions sa-patch-current";
|
||
for (const [label, which] of [
|
||
["\u041F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C \u0432\u0441\u0451", "all"],
|
||
["\u041F\u0440\u043E\u043C\u043F\u0442", "prompt"],
|
||
["LoRAs", "loras"],
|
||
["\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B", "params"]
|
||
]) {
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
btn.className = "basic-button";
|
||
btn.textContent = label;
|
||
btn.addEventListener("click", () => applyPatch(patch, which));
|
||
actions.appendChild(btn);
|
||
}
|
||
const toSession = document.createElement("button");
|
||
toSession.type = "button";
|
||
toSession.className = "basic-button";
|
||
toSession.textContent = "\u0412 \u0441\u0435\u0441\u0441\u0438\u044E";
|
||
toSession.addEventListener("click", async () => {
|
||
const S = window.SA && window.SA.session;
|
||
if (S) {
|
||
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), patch);
|
||
}
|
||
await pushSessionToSwarm(state.chatSession);
|
||
rememberLastPatch(patch);
|
||
try {
|
||
const chat = findChat(state.activeChatId);
|
||
if (chat) {
|
||
chat.params = snapshotChatParams();
|
||
persistChatsStore();
|
||
}
|
||
} catch (e) {
|
||
}
|
||
setStatus("\u041F\u0430\u0442\u0447 \u0432 \u0441\u0435\u0441\u0441\u0438\u0438");
|
||
});
|
||
actions.appendChild(toSession);
|
||
const genBtn = document.createElement("button");
|
||
genBtn.type = "button";
|
||
genBtn.className = "basic-button sa-btn-gen";
|
||
genBtn.textContent = "\u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C";
|
||
genBtn.addEventListener("click", async () => {
|
||
if (isGenerateUnavailable()) {
|
||
setStatus("Generate \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u2014 \u043F\u043E\u0434\u043E\u0436\u0434\u0438 \u0438\u043B\u0438 \u043D\u0430\u0436\u043C\u0438 \u0421\u0442\u043E\u043F");
|
||
return;
|
||
}
|
||
startBusyUi("silent_gen");
|
||
const S = window.SA && window.SA.session;
|
||
if (S) {
|
||
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), patch);
|
||
}
|
||
await pushSessionToSwarm(state.chatSession);
|
||
await runGenerateFromPatch({ ...patch, actions: ["generate"] }, { force: true, fromSession: true });
|
||
});
|
||
actions.appendChild(genBtn);
|
||
host.appendChild(actions);
|
||
syncPatchActionAvailability();
|
||
}
|
||
async function buildCurrentAndGenerate() {
|
||
if (state.busy || state.generating) {
|
||
setStatus("\u0417\u0430\u043D\u044F\u0442\u043E \u2014 \u043F\u043E\u0434\u043E\u0436\u0434\u0438 \u0438\u043B\u0438 \u043D\u0430\u0436\u043C\u0438 \u0421\u0442\u043E\u043F");
|
||
return;
|
||
}
|
||
if (typeof isGenerateUnavailable === "function" && isGenerateUnavailable()) {
|
||
setStatus("Generate \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u2014 \u0434\u043E\u0436\u0434\u0438\u0441\u044C SwarmUI");
|
||
return;
|
||
}
|
||
pullLiveIntoSession();
|
||
const S = window.SA && window.SA.session;
|
||
if (state.lastPatch && S) {
|
||
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), state.lastPatch);
|
||
}
|
||
if (typeof startBusyUi === "function") startBusyUi(state.lastPatch ? "silent_gen" : "generating");
|
||
setStatus(state.lastPatch ? "\u0421\u0435\u0441\u0441\u0438\u044F \u2192 Generate\u2026" : "Generate \u0441 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0441\u0435\u0441\u0441\u0438\u0435\u0439\u2026");
|
||
await pushSessionToSwarm(state.chatSession);
|
||
await runGenerateFromPatch({ actions: ["generate"] }, { force: true, fromSession: true });
|
||
}
|
||
function renderBoard() {
|
||
const board = $2("sa_board");
|
||
if (!board) {
|
||
return;
|
||
}
|
||
ensureBoard();
|
||
const tab = state.boardTab === "refs" ? "refs" : "generate";
|
||
const refs = refSlots();
|
||
const showVariantGrid = tab === "generate" && (state.genResults || []).length > 1;
|
||
board.classList.toggle("sa-board-many", tab === "refs" && (refs.some((s) => s.src) || refs.length > 1) || showVariantGrid);
|
||
board.classList.toggle("sa-board-gen-only", tab === "generate" && !showVariantGrid);
|
||
board.classList.toggle("sa-board-variants", showVariantGrid);
|
||
board.innerHTML = "";
|
||
if (tab === "generate" && showVariantGrid) {
|
||
for (const row of state.genResults) {
|
||
board.appendChild(buildGenResultEl(row));
|
||
}
|
||
} else {
|
||
const toShow = tab === "generate" ? state.slots.filter((s) => s.type === "generate") : state.slots.filter((s) => s.type !== "generate");
|
||
for (const slot of toShow) {
|
||
board.appendChild(buildSlotEl(slot));
|
||
}
|
||
}
|
||
if (tab === "refs" && refs.length < MAX_REF_SLOTS) {
|
||
const add = document.createElement("div");
|
||
add.className = "sa-add-cell";
|
||
add.textContent = "+ Ref";
|
||
add.title = "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043E\u043A\u043D\u043E \u0440\u0435\u0444\u0435\u0440\u0435\u043D\u0441\u0430";
|
||
add.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
addRefSlot({ select: true });
|
||
});
|
||
add.addEventListener("dragover", (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
});
|
||
add.addEventListener("drop", async (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
const created = addRefSlot({ select: true });
|
||
if (created) {
|
||
state.selectedSlotId = created.id;
|
||
await handleDropDataTransfer(e.dataTransfer, created.id);
|
||
}
|
||
});
|
||
board.appendChild(add);
|
||
}
|
||
syncBoardChrome();
|
||
syncGenerateBusy();
|
||
syncLastImageAlias();
|
||
}
|
||
function buildGenResultEl(row) {
|
||
const el = document.createElement("div");
|
||
el.className = "sa-slot sa-slot-gen-result";
|
||
el.dataset.genResultId = row.id;
|
||
if (row.src) {
|
||
el.classList.add("sa-has-image");
|
||
}
|
||
if (row.id === state.selectedGenResultId) {
|
||
el.classList.add("sa-selected");
|
||
}
|
||
const bar = document.createElement("div");
|
||
bar.className = "sa-slot-bar";
|
||
const chip = document.createElement("span");
|
||
chip.className = "sa-slot-chip sa-live";
|
||
chip.textContent = row.label || row.id;
|
||
bar.appendChild(chip);
|
||
const openBtn = document.createElement("button");
|
||
openBtn.type = "button";
|
||
openBtn.className = "sa-slot-open";
|
||
openBtn.textContent = "\u041E\u0442\u043A\u0440\u044B\u0442\u044C";
|
||
openBtn.title = "\u041F\u0440\u043E\u0441\u043C\u043E\u0442\u0440";
|
||
openBtn.hidden = !row.src;
|
||
openBtn.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
selectGenResult(row.id, { restore: true, openViewer: true });
|
||
});
|
||
bar.appendChild(openBtn);
|
||
el.appendChild(bar);
|
||
if (row.src) {
|
||
const img = document.createElement("img");
|
||
img.alt = row.label || row.id;
|
||
img.src = row.src;
|
||
el.appendChild(img);
|
||
} else {
|
||
const empty = document.createElement("div");
|
||
empty.className = "sa-image-empty";
|
||
empty.innerHTML = '<div class="sa-empty-title">\u2026</div><div class="sa-empty-hint">\u0416\u0434\u0443 \u043A\u0430\u0434\u0440</div>';
|
||
el.appendChild(empty);
|
||
}
|
||
const busy = document.createElement("div");
|
||
busy.className = "sa-slot-busy";
|
||
const pending = !row.src && (state.generating || state.busyPhase === "generating");
|
||
busy.hidden = !pending;
|
||
busy.innerHTML = '<span class="sa-spinner" aria-hidden="true"></span>';
|
||
el.appendChild(busy);
|
||
el.addEventListener("click", () => {
|
||
const already = row.id === state.selectedGenResultId;
|
||
selectGenResult(row.id, { restore: true, openViewer: already && !!row.src });
|
||
});
|
||
el.addEventListener("dblclick", (e) => {
|
||
e.preventDefault();
|
||
if (row.src) {
|
||
selectGenResult(row.id, { restore: true, openViewer: true });
|
||
}
|
||
});
|
||
return el;
|
||
}
|
||
function ensureGenLightbox() {
|
||
let root = $2("sa_gen_lightbox");
|
||
if (root) {
|
||
return root;
|
||
}
|
||
const host = $2("swarm_assistent_root") || document.body;
|
||
root = document.createElement("div");
|
||
root.id = "sa_gen_lightbox";
|
||
root.className = "sa-lightbox";
|
||
root.hidden = true;
|
||
root.innerHTML = `
|
||
<div class="sa-lightbox-backdrop" data-lb="close"></div>
|
||
<div class="sa-lightbox-panel" role="dialog" aria-modal="true" aria-label="\u041F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430">
|
||
<div class="sa-lightbox-head">
|
||
<span class="sa-lightbox-title" id="sa_lb_title"></span>
|
||
<span class="sa-lightbox-idx" id="sa_lb_idx"></span>
|
||
<button type="button" class="basic-button sa-icon-btn" data-lb="close" title="\u0417\u0430\u043A\u0440\u044B\u0442\u044C (Esc)">\u2715</button>
|
||
</div>
|
||
<div class="sa-lightbox-body">
|
||
<button type="button" class="sa-lightbox-nav sa-lightbox-prev" data-lb="prev" title="\u041D\u0430\u0437\u0430\u0434">\u2039</button>
|
||
<img id="sa_lb_img" alt="" />
|
||
<button type="button" class="sa-lightbox-nav sa-lightbox-next" data-lb="next" title="\u0412\u043F\u0435\u0440\u0451\u0434">\u203A</button>
|
||
</div>
|
||
<div class="sa-lightbox-actions">
|
||
<button type="button" class="basic-button" data-lb="to_ref">\u0412 Refs</button>
|
||
<button type="button" class="basic-button" data-lb="as_init">\u041A\u0430\u043A Init</button>
|
||
<button type="button" class="basic-button" data-lb="close">\u0417\u0430\u043A\u0440\u044B\u0442\u044C</button>
|
||
</div>
|
||
</div>`;
|
||
host.appendChild(root);
|
||
root.addEventListener("click", async (e) => {
|
||
const act = e.target?.closest?.("[data-lb]")?.getAttribute("data-lb");
|
||
if (!act) {
|
||
return;
|
||
}
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
if (act === "close") {
|
||
closeGenLightbox();
|
||
} else if (act === "prev") {
|
||
stepGenLightbox(-1);
|
||
} else if (act === "next") {
|
||
stepGenLightbox(1);
|
||
} else if (act === "to_ref") {
|
||
const row = currentLightboxRow();
|
||
if (row?.src) {
|
||
const created = addRefSlot({ select: true });
|
||
if (created) {
|
||
created.src = row.src;
|
||
setBoardTab("refs");
|
||
renderBoard();
|
||
setStatus(`\u0421\u043D\u0438\u043C\u043E\u043A \u2192 ${created.label}`);
|
||
}
|
||
}
|
||
} else if (act === "as_init") {
|
||
const row = currentLightboxRow();
|
||
if (row?.src) {
|
||
await setInitFromSrc(row.src);
|
||
}
|
||
}
|
||
});
|
||
return root;
|
||
}
|
||
function currentLightboxRow() {
|
||
const list = (state.genResults || []).filter((r) => r.src);
|
||
if (!list.length || state.lightboxIndex < 0) {
|
||
return null;
|
||
}
|
||
return list[state.lightboxIndex] || null;
|
||
}
|
||
function syncGenLightbox() {
|
||
const root = ensureGenLightbox();
|
||
const list = (state.genResults || []).filter((r) => r.src);
|
||
const row = list[state.lightboxIndex];
|
||
if (!row) {
|
||
root.hidden = true;
|
||
return;
|
||
}
|
||
root.hidden = false;
|
||
const img = $2("sa_lb_img");
|
||
const title = $2("sa_lb_title");
|
||
const idx = $2("sa_lb_idx");
|
||
if (img) {
|
||
img.src = row.src;
|
||
img.alt = row.label || row.id;
|
||
}
|
||
if (title) {
|
||
title.textContent = row.label || row.id;
|
||
}
|
||
if (idx) {
|
||
idx.textContent = `${state.lightboxIndex + 1} / ${list.length}`;
|
||
}
|
||
}
|
||
function openGenLightbox(id) {
|
||
const list = (state.genResults || []).filter((r) => r.src);
|
||
let idx = list.findIndex((r) => r.id === id);
|
||
if (idx < 0) {
|
||
idx = 0;
|
||
}
|
||
if (!list.length) {
|
||
return;
|
||
}
|
||
state.lightboxIndex = idx;
|
||
ensureGenLightbox();
|
||
syncGenLightbox();
|
||
selectGenResult(list[idx].id, { restore: true, openViewer: false });
|
||
}
|
||
function closeGenLightbox() {
|
||
state.lightboxIndex = -1;
|
||
const root = $2("sa_gen_lightbox");
|
||
if (root) {
|
||
root.hidden = true;
|
||
}
|
||
}
|
||
function stepGenLightbox(delta) {
|
||
const list = (state.genResults || []).filter((r) => r.src);
|
||
if (list.length < 2) {
|
||
return;
|
||
}
|
||
state.lightboxIndex = (state.lightboxIndex + delta + list.length) % list.length;
|
||
const row = list[state.lightboxIndex];
|
||
if (row) {
|
||
selectGenResult(row.id, { restore: true, openViewer: false });
|
||
}
|
||
syncGenLightbox();
|
||
}
|
||
function syncBoardChrome() {
|
||
const tab = state.boardTab === "refs" ? "refs" : "generate";
|
||
$2("sa_board_tab_gen")?.classList.toggle("sa-board-tab-active", tab === "generate");
|
||
$2("sa_board_tab_refs")?.classList.toggle("sa-board-tab-active", tab === "refs");
|
||
$2("sa_board_tab_gen")?.setAttribute("aria-selected", tab === "generate" ? "true" : "false");
|
||
$2("sa_board_tab_refs")?.setAttribute("aria-selected", tab === "refs" ? "true" : "false");
|
||
const addBtn = $2("sa_btn_add_ref");
|
||
if (addBtn) {
|
||
addBtn.hidden = tab !== "refs";
|
||
}
|
||
const maskBtn = $2("sa_btn_as_mask");
|
||
const clearSlotBtn = $2("sa_btn_clear_image");
|
||
if (maskBtn) {
|
||
maskBtn.hidden = tab !== "refs";
|
||
}
|
||
if (clearSlotBtn) {
|
||
clearSlotBtn.hidden = tab !== "refs";
|
||
}
|
||
const badge = $2("sa_refs_badge");
|
||
if (badge) {
|
||
const refs = refSlots();
|
||
const withImg = refs.filter((s) => s.src).length;
|
||
const withVision = refs.filter((s) => s.src && s.attach).length;
|
||
if (withImg || withVision) {
|
||
badge.hidden = false;
|
||
badge.textContent = withVision ? `${withImg} \xB7 vision ${withVision}` : String(withImg);
|
||
} else {
|
||
badge.hidden = true;
|
||
}
|
||
}
|
||
let genBadge = $2("sa_gen_badge");
|
||
if (!genBadge) {
|
||
const genTab = $2("sa_board_tab_gen");
|
||
if (genTab) {
|
||
genBadge = document.createElement("span");
|
||
genBadge.id = "sa_gen_badge";
|
||
genBadge.className = "sa-board-badge";
|
||
genBadge.hidden = true;
|
||
genTab.appendChild(genBadge);
|
||
}
|
||
}
|
||
if (genBadge) {
|
||
const n = finishedGenResultCount();
|
||
if (n > 1) {
|
||
genBadge.hidden = false;
|
||
genBadge.textContent = String(n);
|
||
} else {
|
||
genBadge.hidden = true;
|
||
}
|
||
}
|
||
}
|
||
function setBoardTab(tab, { persist = true } = {}) {
|
||
state.boardTab = tab === "refs" ? "refs" : "generate";
|
||
if (persist) {
|
||
try {
|
||
localStorage.setItem(LS_BOARD_TAB, state.boardTab);
|
||
} catch (e) {
|
||
}
|
||
}
|
||
renderBoard();
|
||
}
|
||
function buildSlotEl(slot) {
|
||
const el = document.createElement("div");
|
||
el.className = `sa-slot${slot.type === "generate" ? " sa-slot-gen" : ""}`;
|
||
el.dataset.id = slot.id;
|
||
if (slot.src) {
|
||
el.classList.add("sa-has-image");
|
||
}
|
||
if (slot.id === state.selectedSlotId) {
|
||
el.classList.add("sa-selected");
|
||
}
|
||
const bar = document.createElement("div");
|
||
bar.className = "sa-slot-bar";
|
||
const chip = document.createElement("span");
|
||
chip.className = `sa-slot-chip${slot.type === "generate" ? " sa-live" : ""}`;
|
||
chip.textContent = slot.type === "generate" ? "Generate" : slot.label;
|
||
bar.appendChild(chip);
|
||
const attachLab = document.createElement("label");
|
||
attachLab.className = "sa-slot-attach";
|
||
attachLab.title = "Attach this window to the next chat (vision)";
|
||
const cb = document.createElement("input");
|
||
cb.type = "checkbox";
|
||
cb.checked = !!slot.attach;
|
||
cb.addEventListener("click", (e) => e.stopPropagation());
|
||
cb.addEventListener("change", (e) => {
|
||
e.stopPropagation();
|
||
slot.attach = cb.checked;
|
||
syncLastImageAlias();
|
||
});
|
||
attachLab.appendChild(cb);
|
||
attachLab.appendChild(document.createTextNode(" vision"));
|
||
bar.appendChild(attachLab);
|
||
el.appendChild(bar);
|
||
if (slot.src) {
|
||
const img = document.createElement("img");
|
||
img.alt = slot.label;
|
||
img.src = slot.src;
|
||
el.appendChild(img);
|
||
} else {
|
||
const empty = document.createElement("div");
|
||
empty.className = "sa-image-empty";
|
||
empty.innerHTML = slot.type === "generate" ? '<div class="sa-empty-title">Generate</div><div class="sa-empty-hint">\u0416\u0438\u0432\u043E\u0439 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438</div>' : '<div class="sa-empty-title">Reference</div><div class="sa-empty-hint">Drop \xB7 paste \xB7 \u0421\u043D\u0438\u043C\u043E\u043A gen</div>';
|
||
el.appendChild(empty);
|
||
}
|
||
const busy = document.createElement("div");
|
||
busy.className = "sa-slot-busy";
|
||
busy.hidden = !(slot.type === "generate" && (state.generating || state.busyPhase === "generating"));
|
||
busy.innerHTML = '<span class="sa-spinner" aria-hidden="true"></span>';
|
||
el.appendChild(busy);
|
||
el.addEventListener("click", () => {
|
||
state.selectedSlotId = slot.id;
|
||
renderBoard();
|
||
});
|
||
el.addEventListener("dragover", (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
el.classList.add("sa-dragover");
|
||
if (e.dataTransfer) {
|
||
e.dataTransfer.dropEffect = "copy";
|
||
}
|
||
});
|
||
el.addEventListener("dragleave", () => el.classList.remove("sa-dragover"));
|
||
el.addEventListener("drop", async (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
el.classList.remove("sa-dragover");
|
||
const targetId = slot.type === "generate" ? null : slot.id;
|
||
if (slot.type === "generate") {
|
||
const created = addRefSlot({ select: true });
|
||
await handleDropDataTransfer(e.dataTransfer, created?.id);
|
||
setBoardTab("refs");
|
||
} else {
|
||
await handleDropDataTransfer(e.dataTransfer, targetId);
|
||
}
|
||
});
|
||
return el;
|
||
}
|
||
function syncGenerateSlot() {
|
||
const slot = generateSlot();
|
||
if (!slot) {
|
||
return;
|
||
}
|
||
if (scrubPreviewFromGenerateSlot()) {
|
||
renderBoard();
|
||
}
|
||
const src = findCurrentGenerateSrc();
|
||
if (src && src !== slot.src) {
|
||
slot.src = src;
|
||
const img = document.querySelector(".sa-slot-gen img");
|
||
const empty = document.querySelector(".sa-slot-gen .sa-image-empty");
|
||
const frame = document.querySelector(".sa-slot-gen");
|
||
if (img) {
|
||
img.src = src;
|
||
} else if (frame) {
|
||
renderBoard();
|
||
return;
|
||
}
|
||
if (empty) {
|
||
empty.hidden = true;
|
||
}
|
||
frame?.classList.add("sa-has-image");
|
||
} else if (src && slot.src === src) {
|
||
if (state.generating && !isSwarmGenerateRunning()) {
|
||
const img = document.querySelector(".sa-slot-gen img");
|
||
if (img) {
|
||
const bump = src.includes("?") ? `${src}&sa_t=${Date.now()}` : `${src}?sa_t=${Date.now()}`;
|
||
img.src = bump;
|
||
}
|
||
}
|
||
} else if (!src && !slot.src) {
|
||
const empty = document.querySelector(".sa-slot-gen .sa-image-empty");
|
||
const frame = document.querySelector(".sa-slot-gen");
|
||
const img = document.querySelector(".sa-slot-gen img");
|
||
if (img) {
|
||
img.remove();
|
||
}
|
||
if (empty) {
|
||
empty.hidden = false;
|
||
}
|
||
frame?.classList.remove("sa-has-image", "sa-attached");
|
||
}
|
||
syncGenerateBusy();
|
||
syncPatchActionAvailability();
|
||
}
|
||
function maybeWelcome() {
|
||
if (localStorage.getItem(LS_WELCOMED) === "1") {
|
||
return;
|
||
}
|
||
if (!$2("sa_messages")) {
|
||
return;
|
||
}
|
||
localStorage.setItem(LS_WELCOMED, "1");
|
||
const box = $2("sa_messages");
|
||
hideChatEmpty();
|
||
const div = document.createElement("div");
|
||
div.className = "sa-msg assistant sa-welcome";
|
||
div.innerHTML = WELCOME_HTML;
|
||
box.appendChild(div);
|
||
scrollMessagesToBottom({ force: true });
|
||
}
|
||
function chatUid() {
|
||
return `c_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
||
}
|
||
function stripJsonFencesForHistory(content) {
|
||
return String(content || "").replace(/```(?:json)?\s*[\s\S]*?```/gi, "").replace(/###\s*Critique\b[\s\S]*?(?=###|$)/gi, "").replace(/###\s*JSON\s*Patch\b[\s\S]*$/gi, "").replace(/\n{3,}/g, "\n\n").trim();
|
||
}
|
||
function slimHistoryMessages(list) {
|
||
return (list || []).filter((m) => m && (m.role === "user" || m.role === "assistant") && !m.systemish).slice(-MAX_CHAT_MSGS).map((m) => {
|
||
let content = String(m.content || "");
|
||
if (m.role === "assistant") {
|
||
content = stripJsonFencesForHistory(content);
|
||
}
|
||
return {
|
||
role: m.role,
|
||
content: content.slice(0, 4e3),
|
||
persona: m.persona || void 0,
|
||
pack: m.pack || void 0
|
||
};
|
||
});
|
||
}
|
||
function titleFromMessages(messages) {
|
||
const u = (messages || []).find((m) => m.role === "user" && m.content);
|
||
const t = String(u?.content || "").replace(/\s+/g, " ").trim();
|
||
return t ? t.slice(0, 52) : "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442";
|
||
}
|
||
function readLiveGenFields() {
|
||
let loras = [];
|
||
try {
|
||
if (typeof loraHelper !== "undefined" && loraHelper && Array.isArray(loraHelper.selected)) {
|
||
loras = loraHelper.selected.map((l) => ({
|
||
name: l.name || l,
|
||
weight: loraHelper.loraWeightPref && loraHelper.loraWeightPref[l.name || l] || 1
|
||
}));
|
||
}
|
||
} catch (e) {
|
||
}
|
||
let checkpoint = null;
|
||
try {
|
||
if (typeof resolveCurrentCheckpoint === "function") {
|
||
const m = resolveCurrentCheckpoint();
|
||
if (m && (m.name || m.title)) {
|
||
checkpoint = {
|
||
name: m.name || m.title || null,
|
||
architecture: m.architecture || m.compat_class || m.class || null,
|
||
title: m.title || null
|
||
};
|
||
}
|
||
}
|
||
} catch (e) {
|
||
}
|
||
return {
|
||
prompt: val("alt_prompt_textbox") || val("input_prompt") || "",
|
||
negative: val("input_negativeprompt") || val("alt_negativeprompt_textbox") || "",
|
||
width: parseInt(val("input_width") || "0", 10) || null,
|
||
height: parseInt(val("input_height") || "0", 10) || null,
|
||
steps: parseInt(val("input_steps") || "0", 10) || null,
|
||
cfg: parseFloat(val("input_cfgscale") || val("input_cfg") || "") || null,
|
||
sigma_shift: parseFloat(val("input_sigmashift") || "") || null,
|
||
seed: val("input_seed") || null,
|
||
sampler: val("input_sampler") || null,
|
||
scheduler: val("input_scheduler") || null,
|
||
batch: parseInt(val("input_images") || val("input_batchsize") || "0", 10) || null,
|
||
loras,
|
||
checkpoint
|
||
};
|
||
}
|
||
function boardSnapshotForSession() {
|
||
if (typeof ensureBoard === "function") ensureBoard();
|
||
return {
|
||
slots: (state.slots || []).map((s) => ({
|
||
id: s.id,
|
||
type: s.type,
|
||
label: s.label,
|
||
src: s.src || null,
|
||
attach: !!s.attach,
|
||
note: s.note || null
|
||
})),
|
||
selectedSlotId: state.selectedSlotId || "ref1",
|
||
genResults: Array.isArray(state.genResults) ? state.genResults.map((r) => ({
|
||
id: r.id,
|
||
label: r.label,
|
||
src: r.src || null,
|
||
patch: r.patch || null
|
||
})) : [],
|
||
selectedGenResultId: state.selectedGenResultId || null,
|
||
refSeq: state.refSeq || 1
|
||
};
|
||
}
|
||
function pullLiveIntoSession() {
|
||
const S = window.SA && window.SA.session;
|
||
if (!S || typeof S.snapshotFromLive !== "function") return state.chatSession;
|
||
state.chatSession = S.snapshotFromLive({
|
||
genFields: readLiveGenFields(),
|
||
board: boardSnapshotForSession(),
|
||
persona: $2("sa_persona")?.value || "neutral",
|
||
pack: $2("sa_pack")?.value || "ordinary",
|
||
context_memory: getContextMemory()
|
||
});
|
||
return state.chatSession;
|
||
}
|
||
async function pushSessionToSwarm(session) {
|
||
const S = window.SA && window.SA.session;
|
||
const sess = session || state.chatSession || S && S.emptySession && S.emptySession();
|
||
if (!sess || !sess.gen) return;
|
||
if (typeof applyPatch === "function") await applyPatch({ ...sess.gen }, "all");
|
||
try {
|
||
const name = sess.gen.checkpoint?.name || (typeof sess.gen.checkpoint === "string" ? sess.gen.checkpoint : null);
|
||
if (name && typeof currentModelHelper !== "undefined" && currentModelHelper?.setModel) {
|
||
currentModelHelper.setModel(name);
|
||
}
|
||
} catch (e) {
|
||
}
|
||
if (sess.pack && typeof setPackValue === "function") setPackValue(sess.pack, { flash: false });
|
||
if (sess.persona && typeof applyPersonaForChat === "function") {
|
||
await applyPersonaForChat(sess.persona, { quiet: true });
|
||
}
|
||
state.chatSession = sess;
|
||
}
|
||
function snapshotChatParams() {
|
||
const S = window.SA && window.SA.session;
|
||
const session = pullLiveIntoSession();
|
||
if (S && typeof S.toPersistParams === "function") return S.toPersistParams(session);
|
||
return session || {};
|
||
}
|
||
async function restoreChatParams(params) {
|
||
state.restoringChat = true;
|
||
try {
|
||
state.sessionExact = {};
|
||
state.lastPatch = null;
|
||
state.lastUserParamIntent = false;
|
||
const S = window.SA && window.SA.session;
|
||
state.chatSession = S && typeof S.sessionFromLegacyParams === "function" ? S.sessionFromLegacyParams(params) : params && params.gen ? params : S && S.emptySession ? S.emptySession() : { gen: {}, board: {} };
|
||
if (!params || typeof params !== "object") {
|
||
if (typeof clearGenResults === "function") clearGenResults();
|
||
if (typeof renderBoard === "function") renderBoard();
|
||
setContextMemory(null, { persist: false });
|
||
return { restored: false };
|
||
}
|
||
await pushSessionToSwarm(state.chatSession);
|
||
const board = state.chatSession.board || {};
|
||
if (Array.isArray(board.slots) && board.slots.length) {
|
||
state.slots = board.slots.map((s) => ({ ...s }));
|
||
if (board.selectedSlotId) state.selectedSlotId = board.selectedSlotId;
|
||
if (board.refSeq) state.refSeq = board.refSeq;
|
||
}
|
||
if (Array.isArray(board.genResults) && board.genResults.length) {
|
||
state.genResults = board.genResults.map((r, i) => ({
|
||
id: r.id || "var" + (i + 1),
|
||
label: r.label || "\u0412\u0430\u0440\u0438\u0430\u043D\u0442 " + (i + 1),
|
||
src: r.src || null,
|
||
patch: r.patch || null
|
||
}));
|
||
state.selectedGenResultId = board.selectedGenResultId || state.genResults.find((x) => x.src)?.id || state.genResults[0]?.id || null;
|
||
const selected = state.genResults.find((x) => x.id === state.selectedGenResultId);
|
||
const gen = typeof generateSlot === "function" ? generateSlot() : null;
|
||
if (gen && selected?.src) gen.src = selected.src;
|
||
} else if (typeof clearGenResults === "function") {
|
||
clearGenResults();
|
||
}
|
||
if (typeof syncBuildGenButton === "function") syncBuildGenButton();
|
||
if (typeof syncLiveParamsBar === "function") syncLiveParamsBar();
|
||
if (typeof syncModeBadge === "function") syncModeBadge();
|
||
if (typeof renderLoraChips === "function") renderLoraChips();
|
||
if (typeof renderBoard === "function") renderBoard();
|
||
setContextMemory(state.chatSession?.context_memory || null);
|
||
return { restored: true };
|
||
} finally {
|
||
state.restoringChat = false;
|
||
}
|
||
}
|
||
function applyPersonaForChat(personaId, { quiet = false } = {}) {
|
||
const id = String(personaId || "neutral").trim() || "neutral";
|
||
return new Promise((resolve) => {
|
||
const sel = $2("sa_persona");
|
||
if (sel && [...sel.options].some((o) => o.value === id)) {
|
||
sel.value = id;
|
||
}
|
||
if (!quiet) {
|
||
onPersonaChanged();
|
||
resolve();
|
||
return;
|
||
}
|
||
saveSettings();
|
||
if (typeof genericRequest !== "function") {
|
||
resolve();
|
||
return;
|
||
}
|
||
const packKeep = $2("sa_pack")?.value;
|
||
genericRequest(
|
||
"AssistentGetConfig",
|
||
{ persona: id },
|
||
(data) => {
|
||
applyConfigPayload(data, { applyDefaults: false });
|
||
if (sel && [...sel.options].some((o) => o.value === id)) {
|
||
sel.value = id;
|
||
}
|
||
if (packKeep) {
|
||
setPackValue(packKeep, { flash: false });
|
||
}
|
||
resolve();
|
||
},
|
||
0,
|
||
() => resolve()
|
||
);
|
||
});
|
||
}
|
||
function persistChatsStore() {
|
||
try {
|
||
const chats = (state.chats || []).filter((c) => c && c.id && (c.id === state.activeChatId || (c.messages || []).length > 0)).slice().sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)).slice(0, MAX_CHATS).map((c) => ({
|
||
id: c.id,
|
||
title: c.title || "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442",
|
||
createdAt: c.createdAt || Date.now(),
|
||
updatedAt: c.updatedAt || Date.now(),
|
||
messages: slimHistoryMessages(c.messages || []),
|
||
params: c.params || null
|
||
}));
|
||
state.chats = chats;
|
||
localStorage.setItem(LS_CHATS2, JSON.stringify({ version: 1, chats }));
|
||
saveActiveChatToDisk();
|
||
} catch (e) {
|
||
console.warn("Assistent: persist chats failed", e);
|
||
try {
|
||
const slim = (state.chats || []).filter((c) => c && c.id && (c.id === state.activeChatId || (c.messages || []).length > 0)).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)).slice(0, 12).map((c) => ({
|
||
...c,
|
||
messages: slimHistoryMessages(c.messages).slice(-historyMessageLimit()).map((m) => ({
|
||
...m,
|
||
content: String(m.content || "").slice(0, 1500)
|
||
}))
|
||
}));
|
||
state.chats = slim;
|
||
localStorage.setItem(LS_CHATS2, JSON.stringify({ version: 1, chats: slim }));
|
||
saveActiveChatToDisk();
|
||
} catch (e2) {
|
||
console.warn("Assistent: chats quota fallback failed", e2);
|
||
}
|
||
}
|
||
}
|
||
function saveActiveChatToDisk() {
|
||
const persist = diskPersist();
|
||
if (!persist) {
|
||
return;
|
||
}
|
||
for (const chat of state.chats || []) {
|
||
if (!chat?.id || !(chat.messages || []).length) {
|
||
continue;
|
||
}
|
||
persist.saveChat(chat);
|
||
}
|
||
}
|
||
function loadChatsStore() {
|
||
state.chats = [];
|
||
try {
|
||
const raw = localStorage.getItem(LS_CHATS2);
|
||
if (raw) {
|
||
const parsed = JSON.parse(raw);
|
||
if (Array.isArray(parsed?.chats)) {
|
||
state.chats = parsed.chats.filter((c) => c && c.id);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
}
|
||
}
|
||
async function loadChatsFromDisk() {
|
||
const persist = diskPersist();
|
||
if (!persist) {
|
||
return;
|
||
}
|
||
let diskChats = null;
|
||
try {
|
||
diskChats = await persist.loadChats();
|
||
} catch (e) {
|
||
console.warn("Assistent: disk chats failed", e);
|
||
return;
|
||
}
|
||
if (!Array.isArray(diskChats)) {
|
||
return;
|
||
}
|
||
const byId = /* @__PURE__ */ new Map();
|
||
for (const c of state.chats || []) {
|
||
if (c?.id) {
|
||
byId.set(c.id, c);
|
||
}
|
||
}
|
||
for (const c of diskChats) {
|
||
if (!c?.id) {
|
||
continue;
|
||
}
|
||
const prev = byId.get(c.id);
|
||
if (!prev || (c.updatedAt || 0) >= (prev.updatedAt || 0)) {
|
||
byId.set(c.id, c);
|
||
}
|
||
}
|
||
state.chats = [...byId.values()].sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)).slice(0, MAX_CHATS);
|
||
try {
|
||
localStorage.setItem(LS_CHATS2, JSON.stringify({ version: 1, chats: state.chats }));
|
||
} catch (e) {
|
||
}
|
||
}
|
||
function findChat(id) {
|
||
return (state.chats || []).find((c) => c.id === id) || null;
|
||
}
|
||
function saveActiveChatToStore({ dropEmpty = false } = {}) {
|
||
if (!state.activeChatId || state.restoringChat) {
|
||
return;
|
||
}
|
||
const chat = findChat(state.activeChatId);
|
||
if (!chat) {
|
||
return;
|
||
}
|
||
chat.messages = slimHistoryMessages(state.history);
|
||
chat.params = snapshotChatParams();
|
||
chat.updatedAt = Date.now();
|
||
chat.title = titleFromMessages(chat.messages);
|
||
if (dropEmpty && !chat.messages.length) {
|
||
state.chats = state.chats.filter((c) => c.id !== chat.id);
|
||
if (state.activeChatId === chat.id) {
|
||
state.activeChatId = null;
|
||
}
|
||
}
|
||
persistChatsStore();
|
||
}
|
||
function resetMessagesUi(emptyHint) {
|
||
const box = $2("sa_messages");
|
||
if (!box) {
|
||
return;
|
||
}
|
||
box.innerHTML = "";
|
||
const empty = document.createElement("div");
|
||
empty.className = "sa-chat-empty";
|
||
empty.id = "sa_chat_empty";
|
||
empty.innerHTML = emptyHint || '<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);
|
||
}
|
||
function renderHistoryIntoUi(messages) {
|
||
const box = $2("sa_messages");
|
||
if (!box) {
|
||
return;
|
||
}
|
||
box.innerHTML = "";
|
||
const list = slimHistoryMessages(messages);
|
||
if (!list.length) {
|
||
resetMessagesUi();
|
||
updateCtxChip();
|
||
return;
|
||
}
|
||
const mem = getContextMemory();
|
||
let start = 0;
|
||
if (mem.uiCollapsed && mem.summary && mem.untilCount > 0) {
|
||
const folded = list.slice(0, Math.min(mem.untilCount, list.length));
|
||
start = folded.length;
|
||
const details = document.createElement("details");
|
||
details.className = "sa-msg sa-msg-compress";
|
||
const summary = document.createElement("summary");
|
||
summary.textContent = `\u0421\u0436\u0430\u0442\u044B\u0439 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 \xB7 ${mem.foldedTurns || Math.floor(folded.length / 2)} \u0445\u043E\u0434\u043E\u0432`;
|
||
details.appendChild(summary);
|
||
const body = document.createElement("div");
|
||
body.className = "sa-msg-compress-body";
|
||
const pre = document.createElement("pre");
|
||
pre.className = "sa-ctx-summary";
|
||
pre.textContent = mem.summary;
|
||
body.appendChild(pre);
|
||
for (const m of folded) {
|
||
const row = document.createElement("div");
|
||
row.className = `sa-msg-compress-row sa-msg-compress-${m.role}`;
|
||
row.textContent = `${m.role === "assistant" ? "\u0410\u0441\u0441\u0438\u0441\u0442\u0435\u043D\u0442" : "\u0412\u044B"}: ${String(m.content || "").slice(0, 500)}`;
|
||
body.appendChild(row);
|
||
}
|
||
details.appendChild(body);
|
||
box.appendChild(details);
|
||
}
|
||
for (let i = start; i < list.length; i++) {
|
||
const m = list[i];
|
||
if (m.role === "user") {
|
||
appendMessage("user", m.content, null, null, { historical: true });
|
||
} else {
|
||
appendMessage("assistant", m.content, null, null, {
|
||
persona: m.persona ? { id: m.persona, title: m.persona } : null,
|
||
pack: m.pack,
|
||
historical: true
|
||
});
|
||
}
|
||
}
|
||
updateCtxChip();
|
||
}
|
||
function updateSessionLabel() {
|
||
const el = $2("sa_session_label");
|
||
if (!el) {
|
||
return;
|
||
}
|
||
const chat = findChat(state.activeChatId);
|
||
el.textContent = chat?.title || "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442";
|
||
el.title = chat?.title || "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442";
|
||
}
|
||
function chatHasTranscript(c) {
|
||
return (c?.messages || []).length > 0 || (c?.messages_count || 0) > 0;
|
||
}
|
||
function savedChatsCount() {
|
||
return (state.chats || []).filter(chatHasTranscript).length;
|
||
}
|
||
function isBlankActiveChat() {
|
||
if ((state.history || []).length) {
|
||
return false;
|
||
}
|
||
const chat = findChat(state.activeChatId);
|
||
return !chat || !chatHasTranscript(chat);
|
||
}
|
||
function syncHistoryBadge() {
|
||
const btn = $2("sa_btn_chats");
|
||
const countEl = $2("sa_chats_count");
|
||
const n = (state.chats || []).filter(chatHasTranscript).length;
|
||
const open = !!state.chatsPanelOpen;
|
||
if (btn) {
|
||
btn.title = open ? n ? `\u0421\u043A\u0440\u044B\u0442\u044C \u0447\u0430\u0442\u044B (${n})` : "\u0421\u043A\u0440\u044B\u0442\u044C \u0447\u0430\u0442\u044B" : n ? `\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0447\u0430\u0442\u044B (${n})` : "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0447\u0430\u0442\u044B";
|
||
btn.setAttribute("aria-label", btn.title);
|
||
}
|
||
if (countEl) {
|
||
countEl.hidden = n < 1;
|
||
countEl.textContent = n > 99 ? "99+" : String(n);
|
||
}
|
||
}
|
||
function formatChatWhen(ts) {
|
||
if (!ts) {
|
||
return "";
|
||
}
|
||
const t = Number(ts) || 0;
|
||
if (!t) {
|
||
return "";
|
||
}
|
||
const sec = Math.max(0, Math.floor((Date.now() - t) / 1e3));
|
||
if (sec < 45) {
|
||
return "\u0441\u0435\u0439\u0447\u0430\u0441";
|
||
}
|
||
if (sec < 3600) {
|
||
return `${Math.max(1, Math.floor(sec / 60))}\u043C`;
|
||
}
|
||
if (sec < 86400) {
|
||
return `${Math.floor(sec / 3600)}\u0447`;
|
||
}
|
||
if (sec < 86400 * 14) {
|
||
return `${Math.floor(sec / 86400)}\u0434`;
|
||
}
|
||
try {
|
||
return new Date(t).toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
||
} catch (e) {
|
||
return "";
|
||
}
|
||
}
|
||
function chatMatchesQuery(chat, q) {
|
||
if (!q) {
|
||
return true;
|
||
}
|
||
const title = String(chat?.title || "").toLowerCase();
|
||
if (title.includes(q)) {
|
||
return true;
|
||
}
|
||
const msgs = chat?.messages || [];
|
||
for (const m of msgs) {
|
||
if (String(m?.content || "").toLowerCase().includes(q)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
function chatListParamsBits(params) {
|
||
if (!params || typeof params !== "object") {
|
||
return "";
|
||
}
|
||
const g = params.gen && typeof params.gen === "object" ? params.gen : params;
|
||
const bits = [];
|
||
if (g.width && g.height) {
|
||
bits.push(`${g.width}\xD7${g.height}`);
|
||
} else if (g.aspect) {
|
||
bits.push(String(g.aspect));
|
||
}
|
||
const loras = Array.isArray(g.loras) ? g.loras : [];
|
||
if (loras.length) {
|
||
bits.push(`LoRA ${loras.length}`);
|
||
}
|
||
return bits.join(" \xB7 ");
|
||
}
|
||
function renderChatsList() {
|
||
const root = $2("sa_chats_list");
|
||
if (!root) {
|
||
return;
|
||
}
|
||
root.innerHTML = "";
|
||
syncHistoryBadge();
|
||
const q = (state.chatsQuery || "").trim().toLowerCase();
|
||
let chats = (state.chats || []).slice().sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)).filter((c) => c.id === state.activeChatId || chatHasTranscript(c));
|
||
if (q) {
|
||
const local = chats.filter((c) => chatMatchesQuery(c, q));
|
||
const seen = new Set(local.map((c) => c.id));
|
||
const extra = (state.chatsSearchHits || []).filter((h) => h && h.id && !seen.has(h.id));
|
||
chats = local.concat(extra);
|
||
}
|
||
if (!chats.length) {
|
||
root.innerHTML = q ? '<div class="sa-chats-empty">\u041D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u043D\u0430\u0448\u043B\u043E\u0441\u044C.</div>' : '<div class="sa-chats-empty">\u041F\u043E\u043A\u0430 \u043F\u0443\u0441\u0442\u043E \u2014 \u043D\u0430\u043F\u0438\u0448\u0438 \u0432 \u0447\u0430\u0442, \u0438 \u043E\u043D \u043F\u043E\u044F\u0432\u0438\u0442\u0441\u044F \u0437\u0434\u0435\u0441\u044C.</div>';
|
||
return;
|
||
}
|
||
const ico = '<svg class="sa-chat-row-ico" width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M3 4.5h10M3 8h10M3 11.5h7" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
|
||
for (const c of chats) {
|
||
const row = document.createElement("div");
|
||
row.className = "sa-chat-row" + (c.id === state.activeChatId ? " sa-chat-row-active" : "");
|
||
row.dataset.id = c.id;
|
||
row.setAttribute("role", "listitem");
|
||
const when = formatChatWhen(c.updatedAt);
|
||
const title = escapeHtml2(c.title || "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442");
|
||
const tipBits = [chatListParamsBits(c.params)].filter(Boolean);
|
||
const tip = tipBits.length ? ` title="${escapeHtml2(tipBits.join(" \xB7 "))}"` : "";
|
||
row.innerHTML = `<button type="button" class="sa-chat-row-main" data-open="1"${tip}>${ico}<span class="sa-chat-row-title">${title}</span><span class="sa-chat-row-when">${escapeHtml2(when)}</span></button><button type="button" class="sa-chat-row-del" data-del="1" title="\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0447\u0430\u0442" aria-label="\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0447\u0430\u0442">\xD7</button>`;
|
||
root.appendChild(row);
|
||
}
|
||
}
|
||
function setChatsPanelOpen(open) {
|
||
state.chatsPanelOpen = !!open;
|
||
state.chatsDrawerOpen = state.chatsPanelOpen;
|
||
const panel = $2("sa_chats_panel");
|
||
const btn = $2("sa_btn_chats");
|
||
const root = $2("swarm_assistent_root");
|
||
if (panel) {
|
||
panel.hidden = !state.chatsPanelOpen;
|
||
}
|
||
btn?.setAttribute("aria-expanded", state.chatsPanelOpen ? "true" : "false");
|
||
btn?.classList.toggle("sa-sessions-toggle-active", state.chatsPanelOpen);
|
||
root?.classList.toggle("sa-drawer-open", state.chatsPanelOpen);
|
||
localStorage.setItem(LS_CHATS_DRAWER, state.chatsPanelOpen ? "1" : "0");
|
||
syncHistoryBadge();
|
||
if (state.chatsPanelOpen) {
|
||
saveActiveChatToStore();
|
||
const search = $2("sa_chats_search");
|
||
if (search) {
|
||
search.value = state.chatsQuery || "";
|
||
search.focus();
|
||
}
|
||
renderChatsList();
|
||
}
|
||
saveUiStateToDisk();
|
||
}
|
||
async function startNewChat({ saveCurrent = true, force = false, openDrawer = false } = {}) {
|
||
if (!force && isBlankActiveChat()) {
|
||
setStatus("\u0423\u0436\u0435 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442");
|
||
if (openDrawer) {
|
||
setChatsPanelOpen(true);
|
||
}
|
||
$2("sa_input")?.focus();
|
||
return;
|
||
}
|
||
if (state.busy || state.generating) {
|
||
abortInFlightWork({ status: "", interruptSwarm: !!state.generating });
|
||
}
|
||
if (saveCurrent) {
|
||
saveActiveChatToStore({ dropEmpty: true });
|
||
}
|
||
state.sessionExact = {};
|
||
state.lastUserParamIntent = false;
|
||
state.pendingSilentGen = false;
|
||
state.lastPatch = null;
|
||
clearGenResults();
|
||
const chat = {
|
||
id: chatUid(),
|
||
title: "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442",
|
||
createdAt: Date.now(),
|
||
updatedAt: Date.now(),
|
||
messages: [],
|
||
params: snapshotChatParams()
|
||
};
|
||
state.chats.unshift(chat);
|
||
state.activeChatId = chat.id;
|
||
state.history = [];
|
||
resetTurnHops();
|
||
state.packUserTouched = false;
|
||
state.pendingPersonaNote = null;
|
||
if (state.streamEl) {
|
||
try {
|
||
state.streamEl.remove();
|
||
} catch (e) {
|
||
}
|
||
state.streamEl = null;
|
||
}
|
||
resetContextMemory({ persist: false });
|
||
syncBuildGenButton();
|
||
resetMessagesUi();
|
||
persistChatsStore();
|
||
updateSessionLabel();
|
||
renderBoard();
|
||
syncHistoryBadge();
|
||
renderChatsList();
|
||
updateCtxChip();
|
||
setStatus("\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442 \u2014 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B Generate \u043A\u0430\u043A \u0441\u0435\u0439\u0447\u0430\u0441");
|
||
maybeWelcome();
|
||
if (openDrawer) {
|
||
setChatsPanelOpen(true);
|
||
}
|
||
if (openDrawer || !force) {
|
||
$2("sa_input")?.focus();
|
||
}
|
||
}
|
||
async function switchToChat(id) {
|
||
if (!id || id === state.activeChatId) {
|
||
return;
|
||
}
|
||
if (state.busy || state.generating) {
|
||
setStatus("\u0417\u0430\u043D\u044F\u0442\u043E \u2014 \u043D\u0435\u043B\u044C\u0437\u044F \u0441\u043C\u0435\u043D\u0438\u0442\u044C \u0447\u0430\u0442 \u0441\u0435\u0439\u0447\u0430\u0441");
|
||
return;
|
||
}
|
||
saveActiveChatToStore({ dropEmpty: true });
|
||
let chat = findChat(id);
|
||
if (!chat || !(chat.messages || []).length) {
|
||
try {
|
||
const full = await diskPersist()?.getChat?.(id);
|
||
if (full) {
|
||
const idx = (state.chats || []).findIndex((c) => c.id === id);
|
||
if (idx >= 0) {
|
||
state.chats[idx] = full;
|
||
} else {
|
||
state.chats.unshift(full);
|
||
}
|
||
chat = full;
|
||
}
|
||
} catch (e) {
|
||
console.warn("Assistent: getChat failed", id, e);
|
||
}
|
||
}
|
||
if (!chat) {
|
||
setStatus("\u0427\u0430\u0442 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D");
|
||
return;
|
||
}
|
||
state.activeChatId = chat.id;
|
||
state.history = slimHistoryMessages(chat.messages);
|
||
resetTurnHops();
|
||
state.packUserTouched = false;
|
||
state.pendingPersonaNote = null;
|
||
state.pendingSilentGen = false;
|
||
state.lastUserParamIntent = false;
|
||
if (state.streamEl) {
|
||
try {
|
||
state.streamEl.remove();
|
||
} catch (e) {
|
||
}
|
||
state.streamEl = null;
|
||
}
|
||
setContextMemory(chat.params?.context_memory || null, { persist: false });
|
||
renderHistoryIntoUi(state.history);
|
||
const result = await restoreChatParams(chat.params);
|
||
updateSessionLabel();
|
||
syncHistoryBadge();
|
||
renderChatsList();
|
||
setView("chat");
|
||
if (result?.restored) {
|
||
setStatus(`\u0427\u0430\u0442 \xAB${chat.title}\xBB \xB7 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u044B`);
|
||
} else {
|
||
setStatus(`\u0427\u0430\u0442 \xAB${chat.title}\xBB \xB7 \u0441\u043D\u0438\u043C\u043E\u043A \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u2014 Generate \u043D\u0435 \u043C\u0435\u043D\u044F\u043B\u0441\u044F`);
|
||
}
|
||
}
|
||
function deleteChat(id) {
|
||
if (!id) {
|
||
return;
|
||
}
|
||
const wasActive = id === state.activeChatId;
|
||
state.chats = state.chats.filter((c) => c.id !== id);
|
||
if (wasActive) {
|
||
state.activeChatId = null;
|
||
}
|
||
diskPersist()?.deleteChat(id)?.catch?.((e) => console.warn("Assistent: disk delete failed", e));
|
||
persistChatsStore();
|
||
syncHistoryBadge();
|
||
if (wasActive) {
|
||
startNewChat({ saveCurrent: false, force: true });
|
||
} else {
|
||
renderChatsList();
|
||
}
|
||
}
|
||
async function initChatSessions() {
|
||
loadChatsStore();
|
||
await loadChatsFromDisk();
|
||
startNewChat({ saveCurrent: false, force: true });
|
||
syncHistoryBadge();
|
||
renderChatsList();
|
||
}
|
||
function persistHistory() {
|
||
if (state.restoringChat) {
|
||
return;
|
||
}
|
||
if (!state.activeChatId) {
|
||
const chat2 = {
|
||
id: chatUid(),
|
||
title: "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442",
|
||
createdAt: Date.now(),
|
||
updatedAt: Date.now(),
|
||
messages: [],
|
||
params: snapshotChatParams()
|
||
};
|
||
state.chats.unshift(chat2);
|
||
state.activeChatId = chat2.id;
|
||
}
|
||
const chat = findChat(state.activeChatId);
|
||
if (!chat) {
|
||
return;
|
||
}
|
||
chat.messages = slimHistoryMessages(state.history);
|
||
chat.params = snapshotChatParams();
|
||
chat.updatedAt = Date.now();
|
||
chat.title = titleFromMessages(chat.messages);
|
||
persistChatsStore();
|
||
updateSessionLabel();
|
||
syncHistoryBadge();
|
||
}
|
||
function clearPersistedHistory() {
|
||
if (state.activeChatId) {
|
||
const chat = findChat(state.activeChatId);
|
||
if (chat) {
|
||
chat.messages = [];
|
||
chat.title = "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442";
|
||
chat.params = snapshotChatParams();
|
||
chat.updatedAt = Date.now();
|
||
}
|
||
persistChatsStore();
|
||
}
|
||
updateSessionLabel();
|
||
renderChatsList();
|
||
}
|
||
function clearChatHistory() {
|
||
abortInFlightWork({ status: "" });
|
||
state.history = [];
|
||
resetTurnHops();
|
||
state.packUserTouched = false;
|
||
state.pendingPersonaNote = null;
|
||
state.sessionExact = {};
|
||
state.lastUserParamIntent = false;
|
||
state.pendingSilentGen = false;
|
||
state.lastPatch = null;
|
||
clearGenResults();
|
||
syncBuildGenButton();
|
||
clearPersistedHistory();
|
||
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, \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");
|
||
updateSessionLabel();
|
||
syncHistoryBadge();
|
||
renderBoard();
|
||
updateCtxChip();
|
||
}
|
||
function hideSlashMenu() {
|
||
const menu = $2("sa_slash_menu");
|
||
if (menu) {
|
||
menu.hidden = true;
|
||
menu.innerHTML = "";
|
||
}
|
||
state.slashIndex = 0;
|
||
}
|
||
function slashMatches(text) {
|
||
const t = String(text || "");
|
||
if (!t.startsWith("/")) {
|
||
return [];
|
||
}
|
||
const q = t.toLowerCase();
|
||
return SLASH_COMMANDS.filter((c) => c.cmd.toLowerCase().startsWith(q) || q === "/" || c.cmd.toLowerCase().includes(q.slice(1)));
|
||
}
|
||
function renderSlashMenu(items) {
|
||
const menu = $2("sa_slash_menu");
|
||
if (!menu) {
|
||
return;
|
||
}
|
||
if (!items.length) {
|
||
hideSlashMenu();
|
||
return;
|
||
}
|
||
menu.hidden = false;
|
||
menu.innerHTML = "";
|
||
state.slashIndex = Math.max(0, Math.min(state.slashIndex, items.length - 1));
|
||
items.forEach((item, i) => {
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
btn.className = "sa-slash-item" + (i === state.slashIndex ? " sa-slash-active" : "");
|
||
btn.setAttribute("role", "option");
|
||
btn.innerHTML = `<code>${escapeHtml2(item.cmd.trim())}</code> \u2014 ${escapeHtml2(item.hint)}`;
|
||
btn.addEventListener("mousedown", (e) => {
|
||
e.preventDefault();
|
||
applySlashPick(item);
|
||
});
|
||
menu.appendChild(btn);
|
||
});
|
||
}
|
||
function applySlashPick(item) {
|
||
const input = $2("sa_input");
|
||
if (!input || !item) {
|
||
return;
|
||
}
|
||
input.value = item.cmd;
|
||
hideSlashMenu();
|
||
input.focus();
|
||
const pos = input.value.length;
|
||
input.setSelectionRange(pos, pos);
|
||
}
|
||
function updateSlashMenuFromInput() {
|
||
const text = $2("sa_input")?.value || "";
|
||
if (!text.startsWith("/") || text.includes("\n") || /\s/.test(text.trim().slice(1)) && !text.endsWith(" ")) {
|
||
const token2 = text.split(/\s/)[0] || "";
|
||
if (!token2.startsWith("/") || text.includes(" ") && !SLASH_COMMANDS.some((c) => c.cmd.startsWith(token2))) {
|
||
if (!(token2.startsWith("/") && !text.includes(" "))) {
|
||
hideSlashMenu();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
const token = text.split(/\s/)[0] || "";
|
||
if (!token.startsWith("/") || text.indexOf(" ") > 0) {
|
||
hideSlashMenu();
|
||
return;
|
||
}
|
||
renderSlashMenu(slashMatches(token));
|
||
}
|
||
function onPersonaChanged() {
|
||
const id = $2("sa_persona")?.value || "neutral";
|
||
state.sessionExact = {};
|
||
state.lastUserParamIntent = false;
|
||
saveSettings();
|
||
loadConfig(id, (data) => {
|
||
const title = data?.personas?.find((p) => p.id === id)?.title || (state.personas || []).find((p) => p.id === id)?.title || id;
|
||
if (data?.personas) {
|
||
state.personas = data.personas;
|
||
}
|
||
appendSystemNote(`\u0422\u043E\u043D \u2192 ${title}`);
|
||
state.pendingPersonaNote = `Persona is now ${id} (${title}). Adopt this voice from now on.`;
|
||
if (data?.assistant?.default_pack && $2("sa_pack") && !state.packUserTouched) {
|
||
const packId = data.assistant.default_pack;
|
||
if ([...$2("sa_pack").options || []].some((o) => o.value === packId)) {
|
||
$2("sa_pack").value = packId;
|
||
}
|
||
}
|
||
fillEmptyParamsFromExact();
|
||
renderPersonaControls(data?.controls || {}, data?.control_values || data?.exact?.controls || {});
|
||
syncPersonaDeleteButton(data?.persona_source || data?.personas?.find((p) => p.id === id)?.source);
|
||
if (state.view === "settings") {
|
||
if (state.settingsTab === "user") {
|
||
refreshUserPrefs();
|
||
}
|
||
if (state.settingsTab === "craft") {
|
||
renderMemoryList();
|
||
}
|
||
if (state.settingsTab === "more") {
|
||
fillKnobsFromConfig(data);
|
||
updateCtxChip();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
function countPromptImages() {
|
||
try {
|
||
const box = document.getElementById("alt_prompt_textbox") || document.getElementById("input_prompt");
|
||
if (!box) {
|
||
return 0;
|
||
}
|
||
const text = box.value || "";
|
||
const matches = text.match(/<image(?:\s|\/|>)/gi) || text.match(/data:image\//gi);
|
||
return matches ? matches.length : 0;
|
||
} catch (e) {
|
||
return 0;
|
||
}
|
||
}
|
||
function triggerChangeForEl(el) {
|
||
if (!el) {
|
||
return;
|
||
}
|
||
if (typeof triggerChangeFor === "function") {
|
||
triggerChangeFor(el);
|
||
return;
|
||
}
|
||
el.dispatchEvent(new Event("input", { bubbles: true }));
|
||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||
}
|
||
function openInitImageGroup() {
|
||
try {
|
||
const initEl = document.getElementById("input_initimage");
|
||
if (initEl && typeof toggleGroupOpen === "function") {
|
||
toggleGroupOpen(initEl, true);
|
||
}
|
||
} catch (e) {
|
||
}
|
||
const toggler = document.getElementById("input_group_content_initimage_toggle");
|
||
if (toggler) {
|
||
toggler.checked = true;
|
||
triggerChangeForEl(toggler);
|
||
}
|
||
}
|
||
function hasFileParam(id) {
|
||
const el = document.getElementById(id);
|
||
return !!(el && el.files && el.files.length > 0);
|
||
}
|
||
function clearFileParam(id) {
|
||
const el = document.getElementById(id);
|
||
if (!el) {
|
||
return false;
|
||
}
|
||
try {
|
||
el.value = "";
|
||
if (el.files && typeof DataTransfer !== "undefined") {
|
||
el.files = new DataTransfer().files;
|
||
}
|
||
} catch (e) {
|
||
}
|
||
triggerChangeForEl(el);
|
||
return true;
|
||
}
|
||
function guessImageMime(src) {
|
||
const s = String(src || "");
|
||
if (s.startsWith("data:image/")) {
|
||
const m = s.match(/^data:(image\/[a-z0-9.+-]+)/i);
|
||
return m && m[1] || "image/png";
|
||
}
|
||
const path = s.split("?")[0];
|
||
const ext = path.substring(path.lastIndexOf(".") + 1).toLowerCase();
|
||
if (ext === "jpg" || ext === "jpeg") {
|
||
return "image/jpeg";
|
||
}
|
||
if (ext === "webp") {
|
||
return "image/webp";
|
||
}
|
||
if (ext === "gif") {
|
||
return "image/gif";
|
||
}
|
||
return "image/png";
|
||
}
|
||
async function srcToBlob(src) {
|
||
if (!src) {
|
||
return null;
|
||
}
|
||
const cleaned = String(src).trim().split(/\s+/)[0];
|
||
if (cleaned.startsWith("data:") || cleaned.startsWith("/") || cleaned.startsWith("View/") || cleaned.startsWith("http")) {
|
||
try {
|
||
const resp = await fetch(cleaned);
|
||
return await resp.blob();
|
||
} catch (e) {
|
||
console.warn("Assistent: fetch blob failed", e);
|
||
}
|
||
}
|
||
return await new Promise((resolve) => {
|
||
const tmpImg = new Image();
|
||
tmpImg.crossOrigin = "Anonymous";
|
||
tmpImg.onload = () => {
|
||
try {
|
||
const canvas = document.createElement("canvas");
|
||
canvas.width = tmpImg.naturalWidth;
|
||
canvas.height = tmpImg.naturalHeight;
|
||
const ctx = canvas.getContext("2d");
|
||
ctx.drawImage(tmpImg, 0, 0);
|
||
canvas.toBlob((blob) => resolve(blob), "image/png");
|
||
} catch (e) {
|
||
resolve(null);
|
||
}
|
||
};
|
||
tmpImg.onerror = () => resolve(null);
|
||
tmpImg.src = cleaned;
|
||
});
|
||
}
|
||
async function setFileParamFromSrc(paramId, src, { filename = "assistent.png" } = {}) {
|
||
const el = document.getElementById(paramId);
|
||
if (!el) {
|
||
setStatus(`Missing ${paramId} on Generate tab`);
|
||
return false;
|
||
}
|
||
const blob = await srcToBlob(src);
|
||
if (!blob) {
|
||
setStatus("Could not load image for init/mask");
|
||
return false;
|
||
}
|
||
const mime = blob.type || guessImageMime(src);
|
||
const file = new File([blob], filename, { type: mime });
|
||
const container = new DataTransfer();
|
||
container.items.add(file);
|
||
el.files = container.files;
|
||
triggerChangeForEl(el);
|
||
openInitImageGroup();
|
||
return true;
|
||
}
|
||
async function setInitFromSrc(src) {
|
||
const ok = await setFileParamFromSrc("input_initimage", src, { filename: "assistent_init.png" });
|
||
if (ok) {
|
||
setStatus("Init Image set");
|
||
}
|
||
return ok;
|
||
}
|
||
async function setMaskFromSrc(src) {
|
||
const ok = await setFileParamFromSrc("input_maskimage", src, { filename: "assistent_mask.png" });
|
||
if (ok) {
|
||
setStatus("Mask Image set (white = edit)");
|
||
}
|
||
return ok;
|
||
}
|
||
function clearInitAndMask() {
|
||
clearFileParam("input_initimage");
|
||
clearFileParam("input_maskimage");
|
||
const toggler = document.getElementById("input_group_content_initimage_toggle");
|
||
if (toggler) {
|
||
toggler.checked = false;
|
||
triggerChangeForEl(toggler);
|
||
}
|
||
setStatus("Init Image + Mask cleared");
|
||
}
|
||
function readInitContext() {
|
||
const creativityRaw = val("input_initimagecreativity");
|
||
const creativity = creativityRaw === "" ? null : parseFloat(creativityRaw);
|
||
return {
|
||
has_init_image: hasFileParam("input_initimage"),
|
||
has_mask_image: hasFileParam("input_maskimage"),
|
||
init_creativity: Number.isFinite(creativity) ? creativity : null,
|
||
mask_blur: parseFloat(val("input_maskblur") || "") || null,
|
||
mask_grow: parseInt(val("input_maskgrow") || val("input_maskshrinkgrow") || "", 10) || null,
|
||
init_group_on: !!document.getElementById("input_group_content_initimage_toggle")?.checked
|
||
};
|
||
}
|
||
function slimPromptForContext(raw) {
|
||
let s = String(raw || "");
|
||
s = s.replace(/data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=]+/g, "[image omitted]");
|
||
s = s.replace(/<image\b[^>]*>[\s\S]*?<\/image>/gi, "[image omitted]");
|
||
s = s.replace(/<img\b[^>]*>/gi, "[image omitted]");
|
||
if (s.length > CONTEXT_PROMPT_MAX) {
|
||
s = s.slice(0, CONTEXT_PROMPT_MAX) + "\u2026";
|
||
}
|
||
return s;
|
||
}
|
||
function collectLiveContext() {
|
||
pullLiveIntoSession();
|
||
const S = window.SA && window.SA.session;
|
||
let initCtx = {};
|
||
try {
|
||
if (typeof readInitContext === "function") initCtx = readInitContext();
|
||
} catch (e) {
|
||
}
|
||
const kreaProfile = detectKreaProfileName2();
|
||
const exactDefs = exactProfileDefaults(kreaProfile);
|
||
const extra = {
|
||
prompt_image_count: typeof countPromptImages === "function" ? countPromptImages() : 0,
|
||
has_vision_image: typeof visionReadySlots === "function" ? visionReadySlots().length > 0 : false,
|
||
image_slots: typeof slotCatalog === "function" ? slotCatalog() : [],
|
||
attached_slot_ids: typeof attachableSlots === "function" ? attachableSlots().map((s) => s.id) : [],
|
||
auto_apply: !!$2("sa_auto_apply")?.checked,
|
||
auto_generate: true,
|
||
krea_profile: kreaProfile,
|
||
recommended_params: {
|
||
steps: exactDefs.steps,
|
||
cfg: exactDefs.cfg,
|
||
sigma_shift: exactDefs.sigma_shift
|
||
},
|
||
...initCtx
|
||
};
|
||
if (S && typeof S.compactContext === "function") {
|
||
const ctx = S.compactContext(state.chatSession, {
|
||
architecture_ok: typeof isKreaSelected === "function" ? isKreaSelected() : true,
|
||
promptMax: typeof CONTEXT_PROMPT_MAX !== "undefined" ? CONTEXT_PROMPT_MAX : 2e3,
|
||
extra
|
||
});
|
||
const block = ctxApi()?.conversationMemoryBlock?.(getContextMemory());
|
||
if (block) {
|
||
ctx.conversation_memory = block;
|
||
}
|
||
return ctx;
|
||
}
|
||
const g = state.chatSession && state.chatSession.gen || {};
|
||
return { session: true, prompt: g.prompt || "", negative: g.negative || "", krea_profile: kreaProfile, ...extra };
|
||
}
|
||
function slimInventoryLoras(list, limit) {
|
||
const selected = /* @__PURE__ */ new Set();
|
||
try {
|
||
if (typeof loraHelper !== "undefined" && loraHelper && Array.isArray(loraHelper.selected)) {
|
||
for (const l of loraHelper.selected) {
|
||
selected.add(String(l.name || l || "").toLowerCase());
|
||
}
|
||
}
|
||
} catch (e) {
|
||
}
|
||
const namesCap = Math.min(limit || INVENTORY_PROMPT_NAMES, INVENTORY_PROMPT_NAMES);
|
||
const richCap = Math.max(4, Math.min(INVENTORY_PROMPT_RICH, namesCap));
|
||
const rows = (list || []).map((l) => {
|
||
const sel = selected.has(String(l.name || "").toLowerCase());
|
||
const hasCard = !!l.has_card;
|
||
const krea = !!l.krea_likely;
|
||
const blurb = l.blurb || l.usage_hint || null;
|
||
return {
|
||
name: l.name,
|
||
title: l.title || l.name,
|
||
trigger_phrase: l.trigger_phrase || null,
|
||
triggers: Array.isArray(l.triggers) ? l.triggers.slice(0, 8) : void 0,
|
||
architecture: l.architecture || null,
|
||
compat_class: l.compat_class || null,
|
||
has_card: hasCard,
|
||
krea_likely: krea,
|
||
blurb,
|
||
default_weight: l.default_weight || void 0,
|
||
tags: Array.isArray(l.tags) ? l.tags.slice(0, 6) : void 0,
|
||
_score: (sel ? 1e3 : 0) + (hasCard ? 200 : 0) + (krea ? 50 : 0) + (blurb ? 10 : 0)
|
||
};
|
||
});
|
||
rows.sort((a, b) => b._score - a._score || String(a.name).localeCompare(String(b.name)));
|
||
let richUsed = 0;
|
||
const out = [];
|
||
for (const row of rows) {
|
||
if (out.length >= namesCap) {
|
||
break;
|
||
}
|
||
const sel = selected.has(String(row.name || "").toLowerCase());
|
||
let wantRich = sel;
|
||
if (!wantRich && richUsed < richCap && (row.krea_likely || row.has_card || row.blurb)) {
|
||
wantRich = true;
|
||
}
|
||
if (wantRich) {
|
||
const rich = { name: row.name, title: row.title };
|
||
if (row.trigger_phrase) {
|
||
rich.trigger_phrase = row.trigger_phrase;
|
||
}
|
||
if (row.triggers) {
|
||
rich.triggers = row.triggers;
|
||
}
|
||
if (row.krea_likely) {
|
||
rich.krea_likely = true;
|
||
}
|
||
if (row.has_card) {
|
||
rich.has_card = true;
|
||
}
|
||
if (row.blurb) {
|
||
rich.blurb = row.blurb;
|
||
}
|
||
if (row.default_weight) {
|
||
rich.default_weight = row.default_weight;
|
||
}
|
||
if (row.architecture) {
|
||
rich.architecture = row.architecture;
|
||
}
|
||
out.push(rich);
|
||
if (!sel) {
|
||
richUsed++;
|
||
}
|
||
} else {
|
||
const nameOnly = { name: row.name };
|
||
if (row.krea_likely) {
|
||
nameOnly.krea_likely = true;
|
||
}
|
||
out.push(nameOnly);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
function slimInventoryCheckpoints(list, limit) {
|
||
const rows = (list || []).slice();
|
||
rows.sort((a, b) => (b.krea_likely ? 1 : 0) - (a.krea_likely ? 1 : 0) || (b.has_card ? 1 : 0) - (a.has_card ? 1 : 0) || String(a.name).localeCompare(String(b.name)));
|
||
return rows.slice(0, limit || 8).map((c) => {
|
||
const out = {
|
||
name: c.name,
|
||
title: c.title || c.name
|
||
};
|
||
if (c.architecture) {
|
||
out.architecture = c.architecture;
|
||
}
|
||
if (c.krea_likely) {
|
||
out.krea_likely = true;
|
||
}
|
||
if (c.has_card) {
|
||
out.has_card = true;
|
||
}
|
||
return out;
|
||
});
|
||
}
|
||
function pushUnique(arr, value, max) {
|
||
const v = String(value || "").trim();
|
||
if (!v || v.length < 2) {
|
||
return;
|
||
}
|
||
const lower = v.toLowerCase();
|
||
const next = (arr || []).filter((x) => String(x).toLowerCase() !== lower);
|
||
next.unshift(v.slice(0, 80));
|
||
return next.slice(0, max);
|
||
}
|
||
function extractPatch2(text) {
|
||
if (window.SA && typeof SA.extractPatch === "function") {
|
||
return SA.extractPatch(text);
|
||
}
|
||
return { prose: text || "", patch: null };
|
||
}
|
||
function normalizeAspect(raw) {
|
||
if (raw == null) {
|
||
return null;
|
||
}
|
||
let s = String(raw).trim().toLowerCase().replace(/\s+/g, "");
|
||
if (!s) {
|
||
return null;
|
||
}
|
||
if (s === "square") {
|
||
s = "1:1";
|
||
} else if (s === "portrait" || s === "vert") {
|
||
s = "2:3";
|
||
} else if (s === "landscape" || s === "horiz") {
|
||
s = "16:9";
|
||
} else if (s === "cinematic" || s === "ultrawide") {
|
||
s = "2.35:1";
|
||
}
|
||
return ASPECT_TABLE[s] ? s : null;
|
||
}
|
||
function sizeFromAspect(aspect) {
|
||
const key = normalizeAspect(aspect);
|
||
return key ? ASPECT_TABLE[key] : null;
|
||
}
|
||
function guessAspectFromSize(w, h) {
|
||
const width = parseInt(w, 10);
|
||
const height = parseInt(h, 10);
|
||
if (!width || !height) {
|
||
return null;
|
||
}
|
||
let best = null;
|
||
let bestDist = Infinity;
|
||
for (const [key, [aw, ah]] of Object.entries(ASPECT_TABLE)) {
|
||
const dist = Math.abs(width / height - aw / ah) + Math.abs(width - aw) / 4e3 + Math.abs(height - ah) / 4e3;
|
||
if (dist < bestDist) {
|
||
bestDist = dist;
|
||
best = key;
|
||
}
|
||
}
|
||
return bestDist < 0.12 ? best : null;
|
||
}
|
||
function clearPromptImagesInBox() {
|
||
const box = document.getElementById("alt_prompt_textbox") || document.getElementById("input_prompt");
|
||
if (!box) {
|
||
return false;
|
||
}
|
||
const before = box.value || "";
|
||
const next = before.replace(/<image\b[^>]*>[\s\S]*?<\/image>/gi, "").replace(/<image\b[^>]*\/?>/gi, "").replace(/data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=]+/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
||
if (next === before.trim()) {
|
||
return false;
|
||
}
|
||
box.value = next;
|
||
box.dispatchEvent(new Event("input", { bubbles: true }));
|
||
box.dispatchEvent(new Event("change", { bubbles: true }));
|
||
return true;
|
||
}
|
||
function clearPatchBlocksOnly() {
|
||
document.querySelectorAll("#sa_messages .sa-patch, #sa_messages .sa-patch-stale, #sa_messages .sa-civitai-list").forEach((el) => el.remove());
|
||
state.lastPatch = null;
|
||
syncBuildGenButton();
|
||
setStatus("\u041F\u0430\u0442\u0447\u0438 \u0443\u0431\u0440\u0430\u043D\u044B \u0438\u0437 \u0447\u0430\u0442\u0430");
|
||
}
|
||
function toggleMoreMenu(menuId, btnId) {
|
||
const menu = $2(menuId);
|
||
const btn = $2(btnId);
|
||
if (!menu) {
|
||
return;
|
||
}
|
||
const open = menu.hidden;
|
||
document.querySelectorAll(".sa-more-menu").forEach((m) => {
|
||
m.hidden = true;
|
||
});
|
||
document.querySelectorAll("#sa_btn_board_more, #sa_btn_clear_more").forEach((b) => b.setAttribute("aria-expanded", "false"));
|
||
if (open) {
|
||
menu.hidden = false;
|
||
btn?.setAttribute("aria-expanded", "true");
|
||
}
|
||
}
|
||
function closeAllMoreMenus() {
|
||
document.querySelectorAll(".sa-more-menu").forEach((m) => {
|
||
m.hidden = true;
|
||
});
|
||
document.querySelectorAll("#sa_btn_board_more, #sa_btn_clear_more").forEach((b) => b.setAttribute("aria-expanded", "false"));
|
||
}
|
||
function setPackValue(packName, { flash, user } = {}) {
|
||
const pack = $2("sa_pack");
|
||
if (!pack || !packName) {
|
||
return false;
|
||
}
|
||
const resolved = PACK_ALIASES[String(packName).trim()] || String(packName).trim();
|
||
if (![...pack.options].some((o) => o.value === resolved)) {
|
||
return false;
|
||
}
|
||
if (pack.value !== resolved) {
|
||
pack.value = resolved;
|
||
saveSettings();
|
||
}
|
||
if (user) {
|
||
state.packUserTouched = true;
|
||
}
|
||
if (flash) {
|
||
pack.classList.add("sa-pack-flash");
|
||
setTimeout(() => pack.classList.remove("sa-pack-flash"), 900);
|
||
}
|
||
syncModeBadge();
|
||
return true;
|
||
}
|
||
function autoSelectPack(text) {
|
||
if (state.packUserTouched) {
|
||
return null;
|
||
}
|
||
const cur = $2("sa_pack")?.value || defaultPackId();
|
||
if (cur === "ordinary") {
|
||
return null;
|
||
}
|
||
const t = String(text || "").toLowerCase();
|
||
if (!t.trim()) {
|
||
return null;
|
||
}
|
||
if (userTextMentionsParams(t) || parseAspectFromUserText(t)) {
|
||
return cur === "critique_image" || cur === "describe_ref" ? "ordinary" : "fix_params";
|
||
}
|
||
if (cyrTokenRe("\u043F\u043E\u043F\u0440\u0430\u0432\u044C|\u0438\u0441\u043F\u0440\u0430\u0432\u044C|\u043F\u0435\u0440\u0435\u043F\u0438\u0448\u0438|\u0443\u043B\u0443\u0447\u0448\u0438").test(t) || /\b(fix\s+it|make\s+it\s+better|rewrite)\b/i.test(t)) {
|
||
return "write_prompt";
|
||
}
|
||
if (cyrTokenRe("\u043E\u043F\u0438\u0448\u0438\\s+(\u0440\u0435\u0444|\u0438\u0437\u043E\u0431\u0440\u0430\u0436[\u0430-\u044F\u0451]*|\u044D\u0442\u043E\u0442|\u044D\u0442\u0443|\u043A\u0430\u0440\u0442\u0438\u043D\u043A[\u0430-\u044F\u0451]*|\u0440\u0435\u0444\u0435\u0440\u0435\u043D\u0441)").test(t) || /\b(prompt\s+from\s+image|describe\s+(this|the|ref|image)|reverse\s*prompt)\b/i.test(t) || /опиши\s+(этот|эту|картинк|референс)/i.test(t)) {
|
||
return "describe_ref";
|
||
}
|
||
if (/\b(critique|criticize)\b/i.test(t) || cyrTokenRe("\u043A\u0440\u0438\u0442\u0438\u043A[\u0430-\u044F\u0451]*|\u0447\u0442\u043E\\s+\u043D\u0435\\s+\u0442\u0430\u043A|\u0440\u0430\u0437\u0431\u0435\u0440\u0438").test(t) || /(?:^|[^а-яёa-z0-9_])(посмотри|смотри)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген)/i.test(t)) {
|
||
return "critique_image";
|
||
}
|
||
if (/\b(inpaint|mask|img2img)\b/i.test(t) || cyrTokenRe("\u0437\u0430\u043C\u0430\u0436\u044C|\u0437\u0430\u043A\u0440\u0430\u0441\u044C|\u0440\u0443\u043A\u0438|\u043B\u0438\u0446\u043E|\u043C\u0430\u0441\u043A[\u0430-\u044F\u0451]*").test(t) || /init\s*image/i.test(t)) {
|
||
return "inpaint_edit";
|
||
}
|
||
if (cur === "critique_image") {
|
||
return "ordinary";
|
||
}
|
||
if (/\b(moodboard|compose|scene)\b/i.test(t) || cyrTokenRe("\u0441\u0446\u0435\u043D[\u0430-\u044F\u0451]*|\u0430\u0442\u043C\u043E\u0441\u0444\u0435\u0440[\u0430-\u044F\u0451]*|\u043C\u0438\u0437\u0430\u043D\u0441\u0446\u0435\u043D[\u0430-\u044F\u0451]*").test(t)) {
|
||
return "compose_scene";
|
||
}
|
||
return "write_prompt";
|
||
}
|
||
function restoreDefaultPackAfterHop() {
|
||
if (state.packUserTouched) {
|
||
return;
|
||
}
|
||
const cur = $2("sa_pack")?.value || "";
|
||
if (cur === "critique_image" || cur === "describe_ref") {
|
||
setPackValue(defaultPackId(), { flash: true });
|
||
}
|
||
}
|
||
function patchHasGenTrigger(patch) {
|
||
if (!patch) {
|
||
return false;
|
||
}
|
||
if (Array.isArray(patch.actions) && patch.actions.map(String).includes("generate")) {
|
||
return true;
|
||
}
|
||
return patch.prompt != null || patch.loras || patch.width != null || patch.height != null || patch.aspect != null || patch.steps != null || patch.cfg != null || patch.seed != null || patch.sigma_shift != null || patch.images != null || patch.batch != null || patch.vary === true || Array.isArray(patch.variants) && patch.variants.length > 0 || patch.use_init_image || patch.clear_init_image || patch.init_creativity != null || patch.denoise != null || patch.use_mask_image || patch.clear_mask_image || patch.clear_prompt_images;
|
||
}
|
||
async function applyPatch(patch, which) {
|
||
if (!patch) {
|
||
return;
|
||
}
|
||
const S = window.SA && window.SA.session;
|
||
const wantsGen = S && typeof S.patchWantsGenerate === "function" && S.patchWantsGenerate(patch);
|
||
if (wantsGen) {
|
||
patch = ensureExactParamsForGenerate(patch);
|
||
}
|
||
const doPrompt = !which || which === "all" || which === "prompt";
|
||
const doLoras = !which || which === "all" || which === "loras";
|
||
const doParams = !which || which === "all" || which === "size" || which === "params";
|
||
const doInit = !which || which === "all" || which === "params" || which === "init";
|
||
if (patch.pack) {
|
||
setPackValue(patch.pack, { flash: true });
|
||
}
|
||
if (doPrompt && patch.clear_prompt_images) {
|
||
clearPromptImagesInBox();
|
||
}
|
||
if (doPrompt && patch.prompt != null) {
|
||
const box = document.getElementById("alt_prompt_textbox") || document.getElementById("input_prompt");
|
||
if (box) {
|
||
box.value = patch.prompt;
|
||
box.dispatchEvent(new Event("input", { bubbles: true }));
|
||
box.dispatchEvent(new Event("change", { bubbles: true }));
|
||
}
|
||
if (Array.isArray(patch.loras)) {
|
||
for (const l of patch.loras) {
|
||
const triggers = l.triggers || (l.trigger_phrase ? [l.trigger_phrase] : []);
|
||
for (const t of triggers) {
|
||
if (t && box && box.value && !box.value.includes(t)) {
|
||
box.value = `${box.value.trim()}, ${t}`;
|
||
box.dispatchEvent(new Event("input", { bubbles: true }));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (doPrompt) {
|
||
if (patch.negative != null) {
|
||
setNegativePrompt(patch.negative);
|
||
} else if (patchHasGenTrigger(patch)) {
|
||
ensureNegativeForGenerate(patch);
|
||
}
|
||
}
|
||
if (doLoras && Array.isArray(patch.loras) && typeof loraHelper !== "undefined" && loraHelper) {
|
||
try {
|
||
if (typeof loraHelper.clearLoras === "function") {
|
||
loraHelper.clearLoras();
|
||
}
|
||
} catch (e) {
|
||
}
|
||
for (const l of patch.loras) {
|
||
const name = l.name;
|
||
if (!name) {
|
||
continue;
|
||
}
|
||
try {
|
||
if (typeof loraHelper.selectLora === "function") {
|
||
loraHelper.selectLora(name);
|
||
}
|
||
if (loraHelper.loraWeightPref && l.weight != null) {
|
||
loraHelper.loraWeightPref[name] = l.weight;
|
||
}
|
||
} catch (e) {
|
||
console.warn("Assistent: selectLora failed", name, e);
|
||
}
|
||
}
|
||
try {
|
||
if (typeof loraHelper.rebuildUI === "function") {
|
||
loraHelper.rebuildUI();
|
||
}
|
||
} catch (e) {
|
||
}
|
||
}
|
||
if (doParams) {
|
||
const defaults = mergedGenerationDefaults();
|
||
const aspectSize = sizeFromAspect(patch.aspect);
|
||
if (patch.aspect != null && !shouldSkipSessionRollback("aspect", patch.aspect)) {
|
||
if (aspectSize) {
|
||
setVal("input_width", String(aspectSize[0]));
|
||
setVal("input_height", String(aspectSize[1]));
|
||
}
|
||
if (shouldRememberSessionParam("aspect", patch.aspect)) {
|
||
rememberSessionExact({ aspect: patch.aspect });
|
||
}
|
||
} else if (patch.aspect == null && isEmptyParamField(val("input_width"), { treatZeroEmpty: true }) && isEmptyParamField(val("input_height"), { treatZeroEmpty: true }) && defaults.aspect) {
|
||
const fill = sizeFromAspect(defaults.aspect);
|
||
if (fill) {
|
||
setVal("input_width", String(fill[0]));
|
||
setVal("input_height", String(fill[1]));
|
||
}
|
||
} else {
|
||
if (patch.width != null && !shouldSkipSessionRollback("width", patch.width)) {
|
||
setVal("input_width", String(patch.width));
|
||
if (shouldRememberSessionParam("width", patch.width)) {
|
||
rememberSessionExact({ width: patch.width });
|
||
}
|
||
} else if (patch.width == null && isEmptyParamField(val("input_width"), { treatZeroEmpty: true }) && defaults.width != null) {
|
||
setVal("input_width", String(defaults.width));
|
||
}
|
||
if (patch.height != null && !shouldSkipSessionRollback("height", patch.height)) {
|
||
setVal("input_height", String(patch.height));
|
||
if (shouldRememberSessionParam("height", patch.height)) {
|
||
rememberSessionExact({ height: patch.height });
|
||
}
|
||
} else if (patch.height == null && isEmptyParamField(val("input_height"), { treatZeroEmpty: true }) && defaults.height != null) {
|
||
setVal("input_height", String(defaults.height));
|
||
}
|
||
}
|
||
if (patch.steps != null && !shouldSkipSessionRollback("steps", patch.steps)) {
|
||
setVal("input_steps", String(patch.steps));
|
||
if (shouldRememberSessionParam("steps", patch.steps)) {
|
||
rememberSessionExact({ steps: patch.steps });
|
||
}
|
||
} else if (patch.steps == null && isEmptyParamField(val("input_steps"), { treatZeroEmpty: true }) && defaults.steps != null) {
|
||
setVal("input_steps", String(defaults.steps));
|
||
}
|
||
if (patch.cfg != null && !shouldSkipSessionRollback("cfg", patch.cfg)) {
|
||
if (document.getElementById("input_cfgscale")) {
|
||
setVal("input_cfgscale", String(patch.cfg));
|
||
} else {
|
||
setVal("input_cfg", String(patch.cfg));
|
||
}
|
||
if (shouldRememberSessionParam("cfg", patch.cfg)) {
|
||
rememberSessionExact({ cfg: patch.cfg });
|
||
}
|
||
} else if (patch.cfg == null) {
|
||
const cfgRaw = val("input_cfgscale") || val("input_cfg");
|
||
if (isEmptyParamField(cfgRaw, { treatZeroEmpty: true }) && defaults.cfg != null) {
|
||
if (document.getElementById("input_cfgscale")) {
|
||
setVal("input_cfgscale", String(defaults.cfg));
|
||
} else if (document.getElementById("input_cfg")) {
|
||
setVal("input_cfg", String(defaults.cfg));
|
||
}
|
||
}
|
||
}
|
||
if (patch.vary === true) {
|
||
setVal("input_seed", "-1");
|
||
} else if (patch.lock_seed === true) {
|
||
const cur = val("input_seed");
|
||
if (cur && String(cur) !== "-1") {
|
||
setVal("input_seed", cur);
|
||
}
|
||
} else if (patch.seed != null && !shouldSkipSessionRollback("seed", patch.seed)) {
|
||
setVal("input_seed", String(patch.seed));
|
||
if (shouldRememberSessionParam("seed", patch.seed)) {
|
||
rememberSessionExact({ seed: patch.seed });
|
||
}
|
||
}
|
||
if (patch.sigma_shift != null && !shouldSkipSessionRollback("sigma_shift", patch.sigma_shift)) {
|
||
setVal("input_sigmashift", String(patch.sigma_shift));
|
||
if (shouldRememberSessionParam("sigma_shift", patch.sigma_shift)) {
|
||
rememberSessionExact({ sigma_shift: patch.sigma_shift });
|
||
}
|
||
} else if (patch.sigma_shift == null && isEmptyParamField(val("input_sigmashift")) && defaults.sigma_shift != null) {
|
||
setVal("input_sigmashift", String(defaults.sigma_shift));
|
||
}
|
||
if (patch.sampler != null) {
|
||
if (document.getElementById("input_sampler")) {
|
||
setVal("input_sampler", String(patch.sampler));
|
||
}
|
||
if (shouldRememberSessionParam("sampler", patch.sampler)) {
|
||
rememberSessionExact({ sampler: patch.sampler });
|
||
}
|
||
}
|
||
if (patch.scheduler != null && document.getElementById("input_scheduler")) {
|
||
setVal("input_scheduler", String(patch.scheduler));
|
||
if (shouldRememberSessionParam("scheduler", patch.scheduler)) {
|
||
rememberSessionExact({ scheduler: patch.scheduler });
|
||
}
|
||
}
|
||
const batch = patch.images != null ? patch.images : patch.batch;
|
||
if (batch != null && !shouldSkipSessionRollback("images", batch)) {
|
||
if (document.getElementById("input_images")) {
|
||
setVal("input_images", String(batch));
|
||
} else if (document.getElementById("input_batchsize")) {
|
||
setVal("input_batchsize", String(batch));
|
||
}
|
||
if (shouldRememberSessionParam("images", batch)) {
|
||
rememberSessionExact({ images: batch });
|
||
}
|
||
} else if (batch == null) {
|
||
const batchId = document.getElementById("input_images") ? "input_images" : document.getElementById("input_batchsize") ? "input_batchsize" : null;
|
||
const defBatch = defaults.images != null ? defaults.images : defaults.batch;
|
||
if (batchId && isEmptyParamField(val(batchId), { treatZeroEmpty: true }) && defBatch != null) {
|
||
setVal(batchId, String(defBatch));
|
||
}
|
||
}
|
||
const Sgen = window.SA && window.SA.session;
|
||
if (Sgen?.patchWantsGenerate?.(patch) || patch.generate === true || Array.isArray(patch.actions) && patch.actions.map(String).includes("generate")) {
|
||
forceExactParamsForGenerate({ patch });
|
||
}
|
||
}
|
||
if (doInit) {
|
||
const creativity = patch.init_creativity != null ? patch.init_creativity : patch.denoise;
|
||
if (creativity != null && document.getElementById("input_initimagecreativity")) {
|
||
setVal("input_initimagecreativity", String(creativity));
|
||
openInitImageGroup();
|
||
}
|
||
if (patch.mask_blur != null && document.getElementById("input_maskblur")) {
|
||
setVal("input_maskblur", String(patch.mask_blur));
|
||
}
|
||
if (patch.mask_grow != null) {
|
||
if (document.getElementById("input_maskgrow")) {
|
||
setVal("input_maskgrow", String(patch.mask_grow));
|
||
} else if (document.getElementById("input_maskshrinkgrow")) {
|
||
setVal("input_maskshrinkgrow", String(patch.mask_grow));
|
||
}
|
||
}
|
||
if (patch.clear_init_image || patch.clear_mask_image) {
|
||
if (patch.clear_init_image) {
|
||
clearFileParam("input_initimage");
|
||
}
|
||
if (patch.clear_mask_image) {
|
||
clearFileParam("input_maskimage");
|
||
}
|
||
if (patch.clear_init_image && patch.clear_mask_image) {
|
||
const toggler = document.getElementById("input_group_content_initimage_toggle");
|
||
if (toggler) {
|
||
toggler.checked = false;
|
||
triggerChangeForEl(toggler);
|
||
}
|
||
}
|
||
}
|
||
if (patch.select_slot) {
|
||
const id = normalizeSlotId(patch.select_slot);
|
||
if (slotById(id)) {
|
||
state.selectedSlotId = id;
|
||
renderBoard();
|
||
}
|
||
}
|
||
if (patch.snapshot_generate) {
|
||
snapshotGenerateToRef();
|
||
}
|
||
const initId = patch.slot_to_init || (patch.use_init_image || Array.isArray(patch.actions) && patch.actions.map(String).includes("use_init") ? state.selectedSlotId : null);
|
||
const maskId = patch.slot_to_mask || null;
|
||
const src = resolveSlotSrc(patch.slot_to_init) || selectedSrc() || findCurrentGenerateSrc();
|
||
const wantInit = patch.use_init_image === true || !!patch.slot_to_init || Array.isArray(patch.actions) && patch.actions.map(String).includes("use_init");
|
||
const wantMask = patch.use_mask_image === true || !!patch.slot_to_mask || Array.isArray(patch.actions) && patch.actions.map(String).includes("use_mask");
|
||
if (wantInit) {
|
||
const initSrc = resolveSlotSrc(initId) || src;
|
||
if (initSrc) {
|
||
await setInitFromSrc(initSrc);
|
||
} else {
|
||
setStatus("No image for Init \u2014 drop a ref or wait for Generate");
|
||
}
|
||
}
|
||
if (wantMask) {
|
||
const maskSrc = resolveSlotSrc(maskId) || src;
|
||
if (maskSrc) {
|
||
await setMaskFromSrc(maskSrc);
|
||
} else {
|
||
setStatus("No image for Mask \u2014 drop a mask (white=edit) first");
|
||
}
|
||
}
|
||
if (patch.slot_to_prompt_image) {
|
||
setStatus("Prompt Images: drop the ref into the Swarm prompt box (no auto helper yet)");
|
||
}
|
||
}
|
||
if (patch.controls && typeof patch.controls === "object" && !Array.isArray(patch.controls)) {
|
||
const schema = state.config?.controls || {};
|
||
const filtered = filterControlPatch(patch.controls, patch);
|
||
if (Object.keys(filtered).length) {
|
||
const next = { ...state.config?.control_values || state.exact?.controls || {}, ...filtered };
|
||
savePersonaControls(filtered);
|
||
renderPersonaControls(schema, next);
|
||
}
|
||
}
|
||
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
|
||
const wantSwitch = acts.includes("persona_switch") || patch.persona && typeof patch.persona === "string" || patch._persona_cloned || patch._persona_written;
|
||
if (wantSwitch) {
|
||
const newId = String(patch.persona || patch._persona_cloned || patch._persona_written || "").trim();
|
||
if (newId && AssistentConfigSafeIdClient(newId)) {
|
||
await refreshPersonasAndSwitch(newId);
|
||
} else if (acts.includes("persona_clone") || acts.includes("persona_write") || patch.persona_clone) {
|
||
await refreshPersonasAndSwitch(null);
|
||
}
|
||
}
|
||
syncChipHighlight();
|
||
syncLiveParamsBar();
|
||
syncBuildGenButton();
|
||
if (state.activeChatId && !state.restoringChat) {
|
||
const chat = findChat(state.activeChatId);
|
||
if (chat) {
|
||
chat.params = snapshotChatParams();
|
||
chat.updatedAt = Date.now();
|
||
persistChatsStore();
|
||
}
|
||
}
|
||
if (!state.restoringChat) {
|
||
setStatus(patch._persona_error ? `Persona: ${patch._persona_error}` : "Applied patch");
|
||
}
|
||
}
|
||
function AssistentConfigSafeIdClient(id) {
|
||
return /^[A-Za-z0-9][A-Za-z0-9_\-]{0,63}$/.test(String(id || ""));
|
||
}
|
||
async function refreshPersonasAndSwitch(preferId) {
|
||
await new Promise((resolve) => {
|
||
genericRequest(
|
||
"AssistentListPersonas",
|
||
{},
|
||
async (data) => {
|
||
if (Array.isArray(data?.personas)) {
|
||
state.personas = data.personas.map((p) => ({
|
||
id: p.id,
|
||
title: p.title,
|
||
accent: p.accent,
|
||
source: p.source
|
||
}));
|
||
renderPersonaOptions(state.personas, preferId || $2("sa_persona")?.value);
|
||
}
|
||
if (preferId && $2("sa_persona")) {
|
||
if ([...$2("sa_persona").options].some((o) => o.value === preferId)) {
|
||
$2("sa_persona").value = preferId;
|
||
await applyPersonaForChat(preferId, { quiet: true });
|
||
}
|
||
} else {
|
||
loadConfig($2("sa_persona")?.value, () => resolve());
|
||
return;
|
||
}
|
||
resolve();
|
||
},
|
||
0,
|
||
() => resolve()
|
||
);
|
||
});
|
||
}
|
||
function triggerGenerate() {
|
||
try {
|
||
if (typeof mainGenHandler !== "undefined" && mainGenHandler && typeof mainGenHandler.doGenerate === "function") {
|
||
mainGenHandler.doGenerate();
|
||
return true;
|
||
}
|
||
} catch (e) {
|
||
console.warn("Assistent: doGenerate failed", e);
|
||
}
|
||
const btn = document.getElementById("generate_button") || document.getElementById("alt_generate_button") || document.querySelector("button.generate-button") || document.querySelector('#generate_button, button[id*="generate"]');
|
||
if (btn) {
|
||
btn.click();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function shouldParkLlmBeforeGen() {
|
||
return !!$2("sa_park_llm")?.checked;
|
||
}
|
||
function parkLlm() {
|
||
return new Promise((resolve) => {
|
||
const model = $2("sa_model")?.value;
|
||
if (!shouldParkLlmBeforeGen() || !model || state.llmParked || typeof genericRequest !== "function") {
|
||
resolve(false);
|
||
return;
|
||
}
|
||
const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434";
|
||
let settled = false;
|
||
const finish = (ok) => {
|
||
if (settled) {
|
||
return;
|
||
}
|
||
settled = true;
|
||
if (ok) {
|
||
state.llmParked = true;
|
||
state.expectColdLoad = true;
|
||
}
|
||
resolve(!!ok);
|
||
};
|
||
setTimeout(() => finish(false), 8e3);
|
||
genericRequest("AssistentParkLlm", { baseUrl, model }, () => finish(true), 0, () => finish(false));
|
||
});
|
||
}
|
||
function warmLlm({ force = false } = {}) {
|
||
return new Promise((resolve) => {
|
||
const model = $2("sa_model")?.value;
|
||
if (!model || typeof genericRequest !== "function") {
|
||
resolve({ ok: false, alreadyResident: false });
|
||
return;
|
||
}
|
||
if (!force && !state.llmParked) {
|
||
resolve({ ok: false, alreadyResident: false });
|
||
return;
|
||
}
|
||
const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434";
|
||
let settled = false;
|
||
const finish = (ok, alreadyResident = false) => {
|
||
if (settled) {
|
||
return;
|
||
}
|
||
settled = true;
|
||
state.llmParked = false;
|
||
if (ok) {
|
||
state.expectColdLoad = false;
|
||
}
|
||
resolve({ ok: !!ok, alreadyResident: !!alreadyResident });
|
||
};
|
||
setTimeout(() => finish(false), 18e4);
|
||
genericRequest(
|
||
"AssistentWarmLlm",
|
||
{ baseUrl, model },
|
||
(data) => {
|
||
const skipped = !!(data && (data.skipped === "already_resident" || data.already_resident === true));
|
||
finish(true, skipped);
|
||
},
|
||
0,
|
||
() => finish(false)
|
||
);
|
||
});
|
||
}
|
||
function cancelWaitForNewImage() {
|
||
if (state.waitImageTimer) {
|
||
clearInterval(state.waitImageTimer);
|
||
state.waitImageTimer = null;
|
||
}
|
||
}
|
||
function bumpChatEpoch() {
|
||
state.chatEpoch = (state.chatEpoch || 0) + 1;
|
||
return state.chatEpoch;
|
||
}
|
||
function clearStreamStall() {
|
||
if (state.streamStallTimer) {
|
||
clearInterval(state.streamStallTimer);
|
||
state.streamStallTimer = null;
|
||
}
|
||
}
|
||
function armStreamStall(chatEpoch, onStall) {
|
||
clearStreamStall();
|
||
state.lastDeltaAt = Date.now();
|
||
state.streamStallTimer = setInterval(() => {
|
||
if (chatEpoch !== state.chatEpoch) {
|
||
clearStreamStall();
|
||
return;
|
||
}
|
||
if (!state.gotDelta || !state.busy || state.generating || state.turnSettled) {
|
||
return;
|
||
}
|
||
const waitMs = state.streamFenceDone ? 800 : state.gotDelta ? 2200 : 15e3;
|
||
if (Date.now() - (state.lastDeltaAt || 0) < waitMs) {
|
||
return;
|
||
}
|
||
clearStreamStall();
|
||
const reply = state.streamText || state.streamEl?.querySelector(".sa-msg-body")?.textContent || "";
|
||
try {
|
||
onStall(String(reply || ""));
|
||
} catch (e) {
|
||
console.warn("Assistent stream stall", e);
|
||
}
|
||
}, 800);
|
||
}
|
||
function settlePartialStream() {
|
||
const text = String(state.streamText || "").trim();
|
||
if (!text) {
|
||
return;
|
||
}
|
||
const persona = $2("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral";
|
||
const pack = $2("sa_pack")?.value || defaultPackId();
|
||
const prose = typeof extractPatch2 === "function" ? extractPatch2(text).prose || text : text;
|
||
if (state.streamEl) {
|
||
finalizeStreamMessage(text, []);
|
||
} else {
|
||
appendMessage("assistant", prose);
|
||
}
|
||
state.history.push({ role: "assistant", content: prose, persona, pack });
|
||
persistHistory();
|
||
state.turnSettled = true;
|
||
}
|
||
function clearInFlightUi({ status } = {}) {
|
||
state.busy = false;
|
||
state.generating = false;
|
||
state.pendingSilentGen = false;
|
||
if (state.streamEl) {
|
||
try {
|
||
state.streamEl.remove();
|
||
} catch (e) {
|
||
}
|
||
state.streamEl = null;
|
||
state.streamMeta = null;
|
||
}
|
||
setInterruptVisible(false);
|
||
syncGenerateBusy();
|
||
syncPatchActionAvailability();
|
||
if (status != null) {
|
||
stopBusyUi(status);
|
||
} else {
|
||
stopBusyUi("");
|
||
}
|
||
}
|
||
function abortInFlightWork({ status, interruptSwarm = false } = {}) {
|
||
settlePartialStream();
|
||
bumpChatEpoch();
|
||
cancelWaitForNewImage();
|
||
if (interruptSwarm) {
|
||
try {
|
||
if (typeof doInterrupt === "function") {
|
||
doInterrupt(false);
|
||
} else if (typeof genericRequest === "function") {
|
||
genericRequest("InterruptAll", { other_sessions: false }, () => {
|
||
}, 0, () => {
|
||
});
|
||
}
|
||
} catch (e) {
|
||
}
|
||
}
|
||
clearInFlightUi({ status: status != null ? status : "" });
|
||
}
|
||
function doInterruptNow() {
|
||
settlePartialStream();
|
||
bumpChatEpoch();
|
||
cancelWaitForNewImage();
|
||
try {
|
||
if (typeof doInterrupt === "function") {
|
||
doInterrupt(false);
|
||
}
|
||
} catch (e) {
|
||
}
|
||
if (typeof genericRequest === "function") {
|
||
genericRequest("InterruptAll", { other_sessions: false }, () => {
|
||
}, 0, () => {
|
||
});
|
||
}
|
||
clearInFlightUi({ status: "\u041F\u0440\u0435\u0440\u0432\u0430\u043D\u043E" });
|
||
}
|
||
function waitForNewImage(prevSrc, timeoutMs = 18e4) {
|
||
cancelWaitForNewImage();
|
||
const epoch = state.chatEpoch;
|
||
const prev = String(prevSrc || "");
|
||
return new Promise((resolve) => {
|
||
const start = Date.now();
|
||
let sawRunning = false;
|
||
let idleTicks = 0;
|
||
let candidate = null;
|
||
state.waitImageTimer = setInterval(() => {
|
||
if (epoch !== state.chatEpoch) {
|
||
cancelWaitForNewImage();
|
||
resolve(null);
|
||
return;
|
||
}
|
||
const running = isSwarmGenerateRunning();
|
||
if (running) {
|
||
sawRunning = true;
|
||
idleTicks = 0;
|
||
} else if (sawRunning) {
|
||
idleTicks += 1;
|
||
}
|
||
const raw = findCurrentGenerateSrc();
|
||
const src = raw && !looksLikeModelPreview(raw) ? raw : null;
|
||
if (src && src !== prev) {
|
||
candidate = src;
|
||
}
|
||
if (sawRunning && !running && idleTicks >= 2) {
|
||
cancelWaitForNewImage();
|
||
resolve(candidate || src || null);
|
||
return;
|
||
}
|
||
if (candidate && !running && Date.now() - start > 500) {
|
||
cancelWaitForNewImage();
|
||
resolve(candidate);
|
||
return;
|
||
}
|
||
if (Date.now() - start > timeoutMs) {
|
||
cancelWaitForNewImage();
|
||
resolve(candidate || src || null);
|
||
}
|
||
}, 400);
|
||
});
|
||
}
|
||
const VARIANT_STRIP_KEYS = [
|
||
"variants",
|
||
"label",
|
||
"notes",
|
||
"actions",
|
||
"look_at",
|
||
"vision_from",
|
||
"vision_slots",
|
||
"search_query",
|
||
"civitai_query",
|
||
"memories",
|
||
"memory",
|
||
"memory_query",
|
||
"memory_kind",
|
||
"tag_query",
|
||
"user_prefs",
|
||
"controls",
|
||
"skills",
|
||
"persona_shelves",
|
||
"inventory_query",
|
||
"pack"
|
||
];
|
||
function normalizeVariantList(patch) {
|
||
if (!patch || !Array.isArray(patch.variants)) {
|
||
return null;
|
||
}
|
||
const items = patch.variants.filter((v) => v && typeof v === "object" && !Array.isArray(v));
|
||
if (items.length < 2) {
|
||
return null;
|
||
}
|
||
return items.slice(0, MAX_GEN_VARIANTS);
|
||
}
|
||
function stripMetaPatchKeys(obj) {
|
||
const out = { ...obj || {} };
|
||
for (const k of VARIANT_STRIP_KEYS) {
|
||
delete out[k];
|
||
}
|
||
return out;
|
||
}
|
||
function mergeVariantPatch(base, item, index) {
|
||
const merged = { ...stripMetaPatchKeys(base), ...stripMetaPatchKeys(item) };
|
||
merged.images = 1;
|
||
delete merged.batch;
|
||
if (merged.seed == null && merged.lock_seed !== true) {
|
||
merged.seed = -1;
|
||
merged.vary = true;
|
||
}
|
||
merged.actions = ["generate"];
|
||
const labelRaw = item?.label != null ? String(item.label).trim() : "";
|
||
return {
|
||
id: `var${index + 1}`,
|
||
label: (labelRaw || `\u0412\u0430\u0440\u0438\u0430\u043D\u0442 ${index + 1}`).slice(0, 48),
|
||
patch: merged
|
||
};
|
||
}
|
||
function finishedGenResultCount() {
|
||
return (state.genResults || []).filter((r) => r && r.src).length;
|
||
}
|
||
function isMultiGenResults() {
|
||
return finishedGenResultCount() > 1 || (state.genResults || []).length > 1;
|
||
}
|
||
function clearGenResults() {
|
||
state.genResults = [];
|
||
state.selectedGenResultId = null;
|
||
if (state.lightboxIndex >= 0) {
|
||
closeGenLightbox();
|
||
}
|
||
}
|
||
function selectGenResult(id, { restore = true, openViewer = false } = {}) {
|
||
const row = (state.genResults || []).find((r) => r.id === id);
|
||
if (!row) {
|
||
return false;
|
||
}
|
||
state.selectedGenResultId = row.id;
|
||
const gen = generateSlot();
|
||
if (gen && row.src) {
|
||
gen.src = row.src;
|
||
}
|
||
if (restore && row.patch) {
|
||
applyPatch(row.patch, "all").catch(() => {
|
||
});
|
||
syncLiveParamsBar();
|
||
}
|
||
renderBoard();
|
||
if (openViewer && row.src) {
|
||
openGenLightbox(row.id);
|
||
}
|
||
return true;
|
||
}
|
||
async function runGenerateFromPatch(patch, opts = {}) {
|
||
const deltaBeforeLive = patch && typeof patch === "object" ? { ...patch } : {};
|
||
const S = window.SA && window.SA.session;
|
||
const exactKeys = S && S.EXACT_GENERATE_PARAM_KEYS || ["steps", "cfg", "sigma_shift"];
|
||
if (typeof pullLiveIntoSession === "function") pullLiveIntoSession();
|
||
if (state.chatSession && state.chatSession.gen) {
|
||
for (const key of exactKeys) {
|
||
const fromDelta = deltaBeforeLive[key] != null;
|
||
const fromUser = !!state.lastUserParamIntent && state.sessionExact?.[key] != null;
|
||
if (!fromDelta && !fromUser) {
|
||
state.chatSession.gen[key] = null;
|
||
}
|
||
}
|
||
}
|
||
let working = {
|
||
...deltaBeforeLive,
|
||
generate: true,
|
||
actions: Array.isArray(deltaBeforeLive.actions) && deltaBeforeLive.actions.map(String).includes("generate") ? deltaBeforeLive.actions : [...Array.isArray(deltaBeforeLive.actions) ? deltaBeforeLive.actions : [], "generate"]
|
||
};
|
||
working = ensureExactParamsForGenerate(working);
|
||
if (state.chatSession && state.chatSession.gen && (opts.fromSession || opts.force)) {
|
||
const fromSess = { ...state.chatSession.gen, generate: true, actions: ["generate"] };
|
||
for (const k of Object.keys(working)) {
|
||
if (working[k] != null) fromSess[k] = working[k];
|
||
}
|
||
for (const key of exactKeys) {
|
||
const fromDelta = deltaBeforeLive[key] != null;
|
||
const fromUser = !!state.lastUserParamIntent && state.sessionExact?.[key] != null;
|
||
if (!fromDelta && !fromUser && working[key] != null) {
|
||
fromSess[key] = working[key];
|
||
}
|
||
}
|
||
patch = ensureExactParamsForGenerate(fromSess);
|
||
if (typeof pushSessionToSwarm === "function") await pushSessionToSwarm(state.chatSession);
|
||
} else {
|
||
patch = working;
|
||
}
|
||
const force = !!opts.force;
|
||
if (!force && false || !patchHasGenTrigger(patch)) {
|
||
return null;
|
||
}
|
||
const variantItems = normalizeVariantList(patch);
|
||
const jobs = variantItems ? variantItems.map((item, i) => mergeVariantPatch(patch, item, i)) : null;
|
||
if (jobs) {
|
||
state.genResults = jobs.map((j) => ({
|
||
id: j.id,
|
||
label: j.label,
|
||
src: null,
|
||
patch: j.patch
|
||
}));
|
||
state.selectedGenResultId = null;
|
||
setBoardTab("generate", { persist: true });
|
||
renderBoard();
|
||
} else {
|
||
clearGenResults();
|
||
}
|
||
const epoch = state.chatEpoch;
|
||
if (shouldParkLlmBeforeGen()) {
|
||
startBusyUi("parking");
|
||
setStatus("\u041E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0430\u044E VRAM\u2026");
|
||
await parkLlm();
|
||
if (epoch !== state.chatEpoch) {
|
||
return null;
|
||
}
|
||
}
|
||
state.generating = true;
|
||
setInterruptVisible(true);
|
||
startBusyUi("generating");
|
||
let lastSrc = null;
|
||
const steps = jobs || [{
|
||
id: "var1",
|
||
label: "Generate",
|
||
patch: { ...stripMetaPatchKeys(patch), actions: ["generate"] }
|
||
}];
|
||
for (let i = 0; i < steps.length; i++) {
|
||
if (epoch !== state.chatEpoch) {
|
||
break;
|
||
}
|
||
const job = steps[i];
|
||
if (jobs) {
|
||
setStatus(`\u0412\u0430\u0440\u0438\u0430\u043D\u0442 ${i + 1}/${steps.length}: ${job.label}`);
|
||
} else {
|
||
setStatus("\u0413\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044F\u2026");
|
||
}
|
||
startBusyUi("generating");
|
||
if (jobs) {
|
||
await applyPatch(job.patch, "all");
|
||
syncLiveParamsBar();
|
||
}
|
||
ensureNegativeForGenerate(job.patch);
|
||
forceExactParamsForGenerate({ patch: job.patch });
|
||
if (typeof pullLiveIntoSession === "function") {
|
||
pullLiveIntoSession();
|
||
}
|
||
if (epoch !== state.chatEpoch) {
|
||
break;
|
||
}
|
||
const prev = findCurrentGenerateSrc();
|
||
const ok = triggerGenerate();
|
||
if (!ok) {
|
||
setStatus(jobs ? `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0432\u0430\u0440\u0438\u0430\u043D\u0442 ${i + 1}/${steps.length}` : "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C Generate");
|
||
if (!jobs) {
|
||
break;
|
||
}
|
||
continue;
|
||
}
|
||
const src = await waitForNewImage(prev);
|
||
if (epoch !== state.chatEpoch) {
|
||
break;
|
||
}
|
||
if (src) {
|
||
lastSrc = src;
|
||
if (jobs) {
|
||
const row = state.genResults.find((r) => r.id === job.id);
|
||
if (row) {
|
||
row.src = src;
|
||
}
|
||
state.selectedGenResultId = job.id;
|
||
}
|
||
const gen = generateSlot();
|
||
if (gen) {
|
||
gen.src = src;
|
||
}
|
||
renderBoard();
|
||
}
|
||
}
|
||
state.generating = false;
|
||
setInterruptVisible(state.busy);
|
||
const parkedBeforeWarm = !!state.llmParked;
|
||
state.expectColdLoad = parkedBeforeWarm;
|
||
if (jobs && lastSrc && state.selectedGenResultId && epoch === state.chatEpoch) {
|
||
const row = state.genResults.find((r) => r.id === state.selectedGenResultId);
|
||
if (row?.patch) {
|
||
await applyPatch(row.patch, "all");
|
||
syncLiveParamsBar();
|
||
}
|
||
}
|
||
const paneVisible = !!document.getElementById("swarm_assistent_root")?.offsetParent;
|
||
const multiDone = !!(jobs && finishedGenResultCount() > 1);
|
||
const willAutoCritique = !multiDone && !!$2("sa_auto_critique")?.checked;
|
||
if (state.view === "chat" && paneVisible && !willAutoCritique && epoch === state.chatEpoch) {
|
||
if (parkedBeforeWarm || state.expectColdLoad) {
|
||
startBusyUi("warming");
|
||
setStatus("\u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E LLM \u0432 GPU\u2026");
|
||
}
|
||
const warmResult = await warmLlm({ force: true });
|
||
if (warmResult?.alreadyResident) {
|
||
state.expectColdLoad = false;
|
||
} else if (!warmResult?.ok) {
|
||
state.expectColdLoad = true;
|
||
}
|
||
}
|
||
if (epoch !== state.chatEpoch) {
|
||
return null;
|
||
}
|
||
if (jobs) {
|
||
const n = finishedGenResultCount();
|
||
const msg = n > 0 ? n > 1 ? `\u0413\u043E\u0442\u043E\u0432\u043E \xB7 ${n} \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u043E\u0432` : `\u0413\u043E\u0442\u043E\u0432\u043E \xB7 1 \u0432\u0430\u0440\u0438\u0430\u043D\u0442` : "Generate \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D (\u043D\u043E\u0432\u043E\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E)";
|
||
if (!state.busy) {
|
||
stopBusyUi(msg);
|
||
}
|
||
setStatus(msg);
|
||
return multiDone ? null : lastSrc;
|
||
}
|
||
if (!state.busy) {
|
||
stopBusyUi(lastSrc ? "Generate \u0433\u043E\u0442\u043E\u0432" : "Generate \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D (\u043D\u043E\u0432\u043E\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E)");
|
||
}
|
||
if (lastSrc) {
|
||
const gen = generateSlot();
|
||
if (gen) {
|
||
gen.src = lastSrc;
|
||
renderBoard();
|
||
}
|
||
setStatus("Generate \u0433\u043E\u0442\u043E\u0432");
|
||
return lastSrc;
|
||
}
|
||
if (state.busy) {
|
||
setStatus("Generate \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D (\u043D\u043E\u0432\u043E\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E)");
|
||
}
|
||
return null;
|
||
}
|
||
async function resolveFinishedGenerateSrc(hint, { settleMs = 2e4 } = {}) {
|
||
scrubPreviewFromGenerateSlot();
|
||
let src = hint && !looksLikeModelPreview(hint) ? hint : null;
|
||
if (!src) {
|
||
src = findCurrentGenerateSrc();
|
||
}
|
||
if (isGenerateUnavailable()) {
|
||
const settled = await waitForNewImage(src, settleMs);
|
||
if (settled) {
|
||
src = settled;
|
||
}
|
||
}
|
||
return src && !looksLikeModelPreview(src) ? src : null;
|
||
}
|
||
async function maybeAutoCritique(imageSrc) {
|
||
if (!$2("sa_auto_critique")?.checked || turnHopUsed("critique") || isMultiGenResults()) {
|
||
return;
|
||
}
|
||
const src = await resolveFinishedGenerateSrc(imageSrc);
|
||
if (!src) {
|
||
setStatus("\u0410\u0432\u0442\u043E-\u043A\u0440\u0438\u0442\u0438\u043A\u0430 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u0430 \u2014 \u043D\u0435\u0442 \u0433\u043E\u0442\u043E\u0432\u043E\u0433\u043E \u043A\u0430\u0434\u0440\u0430 Generate");
|
||
return;
|
||
}
|
||
if (!claimTurnHop("critique")) {
|
||
return;
|
||
}
|
||
setPackValue("critique_image", { flash: true });
|
||
if ($2("sa_input")) {
|
||
$2("sa_input").value = "Critique this result and improve the prompt for the next generation.";
|
||
}
|
||
const gen = generateSlot();
|
||
if (gen) {
|
||
gen.attach = true;
|
||
gen.src = src;
|
||
renderBoard();
|
||
}
|
||
setStatus("Auto-critique\u2026");
|
||
await sendChat({ fromAutoCritique: true, forceSlotIds: [GEN_ID] });
|
||
restoreDefaultPackAfterHop();
|
||
}
|
||
async function maybeAutoVisionLook(imageSrc) {
|
||
if (!wantsAutoVision() || $2("sa_auto_critique")?.checked || turnHopUsed("vision") || state.busy || isMultiGenResults()) {
|
||
return;
|
||
}
|
||
const src = await resolveFinishedGenerateSrc(imageSrc);
|
||
if (!src) {
|
||
return;
|
||
}
|
||
const gen = generateSlot();
|
||
if (gen) {
|
||
gen.src = src;
|
||
gen.attach = true;
|
||
renderBoard();
|
||
}
|
||
if (!claimTurnHop("vision")) {
|
||
return;
|
||
}
|
||
setPackValue("critique_image", { flash: true });
|
||
if ($2("sa_input")) {
|
||
$2("sa_input").value = "Look at the Generate result and briefly say what worked and what to fix next.";
|
||
}
|
||
setStatus("Auto look_at\u2026");
|
||
await sendChat({ fromVisionHop: true, forceSlotIds: [GEN_ID], skipAutoPack: true });
|
||
restoreDefaultPackAfterHop();
|
||
}
|
||
async function askLookAtResult() {
|
||
if (state.busy || state.generating) {
|
||
setStatus("\u0417\u0430\u043D\u044F\u0442\u043E \u2014 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043A\u043E\u043D\u0446\u0430 \u043E\u0442\u0432\u0435\u0442\u0430 \u0438\u043B\u0438 \u0421\u0442\u043E\u043F");
|
||
return;
|
||
}
|
||
if (!updateGate()) {
|
||
setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Krea 2");
|
||
return;
|
||
}
|
||
const preferred = (state.genResults || []).find((r) => r.id === state.selectedGenResultId && r.src)?.src || generateSlot()?.src;
|
||
const src = await resolveFinishedGenerateSrc(preferred, { settleMs: 8e3 });
|
||
if (!src) {
|
||
setStatus("\u041D\u0435\u0442 \u0433\u043E\u0442\u043E\u0432\u043E\u0433\u043E \u043A\u0430\u0434\u0440\u0430 Generate \u2014 \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439");
|
||
return;
|
||
}
|
||
const gen = generateSlot();
|
||
if (gen) {
|
||
gen.src = src;
|
||
gen.attach = true;
|
||
}
|
||
setBoardTab("generate");
|
||
renderBoard();
|
||
setView("chat");
|
||
const label = (state.genResults || []).find((r) => r.id === state.selectedGenResultId)?.label;
|
||
setPackValue("critique_image", { flash: true });
|
||
if ($2("sa_input")) {
|
||
$2("sa_input").value = label ? `\u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442 \xAB${label}\xBB: \u0447\u0442\u043E \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C, \u0447\u0442\u043E \u0441\u043B\u043E\u043C\u0430\u043B\u043E\u0441\u044C, \u0438 \u043A\u0430\u043A \u043F\u043E\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u043E\u043C\u043F\u0442 \u0438 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B \u0434\u043B\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043A\u0430\u0434\u0440\u0430.` : "\u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442: \u0447\u0442\u043E \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C, \u0447\u0442\u043E \u0441\u043B\u043E\u043C\u0430\u043B\u043E\u0441\u044C, \u0438 \u043A\u0430\u043A \u043F\u043E\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u043E\u043C\u043F\u0442 \u0438 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B \u0434\u043B\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043A\u0430\u0434\u0440\u0430.";
|
||
}
|
||
await sendChat({ forceSlotIds: [GEN_ID], skipAutoPack: true });
|
||
}
|
||
function currentPersonaInfo() {
|
||
const id = ($2("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral").trim() || "neutral";
|
||
const known = (state.personas || []).find((p) => p && p.id === id);
|
||
return {
|
||
id,
|
||
title: known && known.title || ({
|
||
neutral: "\u041D\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u044B\u0439",
|
||
aggressive: "\u0410\u0433\u0440\u0435\u0441\u0441\u0438\u0432\u043D\u044B\u0439",
|
||
dreamer: "\u041C\u0435\u0447\u0442\u0430\u0442\u0435\u043B\u044C"
|
||
}[id] || id)
|
||
};
|
||
}
|
||
function mountAssistantMeta(div, meta = {}) {
|
||
if (!div || div.querySelector(".sa-msg-meta")) {
|
||
return;
|
||
}
|
||
const persona = meta.persona || currentPersonaInfo();
|
||
const pack = meta.pack || $2("sa_pack")?.value || "";
|
||
div.dataset.persona = persona.id || "neutral";
|
||
if (pack) {
|
||
div.dataset.pack = pack;
|
||
}
|
||
const row = document.createElement("div");
|
||
row.className = "sa-msg-meta";
|
||
const chip = document.createElement("span");
|
||
chip.className = `sa-persona-mark sa-persona-${persona.id || "neutral"}`;
|
||
chip.textContent = persona.title || persona.id;
|
||
chip.title = `\u0425\u0430\u0440\u0430\u043A\u0442\u0435\u0440: ${persona.title || persona.id}${pack ? ` \xB7 \u0440\u0435\u0436\u0438\u043C ${pack}` : ""}`;
|
||
row.appendChild(chip);
|
||
if (pack && pack !== "ordinary" && pack !== "write_prompt") {
|
||
const packEl = document.createElement("span");
|
||
packEl.className = "sa-pack-mark";
|
||
packEl.textContent = pack.replace(/_/g, " ");
|
||
packEl.title = `\u0420\u0435\u0436\u0438\u043C: ${pack}`;
|
||
row.appendChild(packEl);
|
||
}
|
||
div.insertBefore(row, div.firstChild);
|
||
}
|
||
function appendMessage(role, text, patch, civitaiResults, meta) {
|
||
const box = $2("sa_messages");
|
||
if (!box) {
|
||
return null;
|
||
}
|
||
hideChatEmpty();
|
||
const div = document.createElement("div");
|
||
div.className = `sa-msg ${role}`;
|
||
if (role === "assistant") {
|
||
mountAssistantMeta(div, meta);
|
||
}
|
||
const { prose, patch: extracted } = role === "assistant" ? extractPatch2(text) : { prose: text, patch: null };
|
||
const finalPatch = patch || extracted;
|
||
if (role === "assistant") {
|
||
setAssistantBody(div, prose || text || "");
|
||
} else {
|
||
div.textContent = prose || text || "";
|
||
}
|
||
if (finalPatch && !(meta && meta.historical)) {
|
||
const silent = !!(meta && meta.silentPatch);
|
||
mountPatchBlock(div, finalPatch, { silent });
|
||
}
|
||
if (role === "assistant" && !(meta && meta.historical)) {
|
||
mountCurateButtons(div, meta);
|
||
}
|
||
box.appendChild(div);
|
||
scrollMessagesToBottom({ force: true });
|
||
return div;
|
||
}
|
||
function beginStreamMessage(meta) {
|
||
const box = $2("sa_messages");
|
||
if (!box) {
|
||
return null;
|
||
}
|
||
hideChatEmpty();
|
||
const div = document.createElement("div");
|
||
div.className = "sa-msg assistant sa-streaming sa-typing";
|
||
mountAssistantMeta(div, meta);
|
||
const body = document.createElement("div");
|
||
body.className = "sa-msg-body";
|
||
body.innerHTML = '<span class="sa-dots" aria-hidden="true"><i></i><i></i><i></i></span><span class="sa-typing-label">Waiting for the model\u2026</span>';
|
||
div.appendChild(body);
|
||
box.appendChild(div);
|
||
scrollMessagesToBottom({ force: true });
|
||
state.streamEl = div;
|
||
state.streamMeta = meta || null;
|
||
state.streamFenceDone = false;
|
||
return div;
|
||
}
|
||
function parseTerminalPatchObject(raw) {
|
||
try {
|
||
const obj = JSON.parse(String(raw || "").trim());
|
||
const terminal = window.SA && typeof SA.isTerminalStreamPatch === "function" ? SA.isTerminalStreamPatch(obj) : typeof isPatchObject === "function" && isPatchObject(obj);
|
||
return terminal ? obj : null;
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
function streamHasClosedPatchFence(text) {
|
||
const t = String(text || "");
|
||
if (/```[\s\S]*```/.test(t)) {
|
||
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||
let match;
|
||
while ((match = re.exec(t)) !== null) {
|
||
if (parseTerminalPatchObject(match[1])) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
const brace = t.lastIndexOf("{");
|
||
if (brace >= 0 && parseTerminalPatchObject(t.slice(brace))) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function trimToClosedPatchFence(text) {
|
||
const t = String(text || "");
|
||
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||
let match;
|
||
let lastEnd = -1;
|
||
while ((match = re.exec(t)) !== null) {
|
||
if (parseTerminalPatchObject(match[1])) {
|
||
lastEnd = match.index + match[0].length;
|
||
}
|
||
}
|
||
if (lastEnd > 0) {
|
||
return t.slice(0, lastEnd).trimEnd();
|
||
}
|
||
const brace = t.lastIndexOf("{");
|
||
if (brace >= 0 && parseTerminalPatchObject(t.slice(brace))) {
|
||
return t.trimEnd();
|
||
}
|
||
return t;
|
||
}
|
||
function appendStreamDelta(delta) {
|
||
if (state.streamFenceDone) {
|
||
return;
|
||
}
|
||
if (!state.streamEl) {
|
||
beginStreamMessage(state.streamMeta || void 0);
|
||
}
|
||
if (state.streamEl) {
|
||
if (state.streamEl.classList.contains("sa-typing")) {
|
||
state.streamEl.classList.remove("sa-typing");
|
||
state.streamText = "";
|
||
setAssistantBody(state.streamEl, "", { live: true });
|
||
}
|
||
state.gotDelta = true;
|
||
state.lastDeltaAt = Date.now();
|
||
state.expectColdLoad = false;
|
||
if (state.busyPhase !== "refining") {
|
||
setBusyPhase("streaming");
|
||
}
|
||
state.streamText = (state.streamText || "") + (delta || "");
|
||
if (streamHasClosedPatchFence(state.streamText)) {
|
||
state.streamText = trimToClosedPatchFence(state.streamText);
|
||
state.streamFenceDone = true;
|
||
setAssistantBody(state.streamEl, state.streamText, { live: true });
|
||
scrollMessagesToBottom();
|
||
const fn = state.onClosedTerminalFence;
|
||
state.onClosedTerminalFence = null;
|
||
if (typeof fn === "function") {
|
||
try {
|
||
fn(state.streamText);
|
||
} catch (e) {
|
||
console.warn("Assistent closed-fence finalize", e);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
setAssistantBody(state.streamEl, state.streamText, { live: true });
|
||
scrollMessagesToBottom();
|
||
}
|
||
}
|
||
function finalizeStreamMessage(fullReply, civitaiResults) {
|
||
const el = state.streamEl;
|
||
const meta = state.streamMeta;
|
||
state.streamEl = null;
|
||
state.streamMeta = null;
|
||
state.streamText = "";
|
||
state.streamFenceDone = false;
|
||
if (!el) {
|
||
if (state.turnSettled) {
|
||
return;
|
||
}
|
||
appendMessage("assistant", fullReply, null, civitaiResults, meta || void 0);
|
||
return;
|
||
}
|
||
el.classList.remove("sa-streaming", "sa-typing");
|
||
mountAssistantMeta(el, meta || void 0);
|
||
const { prose, patch } = extractPatch2(fullReply);
|
||
setAssistantBody(el, prose || fullReply || "");
|
||
el.querySelectorAll(".sa-patch, .sa-civitai-list").forEach((n) => n.remove());
|
||
if (patch) {
|
||
const silent = !!(meta && meta.silentPatch) || !!state.pendingSilentGen;
|
||
mountPatchBlock(el, patch, { silent });
|
||
}
|
||
if (!(meta && meta.historical)) {
|
||
mountCurateButtons(el, meta);
|
||
}
|
||
scrollMessagesToBottom();
|
||
}
|
||
function mountCurateButtons(msgEl, meta) {
|
||
if (!msgEl || msgEl.querySelector(".sa-msg-curate")) {
|
||
return;
|
||
}
|
||
const wrap = document.createElement("div");
|
||
wrap.className = "sa-msg-curate";
|
||
const ok = document.createElement("button");
|
||
ok.type = "button";
|
||
ok.className = "basic-button";
|
||
ok.title = "\u0412 \u0434\u0430\u0442\u0430\u0441\u0435\u0442 (\u043E\u0434\u043E\u0431\u0440\u0438\u0442\u044C)";
|
||
ok.textContent = "+ \u0434\u0430\u0442\u0430\u0441\u0435\u0442";
|
||
ok.addEventListener("click", () => curateAssistantMessage(msgEl, "approved"));
|
||
const bad = document.createElement("button");
|
||
bad.type = "button";
|
||
bad.className = "basic-button";
|
||
bad.title = "\u041E\u0442\u043A\u043B\u043E\u043D\u0438\u0442\u044C \u0434\u043B\u044F \u0434\u0430\u0442\u0430\u0441\u0435\u0442\u0430";
|
||
bad.textContent = "\u0431\u0440\u0430\u043A";
|
||
bad.addEventListener("click", () => curateAssistantMessage(msgEl, "rejected"));
|
||
wrap.appendChild(ok);
|
||
wrap.appendChild(bad);
|
||
msgEl.appendChild(wrap);
|
||
}
|
||
function curateAssistantMessage(msgEl, status) {
|
||
const hist = state.history || [];
|
||
let asstText = msgEl.querySelector(".sa-msg-body")?.textContent?.trim() || msgEl.textContent?.trim() || "";
|
||
let userText = "";
|
||
for (let i = hist.length - 1; i >= 0; i--) {
|
||
if (hist[i]?.role === "assistant" && (hist[i].content || "").trim() === asstText.trim()) {
|
||
for (let j = i - 1; j >= 0; j--) {
|
||
if (hist[j]?.role === "user") {
|
||
userText = hist[j].content || "";
|
||
break;
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
if (!userText) {
|
||
for (let i = hist.length - 1; i >= 0; i--) {
|
||
if (hist[i]?.role === "user") {
|
||
userText = hist[i].content || "";
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
const messages = [
|
||
{ role: "user", content: userText },
|
||
{ role: "assistant", content: asstText }
|
||
];
|
||
window.SA?.training?.curateFromChat?.(messages, {
|
||
chatId: state.activeChatId,
|
||
persona: $2("sa_persona")?.value || "neutral",
|
||
pack: $2("sa_pack")?.value || defaultPackId(),
|
||
status
|
||
})?.then?.((ok) => {
|
||
if (ok) {
|
||
setStatus(status === "approved" ? "\u041F\u0440\u0438\u043C\u0435\u0440 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D \u0432 \u0434\u0430\u0442\u0430\u0441\u0435\u0442" : "\u041F\u0440\u0438\u043C\u0435\u0440 \u043E\u0442\u043C\u0435\u0447\u0435\u043D \u043A\u0430\u043A \u0431\u0440\u0430\u043A");
|
||
}
|
||
});
|
||
}
|
||
function isTrainingLocked() {
|
||
return !!state.trainingLock || document.getElementById("swarm_assistent_root")?.classList.contains("sa-root-training-lock");
|
||
}
|
||
function wantsAutoVision() {
|
||
return !!$2("sa_auto_vision")?.checked;
|
||
}
|
||
function looksLikeModelPreview(src) {
|
||
const s = String(src || "").toLowerCase();
|
||
if (!s) {
|
||
return false;
|
||
}
|
||
return s.includes(".preview.") || s.includes("placeholder") || s.includes("/viewspecial/") || s.includes("viewspecial/") || s.includes("/view/models/") || /\/view\/models\//.test(s) || /[?&](?:path|file)=[^&]*\.preview\./i.test(s);
|
||
}
|
||
function findCurrentGenerateSrc({ allowPreview = false } = {}) {
|
||
let src = null;
|
||
try {
|
||
const cur = document.getElementById("current_image_img") || document.querySelector("#current_image img") || document.querySelector(".current-image img") || document.querySelector("#current_image_batch img");
|
||
if (cur) {
|
||
src = cur.dataset?.src || cur.getAttribute?.("data-src") || cur.src || null;
|
||
}
|
||
} catch (e) {
|
||
}
|
||
if (!src) {
|
||
try {
|
||
if (typeof currentMetadataMap !== "undefined" && currentMetadataMap && currentMetadataMap.image) {
|
||
src = currentMetadataMap.image;
|
||
}
|
||
} catch (e) {
|
||
}
|
||
}
|
||
if (!src) {
|
||
return null;
|
||
}
|
||
if (!allowPreview && looksLikeModelPreview(src)) {
|
||
return null;
|
||
}
|
||
return src;
|
||
}
|
||
function scrubPreviewFromGenerateSlot() {
|
||
const slot = generateSlot();
|
||
if (!slot?.src) {
|
||
return false;
|
||
}
|
||
if (!looksLikeModelPreview(slot.src)) {
|
||
return false;
|
||
}
|
||
slot.src = null;
|
||
slot.attach = false;
|
||
syncLastImageAlias();
|
||
return true;
|
||
}
|
||
function refreshImagePreview() {
|
||
scrubPreviewFromGenerateSlot();
|
||
if (wantsAutoVision()) {
|
||
const gen = generateSlot();
|
||
if (gen) {
|
||
const src = findCurrentGenerateSrc();
|
||
if (src) {
|
||
gen.attach = true;
|
||
gen.src = src;
|
||
} else {
|
||
gen.attach = false;
|
||
}
|
||
renderBoard();
|
||
}
|
||
}
|
||
syncGenerateSlot();
|
||
}
|
||
function fileToDataUrl(file) {
|
||
return new Promise((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
reader.onload = () => resolve(String(reader.result || ""));
|
||
reader.onerror = reject;
|
||
reader.readAsDataURL(file);
|
||
});
|
||
}
|
||
async function acceptImageFile(file, slotId) {
|
||
if (!file || !String(file.type || "").startsWith("image/")) {
|
||
setStatus("Not an image file");
|
||
return false;
|
||
}
|
||
const dataUrl = await fileToDataUrl(file);
|
||
if (slotId) {
|
||
return setSlotSrc(slotId, dataUrl, { note: `Loaded ${file.name || "image"}` });
|
||
}
|
||
return putImageOnBoard(dataUrl, { note: `Loaded ${file.name || "image"}` });
|
||
}
|
||
async function handleDropDataTransfer(dt, slotId) {
|
||
if (!dt) {
|
||
return false;
|
||
}
|
||
if (dt.files && dt.files.length) {
|
||
for (const file of dt.files) {
|
||
if (String(file.type || "").startsWith("image/")) {
|
||
return acceptImageFile(file, slotId);
|
||
}
|
||
}
|
||
}
|
||
const uri = (dt.getData("text/uri-list") || dt.getData("text/plain") || "").trim();
|
||
if (uri) {
|
||
const first = uri.split("\n").map((l) => l.trim()).find((l) => l && !l.startsWith("#"));
|
||
if (first) {
|
||
if (slotId) {
|
||
return setSlotSrc(slotId, first, { note: "Image from drag" });
|
||
}
|
||
return putImageOnBoard(first, { note: "Image from drag" });
|
||
}
|
||
}
|
||
const html = dt.getData("text/html") || "";
|
||
const m = html.match(/src=["']([^"']+)["']/i);
|
||
if (m && m[1]) {
|
||
if (slotId) {
|
||
return setSlotSrc(slotId, m[1], { note: "Image from drag" });
|
||
}
|
||
return putImageOnBoard(m[1], { note: "Image from drag" });
|
||
}
|
||
return false;
|
||
}
|
||
async function imageToBase64ForOllama(src, maxEdge = 1024) {
|
||
if (!src) {
|
||
return null;
|
||
}
|
||
const dataUrl = await srcToDataUrl(src);
|
||
if (!dataUrl) {
|
||
return null;
|
||
}
|
||
try {
|
||
const img = await new Promise((resolve, reject) => {
|
||
const el = new Image();
|
||
el.onload = () => resolve(el);
|
||
el.onerror = reject;
|
||
el.src = dataUrl;
|
||
});
|
||
const w = img.naturalWidth || img.width || 0;
|
||
const h = img.naturalHeight || img.height || 0;
|
||
const edge = Math.max(w, h);
|
||
const canvas = document.createElement("canvas");
|
||
if (!edge || edge <= maxEdge) {
|
||
canvas.width = Math.max(w, 1);
|
||
canvas.height = Math.max(h, 1);
|
||
canvas.getContext("2d").drawImage(img, 0, 0);
|
||
} else {
|
||
const scale = maxEdge / edge;
|
||
canvas.width = Math.max(1, Math.round(w * scale));
|
||
canvas.height = Math.max(1, Math.round(h * scale));
|
||
canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height);
|
||
}
|
||
const jpeg = canvas.toDataURL("image/jpeg", 0.85);
|
||
const i = jpeg.indexOf(",");
|
||
return i >= 0 ? jpeg.slice(i + 1) : null;
|
||
} catch (e) {
|
||
console.warn("Assistent: vision resize failed", e);
|
||
const i = dataUrl.indexOf(",");
|
||
return i >= 0 ? dataUrl.slice(i + 1) : null;
|
||
}
|
||
}
|
||
async function srcToDataUrl(src) {
|
||
if (src.startsWith("data:")) {
|
||
return src;
|
||
}
|
||
try {
|
||
const resp = await fetch(src);
|
||
const blob = await resp.blob();
|
||
return await new Promise((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
reader.onload = () => resolve(String(reader.result || ""));
|
||
reader.onerror = reject;
|
||
reader.readAsDataURL(blob);
|
||
});
|
||
} catch (e) {
|
||
console.warn("Assistent: vision fetch failed", e);
|
||
return null;
|
||
}
|
||
}
|
||
function loadSettings() {
|
||
const base = localStorage.getItem(LS_BASE);
|
||
const model = localStorage.getItem(LS_MODEL);
|
||
const pack = localStorage.getItem(LS_PACK);
|
||
let persona = localStorage.getItem(LS_PERSONA);
|
||
if (persona === "terse") {
|
||
persona = "aggressive";
|
||
localStorage.setItem(LS_PERSONA, persona);
|
||
}
|
||
let view = localStorage.getItem(LS_VIEW);
|
||
const auto = localStorage.getItem(LS_AUTO_VISION);
|
||
const autoApply = localStorage.getItem(LS_AUTO_APPLY);
|
||
const autoGen = localStorage.getItem(LS_AUTO_GENERATE);
|
||
const autoCrit = localStorage.getItem(LS_AUTO_CRITIQUE);
|
||
const autoDl = localStorage.getItem(LS_AUTO_DOWNLOAD);
|
||
const parkLlm2 = localStorage.getItem(LS_PARK_LLM);
|
||
const paneW = localStorage.getItem(LS_PANE_WIDTH);
|
||
if (base && $2("sa_base_url")) {
|
||
$2("sa_base_url").value = base;
|
||
}
|
||
if (pack && $2("sa_pack")) {
|
||
$2("sa_pack").value = pack;
|
||
}
|
||
if (persona && $2("sa_persona")) {
|
||
$2("sa_persona").value = persona;
|
||
}
|
||
if (auto != null && $2("sa_auto_vision")) {
|
||
$2("sa_auto_vision").checked = auto === "1";
|
||
}
|
||
if ($2("sa_auto_apply")) {
|
||
$2("sa_auto_apply").checked = autoApply == null ? true : autoApply === "1";
|
||
}
|
||
if ($2("sa_auto_generate")) {
|
||
$2("sa_auto_generate").checked = autoGen == null ? true : autoGen === "1";
|
||
}
|
||
if ($2("sa_auto_critique") && autoCrit != null) {
|
||
$2("sa_auto_critique").checked = autoCrit === "1";
|
||
}
|
||
if ($2("sa_auto_download") && autoDl != null) {
|
||
$2("sa_auto_download").checked = autoDl === "1";
|
||
}
|
||
if ($2("sa_park_llm")) {
|
||
$2("sa_park_llm").checked = parkLlm2 === "1";
|
||
}
|
||
if (model) {
|
||
state.preferredModel = model;
|
||
}
|
||
const embed = localStorage.getItem(LS_EMBED);
|
||
if (embed) {
|
||
state.preferredEmbed = embed;
|
||
}
|
||
if (paneW) {
|
||
document.documentElement.style.setProperty("--sa-image-width", paneW);
|
||
}
|
||
if (view === "cards") {
|
||
view = "chat";
|
||
}
|
||
if (view === "chat" || view === "settings" || view === "train") {
|
||
state.view = view;
|
||
}
|
||
const drawer = localStorage.getItem(LS_CHATS_DRAWER);
|
||
if (drawer != null) {
|
||
state.chatsDrawerOpen = drawer === "1";
|
||
state.chatsPanelOpen = state.chatsDrawerOpen;
|
||
}
|
||
const boardTab = localStorage.getItem(LS_BOARD_TAB);
|
||
if (boardTab === "refs" || boardTab === "generate") {
|
||
state.boardTab = boardTab;
|
||
}
|
||
}
|
||
function collectUiState() {
|
||
return {
|
||
pack: $2("sa_pack")?.value || defaultPackId(),
|
||
persona: $2("sa_persona")?.value || "neutral",
|
||
auto_vision: !!$2("sa_auto_vision")?.checked,
|
||
auto_apply: !!$2("sa_auto_apply")?.checked,
|
||
auto_generate: true,
|
||
auto_critique: !!$2("sa_auto_critique")?.checked,
|
||
auto_download: !!$2("sa_auto_download")?.checked,
|
||
park_llm: !!$2("sa_park_llm")?.checked,
|
||
pane_width: localStorage.getItem(LS_PANE_WIDTH) || "",
|
||
embed_model: $2("sa_embed_model")?.value || state.preferredEmbed || "",
|
||
base_url: $2("sa_base_url")?.value || "",
|
||
model: $2("sa_model")?.value || "",
|
||
view: state.view || "chat",
|
||
board_tab: state.boardTab || "generate",
|
||
chats_drawer: state.chatsDrawerOpen ? "1" : "0"
|
||
};
|
||
}
|
||
async function applyDiskUiState() {
|
||
const persist = diskPersist();
|
||
if (!persist) {
|
||
return;
|
||
}
|
||
let ui = null;
|
||
try {
|
||
ui = await persist.loadUiState();
|
||
} catch (e) {
|
||
return;
|
||
}
|
||
if (!ui || typeof ui !== "object") {
|
||
return;
|
||
}
|
||
const fill = (lsKey, value, apply) => {
|
||
if (value == null || value === "" || localStorage.getItem(lsKey) != null) {
|
||
return;
|
||
}
|
||
localStorage.setItem(lsKey, String(value));
|
||
apply?.(String(value));
|
||
};
|
||
fill(LS_BASE, ui.base_url, (v) => {
|
||
if ($2("sa_base_url")) {
|
||
$2("sa_base_url").value = v;
|
||
}
|
||
});
|
||
fill(LS_MODEL, ui.model, (v) => {
|
||
state.preferredModel = v;
|
||
});
|
||
fill(LS_EMBED, ui.embed_model, (v) => {
|
||
state.preferredEmbed = v;
|
||
});
|
||
fill(LS_PACK, ui.pack, (v) => {
|
||
if ($2("sa_pack")) {
|
||
$2("sa_pack").value = v;
|
||
}
|
||
});
|
||
fill(LS_PERSONA, ui.persona, (v) => {
|
||
if ($2("sa_persona")) {
|
||
$2("sa_persona").value = v;
|
||
}
|
||
});
|
||
fill(LS_PANE_WIDTH, ui.pane_width, (v) => document.documentElement.style.setProperty("--sa-image-width", v));
|
||
if (ui.view === "cards") {
|
||
ui.view = "chat";
|
||
}
|
||
if (ui.view === "chat" || ui.view === "settings" || ui.view === "train") {
|
||
fill(LS_VIEW, ui.view, (v) => {
|
||
state.view = v;
|
||
});
|
||
}
|
||
if (ui.board_tab === "refs" || ui.board_tab === "generate") {
|
||
fill(LS_BOARD_TAB, ui.board_tab, (v) => {
|
||
state.boardTab = v;
|
||
});
|
||
}
|
||
if (ui.chats_drawer != null && localStorage.getItem(LS_CHATS_DRAWER) == null) {
|
||
const open = ui.chats_drawer === true || ui.chats_drawer === "1" || ui.chats_drawer === 1;
|
||
localStorage.setItem(LS_CHATS_DRAWER, open ? "1" : "0");
|
||
state.chatsDrawerOpen = open;
|
||
state.chatsPanelOpen = open;
|
||
}
|
||
for (const [key, lsKey, id] of [
|
||
["auto_vision", LS_AUTO_VISION, "sa_auto_vision"],
|
||
["auto_apply", LS_AUTO_APPLY, "sa_auto_apply"],
|
||
["auto_generate", LS_AUTO_GENERATE, "sa_auto_generate"],
|
||
["auto_critique", LS_AUTO_CRITIQUE, "sa_auto_critique"],
|
||
["auto_download", LS_AUTO_DOWNLOAD, "sa_auto_download"],
|
||
["park_llm", LS_PARK_LLM, "sa_park_llm"]
|
||
]) {
|
||
if (ui[key] == null || localStorage.getItem(lsKey) != null) {
|
||
continue;
|
||
}
|
||
const on = ui[key] === true || ui[key] === "1" || ui[key] === 1;
|
||
if (on && key === "auto_download") {
|
||
continue;
|
||
}
|
||
localStorage.setItem(lsKey, on ? "1" : "0");
|
||
const el = $2(id);
|
||
if (el) {
|
||
el.checked = on;
|
||
}
|
||
}
|
||
}
|
||
function saveUiStateToDisk() {
|
||
diskPersist()?.saveUiState(collectUiState());
|
||
}
|
||
function saveSettings() {
|
||
localStorage.setItem(LS_BASE, $2("sa_base_url")?.value || "");
|
||
localStorage.setItem(LS_MODEL, $2("sa_model")?.value || "");
|
||
localStorage.setItem(LS_EMBED, $2("sa_embed_model")?.value || state.preferredEmbed || "");
|
||
localStorage.setItem(LS_PACK, $2("sa_pack")?.value || defaultPackId());
|
||
localStorage.setItem(LS_PERSONA, $2("sa_persona")?.value || "neutral");
|
||
localStorage.setItem(LS_VIEW, state.view || "chat");
|
||
localStorage.setItem(LS_AUTO_VISION, $2("sa_auto_vision")?.checked ? "1" : "0");
|
||
localStorage.setItem(LS_AUTO_APPLY, $2("sa_auto_apply")?.checked ? "1" : "0");
|
||
localStorage.setItem(LS_AUTO_GENERATE, $2("sa_auto_generate")?.checked ? "1" : "0");
|
||
localStorage.setItem(LS_AUTO_CRITIQUE, $2("sa_auto_critique")?.checked ? "1" : "0");
|
||
localStorage.setItem(LS_AUTO_DOWNLOAD, $2("sa_auto_download")?.checked ? "1" : "0");
|
||
localStorage.setItem(LS_PARK_LLM, $2("sa_park_llm")?.checked ? "1" : "0");
|
||
persistServerSettings();
|
||
saveUiStateToDisk();
|
||
}
|
||
function persistServerSettings() {
|
||
if (typeof genericRequest !== "function") {
|
||
return;
|
||
}
|
||
const skills = {};
|
||
document.querySelectorAll("#sa_skills_box input[data-skill]")?.forEach((el) => {
|
||
skills[el.getAttribute("data-skill")] = !!el.checked;
|
||
});
|
||
const persona = $2("sa_persona")?.value || "neutral";
|
||
const settings = {
|
||
embed_model: $2("sa_embed_model")?.value || state.preferredEmbed || "",
|
||
base_url: $2("sa_base_url")?.value || "",
|
||
[persona]: { skills }
|
||
};
|
||
genericRequest("AssistentSaveSettings", { settings }, () => {
|
||
}, 0, () => {
|
||
});
|
||
}
|
||
function applyConfigPayload(data, { applyDefaults = false } = {}) {
|
||
if (!data || data.error) {
|
||
return;
|
||
}
|
||
const prevPersona = state.config?.persona || $2("sa_persona")?.value || "";
|
||
const prevControls = state.config?.control_values && typeof state.config.control_values === "object" ? { ...state.config.control_values } : null;
|
||
state.config = data;
|
||
if (window.SA?.applyConfigPatchKeys) {
|
||
window.SA.applyConfigPatchKeys(data);
|
||
}
|
||
if (data.exact && typeof data.exact === "object") {
|
||
state.exact = data.exact;
|
||
}
|
||
const aspectSource = data.exact?.aspect_table || data.model?.aspect_table;
|
||
if (aspectSource && typeof aspectSource === "object") {
|
||
applyAspectTableFrom(aspectSource);
|
||
}
|
||
const profileSource = data.exact?.profiles || data.model?.profiles;
|
||
if (profileSource && typeof profileSource === "object") {
|
||
state.kreaProfiles = profileSource;
|
||
}
|
||
if (data.ui?.pack_aliases) {
|
||
PACK_ALIASES = { ...PACK_ALIASES, ...data.ui.pack_aliases };
|
||
}
|
||
if (data.ui?.welcome_html) {
|
||
WELCOME_HTML = data.ui.welcome_html;
|
||
}
|
||
if (data.ui?.help_text) {
|
||
HELP_TEXT = data.ui.help_text;
|
||
}
|
||
if (Array.isArray(data.ui?.slash) && data.ui.slash.length) {
|
||
SLASH_COMMANDS = data.ui.slash.map((s) => ({
|
||
cmd: s.cmd || "",
|
||
hint: s.hint || "",
|
||
action: s.action || ""
|
||
}));
|
||
}
|
||
if (Array.isArray(data.ui?.slash_extra) && data.ui.slash_extra.length) {
|
||
for (const s of data.ui.slash_extra) {
|
||
const cmd = s.cmd || "";
|
||
if (!cmd || SLASH_COMMANDS.some((c) => c.cmd === cmd)) {
|
||
continue;
|
||
}
|
||
SLASH_COMMANDS.push({
|
||
cmd,
|
||
hint: s.hint || "",
|
||
action: s.action || ""
|
||
});
|
||
}
|
||
}
|
||
if (data.ui?.help_extra) {
|
||
HELP_TEXT = `${HELP_TEXT || ""}
|
||
|
||
${data.ui.help_extra}`.trim();
|
||
}
|
||
state.enabledSkills = Array.isArray(data.enabled_skills) ? data.enabled_skills.slice() : [];
|
||
if (Array.isArray(data.personas)) {
|
||
state.personas = data.personas;
|
||
}
|
||
renderPersonaOptions(data.personas || [], data.persona || data.default_persona);
|
||
renderPackOptions(data.packs || [], applyDefaults ? data.assistant?.default_pack : null);
|
||
renderChips(data.ui?.chips || []);
|
||
renderSkillChecks(data.skills || [], state.enabledSkills);
|
||
if (applyDefaults && data.assistant?.default_pack && $2("sa_pack") && !localStorage.getItem(LS_PACK)) {
|
||
$2("sa_pack").value = data.assistant.default_pack;
|
||
}
|
||
if (data.assistant?.embed_model && !state.preferredEmbed) {
|
||
state.preferredEmbed = data.assistant.embed_model;
|
||
}
|
||
const asst = data.assistant || {};
|
||
if (asst.history_keep_turns != null) {
|
||
HISTORY_KEEP_TURNS = Math.max(1, Number(asst.history_keep_turns) || 4);
|
||
}
|
||
if (asst.compress_at != null) {
|
||
COMPRESS_AT = Math.min(0.95, Math.max(0.4, Number(asst.compress_at) || 0.7));
|
||
}
|
||
if (asst.chars_per_token != null) {
|
||
CHARS_PER_TOKEN = Math.max(1.5, Number(asst.chars_per_token) || 3.2);
|
||
}
|
||
if (asst.compress_auto != null) {
|
||
COMPRESS_AUTO = !!asst.compress_auto;
|
||
}
|
||
if (asst.max_ref_slots != null) {
|
||
MAX_REF_SLOTS = Math.max(1, Number(asst.max_ref_slots) || 4);
|
||
}
|
||
if (asst.max_gen_variants != null) {
|
||
MAX_GEN_VARIANTS = Math.max(2, Math.min(8, Number(asst.max_gen_variants) || 4));
|
||
}
|
||
if (asst.context_prompt_max != null) {
|
||
CONTEXT_PROMPT_MAX = Math.max(200, Number(asst.context_prompt_max) || 2e3);
|
||
}
|
||
if (asst.inventory_prompt_rich != null) {
|
||
INVENTORY_PROMPT_RICH = Math.max(4, Number(asst.inventory_prompt_rich) || 12);
|
||
}
|
||
if (asst.inventory_prompt_names != null) {
|
||
INVENTORY_PROMPT_NAMES = Math.max(INVENTORY_PROMPT_RICH, Number(asst.inventory_prompt_names) || 24);
|
||
}
|
||
fillKnobsFromConfig(data);
|
||
updateCtxChip();
|
||
if (applyDefaults || data.exact) {
|
||
lastExactForceCkpt = null;
|
||
fillEmptyParamsFromExact();
|
||
if (typeof isKreaSelected === "function" && isKreaSelected()) {
|
||
forceExactParamsForGenerate({});
|
||
lastExactForceCkpt = resolveCurrentCheckpoint()?.name || null;
|
||
}
|
||
}
|
||
const nextPersona = data.persona || $2("sa_persona")?.value || "";
|
||
let controlValues = data.control_values || data.exact?.controls || {};
|
||
if (!applyDefaults && prevControls && nextPersona === prevPersona) {
|
||
controlValues = { ...controlValues, ...prevControls };
|
||
state.config.control_values = controlValues;
|
||
if (state.exact) {
|
||
state.exact.controls = { ...state.exact.controls || {}, ...prevControls };
|
||
}
|
||
}
|
||
renderPersonaControls(data.controls || {}, controlValues);
|
||
syncPersonaDeleteButton(data.persona_source || data.personas?.find((p) => p.id === (data.persona || $2("sa_persona")?.value))?.source);
|
||
}
|
||
function isDeletablePersonaSource(source) {
|
||
const src = String(source || "");
|
||
return src === "overlay" || src === "overlay+bundled" || src === "overlay+pack";
|
||
}
|
||
function syncPersonaDeleteButton(source) {
|
||
const btn = $2("sa_persona_delete");
|
||
if (!btn) {
|
||
return;
|
||
}
|
||
const canDelete = isDeletablePersonaSource(source);
|
||
btn.hidden = !canDelete;
|
||
btn.disabled = !canDelete;
|
||
}
|
||
let controlSaveTimer = null;
|
||
let controlsPointerDown = false;
|
||
let pendingControlsRender = null;
|
||
function renderPersonaControls(schema, values) {
|
||
const box = $2("sa_persona_controls");
|
||
if (!box) {
|
||
return;
|
||
}
|
||
if (controlsPointerDown) {
|
||
pendingControlsRender = { schema, values };
|
||
return;
|
||
}
|
||
pendingControlsRender = null;
|
||
box.innerHTML = "";
|
||
const keys = schema && typeof schema === "object" ? Object.keys(schema) : [];
|
||
if (!keys.length) {
|
||
box.hidden = true;
|
||
return;
|
||
}
|
||
box.hidden = false;
|
||
const ordered = keys.slice().sort((a, b) => {
|
||
const oa = Number(schema[a]?.order ?? 100);
|
||
const ob = Number(schema[b]?.order ?? 100);
|
||
if (oa !== ob) {
|
||
return oa - ob;
|
||
}
|
||
return String(a).localeCompare(String(b));
|
||
});
|
||
for (const id of ordered) {
|
||
const def = schema[id];
|
||
if (!def || typeof def !== "object") {
|
||
continue;
|
||
}
|
||
if (String(def.type || "slider").toLowerCase() !== "slider") {
|
||
continue;
|
||
}
|
||
const min = Number(def.min ?? -1);
|
||
const max = Number(def.max ?? 1);
|
||
const step = Number(def.step ?? 0.05);
|
||
const defVal = Number(def.default ?? 0);
|
||
let cur = values && values[id] != null ? Number(values[id]) : defVal;
|
||
if (Number.isNaN(cur)) {
|
||
cur = defVal;
|
||
}
|
||
const asPercent = String(def.display || "").toLowerCase() === "percent";
|
||
const fmt = (v) => asPercent ? `${Math.round(v)}%` : Number(v).toFixed(2);
|
||
const row = document.createElement("div");
|
||
row.className = "sa-control-row";
|
||
row.title = def.hint || id;
|
||
const lab = document.createElement("label");
|
||
lab.textContent = def.label || id;
|
||
const input = document.createElement("input");
|
||
input.type = "range";
|
||
input.min = String(min);
|
||
input.max = String(max);
|
||
input.step = String(step);
|
||
input.value = String(cur);
|
||
input.dataset.controlId = id;
|
||
const valEl = document.createElement("span");
|
||
valEl.className = "sa-control-val";
|
||
valEl.textContent = fmt(cur);
|
||
const applyLocal = (v) => {
|
||
valEl.textContent = fmt(v);
|
||
if (state.config) {
|
||
state.config.control_values = { ...state.config.control_values || {}, [id]: v };
|
||
}
|
||
if (state.exact) {
|
||
state.exact.controls = { ...state.exact.controls || {}, [id]: v };
|
||
}
|
||
};
|
||
input.addEventListener("pointerdown", () => {
|
||
controlsPointerDown = true;
|
||
});
|
||
const endPointer = () => {
|
||
const v = Number(input.value);
|
||
applyLocal(v);
|
||
controlsPointerDown = false;
|
||
if (pendingControlsRender) {
|
||
const schema2 = pendingControlsRender.schema;
|
||
pendingControlsRender = null;
|
||
renderPersonaControls(
|
||
schema2,
|
||
state.config?.control_values || state.exact?.controls || {}
|
||
);
|
||
}
|
||
if (controlSaveTimer) {
|
||
clearTimeout(controlSaveTimer);
|
||
}
|
||
controlSaveTimer = setTimeout(() => savePersonaControls({ [id]: v }), 50);
|
||
};
|
||
input.addEventListener("pointerup", endPointer);
|
||
input.addEventListener("pointercancel", endPointer);
|
||
input.addEventListener("input", () => {
|
||
applyLocal(Number(input.value));
|
||
});
|
||
input.addEventListener("change", () => {
|
||
const v = Number(input.value);
|
||
applyLocal(v);
|
||
if (controlSaveTimer) {
|
||
clearTimeout(controlSaveTimer);
|
||
}
|
||
controlSaveTimer = setTimeout(() => savePersonaControls({ [id]: v }), 50);
|
||
});
|
||
row.appendChild(lab);
|
||
row.appendChild(input);
|
||
row.appendChild(valEl);
|
||
box.appendChild(row);
|
||
}
|
||
}
|
||
function getControlValue(id, fallback) {
|
||
const v = state.config?.control_values?.[id] ?? state.exact?.controls?.[id];
|
||
const n = Number(v);
|
||
return Number.isFinite(n) ? n : fallback;
|
||
}
|
||
function coolDownHorny() {
|
||
const schema = state.config?.controls || {};
|
||
if (!schema.horny) {
|
||
setStatus("/\u043E\u0441\u0442\u044B\u043D\u044C: \u0443 \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438 \u043D\u0435\u0442 \u0441\u043B\u0430\u0439\u0434\u0435\u0440\u0430 \u0425\u043E\u0440\u043D\u0438");
|
||
return;
|
||
}
|
||
state.lastUserControlIntent = true;
|
||
const min = Number(schema.horny.min ?? 0);
|
||
const max = Number(schema.horny.max ?? 100);
|
||
const cur = getControlValue("horny", Number(schema.horny.default ?? 35));
|
||
const next = Math.max(min, Math.min(max, cur - 30));
|
||
const values = {
|
||
...state.config?.control_values || state.exact?.controls || {},
|
||
horny: next
|
||
};
|
||
if (state.config) {
|
||
state.config.control_values = values;
|
||
}
|
||
if (state.exact) {
|
||
state.exact.controls = values;
|
||
}
|
||
renderPersonaControls(schema, values);
|
||
savePersonaControls({ horny: next });
|
||
appendSystemNote(`\u0425\u043E\u0440\u043D\u0438: ${Math.round(cur)}% \u2192 ${Math.round(next)}% (\u221230)`);
|
||
setStatus(`/\u043E\u0441\u0442\u044B\u043D\u044C \u2192 ${Math.round(next)}%`);
|
||
}
|
||
async function startHornyGame() {
|
||
const schema = state.config?.controls || {};
|
||
if (!schema.horny) {
|
||
setStatus("/horny-game: \u0443 \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438 \u043D\u0435\u0442 \u0441\u043B\u0430\u0439\u0434\u0435\u0440\u0430 \u0425\u043E\u0440\u043D\u0438");
|
||
return;
|
||
}
|
||
const cur = getControlValue("horny", Number(schema.horny.default ?? 35));
|
||
state.lastUserControlIntent = true;
|
||
await sendChat({
|
||
skipSlash: true,
|
||
skipAutoPack: true,
|
||
forcedUserText: `\u041A\u043E\u043C\u0430\u043D\u0434\u0430 /horny-game. \u0422\u0435\u043A\u0443\u0449\u0438\u0439 controls.horny = ${Math.round(cur)} (0\u2013100).
|
||
\u041E\u0446\u0435\u043D\u0438, \u043D\u0430\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0432\u043A\u0443\u0441\u044B \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F \u0432 \u044D\u0442\u043E\u043C \u0447\u0430\u0442\u0435 / \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u043C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0438 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u044E\u0442 \u0441 \u0442\u0432\u043E\u0438\u043C\u0438 (roleplay, outfits, realism, fetishes).
|
||
\u041F\u043E\u0441\u0442\u0430\u0432\u044C \u043D\u043E\u0432\u044B\u0439 controls.horny: \u0443\u043C\u043D\u043E\u0436\u044C/\u0441\u0434\u0432\u0438\u043D\u044C \u0442\u0435\u043A\u0443\u0449\u0435\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u043F\u043E\u0440\u0446\u0438\u043E\u043D\u0430\u043B\u044C\u043D\u043E \xAB\u043D\u0430\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0442\u0435\u0431\u0435 \u044D\u0442\u043E \u0437\u0430\u0448\u043B\u043E\xBB (\u0441\u043B\u0430\u0431\u043E\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0435 \u2192 \u0447\u0443\u0442\u044C \u0432\u043D\u0438\u0437 \u0438\u043B\u0438 \u043F\u043E\u0447\u0442\u0438 \u0431\u0435\u0437 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439; \u0441\u0438\u043B\u044C\u043D\u043E\u0435 \u2192 \u0437\u0430\u043C\u0435\u0442\u043D\u044B\u0439 \u0440\u043E\u0441\u0442, clamp 0\u2013100).
|
||
\u0412 \u043F\u0440\u043E\u0437\u0435 \u0441\u043A\u0430\u0436\u0438 \u043A\u0440\u0430\u0442\u043A\u043E: \u0441\u043E\u0432\u043F\u0430\u043B\u043E \u043B\u0438, \u043A\u0430\u043A\u043E\u0439 \u043C\u043D\u043E\u0436\u0438\u0442\u0435\u043B\u044C/\u0441\u0434\u0432\u0438\u0433 \u0438 \u043D\u043E\u0432\u044B\u0439 %. \u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D JSON patch \u0441 "controls": { "horny": <number> }. \u0411\u0435\u0437 generate, \u0435\u0441\u043B\u0438 \u043D\u0435 \u043F\u0440\u043E\u0441\u0438\u043B\u0438 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443.`
|
||
});
|
||
setStatus("/horny-game\u2026");
|
||
}
|
||
function savePersonaControls(partial) {
|
||
const persona = $2("sa_persona")?.value || "neutral";
|
||
if (typeof genericRequest !== "function") {
|
||
return;
|
||
}
|
||
if (partial && typeof partial === "object") {
|
||
if (state.config) {
|
||
state.config.control_values = { ...state.config.control_values || {}, ...partial };
|
||
}
|
||
if (state.exact) {
|
||
state.exact.controls = { ...state.exact.controls || {}, ...partial };
|
||
}
|
||
}
|
||
genericRequest(
|
||
"AssistentSaveControls",
|
||
{ persona, controls: partial || {} },
|
||
(data) => {
|
||
if (data?.error) {
|
||
setStatus(data.error);
|
||
return;
|
||
}
|
||
if (data?.control_values && state.config) {
|
||
state.config.control_values = data.control_values;
|
||
if (state.exact) {
|
||
state.exact.controls = data.control_values;
|
||
}
|
||
}
|
||
if (!controlsPointerDown && data?.control_values) {
|
||
syncPersonaControlInputs(data.control_values);
|
||
}
|
||
},
|
||
0,
|
||
() => setStatus("controls save failed")
|
||
);
|
||
}
|
||
function syncPersonaControlInputs(values) {
|
||
const box = $2("sa_persona_controls");
|
||
if (!box || !values || typeof values !== "object") {
|
||
return;
|
||
}
|
||
box.querySelectorAll("input[data-control-id]").forEach((input) => {
|
||
const id = input.dataset.controlId;
|
||
if (values[id] == null) {
|
||
return;
|
||
}
|
||
const v = Number(values[id]);
|
||
if (!Number.isFinite(v) || input.value === String(v)) {
|
||
return;
|
||
}
|
||
input.value = String(v);
|
||
const valEl = input.parentElement?.querySelector(".sa-control-val");
|
||
if (valEl) {
|
||
const schema = state.config?.controls?.[id];
|
||
const asPercent = String(schema?.display || "").toLowerCase() === "percent";
|
||
valEl.textContent = asPercent ? `${Math.round(v)}%` : Number(v).toFixed(2);
|
||
}
|
||
});
|
||
}
|
||
async function deleteCurrentOverlayPersona() {
|
||
const id = $2("sa_persona")?.value;
|
||
if (!id) {
|
||
return;
|
||
}
|
||
const meta = (state.personas || []).find((p) => p.id === id);
|
||
const title = meta?.title || id;
|
||
const src = meta?.source || state.config?.persona_source || "";
|
||
if (src !== "overlay" && src !== "overlay+bundled" && src !== "overlay+pack") {
|
||
setStatus("\u041C\u043E\u0436\u043D\u043E \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E overlay-\u0441\u043B\u043E\u0439 (bundled/pack \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E\u0442\u0441\u044F)");
|
||
return;
|
||
}
|
||
if (!window.confirm(`\u0423\u0434\u0430\u043B\u0438\u0442\u044C \xAB${title}\xBB?
|
||
\u041F\u043E\u0441\u0442\u0430\u0432\u043A\u0430 (bundled/pack) \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u0435\u0442\u0441\u044F.`)) {
|
||
return;
|
||
}
|
||
await new Promise((resolve) => {
|
||
genericRequest(
|
||
"AssistentDeletePersona",
|
||
{ persona: id },
|
||
async (data) => {
|
||
if (data?.error) {
|
||
setStatus(data.error);
|
||
resolve();
|
||
return;
|
||
}
|
||
const next = data?.default_persona || "neutral";
|
||
if (Array.isArray(data?.personas)) {
|
||
state.personas = data.personas;
|
||
}
|
||
renderPersonaOptions(state.personas || [], next);
|
||
if ($2("sa_persona")) {
|
||
$2("sa_persona").value = next;
|
||
}
|
||
await applyPersonaForChat(next, { quiet: false });
|
||
setStatus(`\u0423\u0434\u0430\u043B\u0435\u043D\u043E: ${id}`);
|
||
resolve();
|
||
},
|
||
0,
|
||
() => {
|
||
setStatus("delete failed");
|
||
resolve();
|
||
}
|
||
);
|
||
});
|
||
}
|
||
function renderPersonaOptions(personas, selected) {
|
||
const sel = $2("sa_persona");
|
||
if (!sel) {
|
||
return;
|
||
}
|
||
const cur = selected || sel.value || localStorage.getItem(LS_PERSONA) || "neutral";
|
||
sel.innerHTML = "";
|
||
for (const p of personas) {
|
||
const opt = document.createElement("option");
|
||
opt.value = p.id;
|
||
opt.textContent = p.title || p.id;
|
||
if (p.accent) {
|
||
opt.dataset.accent = p.accent;
|
||
}
|
||
sel.appendChild(opt);
|
||
}
|
||
if ([...sel.options].some((o) => o.value === cur)) {
|
||
sel.value = cur;
|
||
}
|
||
const meta = (personas || []).find((p) => p.id === sel.value);
|
||
syncPersonaDeleteButton(meta?.source || state.config?.persona_source);
|
||
}
|
||
function renderPackOptions(packs, preferred) {
|
||
const sel = $2("sa_pack");
|
||
if (!sel) {
|
||
return;
|
||
}
|
||
const cur = preferred || sel.value || localStorage.getItem(LS_PACK) || defaultPackId();
|
||
sel.innerHTML = "";
|
||
const list = (packs || []).slice().sort((a, b) => (a.order || 100) - (b.order || 100));
|
||
for (const p of list) {
|
||
const opt = document.createElement("option");
|
||
opt.value = p.id;
|
||
opt.textContent = p.title || p.id;
|
||
sel.appendChild(opt);
|
||
}
|
||
if ([...sel.options].some((o) => o.value === cur)) {
|
||
sel.value = cur;
|
||
}
|
||
}
|
||
function renderChips(chips) {
|
||
const box = $2("sa_chips");
|
||
if (!box || !Array.isArray(chips) || !chips.length) {
|
||
return;
|
||
}
|
||
box.innerHTML = "";
|
||
for (const c of chips) {
|
||
if (c.sep) {
|
||
const sep = document.createElement("span");
|
||
sep.className = "sa-chip-sep";
|
||
sep.setAttribute("aria-hidden", "true");
|
||
box.appendChild(sep);
|
||
continue;
|
||
}
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
btn.className = "sa-chip";
|
||
btn.textContent = c.label || c.value || "";
|
||
if (c.title) {
|
||
btn.title = c.title;
|
||
}
|
||
const action = c.action || "";
|
||
const value = c.value ?? "";
|
||
if (action === "aspect") {
|
||
btn.setAttribute("data-aspect", value);
|
||
} else if (action === "seed") {
|
||
btn.setAttribute("data-seed", value);
|
||
} else if (action === "vary") {
|
||
btn.setAttribute("data-vary", value || "1");
|
||
} else if (action === "krea_profile") {
|
||
btn.setAttribute("data-krea-profile", value);
|
||
}
|
||
box.appendChild(btn);
|
||
}
|
||
}
|
||
function renderSkillChecks(skills, enabled) {
|
||
const box = $2("sa_skills_box");
|
||
if (!box) {
|
||
return;
|
||
}
|
||
const on = new Set(enabled || []);
|
||
box.innerHTML = "";
|
||
for (const s of skills || []) {
|
||
const label = document.createElement("label");
|
||
label.className = "sa-check";
|
||
const input = document.createElement("input");
|
||
input.type = "checkbox";
|
||
input.setAttribute("data-skill", s.id);
|
||
input.checked = on.has(s.id) || !enabled?.length && !!s.default;
|
||
input.addEventListener("change", () => {
|
||
state.enabledSkills = [...document.querySelectorAll("#sa_skills_box input[data-skill]:checked")].map((el) => el.getAttribute("data-skill"));
|
||
saveSettings();
|
||
});
|
||
label.appendChild(input);
|
||
label.appendChild(document.createTextNode(` ${s.title || s.id}`));
|
||
box.appendChild(label);
|
||
}
|
||
state.enabledSkills = [...document.querySelectorAll("#sa_skills_box input[data-skill]:checked")].map((el) => el.getAttribute("data-skill"));
|
||
}
|
||
function loadConfig(persona, done) {
|
||
if (typeof genericRequest !== "function") {
|
||
done?.(null);
|
||
return;
|
||
}
|
||
genericRequest(
|
||
"AssistentGetConfig",
|
||
{ persona: persona || $2("sa_persona")?.value || "neutral" },
|
||
(data) => {
|
||
applyConfigPayload(data, { applyDefaults: true });
|
||
done?.(data);
|
||
},
|
||
0,
|
||
() => done?.(null)
|
||
);
|
||
}
|
||
function chatModelSeniority(name) {
|
||
const n = String(name || "").toLowerCase();
|
||
let score = 0;
|
||
const m = n.match(/(?:^|[:\-/])(\d+)\s*b\b/);
|
||
if (m) {
|
||
score += Number(m[1]) * 1e6;
|
||
}
|
||
if (n.includes("instruct")) {
|
||
score += 5e4;
|
||
}
|
||
if (n.includes("qwen3")) {
|
||
score += 2e4;
|
||
}
|
||
if (n.includes("thinking") || n.endsWith(":latest")) {
|
||
score -= 1e4;
|
||
}
|
||
return score;
|
||
}
|
||
function pickSeniorChatModel(names) {
|
||
const list = (names || []).map((n) => String(n || "").trim()).filter(Boolean);
|
||
if (!list.length) {
|
||
return "";
|
||
}
|
||
return [...list].sort((a, b) => chatModelSeniority(b) - chatModelSeniority(a) || a.localeCompare(b))[0];
|
||
}
|
||
function resolveChatModel(names, apiPreferred) {
|
||
const list = (names || []).map((n) => String(n || "").trim()).filter(Boolean);
|
||
if (!list.length) {
|
||
return "";
|
||
}
|
||
const preferred = apiPreferred && list.includes(apiPreferred) ? apiPreferred : pickSeniorChatModel(list);
|
||
const ls = state.preferredModel || localStorage.getItem(LS_MODEL) || "";
|
||
if (ls && list.includes(ls)) {
|
||
return ls;
|
||
}
|
||
return preferred || list[0];
|
||
}
|
||
function setModelOptions(models, { error, preferred } = {}) {
|
||
const sel = $2("sa_model");
|
||
const sel2 = $2("sa_settings_chat_model");
|
||
const apply = (target) => {
|
||
if (!target) {
|
||
return;
|
||
}
|
||
let names = (models || []).map((n) => String(n || "").trim()).filter(Boolean);
|
||
names = [...names].sort((a, b) => chatModelSeniority(b) - chatModelSeniority(a) || a.localeCompare(b));
|
||
target.innerHTML = "";
|
||
if (error) {
|
||
const opt = document.createElement("option");
|
||
opt.value = "";
|
||
opt.textContent = `\u26A0 ${String(error).replace(/\s+/g, " ").slice(0, 90)}`;
|
||
target.appendChild(opt);
|
||
target.disabled = true;
|
||
return;
|
||
}
|
||
target.disabled = false;
|
||
if (!names.length) {
|
||
const opt = document.createElement("option");
|
||
opt.value = "";
|
||
opt.textContent = "No Ollama models \u2014 pull / Refresh";
|
||
target.appendChild(opt);
|
||
return;
|
||
}
|
||
for (const name of names) {
|
||
const opt = document.createElement("option");
|
||
opt.value = name;
|
||
opt.textContent = name;
|
||
target.appendChild(opt);
|
||
}
|
||
const pick = resolveChatModel(names, preferred);
|
||
if (pick) {
|
||
target.value = pick;
|
||
}
|
||
};
|
||
apply(sel);
|
||
apply(sel2);
|
||
}
|
||
function setEmbedModelOptions(models) {
|
||
const sel = $2("sa_embed_model");
|
||
if (!sel) {
|
||
return;
|
||
}
|
||
const names = (models || []).map((n) => String(n || "").trim()).filter(Boolean);
|
||
sel.innerHTML = "";
|
||
if (!names.length) {
|
||
const opt = document.createElement("option");
|
||
opt.value = state.preferredEmbed || "nomic-embed-text";
|
||
opt.textContent = opt.value + " (\u043E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F pull)";
|
||
sel.appendChild(opt);
|
||
return;
|
||
}
|
||
for (const name of names) {
|
||
const opt = document.createElement("option");
|
||
opt.value = name;
|
||
opt.textContent = name;
|
||
sel.appendChild(opt);
|
||
}
|
||
const prefer = state.preferredEmbed || localStorage.getItem(LS_EMBED) || state.config?.assistant?.embed_model;
|
||
if (prefer && names.includes(prefer)) {
|
||
sel.value = prefer;
|
||
} else if (prefer && !names.includes(prefer)) {
|
||
const opt = document.createElement("option");
|
||
opt.value = prefer;
|
||
opt.textContent = prefer;
|
||
sel.appendChild(opt);
|
||
sel.value = prefer;
|
||
}
|
||
}
|
||
function refreshModels() {
|
||
const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434";
|
||
setStatus("Loading models\u2026");
|
||
if (typeof genericRequest !== "function") {
|
||
setStatus("SwarmUI API not ready");
|
||
setModelOptions([], { error: "SwarmUI API not ready" });
|
||
return;
|
||
}
|
||
genericRequest(
|
||
"AssistentListModels",
|
||
{ baseUrl },
|
||
(data) => {
|
||
const models = data.models || [];
|
||
const memoryModels = data.memory_models || [];
|
||
const preferred = (data.preferred || "").trim();
|
||
setModelOptions(models, { preferred });
|
||
setEmbedModelOptions(memoryModels);
|
||
const pick = resolveChatModel(models, preferred);
|
||
if (pick && $2("sa_model")) {
|
||
$2("sa_model").value = pick;
|
||
if ($2("sa_settings_chat_model")) {
|
||
$2("sa_settings_chat_model").value = pick;
|
||
}
|
||
state.preferredModel = pick;
|
||
localStorage.setItem(LS_MODEL, pick);
|
||
}
|
||
setStatus(models.length ? `${models.length} chat \xB7 ${memoryModels.length} memory` : "No Ollama models (gpu-rent: ollama pull)");
|
||
if (models.length) {
|
||
setOllamaHealth("ok", `Ollama \xB7 ${models.length}`, `\u0427\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u0435\u0439: ${models.length}, \u043F\u0430\u043C\u044F\u0442\u044C: ${memoryModels.length}`);
|
||
} else {
|
||
setOllamaHealth("warn", "Ollama \xB7 0 \u043C\u043E\u0434\u0435\u043B\u0435\u0439", "\u041D\u0435\u0442 \u0447\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u2014 \u0441\u0434\u0435\u043B\u0430\u0439 ollama pull");
|
||
}
|
||
saveSettings();
|
||
},
|
||
0,
|
||
(err) => {
|
||
const msg = String(err || "Ollama unreachable");
|
||
setStatus(msg);
|
||
setModelOptions([], { error: msg });
|
||
setOllamaHealth("down", "Ollama \u2715", msg);
|
||
appendMessage("error", msg);
|
||
}
|
||
);
|
||
}
|
||
function refreshInventory(done, opts = {}) {
|
||
if (typeof genericRequest !== "function") {
|
||
if (done) {
|
||
done();
|
||
}
|
||
return;
|
||
}
|
||
const rescan = !!opts.rescan;
|
||
genericRequest(
|
||
"AssistentListInventory",
|
||
{ rescan },
|
||
(data) => {
|
||
state.inventory = {
|
||
loras: data.loras || [],
|
||
checkpoints: data.checkpoints || [],
|
||
wildcards: data.wildcards || [],
|
||
has_civitai_key: !!data.has_civitai_key,
|
||
inventory_at: data.inventory_at || Math.floor(Date.now() / 1e3),
|
||
rescanned: !!data.rescanned
|
||
};
|
||
state.inventoryFetchedAt = Date.now();
|
||
const n = state.inventory.loras.length;
|
||
const ck = state.inventory.checkpoints.length;
|
||
setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts${rescan ? " (rescanned)" : ""}`);
|
||
if (done) {
|
||
done(state.inventory);
|
||
}
|
||
},
|
||
0,
|
||
(err) => {
|
||
console.warn("Assistent inventory", err);
|
||
if (done) {
|
||
done(null);
|
||
}
|
||
}
|
||
);
|
||
}
|
||
function refreshInventoryAsync(opts = {}) {
|
||
return new Promise((resolve) => refreshInventory(resolve, opts));
|
||
}
|
||
function memoryKindFilter() {
|
||
return $2("sa_mem_kind")?.value || "all";
|
||
}
|
||
function memoryScopeFilter() {
|
||
return $2("sa_mem_scope")?.value || "all";
|
||
}
|
||
function memorySearchFilter() {
|
||
return ($2("sa_mem_search")?.value || "").trim().toLowerCase();
|
||
}
|
||
function renderMemoryKinds(kinds) {
|
||
const sel = $2("sa_mem_kind");
|
||
if (!sel) {
|
||
return;
|
||
}
|
||
const cur = sel.value || "all";
|
||
sel.innerHTML = "";
|
||
const all = document.createElement("option");
|
||
all.value = "all";
|
||
all.textContent = "\u0412\u0441\u0435 \u0442\u0438\u043F\u044B";
|
||
sel.appendChild(all);
|
||
for (const kind of kinds || []) {
|
||
const opt = document.createElement("option");
|
||
opt.value = kind;
|
||
opt.textContent = kind;
|
||
sel.appendChild(opt);
|
||
}
|
||
if ([...sel.options].some((o) => o.value === cur)) {
|
||
sel.value = cur;
|
||
}
|
||
}
|
||
function filteredMemoryRows() {
|
||
const filter = memoryKindFilter();
|
||
const scope = memoryScopeFilter();
|
||
const q = memorySearchFilter();
|
||
const persona = $2("sa_persona")?.value || "neutral";
|
||
return (state.memoryRows || []).filter((m) => {
|
||
if (filter !== "all" && m.kind !== filter) {
|
||
return false;
|
||
}
|
||
if (scope === "shared" && m.scope !== "shared") {
|
||
return false;
|
||
}
|
||
if (scope === "personal" && !(m.scope === "personal" && (m.persona === persona || !m.persona))) {
|
||
return false;
|
||
}
|
||
if (q) {
|
||
const hay = `${m.kind || ""} ${m.key || ""} ${m.text || ""}`.toLowerCase();
|
||
if (!hay.includes(q)) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
});
|
||
}
|
||
function renderMemoryList() {
|
||
const root = $2("sa_mem_list");
|
||
if (!root) {
|
||
return;
|
||
}
|
||
const rows = filteredMemoryRows();
|
||
root.innerHTML = "";
|
||
if (!rows.length) {
|
||
root.innerHTML = '<div class="sa-mem-empty">\u041A\u0440\u0430\u0444\u0442-\u043F\u0430\u043C\u044F\u0442\u044C \u043F\u0443\u0441\u0442\u0430 \u2014 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0438, seed \u0438 \u043F\u0430\u0442\u0447\u0438 <code>memory_upsert</code>.</div>';
|
||
return;
|
||
}
|
||
for (const row of rows) {
|
||
const el = document.createElement("div");
|
||
el.className = "sa-mem-row";
|
||
const bundled = row.source === "bundled";
|
||
const when = row.updated ? formatChatWhen(row.updated * 1e3) : "";
|
||
const scope = row.scope === "personal" ? `\u043F\u0435\u0440\u0441\u043E\u043D\u0430 ${row.persona || "\u2014"}` : "\u043E\u0431\u0449\u0430\u044F";
|
||
el.innerHTML = `<div class="sa-mem-row-body"><div class="sa-mem-row-head"><span class="sa-mem-kind-badge">${escapeHtml2(row.kind || "note")}</span><span class="sa-mem-row-key">${escapeHtml2(row.key || "")}</span></div><div class="sa-mem-row-text">${escapeHtml2(clipDebug(row.text, 220))}</div><div class="sa-mem-row-meta">${escapeHtml2([scope, row.source || "user", when].filter(Boolean).join(" \xB7 "))}</div></div>`;
|
||
const forget = document.createElement("button");
|
||
forget.type = "button";
|
||
forget.className = "basic-button sa-mem-forget";
|
||
forget.textContent = "\xD7";
|
||
if (bundled) {
|
||
forget.disabled = true;
|
||
forget.title = "Bundled \u2014 \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u043F\u0440\u0438 reseed, \u043F\u0440\u0430\u0432\u044C Config/_base/memory-seed/";
|
||
} else {
|
||
forget.title = "\u0417\u0430\u0431\u044B\u0442\u044C";
|
||
forget.addEventListener("click", () => forgetMemory(row));
|
||
}
|
||
el.appendChild(forget);
|
||
root.appendChild(el);
|
||
}
|
||
}
|
||
function refreshMemoryList() {
|
||
if (typeof genericRequest !== "function") {
|
||
return;
|
||
}
|
||
const list = $2("sa_mem_list");
|
||
if (list && !state.memoryRows.length) {
|
||
list.innerHTML = '<div class="sa-mem-empty">\u0427\u0438\u0442\u0430\u044E \u043F\u0430\u043C\u044F\u0442\u044C\u2026</div>';
|
||
}
|
||
genericRequest(
|
||
"AssistentListMemory",
|
||
{ limit: 200 },
|
||
(data) => {
|
||
state.memoryRows = Array.isArray(data?.memories) ? data.memories : [];
|
||
renderMemoryKinds(data?.kinds || []);
|
||
renderMemoryList();
|
||
const foot = $2("sa_mem_total");
|
||
if (foot) {
|
||
foot.textContent = `\u0412\u0441\u0435\u0433\u043E: ${data?.total ?? state.memoryRows.length} \xB7 ${data?.embed_model || "\u2014"}`;
|
||
}
|
||
},
|
||
0,
|
||
(err) => {
|
||
if (list) {
|
||
list.innerHTML = `<div class="sa-mem-empty">\u041F\u0430\u043C\u044F\u0442\u044C \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430: ${escapeHtml2(String(err || "\u043E\u0448\u0438\u0431\u043A\u0430"))}</div>`;
|
||
}
|
||
}
|
||
);
|
||
}
|
||
function forgetMemory(row) {
|
||
if (!row?.kind || !row?.key || typeof genericRequest !== "function") {
|
||
return;
|
||
}
|
||
genericRequest(
|
||
"AssistentForgetMemory",
|
||
{
|
||
kind: row.kind,
|
||
key: row.key,
|
||
source: row.source || "",
|
||
scope: row.scope || "",
|
||
persona: row.scope === "personal" ? row.persona || "" : ""
|
||
},
|
||
() => {
|
||
state.memoryRows = (state.memoryRows || []).filter((m) => !(m.kind === row.kind && m.key === row.key && m.source === row.source && m.persona === row.persona));
|
||
renderMemoryList();
|
||
setStatus(`\u0417\u0430\u0431\u044B\u0442\u043E: ${row.kind}/${row.key}`);
|
||
},
|
||
0,
|
||
(err) => setStatus(String(err || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0431\u044B\u0442\u044C"))
|
||
);
|
||
}
|
||
function clearCraftMemory(opts = {}) {
|
||
if (typeof genericRequest !== "function") {
|
||
return;
|
||
}
|
||
const label = opts.label || "\u043A\u0440\u0430\u0444\u0442-\u043F\u0430\u043C\u044F\u0442\u044C";
|
||
if (!window.confirm(`\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C ${label}? Bundled seed \u043E\u0441\u0442\u0430\u043D\u0435\u0442\u0441\u044F.`)) {
|
||
return;
|
||
}
|
||
const body = {
|
||
scope: opts.scope || "",
|
||
kind: opts.kind || "",
|
||
persona: opts.persona || ""
|
||
};
|
||
genericRequest(
|
||
"AssistentClearMemory",
|
||
body,
|
||
(data) => {
|
||
setStatus(`\u0423\u0434\u0430\u043B\u0435\u043D\u043E \u043A\u0440\u0430\u0444\u0442-\u0437\u0430\u043F\u0438\u0441\u0435\u0439: ${data?.deleted ?? 0}`);
|
||
refreshMemoryList();
|
||
},
|
||
0,
|
||
(err) => setStatus(String(err || "\u041E\u0447\u0438\u0441\u0442\u043A\u0430 \u043D\u0435 \u0443\u0434\u0430\u043B\u0430\u0441\u044C"))
|
||
);
|
||
}
|
||
function setSettingsTab(id) {
|
||
state.settingsTab = id || "behavior";
|
||
document.querySelectorAll("#sa_settings .sa-stab").forEach((btn) => {
|
||
const on = btn.getAttribute("data-stab") === state.settingsTab;
|
||
btn.classList.toggle("sa-stab-active", on);
|
||
btn.setAttribute("aria-selected", on ? "true" : "false");
|
||
});
|
||
document.querySelectorAll("#sa_settings .sa-spane").forEach((pane) => {
|
||
pane.hidden = pane.getAttribute("data-spane") !== state.settingsTab;
|
||
});
|
||
if (state.settingsTab === "craft") {
|
||
refreshMemoryList();
|
||
}
|
||
if (state.settingsTab === "user") {
|
||
refreshUserPrefs();
|
||
}
|
||
if (state.settingsTab === "personas") {
|
||
renderPersonaSettingsList();
|
||
}
|
||
if (state.settingsTab === "models") {
|
||
syncSettingsHealthLine();
|
||
const m = $2("sa_model")?.value;
|
||
if (m && $2("sa_settings_chat_model")) {
|
||
$2("sa_settings_chat_model").value = m;
|
||
}
|
||
}
|
||
if (state.settingsTab === "more") {
|
||
fillKnobsFromConfig(state.config);
|
||
}
|
||
}
|
||
function fillKnobsFromConfig(data) {
|
||
const asst = data?.assistant || state.config?.assistant || {};
|
||
const exact = data?.exact || state.config?.exact || state.exact || {};
|
||
const setNum = (id, v) => {
|
||
const el = $2(id);
|
||
if (el && v != null && Number.isFinite(Number(v))) {
|
||
el.value = String(v);
|
||
}
|
||
};
|
||
setNum("sa_num_ctx", asst.num_ctx);
|
||
setNum("sa_history_keep", asst.history_keep_turns);
|
||
setNum("sa_compress_at", asst.compress_at != null ? asst.compress_at : COMPRESS_AT);
|
||
setNum("sa_chars_per_token", asst.chars_per_token != null ? asst.chars_per_token : CHARS_PER_TOKEN);
|
||
const autoEl = $2("sa_compress_auto");
|
||
if (autoEl) {
|
||
autoEl.checked = asst.compress_auto != null ? !!asst.compress_auto : COMPRESS_AUTO;
|
||
}
|
||
setNum("sa_memory_top_k", asst.memory_top_k);
|
||
const w = asst.user_prefs_weight != null ? Number(asst.user_prefs_weight) : 1;
|
||
const weightEl = $2("sa_user_prefs_weight");
|
||
if (weightEl) {
|
||
weightEl.value = String(Math.max(0, Math.min(1.5, w)));
|
||
const lab = $2("sa_user_prefs_weight_val");
|
||
if (lab) {
|
||
lab.textContent = Number(weightEl.value).toFixed(1);
|
||
}
|
||
}
|
||
const turbo = exact.profiles?.turbo || {};
|
||
const raw = exact.profiles?.raw || {};
|
||
setNum("sa_exact_turbo_steps", turbo.steps);
|
||
setNum("sa_exact_turbo_cfg", turbo.cfg);
|
||
setNum("sa_exact_turbo_sigma", turbo.sigma_shift);
|
||
setNum("sa_exact_raw_steps", raw.steps);
|
||
setNum("sa_exact_raw_cfg", raw.cfg);
|
||
setNum("sa_exact_raw_sigma", raw.sigma_shift);
|
||
}
|
||
function saveKnobs() {
|
||
if (typeof genericRequest !== "function") {
|
||
return;
|
||
}
|
||
const num = (id) => {
|
||
const v = parseFloat($2(id)?.value);
|
||
return Number.isFinite(v) ? v : null;
|
||
};
|
||
const assistant = {
|
||
num_ctx: num("sa_num_ctx"),
|
||
history_keep_turns: num("sa_history_keep"),
|
||
compress_at: num("sa_compress_at"),
|
||
chars_per_token: num("sa_chars_per_token"),
|
||
compress_auto: $2("sa_compress_auto") ? !!$2("sa_compress_auto").checked : null,
|
||
memory_top_k: num("sa_memory_top_k"),
|
||
user_prefs_weight: num("sa_user_prefs_weight")
|
||
};
|
||
Object.keys(assistant).forEach((k) => {
|
||
if (assistant[k] == null) {
|
||
delete assistant[k];
|
||
}
|
||
});
|
||
const exact = {
|
||
profiles: {
|
||
turbo: {
|
||
steps: num("sa_exact_turbo_steps"),
|
||
cfg: num("sa_exact_turbo_cfg"),
|
||
sigma_shift: num("sa_exact_turbo_sigma")
|
||
},
|
||
raw: {
|
||
steps: num("sa_exact_raw_steps"),
|
||
cfg: num("sa_exact_raw_cfg"),
|
||
sigma_shift: num("sa_exact_raw_sigma")
|
||
}
|
||
}
|
||
};
|
||
genericRequest(
|
||
"AssistentSaveKnobs",
|
||
{ assistant, exact },
|
||
(data) => {
|
||
if (data?.assistant || data?.exact) {
|
||
applyConfigPayload({
|
||
...state.config,
|
||
assistant: data.assistant || state.config?.assistant,
|
||
exact: data.exact || state.config?.exact
|
||
});
|
||
}
|
||
if (assistant.compress_at != null) {
|
||
COMPRESS_AT = Math.min(0.95, Math.max(0.4, Number(assistant.compress_at) || 0.7));
|
||
}
|
||
if (assistant.chars_per_token != null) {
|
||
CHARS_PER_TOKEN = Math.max(1.5, Number(assistant.chars_per_token) || 3.2);
|
||
}
|
||
if (assistant.compress_auto != null) {
|
||
COMPRESS_AUTO = !!assistant.compress_auto;
|
||
}
|
||
updateCtxChip();
|
||
setStatus("Knobs \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u044B \u0432 overlay");
|
||
},
|
||
0,
|
||
(err) => setStatus(String(err || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C knobs"))
|
||
);
|
||
}
|
||
function syncSettingsHealthLine() {
|
||
const line = $2("sa_settings_health_line");
|
||
const badge = $2("sa_ollama_health");
|
||
if (line && badge) {
|
||
line.textContent = badge.textContent || "Ollama \xB7 \u2026";
|
||
line.className = "sa-settings-health " + (badge.className || "").replace("sa-health", "").trim();
|
||
}
|
||
}
|
||
function personaSourceLabel(source) {
|
||
if (source === "overlay") {
|
||
return "\u043C\u043E\u044F";
|
||
}
|
||
if (source === "overlay+bundled") {
|
||
return "\u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043E+\u043F\u0440\u0430\u0432\u043A\u0430";
|
||
}
|
||
if (source === "overlay+pack") {
|
||
return "\u043F\u0430\u043A+\u043F\u0440\u0430\u0432\u043A\u0430";
|
||
}
|
||
if (source === "pack") {
|
||
return "\u043F\u0430\u043A";
|
||
}
|
||
return "\u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043E";
|
||
}
|
||
function renderPersonaSettingsList() {
|
||
const root = $2("sa_persona_list");
|
||
if (!root) {
|
||
return;
|
||
}
|
||
const list = state.personas || state.config?.personas || [];
|
||
const cur = state.settingsPersonaId || $2("sa_persona")?.value || list[0]?.id;
|
||
state.settingsPersonaId = cur;
|
||
root.innerHTML = "";
|
||
for (const p of list) {
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
btn.className = "sa-persona-item" + (p.id === cur ? " sa-persona-item-active" : "");
|
||
const accent = p.accent || "currentColor";
|
||
btn.innerHTML = `<div class="sa-persona-item-title"><span class="sa-persona-dot" style="background:${escapeHtml2(accent)}"></span><span>${escapeHtml2(p.title || p.id)}</span></div><div class="sa-persona-badge">${escapeHtml2(personaSourceLabel(p.source))}</div>`;
|
||
btn.addEventListener("click", () => {
|
||
state.settingsPersonaId = p.id;
|
||
renderPersonaSettingsList();
|
||
loadPersonaPreview(p.id);
|
||
});
|
||
root.appendChild(btn);
|
||
}
|
||
syncPersonaPanelActions();
|
||
if (cur) {
|
||
loadPersonaPreview(cur);
|
||
}
|
||
}
|
||
function syncPersonaPanelActions() {
|
||
const id = state.settingsPersonaId;
|
||
const p = (state.personas || []).find((x) => x.id === id);
|
||
const canDelete = p && isDeletablePersonaSource(p.source);
|
||
const del = $2("sa_btn_persona_delete_panel");
|
||
if (del) {
|
||
del.disabled = !canDelete;
|
||
}
|
||
}
|
||
function loadPersonaPreview(id) {
|
||
const box = $2("sa_persona_preview");
|
||
if (!box || typeof genericRequest !== "function") {
|
||
return;
|
||
}
|
||
box.innerHTML = '<div class="sa-mem-empty">\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026</div>';
|
||
genericRequest(
|
||
"AssistentGetPersonaShelves",
|
||
{ persona: id },
|
||
(data) => {
|
||
const summary = data?.identity_summary || "";
|
||
const src = data?.source || "";
|
||
box.textContent = `${id} \xB7 ${personaSourceLabel(src)}
|
||
|
||
${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
|
||
syncPersonaPanelActions();
|
||
},
|
||
0,
|
||
(err) => {
|
||
box.innerHTML = `<div class="sa-mem-empty">${escapeHtml2(String(err || "\u043E\u0448\u0438\u0431\u043A\u0430"))}</div>`;
|
||
}
|
||
);
|
||
}
|
||
function exportSelectedPersona() {
|
||
const id = state.settingsPersonaId || $2("sa_persona")?.value;
|
||
if (!id || typeof genericRequest !== "function") {
|
||
return;
|
||
}
|
||
genericRequest(
|
||
"AssistentExportPersona",
|
||
{ persona: id },
|
||
(data) => {
|
||
const pack = data?.pack;
|
||
if (!pack) {
|
||
setStatus("\u041F\u0443\u0441\u0442\u043E\u0439 \u044D\u043A\u0441\u043F\u043E\u0440\u0442");
|
||
return;
|
||
}
|
||
const blob = new Blob([JSON.stringify(pack, null, 2)], { type: "application/json" });
|
||
const a = document.createElement("a");
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = `${pack.id || id}.assistent-persona.json`;
|
||
a.click();
|
||
URL.revokeObjectURL(a.href);
|
||
setStatus(`\u042D\u043A\u0441\u043F\u043E\u0440\u0442: ${a.download}`);
|
||
},
|
||
0,
|
||
(err) => setStatus(String(err || "\u042D\u043A\u0441\u043F\u043E\u0440\u0442 \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F"))
|
||
);
|
||
}
|
||
function importPersonaFile(file) {
|
||
if (!file || typeof genericRequest !== "function") {
|
||
return;
|
||
}
|
||
const reader = new FileReader();
|
||
reader.onload = () => {
|
||
let pack;
|
||
try {
|
||
pack = JSON.parse(String(reader.result || ""));
|
||
} catch (e) {
|
||
setStatus("\u041D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 JSON");
|
||
return;
|
||
}
|
||
let newId = pack?.id || "";
|
||
if ((state.personas || []).some((p) => p.id === newId && (p.source === "bundled" || p.source === "pack" || p.source === "overlay+bundled" || p.source === "overlay+pack"))) {
|
||
newId = window.prompt("Id \u0437\u0430\u043D\u044F\u0442 bundled/pack \u2014 \u043D\u043E\u0432\u044B\u0439 id:", `${newId}_import`) || "";
|
||
}
|
||
genericRequest(
|
||
"AssistentImportPersona",
|
||
{ pack, new_id: newId || null, overwrite: false },
|
||
(data) => {
|
||
if (Array.isArray(data?.personas)) {
|
||
state.personas = data.personas;
|
||
renderPersonaOptions(data.personas, data.persona?.id);
|
||
}
|
||
state.settingsPersonaId = data?.persona?.id || newId;
|
||
renderPersonaSettingsList();
|
||
setStatus(`\u0418\u043C\u043F\u043E\u0440\u0442: ${data?.persona?.id || newId}`);
|
||
},
|
||
0,
|
||
(err) => setStatus(String(err || "\u0418\u043C\u043F\u043E\u0440\u0442 \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F"))
|
||
);
|
||
};
|
||
reader.readAsText(file);
|
||
}
|
||
function cloneSelectedPersona() {
|
||
const from = state.settingsPersonaId || $2("sa_persona")?.value;
|
||
if (!from) {
|
||
return;
|
||
}
|
||
const to = window.prompt("\u041D\u043E\u0432\u044B\u0439 id \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438:", `${from}_copy`);
|
||
if (!to) {
|
||
return;
|
||
}
|
||
genericRequest(
|
||
"AssistentClonePersona",
|
||
{ from, to, title: to },
|
||
(data) => {
|
||
if (Array.isArray(data?.personas)) {
|
||
state.personas = data.personas;
|
||
renderPersonaOptions(data.personas, to);
|
||
}
|
||
state.settingsPersonaId = to;
|
||
renderPersonaSettingsList();
|
||
setStatus(`\u041A\u043B\u043E\u043D: ${to}`);
|
||
},
|
||
0,
|
||
(err) => setStatus(String(err || "\u041A\u043B\u043E\u043D \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F"))
|
||
);
|
||
}
|
||
function deleteSelectedOverlayPersona() {
|
||
const id = state.settingsPersonaId;
|
||
const p = (state.personas || []).find((x) => x.id === id);
|
||
if (!p || !isDeletablePersonaSource(p.source)) {
|
||
setStatus("\u041C\u043E\u0436\u043D\u043E \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E overlay");
|
||
return;
|
||
}
|
||
if (!window.confirm(`\u0423\u0434\u0430\u043B\u0438\u0442\u044C overlay-\u043B\u0438\u0447\u043D\u043E\u0441\u0442\u044C \xAB${id}\xBB?`)) {
|
||
return;
|
||
}
|
||
genericRequest(
|
||
"AssistentDeletePersona",
|
||
{ persona: id },
|
||
(data) => {
|
||
if (Array.isArray(data?.personas)) {
|
||
state.personas = data.personas;
|
||
renderPersonaOptions(data.personas, data.default_persona);
|
||
}
|
||
state.settingsPersonaId = data?.default_persona || null;
|
||
renderPersonaSettingsList();
|
||
setStatus(`\u0423\u0434\u0430\u043B\u0435\u043D\u043E: ${id}`);
|
||
},
|
||
0,
|
||
(err) => setStatus(String(err || "\u0423\u0434\u0430\u043B\u0435\u043D\u0438\u0435 \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C"))
|
||
);
|
||
}
|
||
function refreshUserPrefs() {
|
||
if (typeof genericRequest !== "function") {
|
||
return;
|
||
}
|
||
const persona = $2("sa_persona")?.value || "neutral";
|
||
genericRequest(
|
||
"AssistentListUserPrefs",
|
||
{ persona, limit: 200 },
|
||
(data) => {
|
||
state.userPrefs = Array.isArray(data?.prefs) ? data.prefs : [];
|
||
renderUserPrefsLists();
|
||
},
|
||
0,
|
||
(err) => setStatus(String(err || "User prefs \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B"))
|
||
);
|
||
}
|
||
function renderUserPrefsLists() {
|
||
const persona = $2("sa_persona")?.value || "neutral";
|
||
const global = (state.userPrefs || []).filter((p) => p.scope === "global");
|
||
const personal = (state.userPrefs || []).filter((p) => p.scope === "persona" && (p.persona_id === persona || p.persona === persona));
|
||
const fill = (rootId, rows) => {
|
||
const root = $2(rootId);
|
||
if (!root) {
|
||
return;
|
||
}
|
||
root.innerHTML = "";
|
||
if (!rows.length) {
|
||
root.innerHTML = '<div class="sa-mem-empty">\u041F\u0443\u0441\u0442\u043E</div>';
|
||
return;
|
||
}
|
||
for (const row of rows) {
|
||
const el = document.createElement("div");
|
||
el.className = "sa-mem-row";
|
||
const pin = row.pinned ? " \u2605" : "";
|
||
el.innerHTML = `<div class="sa-mem-row-body"><div class="sa-mem-row-head"><span class="sa-mem-row-key">${escapeHtml2(row.key || "")}${pin}</span></div><div class="sa-mem-row-text">${escapeHtml2(clipDebug(row.text, 200))}</div></div>`;
|
||
el.querySelector(".sa-mem-row-body")?.addEventListener("click", () => editUserPref(row));
|
||
el.querySelector(".sa-mem-row-body")?.setAttribute("title", "\u041A\u043B\u0438\u043A \u2014 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C");
|
||
const pinBtn = document.createElement("button");
|
||
pinBtn.type = "button";
|
||
pinBtn.className = "basic-button sa-mem-forget";
|
||
pinBtn.textContent = row.pinned ? "\u2605" : "\u2606";
|
||
pinBtn.title = row.pinned ? "Unpin" : "Pin";
|
||
pinBtn.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
toggleUserPrefPin(row);
|
||
});
|
||
el.appendChild(pinBtn);
|
||
const forget = document.createElement("button");
|
||
forget.type = "button";
|
||
forget.className = "basic-button sa-mem-forget";
|
||
forget.textContent = "\xD7";
|
||
forget.title = "\u0417\u0430\u0431\u044B\u0442\u044C";
|
||
forget.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
forgetUserPref(row);
|
||
});
|
||
el.appendChild(forget);
|
||
root.appendChild(el);
|
||
}
|
||
};
|
||
fill("sa_prefs_global", global);
|
||
fill("sa_prefs_persona", personal);
|
||
}
|
||
function editUserPref(row) {
|
||
const text = window.prompt("\u0422\u0435\u043A\u0441\u0442 \u0444\u0430\u043A\u0442\u0430:", row.text || "");
|
||
if (text == null || !String(text).trim()) {
|
||
return;
|
||
}
|
||
genericRequest(
|
||
"AssistentUpsertUserPref",
|
||
{
|
||
key: row.key,
|
||
text: String(text).trim(),
|
||
scope: row.scope || "global",
|
||
persona: row.persona_id || row.persona || $2("sa_persona")?.value || "neutral",
|
||
source: "user",
|
||
pinned: !!row.pinned
|
||
},
|
||
() => refreshUserPrefs(),
|
||
0,
|
||
(err) => setStatus(String(err || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C"))
|
||
);
|
||
}
|
||
function toggleUserPrefPin(row) {
|
||
genericRequest(
|
||
"AssistentUpsertUserPref",
|
||
{
|
||
key: row.key,
|
||
text: row.text,
|
||
scope: row.scope || "global",
|
||
persona: row.persona_id || row.persona || $2("sa_persona")?.value || "neutral",
|
||
source: row.source || "user",
|
||
pinned: !row.pinned
|
||
},
|
||
() => refreshUserPrefs(),
|
||
0,
|
||
(err) => setStatus(String(err || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C pin"))
|
||
);
|
||
}
|
||
function addUserPref(scope) {
|
||
const key = window.prompt("\u041A\u043B\u044E\u0447 (stable-id):", scope === "global" ? "prefer" : "tone");
|
||
if (!key) {
|
||
return;
|
||
}
|
||
const text = window.prompt("\u0422\u0435\u043A\u0441\u0442 \u0444\u0430\u043A\u0442\u0430:", "");
|
||
if (!text) {
|
||
return;
|
||
}
|
||
genericRequest(
|
||
"AssistentUpsertUserPref",
|
||
{
|
||
key: key.trim(),
|
||
text: text.trim(),
|
||
scope,
|
||
persona: $2("sa_persona")?.value || "neutral",
|
||
source: "user",
|
||
pinned: false
|
||
},
|
||
() => {
|
||
refreshUserPrefs();
|
||
setStatus("\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043E");
|
||
},
|
||
0,
|
||
(err) => setStatus(String(err || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C"))
|
||
);
|
||
}
|
||
function forgetUserPref(row) {
|
||
genericRequest(
|
||
"AssistentForgetUserPref",
|
||
{
|
||
key: row.key,
|
||
scope: row.scope || "global",
|
||
persona: row.persona_id || row.persona || $2("sa_persona")?.value
|
||
},
|
||
() => refreshUserPrefs(),
|
||
0,
|
||
(err) => setStatus(String(err || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0431\u044B\u0442\u044C"))
|
||
);
|
||
}
|
||
function clearUserPrefs(scope) {
|
||
const labels = { global: "\u043E\u0431\u0449\u0438\u0435 prefs", persona: "prefs \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438", all: "\u0432\u0441\u0435 prefs \u043E \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435" };
|
||
if (!window.confirm(`\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C ${labels[scope] || scope}?`)) {
|
||
return;
|
||
}
|
||
genericRequest(
|
||
"AssistentClearUserPrefs",
|
||
{ scope, persona: $2("sa_persona")?.value || "neutral" },
|
||
(data) => {
|
||
setStatus(`\u0423\u0434\u0430\u043B\u0435\u043D\u043E: ${data?.deleted ?? 0}`);
|
||
refreshUserPrefs();
|
||
},
|
||
0,
|
||
(err) => setStatus(String(err || "\u041E\u0447\u0438\u0441\u0442\u043A\u0430 \u043D\u0435 \u0443\u0434\u0430\u043B\u0430\u0441\u044C"))
|
||
);
|
||
}
|
||
function resetUiState() {
|
||
if (!window.confirm("\u0421\u0431\u0440\u043E\u0441\u0438\u0442\u044C UI-state (local + disk)? \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 Ollama \u0438 prefs \u043E\u0441\u0442\u0430\u043D\u0443\u0442\u0441\u044F.")) {
|
||
return;
|
||
}
|
||
const keys = Object.keys(localStorage).filter((k) => k.startsWith("swarm_assistent_"));
|
||
for (const k of keys) {
|
||
localStorage.removeItem(k);
|
||
}
|
||
diskPersist()?.saveUiState?.({});
|
||
setStatus("UI-state \u0441\u0431\u0440\u043E\u0448\u0435\u043D \u2014 \u043E\u0431\u043D\u043E\u0432\u0438 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0443");
|
||
}
|
||
function modelKeyLeaf(name) {
|
||
return String(name || "").replace(/\\/g, "/").split("/").pop().replace(/\.(safetensors|ckpt|pt|pth|gguf|bin)$/i, "").trim().toLowerCase();
|
||
}
|
||
function setOllamaHealth(level, text, title) {
|
||
state.ollamaHealth = level;
|
||
const el = $2("sa_ollama_health");
|
||
if (!el) {
|
||
return;
|
||
}
|
||
el.hidden = false;
|
||
el.textContent = text;
|
||
el.title = title || text;
|
||
el.classList.remove("sa-health-ok", "sa-health-warn", "sa-health-down");
|
||
el.classList.add(`sa-health-${level}`);
|
||
syncSettingsHealthLine();
|
||
}
|
||
function probeOllamaHealth() {
|
||
if (typeof genericRequest !== "function") {
|
||
return;
|
||
}
|
||
const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434";
|
||
genericRequest(
|
||
"AssistentListModels",
|
||
{ baseUrl },
|
||
(data) => {
|
||
if (data?.error) {
|
||
setOllamaHealth("down", "Ollama \u2715", String(data.error));
|
||
return;
|
||
}
|
||
const chat = (data.models || []).length;
|
||
const mem = (data.memory_models || []).length;
|
||
if (!chat) {
|
||
setOllamaHealth("warn", "Ollama \xB7 0 \u043C\u043E\u0434\u0435\u043B\u0435\u0439", "\u041D\u0435\u0442 \u0447\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u2014 \u0441\u0434\u0435\u043B\u0430\u0439 ollama pull");
|
||
return;
|
||
}
|
||
setOllamaHealth("ok", `Ollama \xB7 ${chat}`, `\u0427\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u0435\u0439: ${chat}, \u043F\u0430\u043C\u044F\u0442\u044C: ${mem} \xB7 ${baseUrl}`);
|
||
},
|
||
0,
|
||
(err) => setOllamaHealth("down", "Ollama \u2715", `\u041D\u0435\u0442 \u0441\u0432\u044F\u0437\u0438: ${String(err || "")} \xB7 ${baseUrl}`)
|
||
);
|
||
}
|
||
function setView(view) {
|
||
if (view === "cards") {
|
||
view = "chat";
|
||
}
|
||
if (view === "settings") {
|
||
state.view = "settings";
|
||
} else if (view === "train") {
|
||
state.view = "train";
|
||
} else {
|
||
state.view = "chat";
|
||
}
|
||
const chat = $2("sa_view_chat");
|
||
const settings = $2("sa_view_settings");
|
||
const train = $2("sa_view_train");
|
||
if (chat) {
|
||
chat.hidden = state.view !== "chat";
|
||
}
|
||
if (settings) {
|
||
settings.hidden = state.view !== "settings";
|
||
}
|
||
if (train) {
|
||
train.hidden = state.view !== "train";
|
||
}
|
||
const tabActive = (id, on) => {
|
||
$2(id)?.classList.toggle("sa-subtab-active", on);
|
||
$2(id)?.classList.toggle("sa-app-tab-active", on);
|
||
$2(id)?.setAttribute("aria-selected", on ? "true" : "false");
|
||
};
|
||
tabActive("sa_tab_chat", state.view === "chat");
|
||
tabActive("sa_tab_settings", state.view === "settings");
|
||
tabActive("sa_tab_train", state.view === "train");
|
||
saveSettings();
|
||
if (state.view === "settings") {
|
||
setSettingsTab(state.settingsTab || "behavior");
|
||
} else if (state.view === "train") {
|
||
window.SA?.training?.render?.();
|
||
} else if ((state.llmParked || state.expectColdLoad) && !state.generating) {
|
||
warmLlm({ force: true });
|
||
}
|
||
}
|
||
function openSettings(tab) {
|
||
if (tab) {
|
||
state.settingsTab = tab;
|
||
}
|
||
setView("settings");
|
||
}
|
||
function closeSettings() {
|
||
setView("chat");
|
||
}
|
||
async function addRefFromUrl(url) {
|
||
if (!url) {
|
||
return;
|
||
}
|
||
addRefSlot({ src: url, select: false });
|
||
}
|
||
function shortLoraName(name) {
|
||
const s = String(name || "");
|
||
const base = s.split(/[/\\]/).pop() || s;
|
||
return base.replace(/\.safetensors$/i, "").slice(0, 28);
|
||
}
|
||
function renderLoraChips() {
|
||
const root = $2("sa_lora_chips");
|
||
if (!root) {
|
||
return;
|
||
}
|
||
root.innerHTML = "";
|
||
let selected = [];
|
||
try {
|
||
if (typeof loraHelper !== "undefined" && Array.isArray(loraHelper?.selected)) {
|
||
selected = loraHelper.selected.map((l) => ({
|
||
name: l.name || l,
|
||
weight: loraHelper.loraWeightPref && loraHelper.loraWeightPref[l.name || l] || 1
|
||
}));
|
||
}
|
||
} catch (e) {
|
||
}
|
||
for (const l of selected) {
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
btn.className = "sa-lora-chip";
|
||
btn.title = `${l.name} \xD7${l.weight} \u2014 \u043A\u043B\u0438\u043A \u0441\u043D\u044F\u0442\u044C`;
|
||
btn.textContent = `${shortLoraName(l.name)} ${Number(l.weight).toFixed(2)}`;
|
||
btn.addEventListener("click", () => {
|
||
try {
|
||
if (typeof loraHelper !== "undefined" && typeof loraHelper.removeLora === "function") {
|
||
loraHelper.removeLora(l.name);
|
||
} else if (loraHelper?.selected) {
|
||
loraHelper.selected = loraHelper.selected.filter((x) => (x.name || x) !== l.name);
|
||
if (typeof loraHelper.rebuildUI === "function") {
|
||
loraHelper.rebuildUI();
|
||
}
|
||
}
|
||
} catch (e) {
|
||
}
|
||
renderLoraChips();
|
||
});
|
||
root.appendChild(btn);
|
||
}
|
||
const add = document.createElement("button");
|
||
add.type = "button";
|
||
add.className = "sa-lora-chip sa-lora-add";
|
||
add.textContent = "+ LoRA";
|
||
add.title = "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0438\u0437 inventory";
|
||
add.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
openLoraPicker(add);
|
||
});
|
||
root.appendChild(add);
|
||
}
|
||
function openLoraPicker(anchor) {
|
||
document.querySelectorAll(".sa-lora-picker").forEach((n) => n.remove());
|
||
const picker = document.createElement("div");
|
||
picker.className = "sa-lora-picker";
|
||
const inv = (state.inventory?.loras || []).slice().sort((a, b) => (b.krea_likely ? 1 : 0) - (a.krea_likely ? 1 : 0));
|
||
const filter = document.createElement("input");
|
||
filter.type = "search";
|
||
filter.placeholder = "\u0424\u0438\u043B\u044C\u0442\u0440 LoRA\u2026";
|
||
filter.style.cssText = "width:100%;box-sizing:border-box;margin-bottom:0.25rem;padding:0.3rem;";
|
||
picker.appendChild(filter);
|
||
const list = document.createElement("div");
|
||
picker.appendChild(list);
|
||
const draw = () => {
|
||
list.innerHTML = "";
|
||
const q = filter.value.trim().toLowerCase();
|
||
let n = 0;
|
||
for (const l of inv) {
|
||
const name = l.name || "";
|
||
if (q && !String(name).toLowerCase().includes(q) && !String(l.title || "").toLowerCase().includes(q)) {
|
||
continue;
|
||
}
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
btn.textContent = `${shortLoraName(name)}${l.krea_likely ? " \xB7 krea" : ""}`;
|
||
btn.title = name;
|
||
btn.addEventListener("click", async () => {
|
||
await applyPatch({
|
||
loras: [
|
||
...(() => {
|
||
try {
|
||
return (loraHelper?.selected || []).map((x) => ({
|
||
name: x.name || x,
|
||
weight: loraHelper.loraWeightPref && loraHelper.loraWeightPref[x.name || x] || 1
|
||
}));
|
||
} catch (e) {
|
||
return [];
|
||
}
|
||
})(),
|
||
{ name, weight: l.default_weight ? parseFloat(l.default_weight) : 0.8, triggers: l.triggers || (l.trigger_phrase ? [l.trigger_phrase] : []) }
|
||
]
|
||
}, "loras");
|
||
picker.remove();
|
||
renderLoraChips();
|
||
});
|
||
list.appendChild(btn);
|
||
if (++n >= 40) {
|
||
break;
|
||
}
|
||
}
|
||
if (!n) {
|
||
list.innerHTML = '<div class="sa-chat-empty-hint">\u041D\u0435\u0442 LoRA</div>';
|
||
}
|
||
};
|
||
filter.addEventListener("input", draw);
|
||
draw();
|
||
const composer = $2("sa_composer") || document.body;
|
||
composer.style.position = composer.style.position || "relative";
|
||
composer.appendChild(picker);
|
||
const onDoc = (ev) => {
|
||
if (!picker.contains(ev.target) && ev.target !== anchor) {
|
||
picker.remove();
|
||
document.removeEventListener("mousedown", onDoc);
|
||
}
|
||
};
|
||
setTimeout(() => document.addEventListener("mousedown", onDoc), 0);
|
||
filter.focus();
|
||
}
|
||
function inventoryIsStale(maxAgeMs = 2e4) {
|
||
if (!state.inventoryFetchedAt) {
|
||
return true;
|
||
}
|
||
return Date.now() - state.inventoryFetchedAt > maxAgeMs;
|
||
}
|
||
async function ensureFreshInventory({ forceRescan } = {}) {
|
||
const rescan = forceRescan || inventoryIsStale(2e4);
|
||
await refreshInventoryAsync({ rescan });
|
||
}
|
||
function triggerSwarmModelRefresh(done) {
|
||
if (typeof genericRequest !== "function") {
|
||
if (done) {
|
||
done();
|
||
}
|
||
return;
|
||
}
|
||
genericRequest(
|
||
"TriggerRefresh",
|
||
{ strong: true },
|
||
() => {
|
||
if (done) {
|
||
done();
|
||
}
|
||
},
|
||
0,
|
||
() => {
|
||
if (done) {
|
||
done();
|
||
}
|
||
}
|
||
);
|
||
}
|
||
async function handleReplySideEffects(reply, civitaiResults, opts = {}) {
|
||
const { fromAutoCritique, fromVisionHop, fromDebug } = opts;
|
||
void civitaiResults;
|
||
if (fromDebug) {
|
||
state.pendingSilentGen = false;
|
||
return;
|
||
}
|
||
const extracted = typeof extractPatch2 === "function" ? extractPatch2(reply) : { patch: null };
|
||
let effective = extracted && extracted.patch ? extracted.patch : null;
|
||
if (opts.fromPromptEnRetry && typeof mergePromptEnRewrite === "function" && (effective || state.pendingPromptEnMerge)) {
|
||
effective = mergePromptEnRewrite(effective);
|
||
}
|
||
const S = window.SA && window.SA.session;
|
||
const act = getActivity();
|
||
if (effective && act && typeof act.noteModelCommands === "function") {
|
||
act.noteModelCommands(effective);
|
||
}
|
||
const promptChanged = !!(effective && String(effective.prompt || "").trim());
|
||
if (effective && S) {
|
||
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
||
activityDone("delta", {
|
||
kind: "delta",
|
||
label: promptChanged ? "\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D" : "\u041E\u0431\u043D\u043E\u0432\u0438\u043B \u0441\u0435\u0441\u0441\u0438\u044E",
|
||
detail: Object.keys(effective).filter((k) => effective[k] != null && !["actions", "notes"].includes(k)).slice(0, 10).join(", ")
|
||
});
|
||
try {
|
||
const chat = typeof findChat === "function" ? findChat(state.activeChatId) : null;
|
||
if (chat && typeof snapshotChatParams === "function") {
|
||
chat.params = snapshotChatParams();
|
||
if (typeof persistChatsStore === "function") persistChatsStore();
|
||
}
|
||
} catch (e) {
|
||
}
|
||
if (typeof rememberLastPatch === "function") rememberLastPatch(effective);
|
||
}
|
||
if (Array.isArray(effective?.actions) && effective.actions.map(String).includes("interrupt")) {
|
||
if (typeof doInterruptNow === "function") doInterruptNow();
|
||
}
|
||
let intent = resolveTurnIntent2(effective, opts.userText || "", opts);
|
||
if (opts.userWantsGenerate && effective && !intent.vetoed && !fromAutoCritique) {
|
||
intent = { ...intent, generate: true };
|
||
}
|
||
if (effective) {
|
||
if (intent.generate) {
|
||
const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : [];
|
||
effective = { ...effective, actions: acts.includes("generate") ? acts : acts.concat("generate"), generate: true };
|
||
} else if (typeof stripGenerateAction === "function") {
|
||
effective = stripGenerateAction(effective);
|
||
if (effective && effective.generate) {
|
||
effective = { ...effective };
|
||
delete effective.generate;
|
||
}
|
||
}
|
||
if (!intent.look && typeof stripLookAt === "function") effective = stripLookAt(effective);
|
||
if (typeof rememberLastPatch === "function") rememberLastPatch(effective);
|
||
}
|
||
if (intent.vetoed) {
|
||
state.pendingSilentGen = false;
|
||
const a = getActivity();
|
||
if (a) {
|
||
a.skip("generate", {
|
||
kind: "generate",
|
||
label: "Generate \u043E\u0442\u043C\u0435\u043D\u0451\u043D",
|
||
detail: "\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u043F\u043E\u043F\u0440\u043E\u0441\u0438\u043B \u043D\u0435 \u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C"
|
||
});
|
||
}
|
||
}
|
||
const askList = Array.isArray(intent.ask) ? intent.ask : [];
|
||
if (askList.length && typeof claimTurnHop === "function" && claimTurnHop("ask")) {
|
||
activityStep("ask", {
|
||
kind: "ask",
|
||
label: `\u0417\u0430\u043F\u0440\u043E\u0441\u0438\u043B ${askList.join(", ")}`,
|
||
detail: "\u043F\u043E\u0434\u0433\u0440\u0443\u0436\u0430\u044E \u0434\u0435\u0442\u0430\u043B\u0438\u2026",
|
||
status: "running"
|
||
});
|
||
pullLiveIntoSession();
|
||
const bits = [];
|
||
if (askList.some((a) => /settings/i.test(String(a)))) {
|
||
const dump = S && typeof S.fullSettingsDump === "function" ? S.fullSettingsDump(state.chatSession, {
|
||
exact: state.exact,
|
||
kreaProfiles: state.kreaProfiles,
|
||
sessionExact: state.sessionExact
|
||
}) : collectLiveContext();
|
||
bits.push("SETTINGS_JSON:\n" + JSON.stringify(dump));
|
||
}
|
||
if (askList.some((a) => /inventory/i.test(String(a)))) {
|
||
const inv = state.inventory || {};
|
||
bits.push("INVENTORY_JSON:\n" + JSON.stringify({
|
||
loras: (inv.loras || []).slice(0, 40).map((l) => ({ name: l.name || l, trigger_phrase: l.trigger_phrase || null })),
|
||
checkpoints: (inv.checkpoints || []).slice(0, 16).map((c) => c.name || c),
|
||
wildcards: (inv.wildcards || []).slice(0, 20).map((w) => w.name || w)
|
||
}));
|
||
}
|
||
if (bits.length) {
|
||
activityDone("ask", { detail: "\u0434\u0435\u0442\u0430\u043B\u0438 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u044B \u043C\u043E\u0434\u0435\u043B\u0438" });
|
||
await sendChat({ skipSlash: true, skipAutoPack: true, fromAskHop: true, forcedUserText: bits.join("\n\n") });
|
||
return;
|
||
}
|
||
}
|
||
if (effective && intent.look && !fromVisionHop && !fromAutoCritique) {
|
||
activityStep("look", {
|
||
kind: "look",
|
||
label: "\u0421\u043C\u043E\u0442\u0440\u0438\u0442 \u043D\u0430 \u043A\u0430\u0434\u0440",
|
||
status: "running"
|
||
});
|
||
if (typeof maybeVisionHop === "function") {
|
||
const hopped = await maybeVisionHop(effective, opts.attachedSlotIds || []);
|
||
if (hopped) {
|
||
activityDone("look", { detail: "vision hop" });
|
||
return;
|
||
}
|
||
activityDone("look", { detail: "\u043A\u0430\u0434\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D" });
|
||
}
|
||
}
|
||
if (intent.generate && effective?.prompt && typeof promptNeedsKreaPrep === "function" && promptNeedsKreaPrep(effective.prompt) && !fromVisionHop && typeof claimTurnHop === "function" && claimTurnHop("krea_prep")) {
|
||
state.pendingPromptEnMerge = { ...effective };
|
||
activityStep("prep", {
|
||
kind: "prep",
|
||
label: "\u0413\u043E\u0442\u043E\u0432\u043B\u044E \u043F\u0440\u043E\u043C\u043F\u0442 \u0434\u043B\u044F Krea",
|
||
detail: "EN + \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430",
|
||
status: "running"
|
||
});
|
||
if (typeof appendSystemNote === "function") appendSystemNote("\u0413\u043E\u0442\u043E\u0432\u043B\u044E \u043F\u0440\u043E\u043C\u043F\u0442 \u0434\u043B\u044F Krea\u2026");
|
||
await sendChat({
|
||
skipSlash: true,
|
||
skipAutoPack: true,
|
||
fromPromptEnRetry: true,
|
||
userWantsGenerate: true,
|
||
forcedUserText: typeof buildKreaPromptPrepRequest === "function" ? buildKreaPromptPrepRequest(effective) : String(effective.prompt || "")
|
||
});
|
||
return;
|
||
}
|
||
if (intent.generate) {
|
||
activityStep("generate", {
|
||
kind: "generate",
|
||
label: intent.vetoed ? "Generate \u043E\u0442\u043C\u0435\u043D\u0451\u043D (\u0432\u0435\u0442\u043E)" : "Generate",
|
||
status: intent.vetoed ? "skip" : "running"
|
||
});
|
||
if (typeof startBusyUi === "function") startBusyUi("silent_gen");
|
||
if (effective) {
|
||
effective = ensureExactParamsForGenerate({
|
||
...effective,
|
||
generate: true,
|
||
actions: Array.isArray(effective.actions) && effective.actions.map(String).includes("generate") ? effective.actions : [...Array.isArray(effective.actions) ? effective.actions : [], "generate"]
|
||
});
|
||
}
|
||
if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
||
await pushSessionToSwarm(state.chatSession);
|
||
if (typeof syncLiveParamsBar === "function") syncLiveParamsBar();
|
||
if (typeof appendSystemNote === "function") {
|
||
appendSystemNote(promptChanged ? "\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D \xB7 Generate" : "\u0417\u0430\u043F\u0443\u0441\u043A\u0430\u044E Generate");
|
||
}
|
||
setStatus(promptChanged ? "\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D \xB7 Generate" : "Generate");
|
||
const srcOut = await runGenerateFromPatch(
|
||
{ ...effective || {}, actions: ["generate"], generate: true },
|
||
{ force: true, fromSession: true }
|
||
);
|
||
activityDone("generate", { detail: srcOut ? "\u043A\u0430\u0434\u0440 \u0433\u043E\u0442\u043E\u0432" : "\u0431\u0435\u0437 \u043A\u0430\u0434\u0440\u0430" });
|
||
if (srcOut) {
|
||
if (typeof maybeAutoCritique === "function") await maybeAutoCritique(srcOut);
|
||
if (typeof maybeAutoVisionLook === "function") await maybeAutoVisionLook(srcOut);
|
||
}
|
||
} else if (effective) {
|
||
if (S) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
||
if (promptChanged || $2("sa_auto_apply")?.checked) {
|
||
await pushSessionToSwarm(state.chatSession);
|
||
if (typeof syncLiveParamsBar === "function") syncLiveParamsBar();
|
||
if (promptChanged && typeof appendSystemNote === "function") {
|
||
appendSystemNote("\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D");
|
||
}
|
||
if (promptChanged) {
|
||
setStatus("\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D");
|
||
}
|
||
}
|
||
if (!state.generating && typeof stopBusyUi === "function") {
|
||
stopBusyUi(intent.vetoed ? "\u0417\u0430\u043F\u043E\u043C\u043D\u0438\u043B \xB7 \u0431\u0435\u0437 Generate" : promptChanged ? "\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D" : "");
|
||
}
|
||
}
|
||
state.pendingSilentGen = false;
|
||
}
|
||
async function applyQuickPatch(patch, note) {
|
||
let withActions = { ...patch };
|
||
if (!Array.isArray(withActions.actions) && patchHasGenTrigger(withActions)) {
|
||
withActions.actions = ["generate"];
|
||
}
|
||
const prevIntent = state.lastUserParamIntent;
|
||
state.lastUserParamIntent = true;
|
||
const S = window.SA && window.SA.session;
|
||
if (S && S.patchWantsGenerate && S.patchWantsGenerate(withActions)) {
|
||
withActions = ensureExactParamsForGenerate(withActions);
|
||
}
|
||
if (S) {
|
||
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), withActions);
|
||
}
|
||
await pushSessionToSwarm(state.chatSession);
|
||
state.lastUserParamIntent = prevIntent;
|
||
setStatus(note || "Applied");
|
||
if (patchHasGenTrigger(withActions)) {
|
||
await runGenerateFromPatch(withActions, { force: true, fromSession: true });
|
||
}
|
||
syncChipHighlight();
|
||
}
|
||
function syncChipHighlight() {
|
||
const bar = $2("sa_chips");
|
||
if (!bar) {
|
||
return;
|
||
}
|
||
const cur = guessAspectFromSize(val("input_width"), val("input_height"));
|
||
const seed = val("input_seed");
|
||
bar.querySelectorAll("[data-aspect]").forEach((btn) => {
|
||
btn.classList.toggle("sa-chip-active", btn.getAttribute("data-aspect") === cur);
|
||
});
|
||
bar.querySelectorAll("[data-seed]").forEach((btn) => {
|
||
const mode = btn.getAttribute("data-seed");
|
||
const active = mode === "lock" && seed && seed !== "-1" || mode === "random" && (!seed || seed === "-1");
|
||
btn.classList.toggle("sa-chip-active", active);
|
||
});
|
||
}
|
||
function appendSystemNote(text) {
|
||
const box = $2("sa_messages");
|
||
if (!box) {
|
||
return;
|
||
}
|
||
hideChatEmpty();
|
||
const div = document.createElement("div");
|
||
div.className = "sa-msg assistant sa-system-note";
|
||
div.textContent = text;
|
||
box.appendChild(div);
|
||
scrollMessagesToBottom();
|
||
}
|
||
function clipDebug(s, max) {
|
||
const t = String(s || "").replace(/\s+/g, " ").trim();
|
||
if (!t) {
|
||
return "\u2014";
|
||
}
|
||
return t.length > max ? `${t.slice(0, max)}\u2026` : t;
|
||
}
|
||
function formatDebugLoras(list) {
|
||
if (!Array.isArray(list) || !list.length) {
|
||
return "\u043D\u0435\u0442";
|
||
}
|
||
return list.slice(0, 8).map((l) => {
|
||
const name = l?.name || l;
|
||
const w = l?.weight != null ? `@${l.weight}` : "";
|
||
return `${name}${w}`;
|
||
}).join(", ");
|
||
}
|
||
function buildDebugSummary() {
|
||
const persona = $2("sa_persona")?.value || "neutral";
|
||
const pack = $2("sa_pack")?.value || defaultPackId();
|
||
const chatModel = $2("sa_model")?.value || "\u2014";
|
||
const embed = $2("sa_embed_model")?.value || state.preferredEmbed || "\u2014";
|
||
const profile = detectKreaProfileName2();
|
||
const defaults = mergedGenerationDefaults(profile);
|
||
const session = state.sessionExact || {};
|
||
const exactGen = state.exact?.generation || state.config?.exact?.generation || {};
|
||
const ctx = (() => {
|
||
try {
|
||
return collectLiveContext();
|
||
} catch (e) {
|
||
return {};
|
||
}
|
||
})();
|
||
const aspect = guessAspectFromSize(ctx.width, ctx.height) || defaults.aspect || "\u2014";
|
||
const why = [];
|
||
if (Object.keys(session).length) {
|
||
why.push(`session_exact \u043F\u0435\u0440\u0435\u043A\u0440\u044B\u0432\u0430\u0435\u0442 Exact: ${Object.keys(session).join(", ")}`);
|
||
} else {
|
||
why.push("session_exact \u043F\u0443\u0441\u0442 \u2014 params \u0438\u0437 Exact + \u043F\u0440\u043E\u0444\u0438\u043B\u044C \u0447\u0435\u043A\u043F\u043E\u0438\u043D\u0442\u0430");
|
||
}
|
||
why.push(`\u043F\u0440\u043E\u0444\u0438\u043B\u044C \u0447\u0435\u043A\u043F\u043E\u0438\u043D\u0442\u0430: ${profile} (\u0438\u043C\u044F/title \u2192 turbo|raw)`);
|
||
if (state.exact?.generation?.aspect) {
|
||
why.push(`persona/exact aspect: ${state.exact.generation.aspect || exactGen.aspect || "\u2014"}`);
|
||
}
|
||
if (state.lastPatch) {
|
||
const keys = Object.keys(state.lastPatch).filter((k) => state.lastPatch[k] != null && k !== "notes");
|
||
why.push(`\u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0439 \u043F\u0430\u0442\u0447 \u0437\u0430\u0434\u0430\u043B: ${keys.slice(0, 12).join(", ")}`);
|
||
} else {
|
||
why.push("\u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u0430\u0442\u0447\u0430 Assistent \u0435\u0449\u0451 \u043D\u0435\u0442");
|
||
}
|
||
why.push("\u043F\u0440\u0438\u043E\u0440\u0438\u0442\u0435\u0442: user \u2192 About the user \u2192 session_exact \u2192 exact(+persona) \u2192 live UI \u2192 craft memory_hits");
|
||
const lines = [
|
||
"### Debug Assistent",
|
||
`persona=${persona} \xB7 pack=${pack}`,
|
||
`chat=${chatModel} \xB7 embed=${embed}`,
|
||
`skills=${(state.enabledSkills || []).join(",") || "\u2014"}`,
|
||
`auto: apply=${!!$2("sa_auto_apply")?.checked} gen=${true} vision=${!!$2("sa_auto_vision")?.checked} critique=${!!$2("sa_auto_critique")?.checked}`,
|
||
"",
|
||
"Live SwarmUI:",
|
||
` ckpt=${ctx.checkpoint?.name || "\u2014"} \xB7 krea_profile=${ctx.krea_profile || profile}`,
|
||
` ${ctx.width || "?"}\xD7${ctx.height || "?"} (${aspect}) \xB7 steps=${ctx.steps ?? "\u2014"} \xB7 cfg=${ctx.cfg ?? "\u2014"} \xB7 sigma=${ctx.sigma_shift ?? "\u2014"} \xB7 seed=${ctx.seed ?? "\u2014"} \xB7 batch=${ctx.batch ?? "\u2014"}`,
|
||
` loras: ${formatDebugLoras(ctx.selected_loras || ctx.enabled_loras)}`,
|
||
` available_loras=${(ctx.available_loras || []).length}${ctx.available_loras_truncated ? ` truncated/${ctx.available_loras_total || "?"}` : ""}`,
|
||
` prompt: ${clipDebug(ctx.prompt, 220)}`,
|
||
` negative: ${clipDebug(ctx.negative, 120)}`,
|
||
` init=${!!ctx.has_init_image} mask=${!!ctx.has_mask_image} prompt_images=${ctx.prompt_image_count || 0}`,
|
||
` has_vision_image=${!!ctx.has_vision_image} \xB7 images_in_request=${!!ctx.images_in_request} \xB7 vision_ready=${visionReadySlots().length}`,
|
||
` context_json_chars\u2248${JSON.stringify(ctx).length} \xB7 last_system_chars=${state.lastSystemChars || "\u2014"} \xB7 last_context_chars=${state.lastContextChars || "\u2014"}`,
|
||
state.lastSystemLayers ? ` system_layers: ${Object.entries(state.lastSystemLayers).map(([k, v]) => `${k}=${v}`).join(" \xB7 ")}` : " system_layers: \u2014 (\u043E\u0442\u043F\u0440\u0430\u0432\u044C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435, \u0447\u0442\u043E\u0431\u044B \u0437\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u044C)",
|
||
(() => {
|
||
const b = currentBudgetEstimate();
|
||
const mem = getContextMemory();
|
||
return ` budget\u2248${b.used}/${b.numCtx} (${b.fromEval ? "eval" : "est"}) \xB7 level=${b.level} \xB7 memory_until=${mem.untilCount || 0} \xB7 auto_compress=${COMPRESS_AUTO}`;
|
||
})(),
|
||
"",
|
||
"Exact defaults (merged):",
|
||
` generation=${JSON.stringify(exactGen)}`,
|
||
` effective=${JSON.stringify({
|
||
steps: defaults.steps,
|
||
cfg: defaults.cfg,
|
||
sigma_shift: defaults.sigma_shift,
|
||
aspect: defaults.aspect,
|
||
images: defaults.images,
|
||
profile: defaults.profile
|
||
})}`,
|
||
` session_exact=${Object.keys(session).length ? JSON.stringify(session) : "{}"}`,
|
||
"",
|
||
"\u041F\u043E\u0447\u0435\u043C\u0443 \u0442\u0430\u043A:",
|
||
...why.map((w) => ` \xB7 ${w}`)
|
||
];
|
||
if (state.lastPatch) {
|
||
lines.push("", `last_patch: ${clipDebug(JSON.stringify(state.lastPatch), 360)}`);
|
||
}
|
||
return lines.join("\n");
|
||
}
|
||
async function handleSlashCommand(raw) {
|
||
const text = String(raw || "").trim();
|
||
if (!text.startsWith("/")) {
|
||
return false;
|
||
}
|
||
const parts = text.slice(1).split(/\s+/);
|
||
const cmd = (parts[0] || "").toLowerCase();
|
||
const arg = parts.slice(1).join(" ").trim();
|
||
if (cmd === "help" || cmd === "?") {
|
||
appendSystemNote(HELP_TEXT);
|
||
setStatus("/help");
|
||
return true;
|
||
}
|
||
if (cmd === "new" || cmd === "newchat") {
|
||
await startNewChat({ saveCurrent: true, openDrawer: true });
|
||
return true;
|
||
}
|
||
if (cmd === "history" || cmd === "chats" || cmd === "sessions") {
|
||
setChatsPanelOpen(!state.chatsPanelOpen);
|
||
setStatus(state.chatsPanelOpen ? "/history" : "\u0427\u0430\u0442\u044B \u0441\u043A\u0440\u044B\u0442\u044B");
|
||
return true;
|
||
}
|
||
if (cmd === "compress" || cmd === "compact" || cmd === "\u0441\u0436\u0430\u0442\u044C") {
|
||
await compressNowFromUi();
|
||
return true;
|
||
}
|
||
if (cmd === "debug" || cmd === "dbg" || cmd === "why") {
|
||
const dump = buildDebugSummary();
|
||
appendSystemNote(dump);
|
||
const argL = String(arg || "").toLowerCase().trim();
|
||
const wantLlm = cmd === "why" || /^(ask|llm|explain|поясни|почему|модель)(\s|$)/i.test(argL);
|
||
if (wantLlm) {
|
||
setStatus("/debug ask\u2026");
|
||
await sendChat({
|
||
forcedUserText: "\u041E\u0442\u043B\u0430\u0434\u043A\u0430 Assistent. \u041D\u0438\u0436\u0435 \u0444\u0430\u043A\u0442\u044B UI (\u0443\u0436\u0435 \u0441\u043E\u0431\u0440\u0430\u043D\u044B \u043A\u043B\u0438\u0435\u043D\u0442\u043E\u043C). \u041A\u0440\u0430\u0442\u043A\u043E \u0441\u0432\u043E\u0438\u043C\u0438 \u0441\u043B\u043E\u0432\u0430\u043C\u0438 (5\u201310 \u0441\u0442\u0440\u043E\u043A, \u044F\u0437\u044B\u043A \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F): \u043A\u0430\u043A\u0438\u0435 \u043F\u0440\u043E\u043C\u043F\u0442/params \u0441\u0435\u0439\u0447\u0430\u0441, \u0447\u0442\u043E \u0438\u0437 Exact vs session_exact vs live, \u0447\u0442\u043E \u0441\u0434\u0435\u043B\u0430\u043B \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0439 \u043F\u0430\u0442\u0447 \u0438 \u043F\u043E\u0447\u0435\u043C\u0443 \u0442\u0430\u043A \u043B\u043E\u0433\u0438\u0447\u043D\u043E. \u0411\u0435\u0437 JSON patch, \u0431\u0435\u0437 generate, \u0431\u0435\u0437 look_at.\n\n" + dump,
|
||
skipSlash: true,
|
||
skipAutoPack: true,
|
||
fromDebug: true,
|
||
skipAppendUser: true
|
||
});
|
||
} else {
|
||
setStatus("/debug");
|
||
}
|
||
return true;
|
||
}
|
||
if (cmd === "gen" || cmd === "generate") {
|
||
const prev = findCurrentGenerateSrc();
|
||
startBusyUi("generating");
|
||
state.generating = true;
|
||
setInterruptVisible(true);
|
||
if (!triggerGenerate()) {
|
||
state.generating = false;
|
||
stopBusyUi("Could not start Generate");
|
||
return true;
|
||
}
|
||
const src = await waitForNewImage(prev);
|
||
state.generating = false;
|
||
setInterruptVisible(state.busy);
|
||
if (src) {
|
||
const gen = generateSlot();
|
||
if (gen) {
|
||
gen.src = src;
|
||
renderBoard();
|
||
}
|
||
stopBusyUi("Generate done");
|
||
} else {
|
||
stopBusyUi("Generate finished");
|
||
}
|
||
return true;
|
||
}
|
||
if (cmd === "look") {
|
||
const id = normalizeSlotId(arg || GEN_ID) || GEN_ID;
|
||
const slot = slotById(id);
|
||
if (!slot) {
|
||
setStatus(`\u041D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u044B\u0439 \u0441\u043B\u043E\u0442: ${arg || GEN_ID}`);
|
||
return true;
|
||
}
|
||
if (slot.type !== "generate") {
|
||
setBoardTab("refs");
|
||
} else {
|
||
setBoardTab("generate");
|
||
}
|
||
if (!slot.src && id === GEN_ID) {
|
||
const src = findCurrentGenerateSrc();
|
||
if (src) {
|
||
slot.src = src;
|
||
}
|
||
}
|
||
if (!slot.src) {
|
||
setStatus(`Slot ${id} is empty`);
|
||
return true;
|
||
}
|
||
slot.attach = true;
|
||
renderBoard();
|
||
if ($2("sa_input")) {
|
||
$2("sa_input").value = `Look at ${id} and describe what you see.`;
|
||
}
|
||
setPackValue("critique_image", { flash: true });
|
||
await sendChat({ forceSlotIds: [id], skipAutoPack: true });
|
||
return true;
|
||
}
|
||
if (cmd === "init") {
|
||
const src = selectedSrc() || findCurrentGenerateSrc();
|
||
if (!src) {
|
||
setStatus("No image for Init");
|
||
return true;
|
||
}
|
||
await setInitFromSrc(src);
|
||
setPackValue("inpaint_edit", { flash: true });
|
||
return true;
|
||
}
|
||
if (cmd === "mask") {
|
||
const src = selectedSrc();
|
||
if (!src) {
|
||
setStatus("Select a window with a mask image");
|
||
return true;
|
||
}
|
||
await setMaskFromSrc(src);
|
||
setPackValue("inpaint_edit", { flash: true });
|
||
return true;
|
||
}
|
||
if (cmd === "clear") {
|
||
clearInitAndMask();
|
||
return true;
|
||
}
|
||
if (cmd === "interrupt" || cmd === "stop") {
|
||
doInterruptNow();
|
||
clearInFlightUi({ status: "\u041F\u0440\u0435\u0440\u0432\u0430\u043D\u043E" });
|
||
return true;
|
||
}
|
||
if (cmd === "aspect") {
|
||
const key = normalizeAspect(arg);
|
||
if (!key) {
|
||
setStatus(`Unknown aspect. Try: ${Object.keys(ASPECT_TABLE).join(", ")}`);
|
||
return true;
|
||
}
|
||
await applyQuickPatch({ aspect: key, actions: ["generate"] }, `Aspect ${key}`);
|
||
return true;
|
||
}
|
||
if (cmd === "seed") {
|
||
const mode = (arg || "random").toLowerCase();
|
||
if (mode === "lock" || mode === "keep") {
|
||
await applyQuickPatch({ lock_seed: true }, "Seed locked");
|
||
} else {
|
||
await applyQuickPatch({ seed: -1, vary: true, actions: ["generate"] }, "Seed random");
|
||
}
|
||
return true;
|
||
}
|
||
if (cmd === "vary") {
|
||
await applyQuickPatch({ vary: true, seed: -1, actions: ["generate"] }, "Vary (new seed)");
|
||
return true;
|
||
}
|
||
if (cmd === "inventory" || cmd === "inv") {
|
||
setStatus("Rescanning models\u2026");
|
||
triggerSwarmModelRefresh(async () => {
|
||
await refreshInventoryAsync({ rescan: true });
|
||
const n = state.inventory?.loras?.length || 0;
|
||
const ck = state.inventory?.checkpoints?.length || 0;
|
||
appendSystemNote(`Inventory refreshed: ${n} LoRAs, ${ck} checkpoints.`);
|
||
setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts (rescanned)`);
|
||
});
|
||
return true;
|
||
}
|
||
if (cmd === "pack") {
|
||
if (!setPackValue(arg, { flash: true, user: true })) {
|
||
setStatus("Pack: write|ordinary|critique|compose|params|inpaint|describe|persona");
|
||
} else {
|
||
setStatus(`Pack \u2192 ${$2("sa_pack")?.value}`);
|
||
}
|
||
return true;
|
||
}
|
||
if (cmd === "\u043E\u0441\u0442\u044B\u043D\u044C" || cmd === "ostyn" || cmd === "cool" || cmd === "cooldown") {
|
||
coolDownHorny();
|
||
return true;
|
||
}
|
||
if (cmd === "horny-game" || cmd === "hornygame" || cmd === "horny_game") {
|
||
await startHornyGame();
|
||
return true;
|
||
}
|
||
if (cmd === "persona") {
|
||
const sub = (parts[1] || "new").toLowerCase();
|
||
const rest = parts.slice(2).join(" ").trim();
|
||
setPackValue("author_persona", { flash: true, user: true });
|
||
if (sub === "save") {
|
||
await sendChat({
|
||
skipAutoPack: true,
|
||
forcedUserText: "\u0421\u043E\u0445\u0440\u0430\u043D\u0438 \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438 \u0441\u0435\u0439\u0447\u0430\u0441 (persona_clone / persona_write). \u041D\u0435 \u0443\u0434\u0430\u043B\u044F\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438."
|
||
});
|
||
return true;
|
||
}
|
||
const fromId = sub === "clone" && rest ? rest.split(/\s+/)[0] : $2("sa_persona")?.value || "neutral";
|
||
await sendChat({
|
||
skipAutoPack: true,
|
||
forcedUserText: `\u041D\u0430\u0447\u043D\u0438 \u0438\u043D\u0442\u0435\u0440\u0432\u044C\u044E author_persona: \u043A\u043B\u043E\u043D \u0441 \u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A\u0430 \xAB${fromId}\xBB. \u0421\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0439 \u043F\u043E \u043F\u043E\u043B\u043A\u0430\u043C \u0433\u0440\u0443\u043F\u043F\u0430\u043C\u0438. \u041D\u0435 \u043F\u0438\u0448\u0438 \u043D\u0430 \u0434\u0438\u0441\u043A, \u043F\u043E\u043A\u0430 \u043C\u0430\u043B\u043E \u043E\u0442\u0432\u0435\u0442\u043E\u0432. \u041D\u0435 \u0443\u0434\u0430\u043B\u044F\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438.`
|
||
});
|
||
return true;
|
||
}
|
||
appendSystemNote(`Unknown command /${cmd}.
|
||
|
||
${HELP_TEXT}`);
|
||
setStatus(`Unknown /${cmd}`);
|
||
return true;
|
||
}
|
||
async function maybeVisionHop(patch, attachedSlotIds) {
|
||
const ids = lookAtIdsFromPatch(patch);
|
||
if (!ids.length || turnHopUsed("vision")) {
|
||
return false;
|
||
}
|
||
scrubPreviewFromGenerateSlot();
|
||
const have = ids.map((id) => slotById(id)).filter((s) => s && s.src && !looksLikeModelPreview(s.src));
|
||
if (!have.length) {
|
||
const genSrc = findCurrentGenerateSrc();
|
||
if (ids.includes(GEN_ID) && genSrc) {
|
||
const gen = generateSlot();
|
||
if (gen) {
|
||
gen.src = genSrc;
|
||
have.push(gen);
|
||
}
|
||
}
|
||
}
|
||
if (!have.length) {
|
||
setStatus("look_at: \u043D\u0435\u0442 \u0440\u0435\u0430\u043B\u044C\u043D\u043E\u0433\u043E \u043A\u0430\u0434\u0440\u0430 (\u043F\u0440\u0435\u0432\u044C\u044E \u043C\u043E\u0434\u0435\u043B\u0438 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u043E)");
|
||
return false;
|
||
}
|
||
const already = new Set(attachedSlotIds || []);
|
||
const need = have.filter((s) => !already.has(s.id));
|
||
if (!need.length) {
|
||
return false;
|
||
}
|
||
if (!claimTurnHop("vision")) {
|
||
return false;
|
||
}
|
||
for (const s of need) {
|
||
s.attach = true;
|
||
}
|
||
renderBoard();
|
||
if ($2("sa_input")) {
|
||
$2("sa_input").value = `Look at board slots: ${need.map((s) => s.id).join(", ")}. Continue using these images.`;
|
||
}
|
||
setStatus(`Vision hop \u2190 ${need.map((s) => s.label).join(", ")}`);
|
||
await sendChat({ fromVisionHop: true, forceSlotIds: need.map((s) => s.id) });
|
||
return true;
|
||
}
|
||
async function sendChat(opts = {}) {
|
||
if (isTrainingLocked() && !isContinuationTurn(opts)) {
|
||
setStatus("\u0418\u0434\u0451\u0442 \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430 \u2014 \u0447\u0430\u0442 \u0437\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043D");
|
||
return;
|
||
}
|
||
const rawInput = ($2("sa_input")?.value || "").trim();
|
||
const text = (opts.forcedUserText || rawInput).trim();
|
||
if (!text) {
|
||
return;
|
||
}
|
||
if (state.generating && !isContinuationTurn(opts)) {
|
||
setStatus("\u0418\u0434\u0451\u0442 Generate \u2014 \u043D\u0430\u0436\u043C\u0438 \u0421\u0442\u043E\u043F, \u043F\u043E\u0442\u043E\u043C \u043E\u0442\u043F\u0440\u0430\u0432\u044C");
|
||
return;
|
||
}
|
||
if (state.busy && !isContinuationTurn(opts)) {
|
||
abortInFlightWork({ status: "\u041D\u043E\u0432\u043E\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435", interruptSwarm: false });
|
||
}
|
||
if (!isMachineTurn(opts)) {
|
||
state.lastUserParamIntent = userTextMentionsParams(text);
|
||
state.lastUserControlIntent = userTextMentionsControls(text);
|
||
state.pendingSilentGen = false;
|
||
}
|
||
if (!isMachineTurn(opts) && !opts.skipSlash) {
|
||
if (rawInput.startsWith("/")) {
|
||
if ($2("sa_input")) {
|
||
$2("sa_input").value = "";
|
||
}
|
||
const handled = await handleSlashCommand(rawInput);
|
||
if (handled) {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
if (!isMachineTurn(opts) && isSameButAspectRequest(text)) {
|
||
const aspect = parseAspectFromUserText(text);
|
||
if (aspect) {
|
||
if ($2("sa_input")) {
|
||
$2("sa_input").value = "";
|
||
}
|
||
appendMessage("user", text);
|
||
state.history.push({ role: "user", content: text });
|
||
persistHistory();
|
||
restoreDefaultPackAfterHop();
|
||
const patch = { aspect, actions: ["generate"] };
|
||
if (state.lastPatch?.prompt) {
|
||
patch.prompt = state.lastPatch.prompt;
|
||
}
|
||
if (Array.isArray(state.lastPatch?.loras) && state.lastPatch.loras.length) {
|
||
patch.loras = state.lastPatch.loras;
|
||
}
|
||
appendSystemNote(`\u0421\u0442\u0430\u0432\u043B\u044E ${aspect} \u0438 Generate (\u0442\u043E\u0442 \u0436\u0435 \u043F\u0440\u043E\u043C\u043F\u0442) \u2014 \u0431\u0435\u0437 \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u043E\u0439 \u043A\u0440\u0438\u0442\u0438\u043A\u0438.`);
|
||
await applyQuickPatch(patch, `Aspect ${aspect}`);
|
||
return;
|
||
}
|
||
}
|
||
if (!updateGate()) {
|
||
setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Krea 2");
|
||
return;
|
||
}
|
||
if (!opts.skipAutoPack && !isMachineTurn(opts)) {
|
||
const guessed = autoSelectPack(text);
|
||
if (guessed) {
|
||
setPackValue(guessed, { flash: true });
|
||
}
|
||
}
|
||
if (!opts.fromDebug && false) {
|
||
setPackValue("ordinary", { flash: false });
|
||
}
|
||
const pack = opts.fromDebug ? "debug_explain" : $2("sa_pack")?.value || defaultPackId();
|
||
const persona = $2("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral";
|
||
const model = $2("sa_model")?.value;
|
||
if (!model) {
|
||
setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Ollama \u0432 \u2699");
|
||
refreshModels();
|
||
return;
|
||
}
|
||
const chatEpoch = bumpChatEpoch();
|
||
state.busy = true;
|
||
state.turnSettled = false;
|
||
state.llmParked = false;
|
||
setInterruptVisible(true);
|
||
if (!isContinuationTurn(opts) && !opts.fromAskHop) {
|
||
activityBegin(opts.fromDebug ? "Debug" : "\u0425\u043E\u0434 Assistent");
|
||
activityStep("think", { kind: "think", label: "\u0414\u0443\u043C\u0430\u044E\u2026", status: "running" });
|
||
} else if (opts.fromAskHop) {
|
||
activityStep("think", { kind: "think", label: "\u041E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0441 \u0434\u0435\u0442\u0430\u043B\u044F\u043C\u0438\u2026", status: "running" });
|
||
} else if (opts.fromVisionHop) {
|
||
activityStep("look", { kind: "look", label: "\u0421\u043C\u043E\u0442\u0440\u0438\u0442 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435\u2026", status: "running" });
|
||
} else if (opts.fromPromptEnRetry) {
|
||
activityStep("prep", { kind: "prep", label: "\u0414\u043E\u043F\u0438\u0441\u044B\u0432\u0430\u044E EN-\u043F\u0440\u043E\u043C\u043F\u0442\u2026", status: "running" });
|
||
}
|
||
if (state.expectColdLoad && !isContinuationTurn(opts)) {
|
||
startBusyUi("warming");
|
||
setStatus("\u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E LLM \u0432 GPU\u2026");
|
||
try {
|
||
await warmLlm({ force: true });
|
||
} catch (e) {
|
||
console.warn("Assistent warm before send", e);
|
||
}
|
||
if (chatEpoch !== state.chatEpoch) {
|
||
return;
|
||
}
|
||
}
|
||
startBusyUi(state.expectColdLoad ? "loading" : "thinking");
|
||
saveSettings();
|
||
setStatus("\u041E\u0431\u043D\u043E\u0432\u043B\u044F\u044E inventory\u2026");
|
||
try {
|
||
await ensureFreshInventory({ forceRescan: !!opts.fromDownload });
|
||
} catch (e) {
|
||
console.warn("Assistent inventory refresh", e);
|
||
}
|
||
if (chatEpoch !== state.chatEpoch) {
|
||
return;
|
||
}
|
||
if (!isContinuationTurn(opts) && !opts.fromDownload) {
|
||
resetTurnHops();
|
||
}
|
||
let wantedIds = (opts.forceSlotIds || []).map(normalizeSlotId).filter(Boolean);
|
||
const sendVision = !!(opts.fromVisionHop || wantedIds.length && opts.forceSlotIds);
|
||
if (!sendVision) {
|
||
wantedIds = [];
|
||
}
|
||
const visionSlots = wantedIds.map((id) => slotById(id)).filter((s) => s && s.src && !looksLikeModelPreview(s.src));
|
||
let images = null;
|
||
if (visionSlots.length) {
|
||
startBusyUi("encoding");
|
||
setStatus("\u041A\u043E\u0434\u0438\u0440\u0443\u044E \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435\u2026");
|
||
images = [];
|
||
for (const slot of visionSlots) {
|
||
if (chatEpoch !== state.chatEpoch) {
|
||
return;
|
||
}
|
||
const b64 = await imageToBase64ForOllama(slot.src);
|
||
if (b64) {
|
||
images.push(b64);
|
||
}
|
||
}
|
||
if (!images.length) {
|
||
images = null;
|
||
}
|
||
}
|
||
if (chatEpoch !== state.chatEpoch) {
|
||
return;
|
||
}
|
||
const msgMeta = {
|
||
persona: currentPersonaInfo(),
|
||
pack,
|
||
silentPatch: !!state.pendingSilentGen
|
||
};
|
||
if (!opts.skipAppendUser) {
|
||
state.history.push({ role: "user", content: opts.historyUserText || text });
|
||
if (state.pendingPersonaNote) {
|
||
state.history.push({ role: "user", content: state.pendingPersonaNote, systemish: true });
|
||
state.pendingPersonaNote = null;
|
||
}
|
||
appendMessage("user", opts.historyUserText || text);
|
||
if ($2("sa_input")) {
|
||
$2("sa_input").value = "";
|
||
}
|
||
persistHistory();
|
||
} else {
|
||
const marker = opts.historyUserText || (opts.fromDebug ? "/debug ask" : "");
|
||
if (marker) {
|
||
state.history.push({ role: "user", content: marker });
|
||
persistHistory();
|
||
}
|
||
}
|
||
if (!opts.fromDebug && !opts.fromCompress && !opts.fromAskHop && !opts.fromVisionHop && !opts.fromAutoCritique && !isContinuationTurn(opts)) {
|
||
try {
|
||
await maybeAutoCompressBeforeSend(chatEpoch);
|
||
} catch (e) {
|
||
console.warn("Assistent auto-compress", e);
|
||
}
|
||
if (chatEpoch !== state.chatEpoch) {
|
||
return;
|
||
}
|
||
startBusyUi(state.expectColdLoad ? "loading" : "thinking");
|
||
}
|
||
const context = collectLiveContext();
|
||
context.prior_assistant_turns = (state.history || []).filter((m) => m && m.role === "assistant" && !m.systemish).length;
|
||
context.do_not_greet = context.prior_assistant_turns > 0;
|
||
context.has_vision_image = visionReadySlots().length > 0;
|
||
context.images_in_request = !!(images && images.length);
|
||
context.attached_slot_ids = attachableSlots().map((s) => s.id);
|
||
context.vision_slot_ids = visionSlots.map((s) => s.id);
|
||
context.persona = persona;
|
||
if (chatEpoch !== state.chatEpoch) {
|
||
return;
|
||
}
|
||
let messages = assembleOutgoingMessages(
|
||
opts.skipAppendUser ? { includePendingUser: text } : void 0
|
||
);
|
||
if (!messages.length && text) {
|
||
messages = [{ role: "user", content: text }];
|
||
}
|
||
if (images && messages.length) {
|
||
messages[messages.length - 1].images = images;
|
||
}
|
||
startBusyUi("thinking");
|
||
const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434";
|
||
const payload = {
|
||
baseUrl,
|
||
model,
|
||
pack,
|
||
persona,
|
||
includeBase: !opts.fromDebug,
|
||
messages,
|
||
context_json: JSON.stringify(context),
|
||
skills: opts.fromDebug ? [] : state.enabledSkills || [],
|
||
embed_model: $2("sa_embed_model")?.value || state.preferredEmbed || ""
|
||
};
|
||
const finishOk = async (reply, civitaiResults, meta = {}) => {
|
||
if (chatEpoch !== state.chatEpoch) {
|
||
return;
|
||
}
|
||
if (state.turnSettled) {
|
||
return;
|
||
}
|
||
state.turnSettled = true;
|
||
state.onClosedTerminalFence = null;
|
||
clearStreamStall();
|
||
if (meta.system_chars != null) {
|
||
state.lastSystemChars = Number(meta.system_chars) || 0;
|
||
}
|
||
if (meta.system_layers && typeof meta.system_layers === "object") {
|
||
state.lastSystemLayers = meta.system_layers;
|
||
}
|
||
if (meta.prompt_eval_count != null) {
|
||
state.lastPromptEvalCount = Number(meta.prompt_eval_count) || null;
|
||
const mem = getContextMemory();
|
||
if (mem.summary) {
|
||
setContextMemory({ ...mem, promptEvalCount: state.lastPromptEvalCount }, { persist: true });
|
||
}
|
||
}
|
||
try {
|
||
state.lastContextChars = context && JSON.stringify(context).length || 0;
|
||
} catch (e) {
|
||
state.lastContextChars = 0;
|
||
}
|
||
updateCtxChip();
|
||
const prose = extractPatch2(reply).prose || reply;
|
||
state.history.push({ role: "assistant", content: prose, persona, pack });
|
||
persistHistory();
|
||
setBusyPhase(state.pendingSilentGen ? "silent_gen" : "thinking");
|
||
try {
|
||
await handleReplySideEffects(reply, civitaiResults, {
|
||
...opts,
|
||
userText: text,
|
||
userWantsGenerate: !!opts.userWantsGenerate || !isMachineTurn(opts) && state.pendingSilentGen,
|
||
attachedSlotIds: visionSlots.map((s) => s.id)
|
||
});
|
||
} finally {
|
||
if (chatEpoch !== state.chatEpoch) {
|
||
return;
|
||
}
|
||
if (!state.generating) {
|
||
state.busy = false;
|
||
setInterruptVisible(false);
|
||
stopBusyUi("\u0413\u043E\u0442\u043E\u0432\u043E");
|
||
} else {
|
||
state.busy = false;
|
||
setInterruptVisible(true);
|
||
}
|
||
updateCtxChip();
|
||
}
|
||
};
|
||
const finishErr = (msg) => {
|
||
if (chatEpoch !== state.chatEpoch) {
|
||
return;
|
||
}
|
||
if (state.turnSettled) {
|
||
return;
|
||
}
|
||
state.turnSettled = true;
|
||
state.onClosedTerminalFence = null;
|
||
clearStreamStall();
|
||
state.busy = false;
|
||
setInterruptVisible(state.generating);
|
||
stopBusyUi(msg);
|
||
if (state.streamEl) {
|
||
state.streamEl.classList.remove("sa-streaming", "sa-typing");
|
||
state.streamEl.classList.add("error");
|
||
setAssistantBody(state.streamEl, msg);
|
||
state.streamEl = null;
|
||
state.streamMeta = null;
|
||
} else {
|
||
appendMessage("error", msg);
|
||
}
|
||
};
|
||
if (typeof makeWSRequest === "function") {
|
||
beginStreamMessage(msgMeta);
|
||
const settleStreamReply = (reply) => {
|
||
if (state.turnSettled || chatEpoch !== state.chatEpoch) {
|
||
return;
|
||
}
|
||
const raw = String(reply || state.streamText || "").trim() || (state.streamEl?.querySelector(".sa-msg-body")?.textContent || "");
|
||
if (state.streamEl) {
|
||
finalizeStreamMessage(raw, []);
|
||
}
|
||
finishOk(raw, [], {});
|
||
};
|
||
state.onClosedTerminalFence = (text2) => settleStreamReply(text2);
|
||
armStreamStall(chatEpoch, (reply) => {
|
||
settleStreamReply(reply);
|
||
});
|
||
makeWSRequest(
|
||
"AssistentChatWS",
|
||
payload,
|
||
(data) => {
|
||
if (chatEpoch !== state.chatEpoch) {
|
||
return;
|
||
}
|
||
if (data.phase === "waiting_ollama") {
|
||
setBusyPhase(state.expectColdLoad ? "loading" : "waiting");
|
||
const label = state.streamEl?.querySelector(".sa-typing-label");
|
||
if (label) {
|
||
label.textContent = state.expectColdLoad ? `\u0417\u0430\u0433\u0440\u0443\u0436\u0430\u044E ${modelShort(model)} \u0432 GPU\u2026` : "\u0414\u0443\u043C\u0430\u044E\u2026";
|
||
}
|
||
return;
|
||
}
|
||
if (data.error) {
|
||
finishErr(String(data.error));
|
||
return;
|
||
}
|
||
if (data.clear_stream) {
|
||
if (state.streamEl) {
|
||
state.streamEl.classList.add("sa-typing");
|
||
const body = state.streamEl.querySelector(".sa-msg-body") || state.streamEl;
|
||
body.innerHTML = '<span class="sa-dots" aria-hidden="true"><i></i><i></i><i></i></span><span class="sa-typing-label">\u0423\u0442\u043E\u0447\u043D\u044F\u044E\u2026</span>';
|
||
}
|
||
setBusyPhase("refining");
|
||
return;
|
||
}
|
||
if (data.delta) {
|
||
appendStreamDelta(data.delta);
|
||
}
|
||
if (data.done || typeof data.reply === "string" && data.reply.length > 0 && !data.delta) {
|
||
if (state.turnSettled) {
|
||
return;
|
||
}
|
||
const reply = String(state.streamText || data.reply || "").trim() || state.streamEl?.querySelector(".sa-msg-body")?.textContent || "";
|
||
const civitai = data.civitai_results || [];
|
||
state.onClosedTerminalFence = null;
|
||
if (state.streamEl) {
|
||
finalizeStreamMessage(reply, civitai);
|
||
}
|
||
finishOk(reply, civitai, {
|
||
system_chars: data.system_chars,
|
||
system_layers: data.system_layers,
|
||
prompt_eval_count: data.prompt_eval_count ?? data.raw?.prompt_eval_count
|
||
});
|
||
}
|
||
},
|
||
0,
|
||
(err) => {
|
||
if (chatEpoch !== state.chatEpoch) {
|
||
return;
|
||
}
|
||
console.warn("AssistentChatWS failed, falling back", err);
|
||
if (state.streamEl) {
|
||
state.streamEl.remove();
|
||
state.streamEl = null;
|
||
state.streamMeta = null;
|
||
}
|
||
genericRequest(
|
||
"AssistentChat",
|
||
payload,
|
||
(data) => {
|
||
if (chatEpoch !== state.chatEpoch) {
|
||
return;
|
||
}
|
||
if (data.error) {
|
||
finishErr(String(data.error));
|
||
return;
|
||
}
|
||
const reply = data.reply || "";
|
||
appendMessage("assistant", reply, null, data.civitai_results || [], msgMeta);
|
||
finishOk(reply, data.civitai_results || [], {
|
||
system_chars: data.system_chars,
|
||
system_layers: data.system_layers,
|
||
prompt_eval_count: data.prompt_eval_count ?? data.raw?.prompt_eval_count
|
||
});
|
||
},
|
||
0,
|
||
(err2) => finishErr(String(err2 || err || "Chat failed"))
|
||
);
|
||
}
|
||
);
|
||
return;
|
||
}
|
||
genericRequest(
|
||
"AssistentChat",
|
||
payload,
|
||
(data) => {
|
||
if (chatEpoch !== state.chatEpoch) {
|
||
return;
|
||
}
|
||
if (data.error) {
|
||
finishErr(String(data.error));
|
||
return;
|
||
}
|
||
const reply = data.reply || "";
|
||
appendMessage("assistant", reply, null, data.civitai_results || [], msgMeta);
|
||
finishOk(reply, data.civitai_results || [], {
|
||
system_chars: data.system_chars,
|
||
system_layers: data.system_layers,
|
||
prompt_eval_count: data.prompt_eval_count ?? data.raw?.prompt_eval_count
|
||
});
|
||
},
|
||
0,
|
||
(err) => finishErr(String(err || "Chat failed"))
|
||
);
|
||
}
|
||
function wireDropZone() {
|
||
const board = $2("sa_board");
|
||
const layout = $2("sa_layout");
|
||
layout?.addEventListener("dragover", (e) => {
|
||
if (e.dataTransfer?.types?.includes("Files") || e.dataTransfer?.types?.includes("text/uri-list")) {
|
||
e.preventDefault();
|
||
}
|
||
});
|
||
layout?.addEventListener("drop", async (e) => {
|
||
if (!e.dataTransfer) {
|
||
return;
|
||
}
|
||
if (e.target && e.target.closest && e.target.closest(".sa-slot, .sa-add-cell")) {
|
||
return;
|
||
}
|
||
e.preventDefault();
|
||
await handleDropDataTransfer(e.dataTransfer);
|
||
});
|
||
board?.addEventListener("keydown", (e) => {
|
||
if (e.key === "Escape" && state.lightboxIndex >= 0) {
|
||
e.preventDefault();
|
||
closeGenLightbox();
|
||
return;
|
||
}
|
||
if ((e.key === "Enter" || e.key === " ") && state.boardTab === "generate" && state.selectedGenResultId) {
|
||
const row = (state.genResults || []).find((r) => r.id === state.selectedGenResultId);
|
||
if (row?.src) {
|
||
e.preventDefault();
|
||
openGenLightbox(row.id);
|
||
return;
|
||
}
|
||
}
|
||
if ((e.key === "ArrowLeft" || e.key === "ArrowRight") && state.lightboxIndex >= 0) {
|
||
e.preventDefault();
|
||
stepGenLightbox(e.key === "ArrowRight" ? 1 : -1);
|
||
return;
|
||
}
|
||
if (e.key === "Delete" || e.key === "Backspace") {
|
||
if (e.target && (e.target.tagName === "TEXTAREA" || e.target.tagName === "INPUT")) {
|
||
return;
|
||
}
|
||
e.preventDefault();
|
||
clearSlot(state.selectedSlotId);
|
||
}
|
||
});
|
||
document.addEventListener("paste", async (e) => {
|
||
const pane = document.getElementById("assistent");
|
||
if (!pane || !pane.classList.contains("active")) {
|
||
return;
|
||
}
|
||
if (e.target && (e.target.tagName === "TEXTAREA" || e.target.tagName === "INPUT")) {
|
||
const items2 = e.clipboardData?.items;
|
||
let hasImage = false;
|
||
if (items2) {
|
||
for (const item of items2) {
|
||
if (item.type.startsWith("image/")) {
|
||
hasImage = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (!hasImage) {
|
||
return;
|
||
}
|
||
}
|
||
const items = e.clipboardData?.items;
|
||
if (!items) {
|
||
return;
|
||
}
|
||
for (const item of items) {
|
||
if (item.type.startsWith("image/")) {
|
||
e.preventDefault();
|
||
const file = item.getAsFile();
|
||
if (file) {
|
||
const sel = selectedSlot();
|
||
await acceptImageFile(file, sel && sel.type === "ref" ? sel.id : null);
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
function wireSplitter() {
|
||
const splitter = $2("sa_splitter");
|
||
const layout = $2("sa_layout");
|
||
const pane = $2("sa_image_pane");
|
||
if (!splitter || !layout || !pane) {
|
||
return;
|
||
}
|
||
let dragging = false;
|
||
splitter.addEventListener("mousedown", (e) => {
|
||
e.preventDefault();
|
||
dragging = true;
|
||
splitter.classList.add("sa-dragging");
|
||
document.body.style.cursor = "col-resize";
|
||
document.body.style.userSelect = "none";
|
||
});
|
||
window.addEventListener("mousemove", (e) => {
|
||
if (!dragging) {
|
||
return;
|
||
}
|
||
const rect = layout.getBoundingClientRect();
|
||
const x = e.clientX - rect.left;
|
||
const pct = Math.min(56, Math.max(22, x / rect.width * 100));
|
||
const value = `${pct}%`;
|
||
document.documentElement.style.setProperty("--sa-image-width", value);
|
||
localStorage.setItem(LS_PANE_WIDTH, value);
|
||
});
|
||
window.addEventListener("mouseup", () => {
|
||
if (!dragging) {
|
||
return;
|
||
}
|
||
dragging = false;
|
||
splitter.classList.remove("sa-dragging");
|
||
document.body.style.cursor = "";
|
||
document.body.style.userSelect = "";
|
||
saveUiStateToDisk();
|
||
});
|
||
}
|
||
function registerSendButton() {
|
||
if (typeof registerMediaButton !== "function") {
|
||
setTimeout(registerSendButton, 500);
|
||
return;
|
||
}
|
||
if (window.__swarmAssistentMediaRegistered) {
|
||
return;
|
||
}
|
||
window.__swarmAssistentMediaRegistered = true;
|
||
registerMediaButton(
|
||
"Send to Assistent",
|
||
(src) => {
|
||
putImageOnBoard(src, {
|
||
switchTab: true,
|
||
note: "Image sent to Assistent",
|
||
preferSelected: false
|
||
});
|
||
const pack = $2("sa_pack");
|
||
if (pack && (pack.value === "ordinary" || pack.value === "write_prompt")) {
|
||
pack.value = "critique_image";
|
||
saveSettings();
|
||
}
|
||
},
|
||
"Open Assistent with this image (vision / critique / prompt help)",
|
||
["image"],
|
||
true,
|
||
true
|
||
);
|
||
}
|
||
async function bootstrapPersisted() {
|
||
try {
|
||
await applyDiskUiState();
|
||
} catch (e) {
|
||
console.warn("Assistent: ui-state restore failed", e);
|
||
}
|
||
try {
|
||
await initChatSessions();
|
||
} catch (e) {
|
||
console.warn("Assistent: chat sessions failed", e);
|
||
}
|
||
loadConfig(localStorage.getItem(LS_PERSONA) || "neutral", () => {
|
||
refreshModels();
|
||
refreshInventory(() => {
|
||
renderLoraChips();
|
||
});
|
||
});
|
||
probeOllamaHealth();
|
||
}
|
||
function wire() {
|
||
if (!$2("swarm_assistent_root")) {
|
||
return;
|
||
}
|
||
if (typeof genericRequest !== "function") {
|
||
setTimeout(wire, 300);
|
||
return;
|
||
}
|
||
if (window.__swarmAssistentWired) {
|
||
return;
|
||
}
|
||
window.__swarmAssistentWired = true;
|
||
loadSettings();
|
||
setView(state.view || "chat");
|
||
updateGate();
|
||
ensureBoard();
|
||
setBoardTab(state.boardTab || "generate", { persist: false });
|
||
syncGenerateSlot();
|
||
if (wantsAutoVision()) {
|
||
refreshImagePreview();
|
||
}
|
||
bootstrapPersisted();
|
||
setChatsPanelOpen(!!state.chatsDrawerOpen);
|
||
wireDropZone();
|
||
wireSplitter();
|
||
registerSendButton();
|
||
wireSlashInput();
|
||
$2("sa_btn_new_chat")?.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
startNewChat({ saveCurrent: true, openDrawer: true });
|
||
});
|
||
$2("sa_btn_new_chat_bar")?.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
startNewChat({ saveCurrent: true, openDrawer: true });
|
||
});
|
||
$2("sa_ctx_chip")?.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
toggleCtxPanel();
|
||
});
|
||
$2("sa_ctx_close")?.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
toggleCtxPanel(false);
|
||
});
|
||
$2("sa_ctx_panel")?.addEventListener("click", (e) => e.stopPropagation());
|
||
$2("sa_ctx_compress")?.addEventListener("click", () => compressNowFromUi());
|
||
$2("sa_ctx_reset")?.addEventListener("click", () => resetCompressionFromUi());
|
||
$2("sa_ctx_auto")?.addEventListener("change", () => {
|
||
COMPRESS_AUTO = !!$2("sa_ctx_auto")?.checked;
|
||
const settingsAuto = $2("sa_compress_auto");
|
||
if (settingsAuto) {
|
||
settingsAuto.checked = COMPRESS_AUTO;
|
||
}
|
||
if (state.config?.assistant) {
|
||
state.config.assistant.compress_auto = COMPRESS_AUTO;
|
||
}
|
||
setStatus(COMPRESS_AUTO ? "\u0410\u0432\u0442\u043E\u0441\u0436\u0430\u0442\u0438\u0435 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u043E" : "\u0410\u0432\u0442\u043E\u0441\u0436\u0430\u0442\u0438\u0435 \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E");
|
||
});
|
||
$2("sa_compress_auto")?.addEventListener("change", () => {
|
||
COMPRESS_AUTO = !!$2("sa_compress_auto")?.checked;
|
||
const panelAuto = $2("sa_ctx_auto");
|
||
if (panelAuto) {
|
||
panelAuto.checked = COMPRESS_AUTO;
|
||
}
|
||
});
|
||
$2("sa_btn_chats")?.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
setChatsPanelOpen(!state.chatsPanelOpen);
|
||
});
|
||
$2("sa_btn_chats_close")?.addEventListener("click", () => setChatsPanelOpen(false));
|
||
$2("sa_chats_panel")?.addEventListener("click", (e) => e.stopPropagation());
|
||
$2("sa_chats_list")?.addEventListener("click", (e) => {
|
||
const row = e.target.closest(".sa-chat-row");
|
||
if (!row) {
|
||
return;
|
||
}
|
||
const id = row.dataset.id;
|
||
if (e.target.closest("[data-del]")) {
|
||
e.preventDefault();
|
||
if (window.confirm("\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u044D\u0442\u043E\u0442 \u0447\u0430\u0442 \u0438\u0437 \u0438\u0441\u0442\u043E\u0440\u0438\u0438?")) {
|
||
deleteChat(id);
|
||
}
|
||
return;
|
||
}
|
||
switchToChat(id);
|
||
});
|
||
let chatsSearchTimer = null;
|
||
$2("sa_chats_search")?.addEventListener("input", () => {
|
||
const q = ($2("sa_chats_search")?.value || "").trim();
|
||
state.chatsQuery = q;
|
||
if (!q) {
|
||
state.chatsSearchHits = null;
|
||
renderChatsList();
|
||
return;
|
||
}
|
||
renderChatsList();
|
||
clearTimeout(chatsSearchTimer);
|
||
chatsSearchTimer = setTimeout(async () => {
|
||
try {
|
||
const hits = await diskPersist()?.searchChats?.(q);
|
||
if ((state.chatsQuery || "") !== q) {
|
||
return;
|
||
}
|
||
state.chatsSearchHits = Array.isArray(hits) ? hits : [];
|
||
renderChatsList();
|
||
} catch (e) {
|
||
}
|
||
}, 220);
|
||
});
|
||
$2("sa_tab_chat")?.addEventListener("click", () => setView("chat"));
|
||
$2("sa_tab_train")?.addEventListener("click", () => setView("train"));
|
||
$2("sa_tab_settings")?.addEventListener("click", () => openSettings(state.settingsTab || "behavior"));
|
||
$2("sa_board_tab_gen")?.addEventListener("click", () => setBoardTab("generate"));
|
||
$2("sa_board_tab_refs")?.addEventListener("click", () => setBoardTab("refs"));
|
||
$2("sa_persona")?.addEventListener("change", onPersonaChanged);
|
||
$2("sa_persona_delete")?.addEventListener("click", () => deleteCurrentOverlayPersona());
|
||
document.querySelectorAll("#sa_settings .sa-stab").forEach((btn) => {
|
||
btn.addEventListener("click", () => setSettingsTab(btn.getAttribute("data-stab")));
|
||
});
|
||
$2("sa_btn_mem_refresh")?.addEventListener("click", () => {
|
||
refreshMemoryList();
|
||
});
|
||
$2("sa_mem_kind")?.addEventListener("change", renderMemoryList);
|
||
$2("sa_mem_scope")?.addEventListener("change", renderMemoryList);
|
||
$2("sa_mem_search")?.addEventListener("input", () => renderMemoryList());
|
||
$2("sa_btn_mem_clear_kind")?.addEventListener("click", () => {
|
||
const kind = memoryKindFilter();
|
||
clearCraftMemory({ kind: kind === "all" ? "" : kind, label: kind === "all" ? "\u0432\u0435\u0441\u044C \u043A\u0440\u0430\u0444\u0442 (\u0444\u0438\u043B\u044C\u0442\u0440 \u0442\u0438\u043F\u0430)" : `\u0442\u0438\u043F ${kind}` });
|
||
});
|
||
$2("sa_btn_mem_clear_persona")?.addEventListener("click", () => {
|
||
clearCraftMemory({ scope: "personal", persona: $2("sa_persona")?.value || "neutral", label: "\u043A\u0440\u0430\u0444\u0442 \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438" });
|
||
});
|
||
$2("sa_btn_mem_clear_shared")?.addEventListener("click", () => {
|
||
clearCraftMemory({ scope: "shared", label: "\u043E\u0431\u0449\u0443\u044E \u043A\u0440\u0430\u0444\u0442-\u043F\u0430\u043C\u044F\u0442\u044C" });
|
||
});
|
||
$2("sa_btn_mem_clear_all")?.addEventListener("click", () => {
|
||
clearCraftMemory({ label: "\u0432\u0435\u0441\u044C \u043A\u0440\u0430\u0444\u0442 (non-bundled)" });
|
||
});
|
||
$2("sa_btn_prefs_refresh")?.addEventListener("click", () => refreshUserPrefs());
|
||
$2("sa_btn_pref_add_global")?.addEventListener("click", () => addUserPref("global"));
|
||
$2("sa_btn_pref_add_persona")?.addEventListener("click", () => addUserPref("persona"));
|
||
$2("sa_btn_prefs_clear_global")?.addEventListener("click", () => clearUserPrefs("global"));
|
||
$2("sa_btn_prefs_clear_persona")?.addEventListener("click", () => clearUserPrefs("persona"));
|
||
$2("sa_btn_prefs_clear_all")?.addEventListener("click", () => clearUserPrefs("all"));
|
||
$2("sa_user_prefs_weight")?.addEventListener("input", () => {
|
||
const lab = $2("sa_user_prefs_weight_val");
|
||
if (lab) {
|
||
lab.textContent = Number($2("sa_user_prefs_weight").value).toFixed(1);
|
||
}
|
||
});
|
||
$2("sa_user_prefs_weight")?.addEventListener("change", () => saveKnobs());
|
||
$2("sa_memory_top_k")?.addEventListener("change", () => saveKnobs());
|
||
$2("sa_btn_knobs_save")?.addEventListener("click", () => saveKnobs());
|
||
$2("sa_btn_reset_ui")?.addEventListener("click", () => resetUiState());
|
||
$2("sa_btn_persona_export")?.addEventListener("click", () => exportSelectedPersona());
|
||
$2("sa_btn_persona_import")?.addEventListener("click", () => $2("sa_persona_import_file")?.click());
|
||
$2("sa_persona_import_file")?.addEventListener("change", (e) => {
|
||
const file = e.target?.files?.[0];
|
||
if (file) {
|
||
importPersonaFile(file);
|
||
}
|
||
e.target.value = "";
|
||
});
|
||
$2("sa_btn_persona_clone")?.addEventListener("click", () => cloneSelectedPersona());
|
||
$2("sa_btn_persona_delete_panel")?.addEventListener("click", () => deleteSelectedOverlayPersona());
|
||
$2("sa_btn_settings_health")?.addEventListener("click", () => {
|
||
probeOllamaHealth();
|
||
setTimeout(syncSettingsHealthLine, 400);
|
||
});
|
||
$2("sa_settings_chat_model")?.addEventListener("change", () => {
|
||
const v = $2("sa_settings_chat_model")?.value;
|
||
if (v && $2("sa_model")) {
|
||
$2("sa_model").value = v;
|
||
saveSettings();
|
||
}
|
||
});
|
||
$2("sa_btn_look_result")?.addEventListener("click", () => askLookAtResult());
|
||
$2("sa_ollama_health")?.addEventListener("click", () => probeOllamaHealth());
|
||
document.addEventListener("keydown", (e) => {
|
||
if (state.lightboxIndex >= 0) {
|
||
if (e.key === "Escape") {
|
||
e.preventDefault();
|
||
closeGenLightbox();
|
||
return;
|
||
}
|
||
if (e.key === "ArrowLeft") {
|
||
e.preventDefault();
|
||
stepGenLightbox(-1);
|
||
return;
|
||
}
|
||
if (e.key === "ArrowRight") {
|
||
e.preventDefault();
|
||
stepGenLightbox(1);
|
||
return;
|
||
}
|
||
}
|
||
if (e.key !== "Escape") {
|
||
return;
|
||
}
|
||
let closed = false;
|
||
if (state.view === "settings") {
|
||
closeSettings();
|
||
closed = true;
|
||
}
|
||
if (state.chatsPanelOpen) {
|
||
setChatsPanelOpen(false);
|
||
closed = true;
|
||
}
|
||
const slash = $2("sa_slash_menu");
|
||
if (slash && !slash.hidden) {
|
||
slash.hidden = true;
|
||
closed = true;
|
||
}
|
||
closeAllMoreMenus();
|
||
if (closed) {
|
||
e.preventDefault();
|
||
}
|
||
});
|
||
document.getElementById(TAB_BUTTON_ID)?.addEventListener("click", () => {
|
||
setTimeout(() => $2("sa_input")?.focus(), 80);
|
||
});
|
||
$2("sa_btn_refresh_models")?.addEventListener("click", () => {
|
||
saveSettings();
|
||
refreshModels();
|
||
probeOllamaHealth();
|
||
});
|
||
$2("sa_btn_refresh_inventory")?.addEventListener("click", () => refreshInventory(() => {
|
||
renderLoraChips();
|
||
}, { rescan: true }));
|
||
$2("sa_btn_add_ref")?.addEventListener("click", () => {
|
||
setBoardTab("refs");
|
||
addRefSlot({ select: true });
|
||
});
|
||
$2("sa_btn_use_current")?.addEventListener("click", () => snapshotGenerateToRef());
|
||
$2("sa_btn_as_init")?.addEventListener("click", async () => {
|
||
closeAllMoreMenus();
|
||
const src = selectedSrc() || findCurrentGenerateSrc();
|
||
if (!src) {
|
||
setStatus("\u041D\u0435\u0442 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F \u0434\u043B\u044F Init");
|
||
return;
|
||
}
|
||
await setInitFromSrc(src);
|
||
const pack = $2("sa_pack");
|
||
if (pack && (pack.value === "ordinary" || pack.value === "write_prompt")) {
|
||
setPackValue("inpaint_edit", { flash: true });
|
||
}
|
||
});
|
||
$2("sa_btn_as_mask")?.addEventListener("click", async () => {
|
||
closeAllMoreMenus();
|
||
const src = selectedSrc();
|
||
if (!src) {
|
||
setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043E\u043A\u043D\u043E \u0441 \u043C\u0430\u0441\u043A\u043E\u0439");
|
||
return;
|
||
}
|
||
await setMaskFromSrc(src);
|
||
setPackValue("inpaint_edit", { flash: true });
|
||
});
|
||
$2("sa_btn_clear_init")?.addEventListener("click", () => {
|
||
if (window.confirm("\u0421\u0431\u0440\u043E\u0441\u0438\u0442\u044C Init \u0438 Mask?")) {
|
||
clearInitAndMask();
|
||
}
|
||
closeAllMoreMenus();
|
||
});
|
||
$2("sa_btn_clear_image")?.addEventListener("click", () => clearSlot(state.selectedSlotId));
|
||
$2("sa_btn_board_more")?.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
toggleMoreMenu("sa_board_more_menu", "sa_btn_board_more");
|
||
});
|
||
$2("sa_btn_send")?.addEventListener("click", () => sendChat());
|
||
$2("sa_btn_build_gen")?.addEventListener("click", () => buildCurrentAndGenerate());
|
||
$2("sa_btn_interrupt")?.addEventListener("click", () => {
|
||
doInterruptNow();
|
||
});
|
||
$2("sa_btn_clear")?.addEventListener("click", () => {
|
||
if (window.confirm("\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0435\u0441\u044C \u0447\u0430\u0442 Assistent?")) {
|
||
clearChatHistory();
|
||
}
|
||
});
|
||
$2("sa_btn_clear_more")?.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
toggleMoreMenu("sa_clear_more_menu", "sa_btn_clear_more");
|
||
});
|
||
$2("sa_btn_clear_confirm")?.addEventListener("click", () => {
|
||
closeAllMoreMenus();
|
||
if (window.confirm("\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0435\u0441\u044C \u0447\u0430\u0442 Assistent?")) {
|
||
clearChatHistory();
|
||
}
|
||
});
|
||
$2("sa_btn_clear_patches")?.addEventListener("click", () => {
|
||
closeAllMoreMenus();
|
||
clearPatchBlocksOnly();
|
||
});
|
||
document.addEventListener("click", () => {
|
||
if (state.chatsPanelOpen) {
|
||
setChatsPanelOpen(false);
|
||
}
|
||
if (state.ctxPanelOpen) {
|
||
toggleCtxPanel(false);
|
||
}
|
||
closeAllMoreMenus();
|
||
});
|
||
$2("sa_board_more_menu")?.addEventListener("click", (e) => e.stopPropagation());
|
||
$2("sa_clear_more_menu")?.addEventListener("click", (e) => e.stopPropagation());
|
||
$2("sa_base_url")?.addEventListener("change", saveSettings);
|
||
$2("sa_model")?.addEventListener("change", () => {
|
||
const v = $2("sa_model")?.value;
|
||
if (v && $2("sa_settings_chat_model")) {
|
||
$2("sa_settings_chat_model").value = v;
|
||
}
|
||
saveSettings();
|
||
});
|
||
$2("sa_embed_model")?.addEventListener("change", () => {
|
||
state.preferredEmbed = $2("sa_embed_model")?.value || "";
|
||
saveSettings();
|
||
});
|
||
$2("sa_pack")?.addEventListener("change", () => {
|
||
state.packUserTouched = true;
|
||
saveSettings();
|
||
syncModeBadge();
|
||
});
|
||
$2("sa_chips")?.addEventListener("click", async (e) => {
|
||
const btn = e.target.closest(".sa-chip");
|
||
if (!btn) {
|
||
return;
|
||
}
|
||
if (state.busy || state.generating) {
|
||
setStatus("\u0417\u0430\u043D\u044F\u0442\u043E \u2014 \u043F\u043E\u0434\u043E\u0436\u0434\u0438 \u0438\u043B\u0438 \u043D\u0430\u0436\u043C\u0438 \u0421\u0442\u043E\u043F");
|
||
return;
|
||
}
|
||
const aspect = btn.getAttribute("data-aspect");
|
||
const seed = btn.getAttribute("data-seed");
|
||
const vary = btn.getAttribute("data-vary");
|
||
const profile = btn.getAttribute("data-krea-profile");
|
||
if (aspect) {
|
||
await applyQuickPatch({ aspect, actions: ["generate"] }, `Aspect ${aspect}`);
|
||
} else if (seed === "lock") {
|
||
await applyQuickPatch({ lock_seed: true }, "Seed locked");
|
||
} else if (seed === "random") {
|
||
await applyQuickPatch({ seed: -1, actions: ["generate"] }, "Seed random");
|
||
} else if (vary) {
|
||
await applyQuickPatch({ vary: true, seed: -1, actions: ["generate"] }, "Vary");
|
||
} else if (profile === "turbo") {
|
||
const p = state.kreaProfiles?.turbo || mergedGenerationDefaults("turbo");
|
||
await applyQuickPatch({ steps: p.steps ?? 8, cfg: p.cfg ?? 1, sigma_shift: p.sigma_shift ?? 1.15, actions: ["generate"] }, "Turbo");
|
||
} else if (profile === "raw") {
|
||
const p = state.kreaProfiles?.raw || mergedGenerationDefaults("raw");
|
||
await applyQuickPatch({ steps: p.steps ?? 28, cfg: p.cfg ?? 4.5, sigma_shift: p.sigma_shift, actions: ["generate"] }, "RAW");
|
||
}
|
||
renderLoraChips();
|
||
});
|
||
$2("sa_auto_vision")?.addEventListener("change", () => {
|
||
saveSettings();
|
||
const gen = generateSlot();
|
||
if (gen) {
|
||
gen.attach = wantsAutoVision();
|
||
renderBoard();
|
||
}
|
||
});
|
||
$2("sa_auto_apply")?.addEventListener("change", saveSettings);
|
||
$2("sa_auto_generate")?.addEventListener("change", saveSettings);
|
||
$2("sa_auto_critique")?.addEventListener("change", saveSettings);
|
||
$2("sa_auto_download")?.addEventListener("change", saveSettings);
|
||
$2("sa_park_llm")?.addEventListener("change", saveSettings);
|
||
syncChipHighlight();
|
||
setInterval(syncChipHighlight, 2500);
|
||
setInterval(renderLoraChips, 4e3);
|
||
syncLiveParamsBar();
|
||
setInterval(syncLiveParamsBar, 1200);
|
||
syncModeBadge();
|
||
syncBuildGenButton();
|
||
setInterval(updateGate, 2e3);
|
||
setInterval(syncGenerateSlot, 700);
|
||
setInterval(() => {
|
||
if (!state.busy && !state.generating) {
|
||
probeOllamaHealth();
|
||
}
|
||
}, 45e3);
|
||
setInterval(() => {
|
||
if (!state.busy && !state.generating) {
|
||
}
|
||
}, 12e4);
|
||
window.addEventListener("beforeunload", () => {
|
||
try {
|
||
saveActiveChatToStore({ dropEmpty: true });
|
||
const chat = findChat(state.activeChatId);
|
||
if (chat && (chat.messages || []).length) {
|
||
diskPersist()?.saveChat(chat, { immediate: true });
|
||
}
|
||
diskPersist()?.saveUiState(collectUiState(), { immediate: true });
|
||
} catch (e) {
|
||
}
|
||
});
|
||
setInterval(() => {
|
||
if (!state.busy) {
|
||
const tabOn = !!document.getElementById(TAB_BUTTON_ID)?.classList.contains("tab-button-selected") || !!document.getElementById("swarm_assistent_root")?.offsetParent;
|
||
refreshInventory(null, { rescan: inventoryIsStale(tabOn ? 45e3 : 12e4) });
|
||
}
|
||
}, 3e4);
|
||
window.SA = window.SA || {};
|
||
window.SA.app = {
|
||
getState: () => state,
|
||
setTrainingLock(on) {
|
||
state.trainingLock = !!on;
|
||
},
|
||
refreshModels: () => refreshModels(),
|
||
setStatus: (msg) => setStatus(msg)
|
||
};
|
||
window.swarmAssistent = {
|
||
setImageFromSrc,
|
||
putImageOnBoard,
|
||
clearVisionImage,
|
||
snapshotGenerateToRef,
|
||
openAssistentTab,
|
||
sendToAssistent: (src) => {
|
||
putImageOnBoard(src, { switchTab: true, note: "\u0418\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E \u0432 Assistent", preferSelected: false });
|
||
setBoardTab("refs");
|
||
},
|
||
isKreaSelected,
|
||
resolveCurrentCheckpoint,
|
||
refreshInventory,
|
||
applyPatch,
|
||
triggerGenerate,
|
||
setInitFromSrc,
|
||
setMaskFromSrc,
|
||
clearInitAndMask,
|
||
slotById,
|
||
renderBoard,
|
||
setBoardTab
|
||
};
|
||
}
|
||
function wireSlashInput() {
|
||
const input = $2("sa_input");
|
||
if (!input || input.dataset.saSlashWired) {
|
||
return;
|
||
}
|
||
input.dataset.saSlashWired = "1";
|
||
input.addEventListener("input", () => updateSlashMenuFromInput());
|
||
input.addEventListener("keydown", (e) => {
|
||
const menu = $2("sa_slash_menu");
|
||
const open = menu && !menu.hidden;
|
||
if (open) {
|
||
const items = slashMatches(input.value.split(/\s/)[0] || "");
|
||
if (e.key === "ArrowDown") {
|
||
e.preventDefault();
|
||
state.slashIndex = Math.min(items.length - 1, (state.slashIndex || 0) + 1);
|
||
renderSlashMenu(items);
|
||
return;
|
||
}
|
||
if (e.key === "ArrowUp") {
|
||
e.preventDefault();
|
||
state.slashIndex = Math.max(0, (state.slashIndex || 0) - 1);
|
||
renderSlashMenu(items);
|
||
return;
|
||
}
|
||
if (e.key === "Tab" || e.key === "Enter" && !e.shiftKey) {
|
||
const pick = items[state.slashIndex || 0];
|
||
if (pick && input.value.trim() === (input.value.split(/\s/)[0] || "")) {
|
||
e.preventDefault();
|
||
applySlashPick(pick);
|
||
return;
|
||
}
|
||
}
|
||
if (e.key === "Escape") {
|
||
hideSlashMenu();
|
||
return;
|
||
}
|
||
}
|
||
if (e.key === "Enter" && !e.shiftKey && !e.altKey) {
|
||
e.preventDefault();
|
||
hideSlashMenu();
|
||
sendChat();
|
||
}
|
||
});
|
||
input.addEventListener("blur", () => setTimeout(hideSlashMenu, 150));
|
||
}
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", wire);
|
||
} else {
|
||
wire();
|
||
}
|
||
})();
|
||
|
||
// src/training.js
|
||
var $ = (id) => document.getElementById(id);
|
||
function escapeHtml(s) {
|
||
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||
}
|
||
function attachTraining(SA2) {
|
||
const state = {
|
||
ttab: "dataset",
|
||
samples: [],
|
||
hfResults: [],
|
||
hfSelected: null,
|
||
hfCheck: null,
|
||
hfMapping: null,
|
||
trainWs: null,
|
||
polling: null,
|
||
agentSettings: { enabled: true, auto_link_on_approve: true, heard_quota: 3 },
|
||
agentLinked: 0
|
||
};
|
||
function setAgentHeardStats(linked) {
|
||
const el = $("sa_agent_heard_stats");
|
||
if (el) {
|
||
el.textContent = `\u041F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u043E: ${linked ?? state.agentLinked ?? "\u2014"}`;
|
||
}
|
||
}
|
||
async function loadAgentHeardSettings() {
|
||
try {
|
||
const data = await SA2.request("AssistentGetDatasetAgentSettings", {});
|
||
const s = data?.settings || {};
|
||
state.agentSettings = {
|
||
enabled: s.enabled !== false,
|
||
auto_link_on_approve: s.auto_link_on_approve !== false,
|
||
heard_quota: s.heard_quota ?? 3
|
||
};
|
||
state.agentLinked = data?.linked ?? 0;
|
||
if ($("sa_agent_heard_enabled")) $("sa_agent_heard_enabled").checked = state.agentSettings.enabled;
|
||
if ($("sa_agent_auto_link")) $("sa_agent_auto_link").checked = state.agentSettings.auto_link_on_approve;
|
||
if ($("sa_agent_heard_quota")) $("sa_agent_heard_quota").value = String(state.agentSettings.heard_quota);
|
||
setAgentHeardStats(state.agentLinked);
|
||
} catch (e) {
|
||
console.warn("loadAgentHeardSettings", e);
|
||
}
|
||
}
|
||
async function saveAgentHeardSettings() {
|
||
const settings = {
|
||
enabled: !!$("sa_agent_heard_enabled")?.checked,
|
||
auto_link_on_approve: !!$("sa_agent_auto_link")?.checked,
|
||
heard_quota: Math.max(0, Math.min(8, parseInt($("sa_agent_heard_quota")?.value, 10) || 3))
|
||
};
|
||
try {
|
||
const data = await SA2.request("AssistentSaveDatasetAgentSettings", { settings });
|
||
state.agentSettings = data?.settings || settings;
|
||
setTrainStatus("\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \xAB\u0443\u0441\u043B\u044B\u0448\u0430\u043D\u043D\u043E\u0433\u043E\xBB \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u044B");
|
||
} catch (e) {
|
||
setTrainStatus(String(e.message || e));
|
||
}
|
||
}
|
||
async function syncAllToAgent() {
|
||
setTrainStatus("\u041F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u043A \u0430\u0433\u0435\u043D\u0442\u0443\u2026");
|
||
try {
|
||
await saveAgentHeardSettings();
|
||
const data = await SA2.request("AssistentSyncDatasetToAgent", { approved_only: true, relink: false });
|
||
state.agentLinked = data?.total_linked ?? state.agentLinked;
|
||
setAgentHeardStats(state.agentLinked);
|
||
setTrainStatus(`\u041F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u043E: +${data?.linked_now ?? 0}, \u0432\u0441\u0435\u0433\u043E ${data?.total_linked ?? "\u2014"}`);
|
||
await refreshSamples();
|
||
} catch (e) {
|
||
setTrainStatus(String(e.message || e));
|
||
}
|
||
}
|
||
function setTrainStatus(msg) {
|
||
const el = $("sa_train_status");
|
||
if (el) el.textContent = msg || "";
|
||
}
|
||
function setTrainingTab(id) {
|
||
state.ttab = id || "dataset";
|
||
document.querySelectorAll("#sa_training .sa-ttab").forEach((btn) => {
|
||
const on = btn.getAttribute("data-ttab") === state.ttab;
|
||
btn.classList.toggle("sa-ttab-active", on);
|
||
btn.setAttribute("aria-selected", on ? "true" : "false");
|
||
});
|
||
document.querySelectorAll("#sa_training .sa-tpane").forEach((pane) => {
|
||
pane.hidden = pane.getAttribute("data-tpane") !== state.ttab;
|
||
});
|
||
if (state.ttab === "dataset") {
|
||
refreshSamples();
|
||
loadAgentHeardSettings();
|
||
}
|
||
if (state.ttab === "train") {
|
||
syncModelfileModels();
|
||
syncQloraModels();
|
||
}
|
||
if (state.ttab === "models") refreshTrainModels();
|
||
}
|
||
async function refreshSamples() {
|
||
try {
|
||
const status = $("sa_train_filter_status")?.value || "all";
|
||
const persona = $("sa_train_filter_persona")?.value || "all";
|
||
const data = await SA2.request("AssistentListTrainSamples", { status, persona, limit: 300 });
|
||
state.samples = data?.samples || [];
|
||
const stats = $("sa_train_stats");
|
||
if (stats) stats.textContent = `\u041E\u0434\u043E\u0431\u0440\u0435\u043D\u043E: ${data?.approved ?? "\u2014"} \xB7 \u0432\u0441\u0435\u0433\u043E: ${data?.total ?? "\u2014"}`;
|
||
const personaSel = $("sa_train_filter_persona");
|
||
if (personaSel && $("sa_persona")) {
|
||
const cur = personaSel.value || "all";
|
||
personaSel.innerHTML = '<option value="all">\u0412\u0441\u0435 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438</option>';
|
||
for (const opt of $("sa_persona").options) {
|
||
const o = document.createElement("option");
|
||
o.value = opt.value;
|
||
o.textContent = opt.textContent;
|
||
personaSel.appendChild(o);
|
||
}
|
||
personaSel.value = cur;
|
||
}
|
||
renderSamples();
|
||
} catch (e) {
|
||
setTrainStatus(String(e.message || e));
|
||
}
|
||
}
|
||
function renderSamples() {
|
||
const root = $("sa_train_samples");
|
||
if (!root) return;
|
||
if (!state.samples.length) {
|
||
root.innerHTML = '<div class="sa-mem-empty">\u041D\u0435\u0442 \u043F\u0440\u0438\u043C\u0435\u0440\u043E\u0432. \u041E\u0442\u043C\u0435\u0442\u044C \u043E\u0442\u0432\u0435\u0442\u044B \u0432 \u0447\u0430\u0442\u0435 \u0438\u043B\u0438 \u0438\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u0439 \u0434\u0430\u0442\u0430\u0441\u0435\u0442.</div>';
|
||
return;
|
||
}
|
||
root.innerHTML = "";
|
||
for (const s of state.samples) {
|
||
const div = document.createElement("div");
|
||
div.className = "sa-train-sample";
|
||
div.dataset.id = s.id;
|
||
const msgs = s.messages || [];
|
||
const preview = msgs.map((m) => `${m.role}: ${(m.content || "").slice(0, 120)}`).join("\n");
|
||
const linked = s.agent_linked ? " \xB7 \u{1F517} \u0430\u0433\u0435\u043D\u0442" : "";
|
||
div.innerHTML = `
|
||
<div class="sa-train-sample-head">
|
||
<span class="sa-hf-badge sa-hf-badge-${s.status === "approved" ? "ok" : s.status === "rejected" ? "no" : "map"}">${escapeHtml(s.status)}</span>
|
||
<span>${escapeHtml(s.source)} \xB7 ${escapeHtml(s.persona || "\u2014")} \xB7 ${escapeHtml(s.pack || "\u2014")}${linked}</span>
|
||
<button type="button" class="basic-button" data-approve="1">\u2713</button>
|
||
<button type="button" class="basic-button" data-reject="1">\u2715</button>
|
||
<button type="button" class="basic-button" data-link="1" title="\u041F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043A \u0430\u0433\u0435\u043D\u0442\u0443">\u{1F517}</button>
|
||
<button type="button" class="basic-button" data-unlink="1" title="\u041E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043E\u0442 \u0430\u0433\u0435\u043D\u0442\u0430">\u26D3</button>
|
||
<button type="button" class="basic-button sa-danger-btn" data-del="1">\u0423\u0434\u0430\u043B\u0438\u0442\u044C</button>
|
||
</div>
|
||
<textarea spellcheck="false">${escapeHtml(preview)}</textarea>`;
|
||
root.appendChild(div);
|
||
}
|
||
}
|
||
async function upsertSample(patch) {
|
||
await SA2.request("AssistentUpsertTrainSample", patch);
|
||
await refreshSamples();
|
||
}
|
||
function hfStringColumns(check) {
|
||
const cols = check?.schema?.columns;
|
||
if (Array.isArray(cols) && cols.length) return cols;
|
||
const feats = check?.features;
|
||
if (Array.isArray(feats)) {
|
||
return feats.map((f) => f?.name).filter(Boolean);
|
||
}
|
||
if (feats && typeof feats === "object") return Object.keys(feats);
|
||
return [];
|
||
}
|
||
function renderHfMappingUI(check) {
|
||
const row = $("sa_hf_mapping_row");
|
||
if (!row) return;
|
||
const gate = check?.gate;
|
||
const schemaKind = check?.schema?.kind;
|
||
const needsMapping = gate === "mapping" || schemaKind === "fiction_tags_text";
|
||
row.hidden = !needsMapping;
|
||
if (!needsMapping) {
|
||
state.hfMapping = null;
|
||
return;
|
||
}
|
||
const cols = hfStringColumns(check);
|
||
const userSel = $("sa_hf_user_col");
|
||
const asstSel = $("sa_hf_asst_col");
|
||
const presetSel = $("sa_hf_mapping_preset");
|
||
if (userSel) {
|
||
userSel.innerHTML = cols.map((c) => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`).join("");
|
||
if (cols.includes("tags")) userSel.value = "tags";
|
||
else if (cols.includes("title")) userSel.value = "title";
|
||
}
|
||
if (asstSel) {
|
||
asstSel.innerHTML = cols.map((c) => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`).join("");
|
||
if (cols.includes("text")) asstSel.value = "text";
|
||
else if (cols.includes("output")) asstSel.value = "output";
|
||
}
|
||
if (schemaKind === "fiction_tags_text" && presetSel) {
|
||
presetSel.value = "fiction_tags_text";
|
||
state.hfMapping = { kind: "fiction_tags_text", preset: "fiction_tags_text" };
|
||
}
|
||
}
|
||
function buildHfMappingPayload() {
|
||
const preset = $("sa_hf_mapping_preset")?.value;
|
||
if (preset === "fiction_tags_text") {
|
||
return { kind: "fiction_tags_text", preset: "fiction_tags_text" };
|
||
}
|
||
const userCol = $("sa_hf_user_col")?.value;
|
||
const asstCol = $("sa_hf_asst_col")?.value;
|
||
if (userCol && asstCol) {
|
||
return { kind: "custom", user_col: userCol, assistant_col: asstCol };
|
||
}
|
||
return state.hfMapping;
|
||
}
|
||
async function syncQloraModels() {
|
||
try {
|
||
const baseUrl = $("sa_base_url")?.value || localStorage.getItem("swarm_assistent_base_url") || "";
|
||
const data = await SA2.request("AssistentListModels", { baseUrl });
|
||
const models = data?.models || [];
|
||
const sel = $("sa_qlora_ollama_base");
|
||
if (!sel) return;
|
||
const cur = sel.value;
|
||
sel.innerHTML = '<option value="">\u2014</option>';
|
||
for (const m of models) {
|
||
const opt = document.createElement("option");
|
||
opt.value = m;
|
||
opt.textContent = m;
|
||
sel.appendChild(opt);
|
||
}
|
||
if (cur) sel.value = cur;
|
||
else if ($("sa_model")?.value) sel.value = $("sa_model").value;
|
||
} catch (e) {
|
||
}
|
||
}
|
||
function renderHfList() {
|
||
const root = $("sa_hf_list");
|
||
if (!root) return;
|
||
root.innerHTML = "";
|
||
const showAll = !!$("sa_hf_show_all")?.checked;
|
||
for (const r of state.hfResults) {
|
||
if (!showAll && r.gate === "rejected") continue;
|
||
const row = document.createElement("div");
|
||
row.className = "sa-hf-row" + (state.hfSelected === r.id ? " sa-hf-row-active" : "") + (r.gate === "rejected" ? " sa-hf-rejected" : "");
|
||
row.dataset.id = r.id;
|
||
const badge = r.gate === "ok" ? "ok" : r.gate === "mapping" ? "map" : "no";
|
||
row.innerHTML = `<span class="sa-hf-badge sa-hf-badge-${badge}">${escapeHtml(r.gate)}</span><strong>${escapeHtml(r.id)}</strong><span>${escapeHtml(r.reason || "")}</span>`;
|
||
root.appendChild(row);
|
||
}
|
||
}
|
||
async function searchHf() {
|
||
const q = ($("sa_hf_search")?.value || "").trim();
|
||
setTrainStatus("\u041F\u043E\u0438\u0441\u043A\u2026");
|
||
try {
|
||
const data = await SA2.request("AssistentSearchHfDatasets", {
|
||
q,
|
||
limit: 24,
|
||
show_all: !!$("sa_hf_show_all")?.checked
|
||
});
|
||
state.hfResults = data?.results || [];
|
||
renderHfList();
|
||
setTrainStatus(`\u041D\u0430\u0439\u0434\u0435\u043D\u043E: ${state.hfResults.length}`);
|
||
} catch (e) {
|
||
setTrainStatus(String(e.message || e));
|
||
}
|
||
}
|
||
async function checkHfLink() {
|
||
const link = ($("sa_hf_link")?.value || "").trim();
|
||
const status = $("sa_hf_status");
|
||
if (!link) return;
|
||
if (status) status.textContent = "\u041F\u0440\u043E\u0432\u0435\u0440\u044F\u044E\u2026";
|
||
try {
|
||
const data = await SA2.request("AssistentCheckHfDataset", { dataset: link });
|
||
state.hfCheck = data;
|
||
state.hfSelected = data.id;
|
||
if (status) {
|
||
status.textContent = data.gate === "rejected" ? `\u041E\u0442\u043A\u043B\u043E\u043D\u0435\u043D\u043E: ${data.reason}` : `${data.gate}: ${data.reason || "OK"}`;
|
||
}
|
||
const preview = $("sa_hf_preview");
|
||
if (preview) {
|
||
preview.hidden = false;
|
||
preview.textContent = JSON.stringify(data.sample_rows || data.features || data, null, 2).slice(0, 8e3);
|
||
}
|
||
const importRow = $("sa_hf_import_row");
|
||
if (importRow) importRow.hidden = data.gate === "rejected";
|
||
renderHfMappingUI(data);
|
||
} catch (e) {
|
||
if (status) status.textContent = String(e.message || e);
|
||
}
|
||
}
|
||
async function importHf() {
|
||
if (!state.hfSelected && !state.hfCheck?.id) {
|
||
setTrainStatus("\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043D\u0430\u0431\u043E\u0440");
|
||
return;
|
||
}
|
||
const id = state.hfSelected || state.hfCheck.id;
|
||
const limit = Number($("sa_hf_import_limit")?.value) || 200;
|
||
const mapping = buildHfMappingPayload();
|
||
try {
|
||
const data = await SA2.request("AssistentImportHfDataset", { dataset: id, limit, mapping });
|
||
setTrainStatus(`\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043E: ${data.imported}${data.runner_only ? " (runner-only)" : ""}`);
|
||
await refreshSamples();
|
||
} catch (e) {
|
||
setTrainStatus(String(e.message || e));
|
||
}
|
||
}
|
||
async function syncModelfileModels() {
|
||
try {
|
||
const baseUrl = $("sa_base_url")?.value || localStorage.getItem("swarm_assistent_base_url") || "";
|
||
const data = await SA2.request("AssistentListModels", { baseUrl });
|
||
const models = data?.models || [];
|
||
for (const selId of ["sa_modelfile_base"]) {
|
||
const sel = $(selId);
|
||
if (!sel) continue;
|
||
const cur = sel.value;
|
||
sel.innerHTML = '<option value="">\u2014</option>';
|
||
for (const m of models) {
|
||
const opt = document.createElement("option");
|
||
opt.value = m;
|
||
opt.textContent = m;
|
||
sel.appendChild(opt);
|
||
}
|
||
if (cur) sel.value = cur;
|
||
}
|
||
const personaSel = $("sa_modelfile_persona");
|
||
if (personaSel && $("sa_persona")) {
|
||
personaSel.innerHTML = $("sa_persona").innerHTML;
|
||
personaSel.value = $("sa_persona").value || "neutral";
|
||
}
|
||
} catch (e) {
|
||
}
|
||
}
|
||
async function createModelfile() {
|
||
setTrainStatus("\u0421\u043E\u0437\u0434\u0430\u044E \u043C\u043E\u0434\u0435\u043B\u044C\u2026");
|
||
try {
|
||
const data = await SA2.request("AssistentCreateOllamaModel", {
|
||
base_url: $("sa_base_url")?.value,
|
||
base_model: $("sa_modelfile_base")?.value,
|
||
name: $("sa_modelfile_name")?.value,
|
||
persona: $("sa_modelfile_persona")?.value,
|
||
system: $("sa_modelfile_system")?.value,
|
||
shots: Number($("sa_modelfile_shots")?.value) || 8,
|
||
num_ctx: Number($("sa_modelfile_num_ctx")?.value) || 16384,
|
||
temperature: Number($("sa_modelfile_temp")?.value) || 0.7
|
||
});
|
||
setTrainStatus(`\u0413\u043E\u0442\u043E\u0432\u043E: ${data.name}`);
|
||
SA2.app?.refreshModels?.();
|
||
} catch (e) {
|
||
setTrainStatus(String(e.message || e));
|
||
}
|
||
}
|
||
function setTrainMode(mode) {
|
||
$("sa_train_form_modelfile").hidden = mode !== "modelfile";
|
||
$("sa_train_form_qlora").hidden = mode !== "qlora";
|
||
}
|
||
function setTrainingLock(on, text) {
|
||
const root = $("swarm_assistent_root");
|
||
const banner = $("sa_train_banner");
|
||
if (root) root.classList.toggle("sa-root-training-lock", !!on);
|
||
if (banner) {
|
||
banner.hidden = !on;
|
||
const t = $("sa_train_banner_text");
|
||
if (t && text) t.textContent = text;
|
||
}
|
||
SA2.app?.setTrainingLock?.(!!on);
|
||
}
|
||
async function pollTrainJob() {
|
||
try {
|
||
const data = await SA2.request("AssistentGetTrainJob", {});
|
||
const prog = data?.progress || (data?.job?.progress_json ? JSON.parse(data.job.progress_json) : null);
|
||
const active = data?.training_active || data?.job?.status === "running";
|
||
const status = data?.job?.status || prog?.status;
|
||
setTrainingLock(active, prog?.status === "running" ? `\u0422\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430 \xB7 ${prog?.percent ?? 0}%` : "\u0418\u0434\u0451\u0442 \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430\u2026");
|
||
const logEl = $("sa_train_log");
|
||
const bar = $("sa_train_progress_fill");
|
||
const box = $("sa_train_progress");
|
||
if (prog) {
|
||
if (box) box.hidden = false;
|
||
if (bar && prog.percent != null) bar.style.width = `${prog.percent}%`;
|
||
if (logEl && prog.log) logEl.textContent = prog.log;
|
||
}
|
||
if (!active) {
|
||
clearInterval(state.polling);
|
||
state.polling = null;
|
||
$("sa_btn_qlora_cancel").hidden = true;
|
||
setTrainingLock(false);
|
||
if (status === "completed" || status === "completed_with_warnings") {
|
||
const ollama = prog?.ollama;
|
||
if (ollama?.success) {
|
||
setTrainStatus(`\u0413\u043E\u0442\u043E\u0432\u043E: \u043C\u043E\u0434\u0435\u043B\u044C ${ollama.name} \u0432 Ollama`);
|
||
SA2.app?.refreshModels?.();
|
||
} else if (ollama?.skipped) {
|
||
setTrainStatus(ollama.note || ollama.error || "\u0410\u0434\u0430\u043F\u0442\u0435\u0440 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D, Ollama \u2014 \u0432\u0440\u0443\u0447\u043D\u0443\u044E");
|
||
} else if (ollama?.error) {
|
||
setTrainStatus(`\u041E\u0431\u0443\u0447\u0435\u043D\u0438\u0435 OK, Ollama: ${ollama.error}`);
|
||
} else if (status === "completed_with_warnings") {
|
||
setTrainStatus("\u041E\u0431\u0443\u0447\u0435\u043D\u0438\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E \u0441 \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u044F\u043C\u0438 \u2014 \u0441\u043C. \u043B\u043E\u0433");
|
||
} else {
|
||
setTrainStatus("QLoRA \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E");
|
||
}
|
||
} else if (status === "failed") {
|
||
setTrainStatus(`\u041E\u0448\u0438\u0431\u043A\u0430 \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0438 (exit ${prog?.exit_code ?? "?"})`);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
}
|
||
}
|
||
async function startQlora() {
|
||
setTrainStatus("\u0417\u0430\u043F\u0443\u0441\u043A\u2026");
|
||
try {
|
||
const hfDs = ($("sa_qlora_hf_dataset")?.value || "").trim();
|
||
const mapping = hfDs ? buildHfMappingPayload() : void 0;
|
||
await SA2.request("AssistentStartTrainJob", {
|
||
base_url: $("sa_base_url")?.value,
|
||
chat_model: $("sa_model")?.value,
|
||
base_model: $("sa_qlora_base")?.value,
|
||
ollama_base: $("sa_qlora_ollama_base")?.value,
|
||
output_name: $("sa_qlora_name")?.value,
|
||
rank: Number($("sa_qlora_rank")?.value) || 16,
|
||
alpha: Number($("sa_qlora_alpha")?.value) || 32,
|
||
lr: Number($("sa_qlora_lr")?.value) || 2e-4,
|
||
epochs: Number($("sa_qlora_epochs")?.value) || 3,
|
||
seq_len: Number($("sa_qlora_seq")?.value) || 2048,
|
||
max_samples: Number($("sa_qlora_max_samples")?.value) || 0,
|
||
four_bit: !!$("sa_qlora_4bit")?.checked,
|
||
hf_dataset: hfDs || void 0,
|
||
hf_mapping: mapping
|
||
});
|
||
$("sa_btn_qlora_cancel").hidden = false;
|
||
setTrainingLock(true, "\u0418\u0434\u0451\u0442 \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430\u2026");
|
||
if (state.polling) clearInterval(state.polling);
|
||
state.polling = setInterval(pollTrainJob, 1500);
|
||
pollTrainJob();
|
||
setTrainStatus("\u0422\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430 \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u0430");
|
||
} catch (e) {
|
||
setTrainStatus(String(e.message || e));
|
||
}
|
||
}
|
||
async function cancelQlora() {
|
||
try {
|
||
await SA2.request("AssistentCancelTrainJob", {});
|
||
setTrainingLock(false);
|
||
setTrainStatus("\u041E\u0442\u043C\u0435\u043D\u0435\u043D\u043E");
|
||
} catch (e) {
|
||
setTrainStatus(String(e.message || e));
|
||
}
|
||
}
|
||
async function refreshTrainModels() {
|
||
const root = $("sa_train_models_list");
|
||
if (!root) return;
|
||
try {
|
||
const data = await SA2.request("AssistentListModels", { baseUrl: $("sa_base_url")?.value });
|
||
const models = data?.models || [];
|
||
root.innerHTML = models.length ? models.map((m) => `<div class="sa-hf-row"><strong>${escapeHtml(m)}</strong></div>`).join("") : '<div class="sa-mem-empty">\u041D\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0435\u0439</div>';
|
||
} catch (e) {
|
||
root.innerHTML = `<div class="sa-mem-empty">${escapeHtml(e.message)}</div>`;
|
||
}
|
||
}
|
||
async function saveRunner() {
|
||
try {
|
||
await SA2.request("AssistentSaveRunnerSettings", {
|
||
python: $("sa_runner_python")?.value,
|
||
kind: $("sa_runner_kind")?.value || "builtin",
|
||
workdir: $("sa_runner_workdir")?.value,
|
||
cmd: $("sa_runner_cmd")?.value,
|
||
gguf_script: $("sa_runner_gguf_script")?.value,
|
||
gguf_base_path: $("sa_runner_gguf_base")?.value,
|
||
gguf_cmd: $("sa_runner_gguf_cmd")?.value
|
||
});
|
||
setTrainStatus("\u0420\u0430\u043D\u043D\u0435\u0440 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D");
|
||
} catch (e) {
|
||
setTrainStatus(String(e.message || e));
|
||
}
|
||
}
|
||
async function loadRunner() {
|
||
try {
|
||
const data = await SA2.request("AssistentGetRunnerSettings", {});
|
||
const s = data?.settings || {};
|
||
if ($("sa_runner_python") && s.python) $("sa_runner_python").value = s.python;
|
||
if ($("sa_runner_kind")) $("sa_runner_kind").value = s.kind || "builtin";
|
||
if ($("sa_runner_workdir") && s.workdir) $("sa_runner_workdir").value = s.workdir;
|
||
if ($("sa_runner_cmd") && s.cmd) $("sa_runner_cmd").value = s.cmd;
|
||
if ($("sa_runner_gguf_script") && s.gguf_script) $("sa_runner_gguf_script").value = s.gguf_script;
|
||
if ($("sa_runner_gguf_base") && s.gguf_base_path) $("sa_runner_gguf_base").value = s.gguf_base_path;
|
||
if ($("sa_runner_gguf_cmd") && s.gguf_cmd) $("sa_runner_gguf_cmd").value = s.gguf_cmd;
|
||
} catch (e) {
|
||
}
|
||
}
|
||
function wireTraining() {
|
||
if (window.__saTrainingWired) return;
|
||
window.__saTrainingWired = true;
|
||
document.querySelectorAll("#sa_training .sa-ttab").forEach((btn) => {
|
||
btn.addEventListener("click", () => setTrainingTab(btn.getAttribute("data-ttab")));
|
||
});
|
||
$("sa_btn_agent_sync")?.addEventListener("click", syncAllToAgent);
|
||
$("sa_agent_heard_enabled")?.addEventListener("change", saveAgentHeardSettings);
|
||
$("sa_agent_auto_link")?.addEventListener("change", saveAgentHeardSettings);
|
||
$("sa_agent_heard_quota")?.addEventListener("change", saveAgentHeardSettings);
|
||
loadAgentHeardSettings();
|
||
$("sa_btn_train_from_chats")?.addEventListener("click", async () => {
|
||
try {
|
||
const data = await SA2.request("AssistentBuildDatasetFromChats", {});
|
||
setTrainStatus(`\u0418\u0437 \u0447\u0430\u0442\u043E\u0432: +${data.added}`);
|
||
await refreshSamples();
|
||
} catch (e) {
|
||
setTrainStatus(String(e.message || e));
|
||
}
|
||
});
|
||
$("sa_btn_train_import_file")?.addEventListener("click", () => $("sa_train_import_file")?.click());
|
||
$("sa_train_import_file")?.addEventListener("change", async (e) => {
|
||
const file = e.target?.files?.[0];
|
||
if (!file) return;
|
||
const text = await file.text();
|
||
try {
|
||
const data = await SA2.request("AssistentImportDataset", { format: "auto", content: text });
|
||
setTrainStatus(`\u0418\u043C\u043F\u043E\u0440\u0442: ${data.imported}`);
|
||
await refreshSamples();
|
||
} catch (err) {
|
||
setTrainStatus(String(err.message || err));
|
||
}
|
||
e.target.value = "";
|
||
});
|
||
$("sa_btn_train_export")?.addEventListener("click", async () => {
|
||
try {
|
||
const data = await SA2.request("AssistentExportDataset", { status: "approved" });
|
||
if (data.content) {
|
||
const blob = new Blob([data.content], { type: "application/jsonl" });
|
||
const a = document.createElement("a");
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = "assistent-dataset.jsonl";
|
||
a.click();
|
||
}
|
||
setTrainStatus(`\u042D\u043A\u0441\u043F\u043E\u0440\u0442: ${data.count} \u043F\u0440\u0438\u043C\u0435\u0440\u043E\u0432`);
|
||
} catch (e) {
|
||
setTrainStatus(String(e.message || e));
|
||
}
|
||
});
|
||
$("sa_train_filter_status")?.addEventListener("change", refreshSamples);
|
||
$("sa_train_filter_persona")?.addEventListener("change", refreshSamples);
|
||
$("sa_train_samples")?.addEventListener("click", async (e) => {
|
||
const row = e.target.closest(".sa-train-sample");
|
||
if (!row) return;
|
||
const id = row.dataset.id;
|
||
const sample = state.samples.find((s) => s.id === id);
|
||
if (!sample) return;
|
||
if (e.target.closest("[data-approve]")) {
|
||
await upsertSample({ ...sample, status: "approved" });
|
||
await loadAgentHeardSettings();
|
||
} else if (e.target.closest("[data-reject]")) {
|
||
await upsertSample({ ...sample, status: "rejected" });
|
||
await loadAgentHeardSettings();
|
||
} else if (e.target.closest("[data-link]")) {
|
||
try {
|
||
const data = await SA2.request("AssistentLinkTrainSampleToAgent", { id });
|
||
state.agentLinked = data?.linked ?? state.agentLinked;
|
||
setAgentHeardStats(state.agentLinked);
|
||
setTrainStatus("\u041F\u0440\u0438\u043C\u0435\u0440 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D \u043A \u0430\u0433\u0435\u043D\u0442\u0443");
|
||
await refreshSamples();
|
||
} catch (err) {
|
||
setTrainStatus(String(err.message || err));
|
||
}
|
||
} else if (e.target.closest("[data-unlink]")) {
|
||
try {
|
||
const data = await SA2.request("AssistentUnlinkTrainSampleFromAgent", { id });
|
||
state.agentLinked = data?.linked ?? state.agentLinked;
|
||
setAgentHeardStats(state.agentLinked);
|
||
setTrainStatus("\u041F\u0440\u0438\u043C\u0435\u0440 \u043E\u0442\u043A\u043B\u044E\u0447\u0451\u043D \u043E\u0442 \u0430\u0433\u0435\u043D\u0442\u0430");
|
||
await refreshSamples();
|
||
} catch (err) {
|
||
setTrainStatus(String(err.message || err));
|
||
}
|
||
} else if (e.target.closest("[data-del]")) {
|
||
if (window.confirm("\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u0440\u0438\u043C\u0435\u0440?")) {
|
||
await SA2.request("AssistentDeleteTrainSample", { id });
|
||
await refreshSamples();
|
||
}
|
||
}
|
||
});
|
||
$("sa_btn_hf_search")?.addEventListener("click", searchHf);
|
||
$("sa_hf_show_all")?.addEventListener("change", () => {
|
||
renderHfList();
|
||
});
|
||
$("sa_hf_list")?.addEventListener("click", async (e) => {
|
||
const row = e.target.closest(".sa-hf-row");
|
||
if (!row || row.classList.contains("sa-hf-rejected")) return;
|
||
state.hfSelected = row.dataset.id;
|
||
$("sa_hf_link").value = row.dataset.id;
|
||
renderHfList();
|
||
await checkHfLink();
|
||
});
|
||
$("sa_btn_hf_check")?.addEventListener("click", checkHfLink);
|
||
$("sa_hf_mapping_preset")?.addEventListener("change", () => {
|
||
state.hfMapping = buildHfMappingPayload();
|
||
});
|
||
$("sa_btn_hf_import")?.addEventListener("click", importHf);
|
||
document.querySelectorAll('input[name="sa_train_mode"]').forEach((r) => {
|
||
r.addEventListener("change", () => setTrainMode(r.value));
|
||
});
|
||
$("sa_btn_modelfile_create")?.addEventListener("click", createModelfile);
|
||
$("sa_btn_qlora_start")?.addEventListener("click", startQlora);
|
||
$("sa_btn_qlora_cancel")?.addEventListener("click", cancelQlora);
|
||
$("sa_btn_train_models_refresh")?.addEventListener("click", refreshTrainModels);
|
||
$("sa_btn_save_runner")?.addEventListener("click", saveRunner);
|
||
loadRunner();
|
||
setTrainMode("modelfile");
|
||
}
|
||
SA2.training = {
|
||
render() {
|
||
wireTraining();
|
||
setTrainingTab(state.ttab);
|
||
},
|
||
async curateFromChat(messages, meta) {
|
||
try {
|
||
await SA2.request("AssistentUpsertTrainSample", {
|
||
source: "chat",
|
||
chat_id: meta?.chatId,
|
||
persona: meta?.persona,
|
||
pack: meta?.pack,
|
||
status: meta?.status || "approved",
|
||
messages
|
||
});
|
||
return true;
|
||
} catch (e) {
|
||
console.warn("curateFromChat", e);
|
||
return false;
|
||
}
|
||
},
|
||
setTrainingLock,
|
||
pollTrainJob
|
||
};
|
||
}
|
||
|
||
// src/main.js
|
||
window.SA = window.SA || {};
|
||
attachApi(window.SA);
|
||
attachPatch(window.SA);
|
||
attachPersist(window.SA);
|
||
attachSession(window.SA);
|
||
attachContext(window.SA);
|
||
attachActivity(window.SA);
|
||
attachKreaProfile(window.SA);
|
||
window.SA.applyConfigPatchKeys = function(config) {
|
||
const keys = config?.patch_keys;
|
||
if (Array.isArray(keys) && keys.length) {
|
||
setPatchKeys(keys);
|
||
window.SA.PATCH_KEYS = keys;
|
||
}
|
||
};
|
||
attachTraining(window.SA);
|
||
})();
|