${escapeHtml(
+ p.innerHTML = seen ? `Swarm Assistent is for Krea 2 models only. Current: ${escapeHtml2(
seen
)} \u2014 pick a checkpoint with architecture krea-2.` : "Swarm Assistent is for Krea 2 models only. Select a Krea 2 checkpoint on Generate to enable the chat.";
}
@@ -805,7 +808,7 @@
}
return ok;
}
- function escapeHtml(s) {
+ function escapeHtml2(s) {
return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
}
const PROSE_SECTION_TITLES = {
@@ -874,7 +877,7 @@
return;
}
parts.push(
- `${listItems.map((li) => `- ${formatProseInline(escapeHtml(li))}
`).join("")}
`
+ `${listItems.map((li) => `- ${formatProseInline(escapeHtml2(li))}
`).join("")}
`
);
listItems = [];
};
@@ -888,7 +891,7 @@
}
const level = Math.min((line.match(/^#+/) || ["###"])[0].length, 3);
parts.push(
- `${escapeHtml(title)}`
+ `${escapeHtml2(title)}`
);
continue;
}
@@ -902,7 +905,7 @@
parts.push('');
continue;
}
- parts.push(`${formatProseInline(escapeHtml(line))}
`);
+ parts.push(`${formatProseInline(escapeHtml2(line))}
`);
}
flushList();
return parts.join("");
@@ -1352,13 +1355,13 @@ ${patch.prompt}`;
generate = false;
} else if (commanded) {
generate = true;
- } else if (packBlocksAutoGenerate($("sa_pack")?.value || "")) {
+ } else if (packBlocksAutoGenerate($2("sa_pack")?.value || "")) {
generate = false;
} else {
generate = modelAsked || implied;
}
const hasLook = !!patch && (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
- const honorLook = opts.fromAutoCritique || opts.fromVisionHop || !machine && userAsksLook(userText) || packWantsVision($("sa_pack")?.value);
+ const honorLook = opts.fromAutoCritique || opts.fromVisionHop || !machine && userAsksLook(userText) || packWantsVision($2("sa_pack")?.value);
const look = !!(hasLook && !vetoed && !generate && honorLook);
return { generate, look, vetoed };
}
@@ -1401,7 +1404,7 @@ ${patch.prompt}`;
}
}
function syncBuildGenButton() {
- const btn = $("sa_btn_build_gen");
+ const btn = $2("sa_btn_build_gen");
if (!btn) {
return;
}
@@ -1415,11 +1418,11 @@ ${patch.prompt}`;
}
}
function defaultPackId() {
- return state.config?.assistant?.default_pack || $("sa_pack")?.querySelector("option")?.value || "ordinary";
+ return state.config?.assistant?.default_pack || $2("sa_pack")?.querySelector("option")?.value || "ordinary";
}
function syncModeBadge() {
- const badge = $("sa_mode_badge");
- const pack = $("sa_pack")?.value || defaultPackId();
+ const badge = $2("sa_mode_badge");
+ const pack = $2("sa_pack")?.value || defaultPackId();
if (!badge) {
return;
}
@@ -1441,7 +1444,7 @@ ${patch.prompt}`;
badge.classList.toggle("sa-mode-hot", pack === "critique_image" || pack === "inpaint_edit");
}
function syncLiveParamsBar() {
- const el = $("sa_live_params");
+ const el = $2("sa_live_params");
if (!el) {
return;
}
@@ -1604,7 +1607,7 @@ ${patch.prompt}`;
const tab = document.getElementById(TAB_BUTTON_ID);
if (tab) {
tab.click();
- setTimeout(() => $("sa_input")?.focus(), 50);
+ setTimeout(() => $2("sa_input")?.focus(), 50);
return true;
}
const pane = document.getElementById("assistent");
@@ -1614,7 +1617,7 @@ ${patch.prompt}`;
} catch (e) {
}
}
- setTimeout(() => $("sa_input")?.focus(), 50);
+ setTimeout(() => $2("sa_input")?.focus(), 50);
return !!tab;
}
function historyMessageLimit() {
@@ -2034,7 +2037,7 @@ ${patch.prompt}`;
await runGenerateFromPatch({ actions: ["generate"] }, { force: true });
}
function renderBoard() {
- const board = $("sa_board");
+ const board = $2("sa_board");
if (!board) {
return;
}
@@ -2142,11 +2145,11 @@ ${patch.prompt}`;
return el;
}
function ensureGenLightbox() {
- let root = $("sa_gen_lightbox");
+ let root = $2("sa_gen_lightbox");
if (root) {
return root;
}
- const host = $("swarm_assistent_root") || document.body;
+ const host = $2("swarm_assistent_root") || document.body;
root = document.createElement("div");
root.id = "sa_gen_lightbox";
root.className = "sa-lightbox";
@@ -2220,9 +2223,9 @@ ${patch.prompt}`;
return;
}
root.hidden = false;
- const img = $("sa_lb_img");
- const title = $("sa_lb_title");
- const idx = $("sa_lb_idx");
+ 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;
@@ -2250,7 +2253,7 @@ ${patch.prompt}`;
}
function closeGenLightbox() {
state.lightboxIndex = -1;
- const root = $("sa_gen_lightbox");
+ const root = $2("sa_gen_lightbox");
if (root) {
root.hidden = true;
}
@@ -2269,23 +2272,23 @@ ${patch.prompt}`;
}
function syncBoardChrome() {
const tab = state.boardTab === "refs" ? "refs" : "generate";
- $("sa_board_tab_gen")?.classList.toggle("sa-board-tab-active", tab === "generate");
- $("sa_board_tab_refs")?.classList.toggle("sa-board-tab-active", tab === "refs");
- $("sa_board_tab_gen")?.setAttribute("aria-selected", tab === "generate" ? "true" : "false");
- $("sa_board_tab_refs")?.setAttribute("aria-selected", tab === "refs" ? "true" : "false");
- const addBtn = $("sa_btn_add_ref");
+ $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 = $("sa_btn_as_mask");
- const clearSlotBtn = $("sa_btn_clear_image");
+ 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 = $("sa_refs_badge");
+ const badge = $2("sa_refs_badge");
if (badge) {
const refs = refSlots();
const withImg = refs.filter((s) => s.src).length;
@@ -2297,9 +2300,9 @@ ${patch.prompt}`;
badge.hidden = true;
}
}
- let genBadge = $("sa_gen_badge");
+ let genBadge = $2("sa_gen_badge");
if (!genBadge) {
- const genTab = $("sa_board_tab_gen");
+ const genTab = $2("sa_board_tab_gen");
if (genTab) {
genBadge = document.createElement("span");
genBadge.id = "sa_gen_badge";
@@ -2455,11 +2458,11 @@ ${patch.prompt}`;
if (localStorage.getItem(LS_WELCOMED) === "1") {
return;
}
- if (!$("sa_messages")) {
+ if (!$2("sa_messages")) {
return;
}
localStorage.setItem(LS_WELCOMED, "1");
- const box = $("sa_messages");
+ const box = $2("sa_messages");
hideChatEmpty();
const div = document.createElement("div");
div.className = "sa-msg assistant sa-welcome";
@@ -2528,8 +2531,8 @@ ${patch.prompt}`;
scheduler: val("input_scheduler") || null,
batch: parseInt(val("input_images") || val("input_batchsize") || "0", 10) || null,
loras,
- persona: $("sa_persona")?.value || "neutral",
- pack: $("sa_pack")?.value || defaultPackId(),
+ persona: $2("sa_persona")?.value || "neutral",
+ pack: $2("sa_pack")?.value || defaultPackId(),
sessionExact,
lastPatch,
genResults: Array.isArray(state.genResults) ? state.genResults.map((r) => ({
@@ -2640,7 +2643,7 @@ ${patch.prompt}`;
function applyPersonaForChat(personaId, { quiet = false } = {}) {
const id = String(personaId || "neutral").trim() || "neutral";
return new Promise((resolve) => {
- const sel = $("sa_persona");
+ const sel = $2("sa_persona");
if (sel && [...sel.options].some((o) => o.value === id)) {
sel.value = id;
}
@@ -2654,7 +2657,7 @@ ${patch.prompt}`;
resolve();
return;
}
- const packKeep = $("sa_pack")?.value;
+ const packKeep = $2("sa_pack")?.value;
genericRequest(
"AssistentGetConfig",
{ persona: id },
@@ -2789,7 +2792,7 @@ ${patch.prompt}`;
persistChatsStore();
}
function resetMessagesUi(emptyHint) {
- const box = $("sa_messages");
+ const box = $2("sa_messages");
if (!box) {
return;
}
@@ -2801,7 +2804,7 @@ ${patch.prompt}`;
box.appendChild(empty);
}
function renderHistoryIntoUi(messages) {
- const box = $("sa_messages");
+ const box = $2("sa_messages");
if (!box) {
return;
}
@@ -2824,7 +2827,7 @@ ${patch.prompt}`;
}
}
function updateSessionLabel() {
- const el = $("sa_session_label");
+ const el = $2("sa_session_label");
if (!el) {
return;
}
@@ -2836,7 +2839,7 @@ ${patch.prompt}`;
return (state.chats || []).filter((c) => (c.messages || []).length > 0).length;
}
function syncHistoryBadge() {
- const btn = $("sa_btn_chats");
+ const btn = $2("sa_btn_chats");
if (!btn) {
return;
}
@@ -2871,7 +2874,7 @@ ${patch.prompt}`;
return false;
}
function renderChatsList() {
- const root = $("sa_chats_list");
+ const root = $2("sa_chats_list");
if (!root) {
return;
}
@@ -2908,27 +2911,33 @@ ${patch.prompt}`;
bits.push(`LoRA ${c.params.loras.length}`);
}
const noParams = !c.params ? " \xB7 \u0431\u0435\u0437 \u0441\u043D\u0438\u043C\u043A\u0430 params" : "";
- row.innerHTML = ``;
+ row.innerHTML = ``;
root.appendChild(row);
}
}
function setChatsPanelOpen(open) {
state.chatsPanelOpen = !!open;
- const panel = $("sa_chats_panel");
- const btn = $("sa_btn_chats");
+ 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");
if (state.chatsPanelOpen) {
saveActiveChatToStore();
- const search = $("sa_chats_search");
+ const search = $2("sa_chats_search");
if (search) {
search.value = state.chatsQuery || "";
search.focus();
}
renderChatsList();
}
+ saveUiStateToDisk();
}
async function startNewChat({ saveCurrent = true, force = false } = {}) {
if (!force && (state.busy || state.generating)) {
@@ -2938,7 +2947,6 @@ ${patch.prompt}`;
if (force) {
abortInFlightWork({ status: "" });
}
- setChatsPanelOpen(false);
if (saveCurrent) {
saveActiveChatToStore({ dropEmpty: true });
}
@@ -2980,7 +2988,6 @@ ${patch.prompt}`;
}
async function switchToChat(id) {
if (!id || id === state.activeChatId) {
- setChatsPanelOpen(false);
return;
}
if (state.busy || state.generating) {
@@ -3028,7 +3035,6 @@ ${patch.prompt}`;
updateSessionLabel();
syncHistoryBadge();
renderChatsList();
- setChatsPanelOpen(false);
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`);
@@ -3123,7 +3129,7 @@ ${patch.prompt}`;
renderBoard();
}
function hideSlashMenu() {
- const menu = $("sa_slash_menu");
+ const menu = $2("sa_slash_menu");
if (menu) {
menu.hidden = true;
menu.innerHTML = "";
@@ -3139,7 +3145,7 @@ ${patch.prompt}`;
return SLASH_COMMANDS.filter((c) => c.cmd.toLowerCase().startsWith(q) || q === "/" || c.cmd.toLowerCase().includes(q.slice(1)));
}
function renderSlashMenu(items) {
- const menu = $("sa_slash_menu");
+ const menu = $2("sa_slash_menu");
if (!menu) {
return;
}
@@ -3155,7 +3161,7 @@ ${patch.prompt}`;
btn.type = "button";
btn.className = "sa-slash-item" + (i === state.slashIndex ? " sa-slash-active" : "");
btn.setAttribute("role", "option");
- btn.innerHTML = `${escapeHtml(item.cmd.trim())} \u2014 ${escapeHtml(item.hint)}`;
+ btn.innerHTML = `${escapeHtml2(item.cmd.trim())} \u2014 ${escapeHtml2(item.hint)}`;
btn.addEventListener("mousedown", (e) => {
e.preventDefault();
applySlashPick(item);
@@ -3164,7 +3170,7 @@ ${patch.prompt}`;
});
}
function applySlashPick(item) {
- const input = $("sa_input");
+ const input = $2("sa_input");
if (!input || !item) {
return;
}
@@ -3175,7 +3181,7 @@ ${patch.prompt}`;
input.setSelectionRange(pos, pos);
}
function updateSlashMenuFromInput() {
- const text = $("sa_input")?.value || "";
+ 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))) {
@@ -3193,7 +3199,7 @@ ${patch.prompt}`;
renderSlashMenu(slashMatches(token));
}
function onPersonaChanged() {
- const id = $("sa_persona")?.value || "neutral";
+ const id = $2("sa_persona")?.value || "neutral";
state.sessionExact = {};
state.lastUserParamIntent = false;
saveSettings();
@@ -3204,10 +3210,10 @@ ${patch.prompt}`;
}
appendSystemNote(`\u0422\u043E\u043D \u2192 ${title}`);
state.pendingPersonaNote = `Persona is now ${id} (${title}). Adopt this voice from now on.`;
- if (data?.assistant?.default_pack && $("sa_pack") && !state.packUserTouched) {
+ if (data?.assistant?.default_pack && $2("sa_pack") && !state.packUserTouched) {
const packId = data.assistant.default_pack;
- if ([...$("sa_pack").options || []].some((o) => o.value === packId)) {
- $("sa_pack").value = packId;
+ if ([...$2("sa_pack").options || []].some((o) => o.value === packId)) {
+ $2("sa_pack").value = packId;
}
}
fillEmptyParamsFromExact();
@@ -3434,9 +3440,9 @@ ${patch.prompt}`;
})),
selected_gen_result: state.selectedGenResultId || null,
has_civitai_key: !!inv.has_civitai_key,
- auto_apply: !!$("sa_auto_apply")?.checked,
- auto_generate: !!$("sa_auto_generate")?.checked,
- persona: $("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral",
+ auto_apply: !!$2("sa_auto_apply")?.checked,
+ auto_generate: !!$2("sa_auto_generate")?.checked,
+ persona: $2("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral",
model_cards: [],
user_prefs_count: 0,
...initCtx
@@ -3828,8 +3834,8 @@ ${patch.prompt}`;
setStatus("\u041F\u0430\u0442\u0447\u0438 \u0443\u0431\u0440\u0430\u043D\u044B \u0438\u0437 \u0447\u0430\u0442\u0430");
}
function toggleMoreMenu(menuId, btnId) {
- const menu = $(menuId);
- const btn = $(btnId);
+ const menu = $2(menuId);
+ const btn = $2(btnId);
if (!menu) {
return;
}
@@ -3850,7 +3856,7 @@ ${patch.prompt}`;
document.querySelectorAll("#sa_btn_board_more, #sa_btn_clear_more").forEach((b) => b.setAttribute("aria-expanded", "false"));
}
function setPackValue(packName, { flash, user } = {}) {
- const pack = $("sa_pack");
+ const pack = $2("sa_pack");
if (!pack || !packName) {
return false;
}
@@ -3876,7 +3882,7 @@ ${patch.prompt}`;
if (state.packUserTouched) {
return null;
}
- const cur = $("sa_pack")?.value || defaultPackId();
+ const cur = $2("sa_pack")?.value || defaultPackId();
if (cur === "ordinary") {
return null;
}
@@ -3911,7 +3917,7 @@ ${patch.prompt}`;
if (state.packUserTouched) {
return;
}
- const cur = $("sa_pack")?.value || "";
+ const cur = $2("sa_pack")?.value || "";
if (cur === "critique_image" || cur === "describe_ref") {
setPackValue(defaultPackId(), { flash: true });
}
@@ -4226,15 +4232,15 @@ ${patch.prompt}`;
accent: p.accent,
source: p.source
}));
- renderPersonaOptions(state.personas, preferId || $("sa_persona")?.value);
+ renderPersonaOptions(state.personas, preferId || $2("sa_persona")?.value);
}
- if (preferId && $("sa_persona")) {
- if ([...$("sa_persona").options].some((o) => o.value === preferId)) {
- $("sa_persona").value = preferId;
+ 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($("sa_persona")?.value, () => resolve());
+ loadConfig($2("sa_persona")?.value, () => resolve());
return;
}
resolve();
@@ -4261,16 +4267,16 @@ ${patch.prompt}`;
return false;
}
function shouldParkLlmBeforeGen() {
- return !!$("sa_park_llm")?.checked;
+ return !!$2("sa_park_llm")?.checked;
}
function parkLlm() {
return new Promise((resolve) => {
- const model = $("sa_model")?.value;
+ const model = $2("sa_model")?.value;
if (!shouldParkLlmBeforeGen() || !model || state.llmParked || typeof genericRequest !== "function") {
resolve(false);
return;
}
- const baseUrl = $("sa_base_url")?.value || "http://127.0.0.1:11434";
+ const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434";
let settled = false;
const finish = (ok) => {
if (settled) {
@@ -4289,7 +4295,7 @@ ${patch.prompt}`;
}
function warmLlm({ force = false } = {}) {
return new Promise((resolve) => {
- const model = $("sa_model")?.value;
+ const model = $2("sa_model")?.value;
if (!model || typeof genericRequest !== "function") {
resolve(false);
return;
@@ -4298,7 +4304,7 @@ ${patch.prompt}`;
resolve(false);
return;
}
- const baseUrl = $("sa_base_url")?.value || "http://127.0.0.1:11434";
+ const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434";
let settled = false;
const finish = (ok) => {
if (settled) {
@@ -4514,7 +4520,7 @@ ${patch.prompt}`;
}
async function runGenerateFromPatch(patch, opts = {}) {
const force = !!opts.force;
- if (!force && !$("sa_auto_generate")?.checked || !patchHasGenTrigger(patch)) {
+ if (!force && !$2("sa_auto_generate")?.checked || !patchHasGenTrigger(patch)) {
return null;
}
const variantItems = normalizeVariantList(patch);
@@ -4610,7 +4616,7 @@ ${patch.prompt}`;
}
const paneVisible = !!document.getElementById("swarm_assistent_root")?.offsetParent;
const multiDone = !!(jobs && finishedGenResultCount() > 1);
- const willAutoCritique = !multiDone && !!$("sa_auto_critique")?.checked;
+ const willAutoCritique = !multiDone && !!$2("sa_auto_critique")?.checked;
if (state.view === "chat" && paneVisible && !willAutoCritique && epoch === state.chatEpoch) {
startBusyUi("warming");
setStatus("\u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E LLM \u0432 GPU\u2026");
@@ -4660,7 +4666,7 @@ ${patch.prompt}`;
return src && !looksLikeModelPreview(src) ? src : null;
}
async function maybeAutoCritique(imageSrc) {
- if (!$("sa_auto_critique")?.checked || turnHopUsed("critique") || isMultiGenResults()) {
+ if (!$2("sa_auto_critique")?.checked || turnHopUsed("critique") || isMultiGenResults()) {
return;
}
const src = await resolveFinishedGenerateSrc(imageSrc);
@@ -4672,8 +4678,8 @@ ${patch.prompt}`;
return;
}
setPackValue("critique_image", { flash: true });
- if ($("sa_input")) {
- $("sa_input").value = "Critique this result and improve the prompt for the next generation.";
+ if ($2("sa_input")) {
+ $2("sa_input").value = "Critique this result and improve the prompt for the next generation.";
}
const gen = generateSlot();
if (gen) {
@@ -4686,7 +4692,7 @@ ${patch.prompt}`;
restoreDefaultPackAfterHop();
}
async function maybeAutoVisionLook(imageSrc) {
- if (!wantsAutoVision() || $("sa_auto_critique")?.checked || turnHopUsed("vision") || state.busy || isMultiGenResults()) {
+ if (!wantsAutoVision() || $2("sa_auto_critique")?.checked || turnHopUsed("vision") || state.busy || isMultiGenResults()) {
return;
}
const src = await resolveFinishedGenerateSrc(imageSrc);
@@ -4703,8 +4709,8 @@ ${patch.prompt}`;
return;
}
setPackValue("critique_image", { flash: true });
- if ($("sa_input")) {
- $("sa_input").value = "Look at the Generate result and briefly say what worked and what to fix next.";
+ 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 });
@@ -4735,13 +4741,13 @@ ${patch.prompt}`;
setView("chat");
const label = (state.genResults || []).find((r) => r.id === state.selectedGenResultId)?.label;
setPackValue("critique_image", { flash: true });
- if ($("sa_input")) {
- $("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.";
+ 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 = ($("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral").trim() || "neutral";
+ 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,
@@ -4757,7 +4763,7 @@ ${patch.prompt}`;
return;
}
const persona = meta.persona || currentPersonaInfo();
- const pack = meta.pack || $("sa_pack")?.value || "";
+ const pack = meta.pack || $2("sa_pack")?.value || "";
div.dataset.persona = persona.id || "neutral";
if (pack) {
div.dataset.pack = pack;
@@ -4779,7 +4785,7 @@ ${patch.prompt}`;
div.insertBefore(row, div.firstChild);
}
function appendMessage(role, text, patch, civitaiResults, meta) {
- const box = $("sa_messages");
+ const box = $2("sa_messages");
if (!box) {
return null;
}
@@ -4803,12 +4809,15 @@ ${patch.prompt}`;
if (civitaiResults && civitaiResults.length) {
div.appendChild(buildCivitaiCards(civitaiResults));
}
+ if (role === "assistant" && !(meta && meta.historical)) {
+ mountCurateButtons(div, meta);
+ }
box.appendChild(div);
scrollMessagesToBottom({ force: true });
return div;
}
function beginStreamMessage(meta) {
- const box = $("sa_messages");
+ const box = $2("sa_messages");
if (!box) {
return null;
}
@@ -4921,8 +4930,74 @@ ${patch.prompt}`;
if (civitaiResults && civitaiResults.length) {
el.appendChild(buildCivitaiCards(civitaiResults));
}
+ 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 buildCivitaiCards(results) {
const list = document.createElement("div");
list.className = "sa-civitai-list";
@@ -5039,8 +5114,8 @@ ${patch.prompt}`;
version_id: civitai?.version_id || civitai?.modelVersionId,
name: display
};
- if ($("sa_input")) {
- $("sa_input").value = "";
+ if ($2("sa_input")) {
+ $2("sa_input").value = "";
}
await sendChat({
forcedUserText: `LoRA "${display}" is now installed. Write a recommendation card (JSON) using its triggers/metadata. Then briefly suggest how to enable it in the next generate.`,
@@ -5052,7 +5127,7 @@ ${patch.prompt}`;
});
}
function wantsAutoVision() {
- return !!$("sa_auto_vision")?.checked;
+ return !!$2("sa_auto_vision")?.checked;
}
function looksLikeModelPreview(src) {
const s = String(src || "").toLowerCase();
@@ -5239,32 +5314,32 @@ ${patch.prompt}`;
const autoDl = localStorage.getItem(LS_AUTO_DOWNLOAD);
const parkLlm2 = localStorage.getItem(LS_PARK_LLM);
const paneW = localStorage.getItem(LS_PANE_WIDTH);
- if (base && $("sa_base_url")) {
- $("sa_base_url").value = base;
+ if (base && $2("sa_base_url")) {
+ $2("sa_base_url").value = base;
}
- if (pack && $("sa_pack")) {
- $("sa_pack").value = pack;
+ if (pack && $2("sa_pack")) {
+ $2("sa_pack").value = pack;
}
- if (persona && $("sa_persona")) {
- $("sa_persona").value = persona;
+ if (persona && $2("sa_persona")) {
+ $2("sa_persona").value = persona;
}
- if (auto != null && $("sa_auto_vision")) {
- $("sa_auto_vision").checked = auto === "1";
+ if (auto != null && $2("sa_auto_vision")) {
+ $2("sa_auto_vision").checked = auto === "1";
}
- if ($("sa_auto_apply")) {
- $("sa_auto_apply").checked = autoApply == null ? true : autoApply === "1";
+ if ($2("sa_auto_apply")) {
+ $2("sa_auto_apply").checked = autoApply == null ? true : autoApply === "1";
}
- if ($("sa_auto_generate")) {
- $("sa_auto_generate").checked = autoGen == null ? true : autoGen === "1";
+ if ($2("sa_auto_generate")) {
+ $2("sa_auto_generate").checked = autoGen == null ? true : autoGen === "1";
}
- if ($("sa_auto_critique") && autoCrit != null) {
- $("sa_auto_critique").checked = autoCrit === "1";
+ if ($2("sa_auto_critique") && autoCrit != null) {
+ $2("sa_auto_critique").checked = autoCrit === "1";
}
- if ($("sa_auto_download") && autoDl != null) {
- $("sa_auto_download").checked = autoDl === "1";
+ if ($2("sa_auto_download") && autoDl != null) {
+ $2("sa_auto_download").checked = autoDl === "1";
}
- if ($("sa_park_llm")) {
- $("sa_park_llm").checked = parkLlm2 === "1";
+ if ($2("sa_park_llm")) {
+ $2("sa_park_llm").checked = parkLlm2 === "1";
}
if (model) {
state.preferredModel = model;
@@ -5276,9 +5351,14 @@ ${patch.prompt}`;
if (paneW) {
document.documentElement.style.setProperty("--sa-image-width", paneW);
}
- if (view === "cards" || view === "chat" || view === "settings") {
+ if (view === "cards" || 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;
@@ -5286,20 +5366,21 @@ ${patch.prompt}`;
}
function collectUiState() {
return {
- pack: $("sa_pack")?.value || defaultPackId(),
- persona: $("sa_persona")?.value || "neutral",
- auto_vision: !!$("sa_auto_vision")?.checked,
- auto_apply: !!$("sa_auto_apply")?.checked,
- auto_generate: !!$("sa_auto_generate")?.checked,
- auto_critique: !!$("sa_auto_critique")?.checked,
- auto_download: !!$("sa_auto_download")?.checked,
- park_llm: !!$("sa_park_llm")?.checked,
+ 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: !!$2("sa_auto_generate")?.checked,
+ 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: $("sa_embed_model")?.value || state.preferredEmbed || "",
- base_url: $("sa_base_url")?.value || "",
- model: $("sa_model")?.value || "",
+ 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"
+ board_tab: state.boardTab || "generate",
+ chats_drawer: state.chatsDrawerOpen ? "1" : "0"
};
}
async function applyDiskUiState() {
@@ -5324,8 +5405,8 @@ ${patch.prompt}`;
apply?.(String(value));
};
fill(LS_BASE, ui.base_url, (v) => {
- if ($("sa_base_url")) {
- $("sa_base_url").value = v;
+ if ($2("sa_base_url")) {
+ $2("sa_base_url").value = v;
}
});
fill(LS_MODEL, ui.model, (v) => {
@@ -5335,17 +5416,17 @@ ${patch.prompt}`;
state.preferredEmbed = v;
});
fill(LS_PACK, ui.pack, (v) => {
- if ($("sa_pack")) {
- $("sa_pack").value = v;
+ if ($2("sa_pack")) {
+ $2("sa_pack").value = v;
}
});
fill(LS_PERSONA, ui.persona, (v) => {
- if ($("sa_persona")) {
- $("sa_persona").value = 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" || ui.view === "settings") {
+ if (ui.view === "cards" || ui.view === "chat" || ui.view === "settings" || ui.view === "train") {
fill(LS_VIEW, ui.view, (v) => {
state.view = v;
});
@@ -5355,6 +5436,12 @@ ${patch.prompt}`;
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"],
@@ -5371,7 +5458,7 @@ ${patch.prompt}`;
continue;
}
localStorage.setItem(lsKey, on ? "1" : "0");
- const el = $(id);
+ const el = $2(id);
if (el) {
el.checked = on;
}
@@ -5381,18 +5468,18 @@ ${patch.prompt}`;
diskPersist()?.saveUiState(collectUiState());
}
function saveSettings() {
- localStorage.setItem(LS_BASE, $("sa_base_url")?.value || "");
- localStorage.setItem(LS_MODEL, $("sa_model")?.value || "");
- localStorage.setItem(LS_EMBED, $("sa_embed_model")?.value || state.preferredEmbed || "");
- localStorage.setItem(LS_PACK, $("sa_pack")?.value || defaultPackId());
- localStorage.setItem(LS_PERSONA, $("sa_persona")?.value || "neutral");
+ 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, $("sa_auto_vision")?.checked ? "1" : "0");
- localStorage.setItem(LS_AUTO_APPLY, $("sa_auto_apply")?.checked ? "1" : "0");
- localStorage.setItem(LS_AUTO_GENERATE, $("sa_auto_generate")?.checked ? "1" : "0");
- localStorage.setItem(LS_AUTO_CRITIQUE, $("sa_auto_critique")?.checked ? "1" : "0");
- localStorage.setItem(LS_AUTO_DOWNLOAD, $("sa_auto_download")?.checked ? "1" : "0");
- localStorage.setItem(LS_PARK_LLM, $("sa_park_llm")?.checked ? "1" : "0");
+ 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();
}
@@ -5404,10 +5491,10 @@ ${patch.prompt}`;
document.querySelectorAll("#sa_skills_box input[data-skill]")?.forEach((el) => {
skills[el.getAttribute("data-skill")] = !!el.checked;
});
- const persona = $("sa_persona")?.value || "neutral";
+ const persona = $2("sa_persona")?.value || "neutral";
const settings = {
- embed_model: $("sa_embed_model")?.value || state.preferredEmbed || "",
- base_url: $("sa_base_url")?.value || "",
+ embed_model: $2("sa_embed_model")?.value || state.preferredEmbed || "",
+ base_url: $2("sa_base_url")?.value || "",
[persona]: { skills }
};
genericRequest("AssistentSaveSettings", { settings }, () => {
@@ -5418,7 +5505,7 @@ ${patch.prompt}`;
if (!data || data.error) {
return;
}
- const prevPersona = state.config?.persona || $("sa_persona")?.value || "";
+ 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) {
@@ -5477,8 +5564,8 @@ ${data.ui.help_extra}`.trim();
renderPackOptions(data.packs || [], applyDefaults ? data.assistant?.default_pack : null);
renderChips(data.ui?.chips || []);
renderSkillChecks(data.skills || [], state.enabledSkills);
- if (applyDefaults && data.assistant?.default_pack && $("sa_pack") && !localStorage.getItem(LS_PACK)) {
- $("sa_pack").value = data.assistant.default_pack;
+ 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;
@@ -5506,7 +5593,7 @@ ${data.ui.help_extra}`.trim();
if (applyDefaults || data.exact) {
fillEmptyParamsFromExact();
}
- const nextPersona = data.persona || $("sa_persona")?.value || "";
+ const nextPersona = data.persona || $2("sa_persona")?.value || "";
let controlValues = data.control_values || data.exact?.controls || {};
if (!applyDefaults && prevControls && nextPersona === prevPersona) {
controlValues = { ...controlValues, ...prevControls };
@@ -5516,10 +5603,10 @@ ${data.ui.help_extra}`.trim();
}
}
renderPersonaControls(data.controls || {}, controlValues);
- syncPersonaDeleteButton(data.persona_source || data.personas?.find((p) => p.id === (data.persona || $("sa_persona")?.value))?.source);
+ syncPersonaDeleteButton(data.persona_source || data.personas?.find((p) => p.id === (data.persona || $2("sa_persona")?.value))?.source);
}
function syncPersonaDeleteButton(source) {
- const btn = $("sa_persona_delete");
+ const btn = $2("sa_persona_delete");
if (!btn) {
return;
}
@@ -5532,7 +5619,7 @@ ${data.ui.help_extra}`.trim();
let controlsPointerDown = false;
let pendingControlsRender = null;
function renderPersonaControls(schema, values) {
- const box = $("sa_persona_controls");
+ const box = $2("sa_persona_controls");
if (!box) {
return;
}
@@ -5643,7 +5730,7 @@ ${data.ui.help_extra}`.trim();
return Number.isFinite(n) ? n : fallback;
}
function coolDownHorny() {
- const persona = $("sa_persona")?.value || "";
+ const persona = $2("sa_persona")?.value || "";
if (persona !== "leonid") {
setStatus("/\u043E\u0441\u0442\u044B\u043D\u044C \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F Leonid");
return;
@@ -5674,7 +5761,7 @@ ${data.ui.help_extra}`.trim();
setStatus(`/\u043E\u0441\u0442\u044B\u043D\u044C \u2192 ${Math.round(next)}%`);
}
async function startHornyGame() {
- const persona = $("sa_persona")?.value || "";
+ const persona = $2("sa_persona")?.value || "";
if (persona !== "leonid") {
setStatus("/horny-game \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F Leonid");
return;
@@ -5692,7 +5779,7 @@ ${data.ui.help_extra}`.trim();
setStatus("/horny-game\u2026");
}
function savePersonaControls(partial) {
- const persona = $("sa_persona")?.value || "neutral";
+ const persona = $2("sa_persona")?.value || "neutral";
if (typeof genericRequest !== "function") {
return;
}
@@ -5727,7 +5814,7 @@ ${data.ui.help_extra}`.trim();
);
}
function syncPersonaControlInputs(values) {
- const box = $("sa_persona_controls");
+ const box = $2("sa_persona_controls");
if (!box || !values || typeof values !== "object") {
return;
}
@@ -5750,7 +5837,7 @@ ${data.ui.help_extra}`.trim();
});
}
async function deleteCurrentOverlayPersona() {
- const id = $("sa_persona")?.value;
+ const id = $2("sa_persona")?.value;
if (!id) {
return;
}
@@ -5780,8 +5867,8 @@ ${data.ui.help_extra}`.trim();
state.personas = data.personas;
}
renderPersonaOptions(state.personas || [], next);
- if ($("sa_persona")) {
- $("sa_persona").value = next;
+ if ($2("sa_persona")) {
+ $2("sa_persona").value = next;
}
await applyPersonaForChat(next, { quiet: false });
setStatus(`\u0423\u0434\u0430\u043B\u0435\u043D\u043E: ${id}`);
@@ -5796,7 +5883,7 @@ ${data.ui.help_extra}`.trim();
});
}
function renderPersonaOptions(personas, selected) {
- const sel = $("sa_persona");
+ const sel = $2("sa_persona");
if (!sel) {
return;
}
@@ -5818,7 +5905,7 @@ ${data.ui.help_extra}`.trim();
syncPersonaDeleteButton(meta?.source || state.config?.persona_source);
}
function renderPackOptions(packs, preferred) {
- const sel = $("sa_pack");
+ const sel = $2("sa_pack");
if (!sel) {
return;
}
@@ -5836,7 +5923,7 @@ ${data.ui.help_extra}`.trim();
}
}
function renderChips(chips) {
- const box = $("sa_chips");
+ const box = $2("sa_chips");
if (!box || !Array.isArray(chips) || !chips.length) {
return;
}
@@ -5871,7 +5958,7 @@ ${data.ui.help_extra}`.trim();
}
}
function renderSkillChecks(skills, enabled) {
- const box = $("sa_skills_box");
+ const box = $2("sa_skills_box");
if (!box) {
return;
}
@@ -5901,7 +5988,7 @@ ${data.ui.help_extra}`.trim();
}
genericRequest(
"AssistentGetConfig",
- { persona: persona || $("sa_persona")?.value || "neutral" },
+ { persona: persona || $2("sa_persona")?.value || "neutral" },
(data) => {
applyConfigPayload(data, { applyDefaults: true });
done?.(data);
@@ -5948,8 +6035,8 @@ ${data.ui.help_extra}`.trim();
return preferred || list[0];
}
function setModelOptions(models, { error, preferred } = {}) {
- const sel = $("sa_model");
- const sel2 = $("sa_settings_chat_model");
+ const sel = $2("sa_model");
+ const sel2 = $2("sa_settings_chat_model");
const apply = (target) => {
if (!target) {
return;
@@ -5988,7 +6075,7 @@ ${data.ui.help_extra}`.trim();
apply(sel2);
}
function setEmbedModelOptions(models) {
- const sel = $("sa_embed_model");
+ const sel = $2("sa_embed_model");
if (!sel) {
return;
}
@@ -6019,7 +6106,7 @@ ${data.ui.help_extra}`.trim();
}
}
function refreshModels() {
- const baseUrl = $("sa_base_url")?.value || "http://127.0.0.1:11434";
+ 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");
@@ -6036,10 +6123,10 @@ ${data.ui.help_extra}`.trim();
setModelOptions(models, { preferred });
setEmbedModelOptions(memoryModels);
const pick = resolveChatModel(models, preferred);
- if (pick && $("sa_model")) {
- $("sa_model").value = pick;
- if ($("sa_settings_chat_model")) {
- $("sa_settings_chat_model").value = pick;
+ 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);
@@ -6107,16 +6194,16 @@ ${data.ui.help_extra}`.trim();
return new Promise((resolve) => refreshInventory(resolve, opts));
}
function memoryKindFilter() {
- return $("sa_mem_kind")?.value || "all";
+ return $2("sa_mem_kind")?.value || "all";
}
function memoryScopeFilter() {
- return $("sa_mem_scope")?.value || "all";
+ return $2("sa_mem_scope")?.value || "all";
}
function memorySearchFilter() {
- return ($("sa_mem_search")?.value || "").trim().toLowerCase();
+ return ($2("sa_mem_search")?.value || "").trim().toLowerCase();
}
function renderMemoryKinds(kinds) {
- const sel = $("sa_mem_kind");
+ const sel = $2("sa_mem_kind");
if (!sel) {
return;
}
@@ -6140,7 +6227,7 @@ ${data.ui.help_extra}`.trim();
const filter = memoryKindFilter();
const scope = memoryScopeFilter();
const q = memorySearchFilter();
- const persona = $("sa_persona")?.value || "neutral";
+ const persona = $2("sa_persona")?.value || "neutral";
return (state.memoryRows || []).filter((m) => {
if (filter !== "all" && m.kind !== filter) {
return false;
@@ -6161,7 +6248,7 @@ ${data.ui.help_extra}`.trim();
});
}
function renderMemoryList() {
- const root = $("sa_mem_list");
+ const root = $2("sa_mem_list");
if (!root) {
return;
}
@@ -6177,7 +6264,7 @@ ${data.ui.help_extra}`.trim();
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 = `${escapeHtml(row.kind || "note")}${escapeHtml(row.key || "")}${escapeHtml(clipDebug(row.text, 220))}`;
+ el.innerHTML = `${escapeHtml2(row.kind || "note")}${escapeHtml2(row.key || "")}${escapeHtml2(clipDebug(row.text, 220))}`;
const forget = document.createElement("button");
forget.type = "button";
forget.className = "basic-button sa-mem-forget";
@@ -6197,7 +6284,7 @@ ${data.ui.help_extra}`.trim();
if (typeof genericRequest !== "function") {
return;
}
- const list = $("sa_mem_list");
+ const list = $2("sa_mem_list");
if (list && !state.memoryRows.length) {
list.innerHTML = '\u0427\u0438\u0442\u0430\u044E \u043F\u0430\u043C\u044F\u0442\u044C\u2026';
}
@@ -6208,7 +6295,7 @@ ${data.ui.help_extra}`.trim();
state.memoryRows = Array.isArray(data?.memories) ? data.memories : [];
renderMemoryKinds(data?.kinds || []);
renderMemoryList();
- const foot = $("sa_mem_total");
+ 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"}`;
}
@@ -6216,7 +6303,7 @@ ${data.ui.help_extra}`.trim();
0,
(err) => {
if (list) {
- list.innerHTML = `\u041F\u0430\u043C\u044F\u0442\u044C \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430: ${escapeHtml(String(err || "\u043E\u0448\u0438\u0431\u043A\u0430"))}`;
+ list.innerHTML = `\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"))}`;
}
}
);
@@ -6289,9 +6376,9 @@ ${data.ui.help_extra}`.trim();
}
if (state.settingsTab === "models") {
syncSettingsHealthLine();
- const m = $("sa_model")?.value;
- if (m && $("sa_settings_chat_model")) {
- $("sa_settings_chat_model").value = m;
+ const m = $2("sa_model")?.value;
+ if (m && $2("sa_settings_chat_model")) {
+ $2("sa_settings_chat_model").value = m;
}
}
if (state.settingsTab === "more") {
@@ -6302,7 +6389,7 @@ ${data.ui.help_extra}`.trim();
const asst = data?.assistant || state.config?.assistant || {};
const exact = data?.exact || state.config?.exact || state.exact || {};
const setNum = (id, v) => {
- const el = $(id);
+ const el = $2(id);
if (el && v != null && Number.isFinite(Number(v))) {
el.value = String(v);
}
@@ -6311,10 +6398,10 @@ ${data.ui.help_extra}`.trim();
setNum("sa_history_keep", asst.history_keep_turns);
setNum("sa_memory_top_k", asst.memory_top_k);
const w = asst.user_prefs_weight != null ? Number(asst.user_prefs_weight) : 1;
- const weightEl = $("sa_user_prefs_weight");
+ const weightEl = $2("sa_user_prefs_weight");
if (weightEl) {
weightEl.value = String(Math.max(0, Math.min(1.5, w)));
- const lab = $("sa_user_prefs_weight_val");
+ const lab = $2("sa_user_prefs_weight_val");
if (lab) {
lab.textContent = Number(weightEl.value).toFixed(1);
}
@@ -6333,7 +6420,7 @@ ${data.ui.help_extra}`.trim();
return;
}
const num = (id) => {
- const v = parseFloat($(id)?.value);
+ const v = parseFloat($2(id)?.value);
return Number.isFinite(v) ? v : null;
};
const assistant = {
@@ -6379,8 +6466,8 @@ ${data.ui.help_extra}`.trim();
);
}
function syncSettingsHealthLine() {
- const line = $("sa_settings_health_line");
- const badge = $("sa_ollama_health");
+ 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();
@@ -6396,12 +6483,12 @@ ${data.ui.help_extra}`.trim();
return "\u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043E";
}
function renderPersonaSettingsList() {
- const root = $("sa_persona_list");
+ const root = $2("sa_persona_list");
if (!root) {
return;
}
const list = state.personas || state.config?.personas || [];
- const cur = state.settingsPersonaId || $("sa_persona")?.value || list[0]?.id;
+ const cur = state.settingsPersonaId || $2("sa_persona")?.value || list[0]?.id;
state.settingsPersonaId = cur;
root.innerHTML = "";
for (const p of list) {
@@ -6409,7 +6496,7 @@ ${data.ui.help_extra}`.trim();
btn.type = "button";
btn.className = "sa-persona-item" + (p.id === cur ? " sa-persona-item-active" : "");
const accent = p.accent || "currentColor";
- btn.innerHTML = `${escapeHtml(p.title || p.id)}${escapeHtml(personaSourceLabel(p.source))}`;
+ btn.innerHTML = `${escapeHtml2(p.title || p.id)}${escapeHtml2(personaSourceLabel(p.source))}`;
btn.addEventListener("click", () => {
state.settingsPersonaId = p.id;
renderPersonaSettingsList();
@@ -6426,13 +6513,13 @@ ${data.ui.help_extra}`.trim();
const id = state.settingsPersonaId;
const p = (state.personas || []).find((x) => x.id === id);
const canDelete = p && (p.source === "overlay" || p.source === "overlay+bundled");
- const del = $("sa_btn_persona_delete_panel");
+ const del = $2("sa_btn_persona_delete_panel");
if (del) {
del.disabled = !canDelete;
}
}
function loadPersonaPreview(id) {
- const box = $("sa_persona_preview");
+ const box = $2("sa_persona_preview");
if (!box || typeof genericRequest !== "function") {
return;
}
@@ -6450,12 +6537,12 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
},
0,
(err) => {
- box.innerHTML = `${escapeHtml(String(err || "\u043E\u0448\u0438\u0431\u043A\u0430"))}`;
+ box.innerHTML = `${escapeHtml2(String(err || "\u043E\u0448\u0438\u0431\u043A\u0430"))}`;
}
);
}
function exportSelectedPersona() {
- const id = state.settingsPersonaId || $("sa_persona")?.value;
+ const id = state.settingsPersonaId || $2("sa_persona")?.value;
if (!id || typeof genericRequest !== "function") {
return;
}
@@ -6516,7 +6603,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
reader.readAsText(file);
}
function cloneSelectedPersona() {
- const from = state.settingsPersonaId || $("sa_persona")?.value;
+ const from = state.settingsPersonaId || $2("sa_persona")?.value;
if (!from) {
return;
}
@@ -6570,7 +6657,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
if (typeof genericRequest !== "function") {
return;
}
- const persona = $("sa_persona")?.value || "neutral";
+ const persona = $2("sa_persona")?.value || "neutral";
genericRequest(
"AssistentListUserPrefs",
{ persona, limit: 200 },
@@ -6583,11 +6670,11 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
);
}
function renderUserPrefsLists() {
- const persona = $("sa_persona")?.value || "neutral";
+ 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 = $(rootId);
+ const root = $2(rootId);
if (!root) {
return;
}
@@ -6600,7 +6687,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
const el = document.createElement("div");
el.className = "sa-mem-row";
const pin = row.pinned ? " \u2605" : "";
- el.innerHTML = `${escapeHtml(row.key || "")}${pin}${escapeHtml(clipDebug(row.text, 200))}`;
+ el.innerHTML = `${escapeHtml2(row.key || "")}${pin}${escapeHtml2(clipDebug(row.text, 200))}`;
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");
@@ -6640,7 +6727,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
key: row.key,
text: String(text).trim(),
scope: row.scope || "global",
- persona: row.persona_id || row.persona || $("sa_persona")?.value || "neutral",
+ persona: row.persona_id || row.persona || $2("sa_persona")?.value || "neutral",
source: "user",
pinned: !!row.pinned
},
@@ -6656,7 +6743,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
key: row.key,
text: row.text,
scope: row.scope || "global",
- persona: row.persona_id || row.persona || $("sa_persona")?.value || "neutral",
+ persona: row.persona_id || row.persona || $2("sa_persona")?.value || "neutral",
source: row.source || "user",
pinned: !row.pinned
},
@@ -6680,7 +6767,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
key: key.trim(),
text: text.trim(),
scope,
- persona: $("sa_persona")?.value || "neutral",
+ persona: $2("sa_persona")?.value || "neutral",
source: "user",
pinned: false
},
@@ -6698,7 +6785,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
{
key: row.key,
scope: row.scope || "global",
- persona: row.persona_id || row.persona || $("sa_persona")?.value
+ persona: row.persona_id || row.persona || $2("sa_persona")?.value
},
() => refreshUserPrefs(),
0,
@@ -6712,7 +6799,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
}
genericRequest(
"AssistentClearUserPrefs",
- { scope, persona: $("sa_persona")?.value || "neutral" },
+ { scope, persona: $2("sa_persona")?.value || "neutral" },
(data) => {
setStatus(`\u0423\u0434\u0430\u043B\u0435\u043D\u043E: ${data?.deleted ?? 0}`);
refreshUserPrefs();
@@ -6756,7 +6843,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
}
}
state.wantedKeys = keys;
- const el = $("sa_mem_wanted");
+ const el = $2("sa_mem_wanted");
if (el) {
el.textContent = state.wanted.count ? `\u041E\u0447\u0435\u0440\u0435\u0434\u044C wanted: ${state.wanted.count} (\u0441\u043A\u0430\u0447\u0430\u0435\u0442\u0441\u044F \u043D\u0430 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u043C up)` : "\u041E\u0447\u0435\u0440\u0435\u0434\u044C wanted: \u043F\u0443\u0441\u0442\u043E";
el.title = items.slice(0, 12).map((i) => `${i.kind}: ${i.title || i.url}`).join("\n");
@@ -6767,7 +6854,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
},
0,
() => {
- const el = $("sa_mem_wanted");
+ const el = $2("sa_mem_wanted");
if (el) {
el.textContent = "\u041E\u0447\u0435\u0440\u0435\u0434\u044C wanted: \u2014";
}
@@ -6789,7 +6876,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
}
function setOllamaHealth(level, text, title) {
state.ollamaHealth = level;
- const el = $("sa_ollama_health");
+ const el = $2("sa_ollama_health");
if (!el) {
return;
}
@@ -6804,7 +6891,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
if (typeof genericRequest !== "function") {
return;
}
- const baseUrl = $("sa_base_url")?.value || "http://127.0.0.1:11434";
+ const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434";
genericRequest(
"AssistentListModels",
{ baseUrl },
@@ -6826,7 +6913,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
);
}
function setCardStatus(msg) {
- const el = $("sa_card_status");
+ const el = $2("sa_card_status");
if (el) {
el.textContent = msg || "";
}
@@ -6836,12 +6923,15 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
state.view = "cards";
} else if (view === "settings") {
state.view = "settings";
+ } else if (view === "train") {
+ state.view = "train";
} else {
state.view = "chat";
}
- const chat = $("sa_view_chat");
- const cards = $("sa_view_cards");
- const settings = $("sa_view_settings");
+ const chat = $2("sa_view_chat");
+ const cards = $2("sa_view_cards");
+ const settings = $2("sa_view_settings");
+ const train = $2("sa_view_train");
if (chat) {
chat.hidden = state.view !== "chat";
}
@@ -6851,18 +6941,25 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
if (settings) {
settings.hidden = state.view !== "settings";
}
- $("sa_tab_chat")?.classList.toggle("sa-subtab-active", state.view === "chat");
- $("sa_tab_cards")?.classList.toggle("sa-subtab-active", state.view === "cards");
- $("sa_tab_settings")?.classList.toggle("sa-subtab-active", state.view === "settings");
- $("sa_btn_settings")?.classList.toggle("sa-subtab-active", state.view === "settings");
- $("sa_tab_chat")?.setAttribute("aria-selected", state.view === "chat" ? "true" : "false");
- $("sa_tab_cards")?.setAttribute("aria-selected", state.view === "cards" ? "true" : "false");
- $("sa_tab_settings")?.setAttribute("aria-selected", state.view === "settings" ? "true" : "false");
+ 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_cards", state.view === "cards");
+ tabActive("sa_tab_settings", state.view === "settings");
+ tabActive("sa_tab_train", state.view === "train");
saveSettings();
if (state.view === "cards") {
renderCardsList();
} else 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 });
}
@@ -6945,7 +7042,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
await Promise.all(keys.map((k) => prefetchCard(k.kind, k.name)));
}
function cardsCatalog() {
- const kind = $("sa_cards_kind")?.value || "all";
+ const kind = $2("sa_cards_kind")?.value || "all";
const inv = state.inventory || {};
const rows = [];
if (kind === "all" || kind === "checkpoint") {
@@ -6978,7 +7075,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
return rows;
}
function renderCardsList() {
- const root = $("sa_cards_list");
+ const root = $2("sa_cards_list");
if (!root) {
return;
}
@@ -6995,7 +7092,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
if (state.cardsSelection && state.cardsSelection.kind === row.kind && state.cardsSelection.name === row.name) {
btn.classList.add("sa-selected");
}
- const thumb = row.preview_url ? `
` : '';
+ const thumb = row.preview_url ? `
` : '';
const metaBits = [];
metaBits.push(row.has_card ? "card \u2713" : "\u043D\u0435\u0442 card");
if (row.has_sidecar) {
@@ -7008,7 +7105,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
if (row.trigger) {
metaBits.push(String(row.trigger).slice(0, 40));
}
- btn.innerHTML = `${thumb}${escapeHtml(row.kind)}${escapeHtml(row.title || row.name)}`;
+ btn.innerHTML = `${thumb}${escapeHtml2(row.kind)}${escapeHtml2(row.title || row.name)}`;
btn.addEventListener("click", (e) => {
if (e.target?.closest?.("[data-chat]")) {
e.preventDefault();
@@ -7029,22 +7126,22 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
setView("chat");
const kind = row.kind === "checkpoint" ? "checkpoint" : "LoRA";
const triggers = row.trigger ? ` Triggers: ${row.trigger}.` : "";
- if ($("sa_input")) {
- $("sa_input").value = `\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 ${kind} \xAB${row.name}\xBB.${triggers} \u0423\u0447\u0442\u0438 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0443/triggers \u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438 \u043F\u0430\u0442\u0447.`;
- $("sa_input").focus();
+ if ($2("sa_input")) {
+ $2("sa_input").value = `\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 ${kind} \xAB${row.name}\xBB.${triggers} \u0423\u0447\u0442\u0438 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0443/triggers \u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438 \u043F\u0430\u0442\u0447.`;
+ $2("sa_input").focus();
}
setStatus(`\u0412 \u0447\u0430\u0442 \u2192 ${row.name}`);
}
function wireCardForm() {
const sync = () => {
- if ($("sa_card_show_json")?.checked) {
+ if ($2("sa_card_show_json")?.checked) {
syncCardJsonFromForm();
}
};
- ["sa_card_triggers", "sa_card_weight", "sa_card_when", "sa_card_avoid", "sa_card_hint", "sa_card_notes", "sa_card_url"].forEach((id) => $(id)?.addEventListener("change", sync));
- $("sa_card_show_json")?.addEventListener("change", () => {
- const on = !!$("sa_card_show_json")?.checked;
- const ta = $("sa_card_json");
+ ["sa_card_triggers", "sa_card_weight", "sa_card_when", "sa_card_avoid", "sa_card_hint", "sa_card_notes", "sa_card_url"].forEach((id) => $2(id)?.addEventListener("change", sync));
+ $2("sa_card_show_json")?.addEventListener("change", () => {
+ const on = !!$2("sa_card_show_json")?.checked;
+ const ta = $2("sa_card_json");
if (ta) {
ta.hidden = !on;
if (on) {
@@ -7052,8 +7149,8 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
}
}
});
- $("sa_card_json")?.addEventListener("change", () => {
- if ($("sa_card_show_json")?.checked) {
+ $2("sa_card_json")?.addEventListener("change", () => {
+ if ($2("sa_card_show_json")?.checked) {
applyCardToForm(readCardDraft() || {});
}
});
@@ -7061,60 +7158,60 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
function applyCardToForm(card) {
card = card || {};
const triggers = Array.isArray(card.triggers) ? card.triggers.join(", ") : card.triggers || "";
- if ($("sa_card_triggers")) {
- $("sa_card_triggers").value = triggers;
+ if ($2("sa_card_triggers")) {
+ $2("sa_card_triggers").value = triggers;
}
- if ($("sa_card_weight")) {
- $("sa_card_weight").value = card.weight != null ? card.weight : state.cardsSelection?.kind === "lora" ? 0.8 : 1;
+ if ($2("sa_card_weight")) {
+ $2("sa_card_weight").value = card.weight != null ? card.weight : state.cardsSelection?.kind === "lora" ? 0.8 : 1;
}
- if ($("sa_card_when")) {
- $("sa_card_when").value = card.when || "";
+ if ($2("sa_card_when")) {
+ $2("sa_card_when").value = card.when || "";
}
- if ($("sa_card_avoid")) {
- $("sa_card_avoid").value = card.avoid || "";
+ if ($2("sa_card_avoid")) {
+ $2("sa_card_avoid").value = card.avoid || "";
}
- if ($("sa_card_hint")) {
- $("sa_card_hint").value = card.prompt_hint || "";
+ if ($2("sa_card_hint")) {
+ $2("sa_card_hint").value = card.prompt_hint || "";
}
- if ($("sa_card_notes")) {
- $("sa_card_notes").value = card.notes || "";
+ if ($2("sa_card_notes")) {
+ $2("sa_card_notes").value = card.notes || "";
}
- if ($("sa_card_url")) {
- $("sa_card_url").value = card.civitai_url || "";
+ if ($2("sa_card_url")) {
+ $2("sa_card_url").value = card.civitai_url || "";
}
- if ($("sa_card_json")) {
- $("sa_card_json").value = JSON.stringify(card, null, 2);
+ if ($2("sa_card_json")) {
+ $2("sa_card_json").value = JSON.stringify(card, null, 2);
}
}
function syncCardJsonFromForm() {
const sel = state.cardsSelection || {};
let base = {};
try {
- base = JSON.parse($("sa_card_json")?.value || "{}");
+ base = JSON.parse($2("sa_card_json")?.value || "{}");
} catch (e) {
base = {};
}
- const triggers = String($("sa_card_triggers")?.value || "").split(/[,;]/).map((s) => s.trim()).filter(Boolean);
+ const triggers = String($2("sa_card_triggers")?.value || "").split(/[,;]/).map((s) => s.trim()).filter(Boolean);
const card = {
...base,
kind: sel.kind || base.kind || "lora",
name: sel.name || base.name || "",
triggers,
- weight: parseFloat($("sa_card_weight")?.value || "0.8") || 0.8,
- when: $("sa_card_when")?.value || "",
- avoid: $("sa_card_avoid")?.value || "",
- prompt_hint: $("sa_card_hint")?.value || "",
- notes: $("sa_card_notes")?.value || "",
- civitai_url: $("sa_card_url")?.value || "",
+ weight: parseFloat($2("sa_card_weight")?.value || "0.8") || 0.8,
+ when: $2("sa_card_when")?.value || "",
+ avoid: $2("sa_card_avoid")?.value || "",
+ prompt_hint: $2("sa_card_hint")?.value || "",
+ notes: $2("sa_card_notes")?.value || "",
+ civitai_url: $2("sa_card_url")?.value || "",
version_id: base.version_id != null ? base.version_id : null
};
- if ($("sa_card_json")) {
- $("sa_card_json").value = JSON.stringify(card, null, 2);
+ if ($2("sa_card_json")) {
+ $2("sa_card_json").value = JSON.stringify(card, null, 2);
}
return card;
}
function renderCardPreviews(urls) {
- const root = $("sa_card_previews");
+ const root = $2("sa_card_previews");
if (!root) {
return;
}
@@ -7232,7 +7329,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
...data.example_urls || []
];
renderCardPreviews(urls);
- const badge = $("sa_card_badge");
+ const badge = $2("sa_card_badge");
if (badge) {
badge.hidden = false;
badge.textContent = data.has_card ? "card \u2713" : data.has_sidecar || data.fetched ? "meta \u2713" : "\u043D\u0435\u0442 \u043C\u0435\u0442\u044B";
@@ -7242,10 +7339,10 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
function selectCardModel(row) {
state.cardsSelection = row;
renderCardsList();
- if ($("sa_card_title")) {
- $("sa_card_title").textContent = row.title || row.name;
+ if ($2("sa_card_title")) {
+ $2("sa_card_title").textContent = row.title || row.name;
}
- const badge = $("sa_card_badge");
+ const badge = $2("sa_card_badge");
if (badge) {
badge.hidden = false;
badge.textContent = "\u2026";
@@ -7282,8 +7379,8 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
}
function readCardDraft() {
const fromForm = syncCardJsonFromForm();
- if ($("sa_card_show_json")?.checked) {
- const raw = $("sa_card_json")?.value || "";
+ if ($2("sa_card_show_json")?.checked) {
+ const raw = $2("sa_card_json")?.value || "";
try {
return JSON.parse(raw);
} catch (e) {
@@ -7355,7 +7452,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
return base.replace(/\.safetensors$/i, "").slice(0, 28);
}
function renderLoraChips() {
- const root = $("sa_lora_chips");
+ const root = $2("sa_lora_chips");
if (!root) {
return;
}
@@ -7458,7 +7555,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
};
filter.addEventListener("input", draw);
draw();
- const composer = $("sa_composer") || document.body;
+ const composer = $2("sa_composer") || document.body;
composer.style.position = composer.style.position || "relative";
composer.appendChild(picker);
const onDoc = (ev) => {
@@ -7542,8 +7639,8 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
if (fromCards) {
const card = extractCardJson(reply);
if (card) {
- if ($("sa_card_json")) {
- $("sa_card_json").value = JSON.stringify(card, null, 2);
+ if ($2("sa_card_json")) {
+ $2("sa_card_json").value = JSON.stringify(card, null, 2);
}
if (opts.fromDownload || opts.cardTarget) {
const kind = card.kind || opts.cardTarget?.kind || "lora";
@@ -7614,7 +7711,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
if (Array.isArray(effective?.actions) && effective.actions.map(String).includes("interrupt")) {
doInterruptNow();
}
- if (civitaiResults && civitaiResults.length && $("sa_auto_download")?.checked) {
+ if (civitaiResults && civitaiResults.length && $2("sa_auto_download")?.checked) {
const pick = civitaiResults.find((r) => !r.already_installed && r.download_url && r.krea_likely) || civitaiResults.find((r) => !r.already_installed && r.download_url);
if (pick) {
downloadCivitaiLoRA(pick, null);
@@ -7657,7 +7754,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
});
return;
}
- const doApply = !!(effective && (intent.generate || $("sa_auto_apply")?.checked));
+ const doApply = !!(effective && (intent.generate || $2("sa_auto_apply")?.checked));
if (doApply) {
if (intent.generate) {
startBusyUi("silent_gen");
@@ -7696,13 +7793,13 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
await applyPatch(withActions, "all");
state.lastUserParamIntent = prevIntent;
setStatus(note || "Applied");
- if ($("sa_auto_generate")?.checked) {
+ if ($2("sa_auto_generate")?.checked) {
await runGenerateFromPatch(withActions);
}
syncChipHighlight();
}
function syncChipHighlight() {
- const bar = $("sa_chips");
+ const bar = $2("sa_chips");
if (!bar) {
return;
}
@@ -7718,7 +7815,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
});
}
function appendSystemNote(text) {
- const box = $("sa_messages");
+ const box = $2("sa_messages");
if (!box) {
return;
}
@@ -7747,10 +7844,10 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
}).join(", ");
}
function buildDebugSummary() {
- const persona = $("sa_persona")?.value || "neutral";
- const pack = $("sa_pack")?.value || defaultPackId();
- const chatModel = $("sa_model")?.value || "\u2014";
- const embed = $("sa_embed_model")?.value || state.preferredEmbed || "\u2014";
+ 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 = detectKreaProfileName();
const defaults = mergedGenerationDefaults(profile);
const session = state.sessionExact || {};
@@ -7785,7 +7882,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
`persona=${persona} \xB7 pack=${pack}`,
`chat=${chatModel} \xB7 embed=${embed}`,
`skills=${(state.enabledSkills || []).join(",") || "\u2014"}`,
- `auto: apply=${!!$("sa_auto_apply")?.checked} gen=${!!$("sa_auto_generate")?.checked} vision=${!!$("sa_auto_vision")?.checked} critique=${!!$("sa_auto_critique")?.checked}`,
+ `auto: apply=${!!$2("sa_auto_apply")?.checked} gen=${!!$2("sa_auto_generate")?.checked} 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}`,
@@ -7909,8 +8006,8 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
}
slot.attach = true;
renderBoard();
- if ($("sa_input")) {
- $("sa_input").value = `Look at ${id} and describe what you see.`;
+ 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 });
@@ -7982,7 +8079,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
if (!setPackValue(arg, { flash: true, user: true })) {
setStatus("Pack: write|ordinary|critique|compose|params|inpaint|describe|card|persona");
} else {
- setStatus(`Pack \u2192 ${$("sa_pack")?.value}`);
+ setStatus(`Pack \u2192 ${$2("sa_pack")?.value}`);
}
return true;
}
@@ -7999,8 +8096,8 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
setStatus("/civitai ");
return true;
}
- if ($("sa_input")) {
- $("sa_input").value = `Find a Krea 2 LoRA for: ${arg}`;
+ if ($2("sa_input")) {
+ $2("sa_input").value = `Find a Krea 2 LoRA for: ${arg}`;
}
setPackValue(defaultPackId(), { flash: true });
await sendChat({
@@ -8020,7 +8117,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
});
return true;
}
- const fromId = sub === "clone" && rest ? rest.split(/\s+/)[0] : $("sa_persona")?.value || "neutral";
+ 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.`
@@ -8066,8 +8163,8 @@ ${HELP_TEXT}`);
s.attach = true;
}
renderBoard();
- if ($("sa_input")) {
- $("sa_input").value = `Look at board slots: ${need.map((s) => s.id).join(", ")}. Continue using these images.`;
+ 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) });
@@ -8077,7 +8174,11 @@ ${HELP_TEXT}`);
if ((state.busy || state.generating) && !isContinuationTurn(opts)) {
return;
}
- const rawInput = ($("sa_input")?.value || "").trim();
+ 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;
@@ -8089,8 +8190,8 @@ ${HELP_TEXT}`);
}
if (!isMachineTurn(opts) && !opts.skipSlash) {
if (rawInput.startsWith("/")) {
- if ($("sa_input")) {
- $("sa_input").value = "";
+ if ($2("sa_input")) {
+ $2("sa_input").value = "";
}
const handled = await handleSlashCommand(rawInput);
if (handled) {
@@ -8101,8 +8202,8 @@ ${HELP_TEXT}`);
if (!isMachineTurn(opts) && isSameButAspectRequest(text)) {
const aspect = parseAspectFromUserText(text);
if (aspect) {
- if ($("sa_input")) {
- $("sa_input").value = "";
+ if ($2("sa_input")) {
+ $2("sa_input").value = "";
}
appendMessage("user", text);
state.history.push({ role: "user", content: text });
@@ -8133,9 +8234,9 @@ ${HELP_TEXT}`);
if (!opts.fromDebug && (opts.fromCards || state.view === "cards")) {
setPackValue("catalog_card", { flash: false });
}
- const pack = opts.fromDebug ? "debug_explain" : $("sa_pack")?.value || defaultPackId();
- const persona = $("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral";
- const model = $("sa_model")?.value;
+ 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();
@@ -8211,8 +8312,8 @@ ${HELP_TEXT}`);
state.pendingPersonaNote = null;
}
appendMessage("user", opts.historyUserText || text);
- if ($("sa_input")) {
- $("sa_input").value = "";
+ if ($2("sa_input")) {
+ $2("sa_input").value = "";
}
persistHistory();
} else {
@@ -8255,7 +8356,7 @@ ${HELP_TEXT}`);
messages[messages.length - 1].images = images;
}
startBusyUi("thinking");
- const baseUrl = $("sa_base_url")?.value || "http://127.0.0.1:11434";
+ const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434";
const payload = {
baseUrl,
model,
@@ -8265,7 +8366,7 @@ ${HELP_TEXT}`);
messages,
context_json: JSON.stringify(context),
skills: opts.fromDebug ? [] : state.enabledSkills || [],
- embed_model: $("sa_embed_model")?.value || state.preferredEmbed || ""
+ embed_model: $2("sa_embed_model")?.value || state.preferredEmbed || ""
};
const finishOk = async (reply, civitaiResults, meta = {}) => {
if (chatEpoch !== state.chatEpoch) {
@@ -8418,8 +8519,8 @@ ${HELP_TEXT}`);
);
}
function wireDropZone() {
- const board = $("sa_board");
- const layout = $("sa_layout");
+ 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();
@@ -8500,9 +8601,9 @@ ${HELP_TEXT}`);
});
}
function wireSplitter() {
- const splitter = $("sa_splitter");
- const layout = $("sa_layout");
- const pane = $("sa_image_pane");
+ const splitter = $2("sa_splitter");
+ const layout = $2("sa_layout");
+ const pane = $2("sa_image_pane");
if (!splitter || !layout || !pane) {
return;
}
@@ -8553,7 +8654,7 @@ ${HELP_TEXT}`);
note: "Image sent to Assistent",
preferSelected: false
});
- const pack = $("sa_pack");
+ const pack = $2("sa_pack");
if (pack && (pack.value === "ordinary" || pack.value === "write_prompt")) {
pack.value = "critique_image";
saveSettings();
@@ -8587,7 +8688,7 @@ ${HELP_TEXT}`);
refreshWantedQueue();
}
function wire() {
- if (!$("swarm_assistent_root")) {
+ if (!$2("swarm_assistent_root")) {
return;
}
if (typeof genericRequest !== "function") {
@@ -8608,28 +8709,30 @@ ${HELP_TEXT}`);
refreshImagePreview();
}
bootstrapPersisted();
+ setChatsPanelOpen(!!state.chatsDrawerOpen);
wireDropZone();
wireSplitter();
registerSendButton();
wireSlashInput();
wireCardForm();
- $("sa_btn_new_chat")?.addEventListener("click", () => startNewChat({ saveCurrent: true }));
- $("sa_btn_chats")?.addEventListener("click", (e) => {
+ $2("sa_btn_new_chat")?.addEventListener("click", () => startNewChat({ saveCurrent: true }));
+ $2("sa_btn_chats")?.addEventListener("click", (e) => {
e.stopPropagation();
setChatsPanelOpen(!state.chatsPanelOpen);
});
- $("sa_session_label")?.addEventListener("click", (e) => {
+ $2("sa_btn_chats_close")?.addEventListener("click", () => setChatsPanelOpen(false));
+ $2("sa_session_label")?.addEventListener("click", (e) => {
e.stopPropagation();
setChatsPanelOpen(!state.chatsPanelOpen);
});
- $("sa_session_label")?.addEventListener("keydown", (e) => {
+ $2("sa_session_label")?.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setChatsPanelOpen(!state.chatsPanelOpen);
}
});
- $("sa_chats_panel")?.addEventListener("click", (e) => e.stopPropagation());
- $("sa_chats_list")?.addEventListener("click", (e) => {
+ $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;
@@ -8647,8 +8750,8 @@ ${HELP_TEXT}`);
}
});
let chatsSearchTimer = null;
- $("sa_chats_search")?.addEventListener("input", () => {
- const q = ($("sa_chats_search")?.value || "").trim();
+ $2("sa_chats_search")?.addEventListener("input", () => {
+ const q = ($2("sa_chats_search")?.value || "").trim();
state.chatsQuery = q;
if (!q) {
state.chatsSearchHits = null;
@@ -8669,90 +8772,83 @@ ${HELP_TEXT}`);
}
}, 220);
});
- $("sa_tab_chat")?.addEventListener("click", () => setView("chat"));
- $("sa_tab_cards")?.addEventListener("click", () => setView("cards"));
- $("sa_tab_settings")?.addEventListener("click", () => openSettings(state.settingsTab || "behavior"));
- $("sa_board_tab_gen")?.addEventListener("click", () => setBoardTab("generate"));
- $("sa_board_tab_refs")?.addEventListener("click", () => setBoardTab("refs"));
- $("sa_persona")?.addEventListener("change", onPersonaChanged);
- $("sa_persona_delete")?.addEventListener("click", () => deleteCurrentOverlayPersona());
- $("sa_cards_kind")?.addEventListener("change", renderCardsList);
- $("sa_btn_cards_refresh")?.addEventListener("click", () => refreshInventory(() => renderCardsList(), { rescan: true }));
- $("sa_btn_card_meta")?.addEventListener("click", () => fetchCardMetaLive());
- $("sa_btn_card_generate")?.addEventListener("click", () => generateCardWithAssistent());
- $("sa_btn_card_save")?.addEventListener("click", () => saveCurrentCard());
- $("sa_btn_card_wanted")?.addEventListener("click", () => enqueueWantedOnly());
- $("sa_btn_settings")?.addEventListener("click", () => {
- if (state.view === "settings") {
- closeSettings();
- } else {
- openSettings(state.settingsTab || "behavior");
- }
- });
- $("sa_settings_close")?.addEventListener("click", () => closeSettings());
+ $2("sa_tab_chat")?.addEventListener("click", () => setView("chat"));
+ $2("sa_tab_cards")?.addEventListener("click", () => setView("cards"));
+ $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());
+ $2("sa_cards_kind")?.addEventListener("change", renderCardsList);
+ $2("sa_btn_cards_refresh")?.addEventListener("click", () => refreshInventory(() => renderCardsList(), { rescan: true }));
+ $2("sa_btn_card_meta")?.addEventListener("click", () => fetchCardMetaLive());
+ $2("sa_btn_card_generate")?.addEventListener("click", () => generateCardWithAssistent());
+ $2("sa_btn_card_save")?.addEventListener("click", () => saveCurrentCard());
+ $2("sa_btn_card_wanted")?.addEventListener("click", () => enqueueWantedOnly());
document.querySelectorAll("#sa_settings .sa-stab").forEach((btn) => {
btn.addEventListener("click", () => setSettingsTab(btn.getAttribute("data-stab")));
});
- $("sa_btn_mem_refresh")?.addEventListener("click", () => {
+ $2("sa_btn_mem_refresh")?.addEventListener("click", () => {
refreshMemoryList();
refreshWantedQueue();
});
- $("sa_mem_kind")?.addEventListener("change", renderMemoryList);
- $("sa_mem_scope")?.addEventListener("change", renderMemoryList);
- $("sa_mem_search")?.addEventListener("input", () => renderMemoryList());
- $("sa_btn_mem_clear_kind")?.addEventListener("click", () => {
+ $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}` });
});
- $("sa_btn_mem_clear_persona")?.addEventListener("click", () => {
- clearCraftMemory({ scope: "personal", persona: $("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_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" });
});
- $("sa_btn_mem_clear_shared")?.addEventListener("click", () => {
+ $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" });
});
- $("sa_btn_mem_clear_all")?.addEventListener("click", () => {
+ $2("sa_btn_mem_clear_all")?.addEventListener("click", () => {
clearCraftMemory({ label: "\u0432\u0435\u0441\u044C \u043A\u0440\u0430\u0444\u0442 (non-bundled)" });
});
- $("sa_btn_prefs_refresh")?.addEventListener("click", () => refreshUserPrefs());
- $("sa_btn_pref_add_global")?.addEventListener("click", () => addUserPref("global"));
- $("sa_btn_pref_add_persona")?.addEventListener("click", () => addUserPref("persona"));
- $("sa_btn_prefs_clear_global")?.addEventListener("click", () => clearUserPrefs("global"));
- $("sa_btn_prefs_clear_persona")?.addEventListener("click", () => clearUserPrefs("persona"));
- $("sa_btn_prefs_clear_all")?.addEventListener("click", () => clearUserPrefs("all"));
- $("sa_user_prefs_weight")?.addEventListener("input", () => {
- const lab = $("sa_user_prefs_weight_val");
+ $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($("sa_user_prefs_weight").value).toFixed(1);
+ lab.textContent = Number($2("sa_user_prefs_weight").value).toFixed(1);
}
});
- $("sa_user_prefs_weight")?.addEventListener("change", () => saveKnobs());
- $("sa_memory_top_k")?.addEventListener("change", () => saveKnobs());
- $("sa_btn_knobs_save")?.addEventListener("click", () => saveKnobs());
- $("sa_btn_reset_ui")?.addEventListener("click", () => resetUiState());
- $("sa_btn_persona_export")?.addEventListener("click", () => exportSelectedPersona());
- $("sa_btn_persona_import")?.addEventListener("click", () => $("sa_persona_import_file")?.click());
- $("sa_persona_import_file")?.addEventListener("change", (e) => {
+ $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 = "";
});
- $("sa_btn_persona_clone")?.addEventListener("click", () => cloneSelectedPersona());
- $("sa_btn_persona_delete_panel")?.addEventListener("click", () => deleteSelectedOverlayPersona());
- $("sa_btn_settings_health")?.addEventListener("click", () => {
+ $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);
});
- $("sa_settings_chat_model")?.addEventListener("change", () => {
- const v = $("sa_settings_chat_model")?.value;
- if (v && $("sa_model")) {
- $("sa_model").value = v;
+ $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();
}
});
- $("sa_btn_look_result")?.addEventListener("click", () => askLookAtResult());
- $("sa_ollama_health")?.addEventListener("click", () => probeOllamaHealth());
+ $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") {
@@ -8783,7 +8879,7 @@ ${HELP_TEXT}`);
setChatsPanelOpen(false);
closed = true;
}
- const slash = $("sa_slash_menu");
+ const slash = $2("sa_slash_menu");
if (slash && !slash.hidden) {
slash.hidden = true;
closed = true;
@@ -8794,23 +8890,23 @@ ${HELP_TEXT}`);
}
});
document.getElementById(TAB_BUTTON_ID)?.addEventListener("click", () => {
- setTimeout(() => $("sa_input")?.focus(), 80);
+ setTimeout(() => $2("sa_input")?.focus(), 80);
});
- $("sa_btn_refresh_models")?.addEventListener("click", () => {
+ $2("sa_btn_refresh_models")?.addEventListener("click", () => {
saveSettings();
refreshModels();
probeOllamaHealth();
});
- $("sa_btn_refresh_inventory")?.addEventListener("click", () => refreshInventory(() => {
+ $2("sa_btn_refresh_inventory")?.addEventListener("click", () => refreshInventory(() => {
renderCardsList();
renderLoraChips();
}, { rescan: true }));
- $("sa_btn_add_ref")?.addEventListener("click", () => {
+ $2("sa_btn_add_ref")?.addEventListener("click", () => {
setBoardTab("refs");
addRefSlot({ select: true });
});
- $("sa_btn_use_current")?.addEventListener("click", () => snapshotGenerateToRef());
- $("sa_btn_as_init")?.addEventListener("click", async () => {
+ $2("sa_btn_use_current")?.addEventListener("click", () => snapshotGenerateToRef());
+ $2("sa_btn_as_init")?.addEventListener("click", async () => {
closeAllMoreMenus();
const src = selectedSrc() || findCurrentGenerateSrc();
if (!src) {
@@ -8818,12 +8914,12 @@ ${HELP_TEXT}`);
return;
}
await setInitFromSrc(src);
- const pack = $("sa_pack");
+ const pack = $2("sa_pack");
if (pack && (pack.value === "ordinary" || pack.value === "write_prompt")) {
setPackValue("inpaint_edit", { flash: true });
}
});
- $("sa_btn_as_mask")?.addEventListener("click", async () => {
+ $2("sa_btn_as_mask")?.addEventListener("click", async () => {
closeAllMoreMenus();
const src = selectedSrc();
if (!src) {
@@ -8833,43 +8929,43 @@ ${HELP_TEXT}`);
await setMaskFromSrc(src);
setPackValue("inpaint_edit", { flash: true });
});
- $("sa_btn_clear_init")?.addEventListener("click", () => {
+ $2("sa_btn_clear_init")?.addEventListener("click", () => {
if (window.confirm("\u0421\u0431\u0440\u043E\u0441\u0438\u0442\u044C Init \u0438 Mask?")) {
clearInitAndMask();
}
closeAllMoreMenus();
});
- $("sa_btn_clear_image")?.addEventListener("click", () => clearSlot(state.selectedSlotId));
- $("sa_btn_board_more")?.addEventListener("click", (e) => {
+ $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");
});
- $("sa_btn_send")?.addEventListener("click", () => sendChat());
- $("sa_btn_build_gen")?.addEventListener("click", () => buildCurrentAndGenerate());
- $("sa_btn_interrupt")?.addEventListener("click", () => {
+ $2("sa_btn_send")?.addEventListener("click", () => sendChat());
+ $2("sa_btn_build_gen")?.addEventListener("click", () => buildCurrentAndGenerate());
+ $2("sa_btn_interrupt")?.addEventListener("click", () => {
doInterruptNow();
clearInFlightUi({ status: "\u041F\u0440\u0435\u0440\u0432\u0430\u043D\u043E" });
});
- $("sa_btn_clear")?.addEventListener("click", () => {
+ $2("sa_btn_clear")?.addEventListener("click", () => {
if (window.confirm("\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0435\u0441\u044C \u0447\u0430\u0442 Assistent?")) {
clearChatHistory();
}
});
- $("sa_btn_clear_more")?.addEventListener("click", (e) => {
+ $2("sa_btn_clear_more")?.addEventListener("click", (e) => {
e.stopPropagation();
toggleMoreMenu("sa_clear_more_menu", "sa_btn_clear_more");
});
- $("sa_btn_clear_confirm")?.addEventListener("click", () => {
+ $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();
}
});
- $("sa_btn_clear_patches")?.addEventListener("click", () => {
+ $2("sa_btn_clear_patches")?.addEventListener("click", () => {
closeAllMoreMenus();
clearPatchBlocksOnly();
});
- $("sa_btn_card_to_chat")?.addEventListener("click", () => {
+ $2("sa_btn_card_to_chat")?.addEventListener("click", () => {
if (state.cardsSelection) {
sendCardToChat(state.cardsSelection);
} else {
@@ -8882,26 +8978,26 @@ ${HELP_TEXT}`);
}
closeAllMoreMenus();
});
- $("sa_board_more_menu")?.addEventListener("click", (e) => e.stopPropagation());
- $("sa_clear_more_menu")?.addEventListener("click", (e) => e.stopPropagation());
- $("sa_base_url")?.addEventListener("change", saveSettings);
- $("sa_model")?.addEventListener("change", () => {
- const v = $("sa_model")?.value;
- if (v && $("sa_settings_chat_model")) {
- $("sa_settings_chat_model").value = v;
+ $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();
});
- $("sa_embed_model")?.addEventListener("change", () => {
- state.preferredEmbed = $("sa_embed_model")?.value || "";
+ $2("sa_embed_model")?.addEventListener("change", () => {
+ state.preferredEmbed = $2("sa_embed_model")?.value || "";
saveSettings();
});
- $("sa_pack")?.addEventListener("change", () => {
+ $2("sa_pack")?.addEventListener("change", () => {
state.packUserTouched = true;
saveSettings();
syncModeBadge();
});
- $("sa_chips")?.addEventListener("click", async (e) => {
+ $2("sa_chips")?.addEventListener("click", async (e) => {
const btn = e.target.closest(".sa-chip");
if (!btn || state.busy || state.generating) {
return;
@@ -8927,7 +9023,7 @@ ${HELP_TEXT}`);
}
renderLoraChips();
});
- $("sa_auto_vision")?.addEventListener("change", () => {
+ $2("sa_auto_vision")?.addEventListener("change", () => {
saveSettings();
const gen = generateSlot();
if (gen) {
@@ -8935,11 +9031,11 @@ ${HELP_TEXT}`);
renderBoard();
}
});
- $("sa_auto_apply")?.addEventListener("change", saveSettings);
- $("sa_auto_generate")?.addEventListener("change", saveSettings);
- $("sa_auto_critique")?.addEventListener("change", saveSettings);
- $("sa_auto_download")?.addEventListener("change", saveSettings);
- $("sa_park_llm")?.addEventListener("change", saveSettings);
+ $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);
@@ -8976,6 +9072,15 @@ ${HELP_TEXT}`);
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,
@@ -9000,14 +9105,14 @@ ${HELP_TEXT}`);
};
}
function wireSlashInput() {
- const input = $("sa_input");
+ 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 = $("sa_slash_menu");
+ const menu = $2("sa_slash_menu");
const open = menu && !menu.hidden;
if (open) {
const items = slashMatches(input.value.split(/\s/)[0] || "");
@@ -9051,6 +9156,517 @@ ${HELP_TEXT}`);
}
})();
+ // src/training.js
+ var $ = (id) => document.getElementById(id);
+ function escapeHtml(s) {
+ return String(s ?? "").replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
+ }
+ function attachTraining(SA2) {
+ const state = {
+ ttab: "dataset",
+ samples: [],
+ hfResults: [],
+ hfSelected: null,
+ hfCheck: 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();
+ 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 = '';
+ 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 = '\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.';
+ 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 = `
+
+ ${escapeHtml(s.status)}
+ ${escapeHtml(s.source)} \xB7 ${escapeHtml(s.persona || "\u2014")} \xB7 ${escapeHtml(s.pack || "\u2014")}${linked}
+
+
+
+
+
+
+ `;
+ root.appendChild(div);
+ }
+ }
+ async function upsertSample(patch) {
+ await SA2.request("AssistentUpsertTrainSample", patch);
+ await refreshSamples();
+ }
+ 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 = `${escapeHtml(r.gate)}${escapeHtml(r.id)}${escapeHtml(r.reason || "")}`;
+ 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";
+ } 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;
+ try {
+ const data = await SA2.request("AssistentImportHfDataset", { dataset: id, limit });
+ 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 = '';
+ 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?.job?.progress_json ? JSON.parse(data.job.progress_json) : null;
+ const active = data?.training_active || data?.job?.status === "running";
+ 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;
+ }
+ } catch (e) {
+ }
+ }
+ async function startQlora() {
+ setTrainStatus("\u0417\u0430\u043F\u0443\u0441\u043A\u2026");
+ try {
+ await SA2.request("AssistentStartTrainJob", {
+ base_url: $("sa_base_url")?.value,
+ chat_model: $("sa_model")?.value,
+ base_model: $("sa_qlora_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,
+ four_bit: !!$("sa_qlora_4bit")?.checked,
+ hf_dataset: ($("sa_qlora_hf_dataset")?.value || "").trim() || void 0
+ });
+ $("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) => `${escapeHtml(m)}`).join("") : '\u041D\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0435\u0439';
+ } catch (e) {
+ root.innerHTML = `${escapeHtml(e.message)}`;
+ }
+ }
+ async function saveRunner() {
+ try {
+ await SA2.request("AssistentSaveRunnerSettings", {
+ python: $("sa_runner_python")?.value,
+ kind: $("sa_runner_kind")?.value,
+ workdir: $("sa_runner_workdir")?.value,
+ cmd: $("sa_runner_cmd")?.value,
+ gguf_script: $("sa_runner_gguf_script")?.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") && s.kind) $("sa_runner_kind").value = s.kind;
+ 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;
+ } 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_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);
@@ -9063,4 +9679,5 @@ ${HELP_TEXT}`);
window.SA.PATCH_KEYS = keys;
}
};
+ attachTraining(window.SA);
})();
diff --git a/Assets/assistent.css b/Assets/assistent.css
index ecced26..32f691e 100644
--- a/Assets/assistent.css
+++ b/Assets/assistent.css
@@ -17,6 +17,137 @@
opacity: 0.95;
}
+.sa-appbar {
+ display: flex;
+ align-items: center;
+ gap: 0.65rem;
+ flex-wrap: wrap;
+ padding: 0.35rem 0.5rem 0.55rem;
+ border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent);
+ flex: 0 0 auto;
+}
+
+.sa-appbar-brand {
+ display: flex;
+ align-items: center;
+ gap: 0.45rem;
+ min-width: 0;
+}
+
+.sa-appbar-title {
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ font-size: 0.92rem;
+}
+
+.sa-app-tabs {
+ display: inline-flex;
+ gap: 0.25rem;
+ flex: 1;
+ min-width: 0;
+ flex-wrap: wrap;
+}
+
+.sa-app-tab {
+ border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
+ background: transparent;
+ color: inherit;
+ border-radius: 999px;
+ padding: 0.22rem 0.75rem;
+ font-size: 0.82rem;
+ font-weight: 600;
+ cursor: pointer;
+ opacity: 0.72;
+}
+
+.sa-app-tab-active {
+ opacity: 1;
+ background: color-mix(in srgb, currentColor 14%, transparent);
+ border-color: color-mix(in srgb, currentColor 42%, transparent);
+}
+
+.sa-train-banner {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.45rem;
+ padding: 0.25rem 0.65rem;
+ border-radius: 999px;
+ font-size: 0.78rem;
+ font-weight: 600;
+ border: 1px solid color-mix(in srgb, #f5a623 55%, currentColor);
+ background: color-mix(in srgb, #f5a623 16%, transparent);
+ flex: 0 0 auto;
+}
+
+.sa-views {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+}
+
+.sa-views > .sa-view {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.sa-views > .sa-view[hidden] {
+ display: none !important;
+}
+
+#sa_view_chat .sa-layout {
+ flex: 1;
+ min-height: 0;
+}
+
+.sa-chat-workspace {
+ flex: 1 1 auto;
+ display: flex;
+ min-width: 0;
+ min-height: 0;
+ gap: 0;
+}
+
+.sa-chat-body {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+}
+
+.sa-chats-drawer {
+ flex: 0 0 var(--sa-chats-drawer-width, 16rem);
+ width: var(--sa-chats-drawer-width, 16rem);
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ min-height: 0;
+ border-left: 1px solid color-mix(in srgb, currentColor 18%, transparent);
+ background: color-mix(in srgb, currentColor 4%, transparent);
+ padding: 0.45rem 0.5rem;
+ overflow: hidden;
+ transition: width 0.2s ease, flex-basis 0.2s ease, opacity 0.2s ease;
+}
+
+.sa-chats-drawer[hidden] {
+ display: none !important;
+}
+
+.sa-chats-drawer-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.35rem;
+ font-size: 0.82rem;
+}
+
+.sa-chats-drawer-actions {
+ display: inline-flex;
+ gap: 0.2rem;
+}
+
.sa-layout {
display: flex;
flex: 1;
@@ -411,22 +542,21 @@
background: color-mix(in srgb, currentColor 10%, transparent);
}
-.sa-chats-panel {
- position: absolute;
- z-index: 50;
- top: calc(100% - 1px);
- left: 0.5rem;
- right: 0.5rem;
- max-width: 26rem;
- max-height: min(50vh, 22rem);
- display: flex;
- flex-direction: column;
- gap: 0.35rem;
- padding: 0.55rem;
- border-radius: 0 0 0.5rem 0.5rem;
- border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
- background: color-mix(in srgb, #161616 94%, currentColor);
- box-shadow: 0 12px 28px color-mix(in srgb, #000 40%, transparent);
+.sa-chats-panel,
+.sa-chats-drawer {
+ /* drawer lives in .sa-chat-workspace — not a dropdown */
+ position: static;
+ z-index: auto;
+ top: auto;
+ left: auto;
+ right: auto;
+ max-width: none;
+ max-height: none;
+ box-shadow: none;
+ border-radius: 0;
+ border: none;
+ border-left: 1px solid color-mix(in srgb, currentColor 18%, transparent);
+ background: color-mix(in srgb, currentColor 4%, transparent);
}
.sa-chats-panel-head {
@@ -1491,7 +1621,13 @@
vertical-align: middle;
}
-.sa-subtab {
+/* app-level tabs replace in-pane subtabs */
+.sa-subtabs {
+ display: none;
+}
+
+.sa-subtab,
+.sa-app-tab {
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
background: transparent;
color: inherit;
@@ -1503,7 +1639,8 @@
opacity: 0.7;
}
-.sa-subtab-active {
+.sa-subtab-active,
+.sa-app-tab-active {
opacity: 1;
background: color-mix(in srgb, currentColor 12%, transparent);
}
@@ -2016,4 +2153,316 @@
width: 98%;
max-height: 96%;
}
+ .sa-chat-workspace {
+ position: relative;
+ }
+ .sa-chats-drawer:not([hidden]) {
+ position: absolute;
+ z-index: 60;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: min(18rem, 92vw);
+ flex: none;
+ box-shadow: -8px 0 24px color-mix(in srgb, #000 35%, transparent);
+ }
+}
+
+/* Training tab */
+.sa-training {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
+ border-radius: 0.55rem;
+ background: color-mix(in srgb, currentColor 3%, transparent);
+ overflow: hidden;
+}
+
+.sa-training-tabs {
+ display: flex;
+ gap: 0.25rem;
+ padding: 0.45rem 0.55rem;
+ border-bottom: 1px solid color-mix(in srgb, currentColor 16%, transparent);
+ flex-wrap: wrap;
+}
+
+.sa-ttab {
+ border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
+ background: transparent;
+ color: inherit;
+ border-radius: 999px;
+ padding: 0.15rem 0.65rem;
+ font-size: 0.78rem;
+ font-weight: 600;
+ cursor: pointer;
+ opacity: 0.72;
+}
+
+.sa-ttab-active {
+ opacity: 1;
+ background: color-mix(in srgb, currentColor 12%, transparent);
+}
+
+.sa-training-panes {
+ flex: 1;
+ overflow: auto;
+ min-height: 0;
+ padding: 0.55rem 0.65rem 0.75rem;
+}
+
+.sa-tpane {
+ display: flex;
+ flex-direction: column;
+ gap: 0.55rem;
+ min-height: 0;
+}
+
+.sa-train-toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+ align-items: center;
+}
+
+.sa-train-stats {
+ font-size: 0.78rem;
+ font-weight: 600;
+ opacity: 0.85;
+ margin-right: 0.25rem;
+}
+
+.sa-agent-heard-panel {
+ border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
+ border-radius: 0.45rem;
+ padding: 0.55rem 0.65rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ margin-bottom: 0.35rem;
+}
+
+.sa-agent-heard-head {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.65rem;
+}
+
+.sa-agent-heard-stats {
+ font-size: 0.78rem;
+ font-weight: 600;
+ opacity: 0.8;
+}
+
+.sa-agent-heard-hint {
+ margin: 0;
+ font-size: 0.78rem;
+ opacity: 0.78;
+ line-height: 1.35;
+}
+
+.sa-agent-heard-controls {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.45rem 0.75rem;
+}
+
+.sa-agent-heard-controls label {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3rem;
+ font-size: 0.82rem;
+}
+
+.sa-agent-heard-controls input[type="number"] {
+ width: 2.75rem;
+}
+
+.sa-hf-panel {
+ border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
+ border-radius: 0.45rem;
+ padding: 0.55rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.45rem;
+}
+
+.sa-hf-search-row,
+.sa-hf-link-row,
+.sa-hf-import-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+ align-items: center;
+}
+
+.sa-hf-search,
+.sa-hf-link {
+ flex: 1 1 12rem;
+ min-width: 8rem;
+}
+
+.sa-hf-status {
+ font-size: 0.76rem;
+ opacity: 0.85;
+ min-height: 1.1rem;
+}
+
+.sa-hf-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ max-height: 12rem;
+ overflow: auto;
+}
+
+.sa-hf-row {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.35rem;
+ padding: 0.35rem 0.45rem;
+ border-radius: 0.35rem;
+ border: 1px solid color-mix(in srgb, currentColor 14%, transparent);
+ cursor: pointer;
+ font-size: 0.76rem;
+}
+
+.sa-hf-row:hover {
+ background: color-mix(in srgb, currentColor 8%, transparent);
+}
+
+.sa-hf-row.sa-hf-row-active {
+ border-color: color-mix(in srgb, currentColor 42%, transparent);
+ background: color-mix(in srgb, currentColor 12%, transparent);
+}
+
+.sa-hf-row.sa-hf-rejected {
+ opacity: 0.55;
+ cursor: not-allowed;
+}
+
+.sa-hf-badge {
+ font-size: 0.68rem;
+ padding: 0.05rem 0.35rem;
+ border-radius: 999px;
+ border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
+ white-space: nowrap;
+}
+
+.sa-hf-badge-ok {
+ border-color: color-mix(in srgb, #4caf50 50%, currentColor);
+}
+
+.sa-hf-badge-map {
+ border-color: color-mix(in srgb, #f5a623 50%, currentColor);
+}
+
+.sa-hf-badge-no {
+ border-color: color-mix(in srgb, #e74c3c 50%, currentColor);
+}
+
+.sa-hf-preview {
+ font-size: 0.72rem;
+ max-height: 10rem;
+ overflow: auto;
+ border: 1px solid color-mix(in srgb, currentColor 14%, transparent);
+ border-radius: 0.35rem;
+ padding: 0.4rem;
+ white-space: pre-wrap;
+ font-family: ui-monospace, monospace;
+}
+
+.sa-train-samples {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ max-height: 24rem;
+ overflow: auto;
+}
+
+.sa-train-sample {
+ border: 1px solid color-mix(in srgb, currentColor 16%, transparent);
+ border-radius: 0.4rem;
+ padding: 0.45rem 0.55rem;
+ font-size: 0.76rem;
+}
+
+.sa-train-sample-head {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+ align-items: center;
+ margin-bottom: 0.35rem;
+}
+
+.sa-train-sample textarea {
+ width: 100%;
+ box-sizing: border-box;
+ min-height: 3rem;
+ font-size: 0.74rem;
+}
+
+.sa-msg-curate {
+ display: inline-flex;
+ gap: 0.25rem;
+ margin-top: 0.35rem;
+}
+
+.sa-msg-curate button {
+ font-size: 0.72rem !important;
+ padding: 0.1rem 0.4rem !important;
+}
+
+.sa-train-modes {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.sa-train-form label {
+ display: flex;
+ flex-direction: column;
+ gap: 0.2rem;
+ font-size: 0.78rem;
+ margin-bottom: 0.35rem;
+}
+
+.sa-train-progress-bar {
+ height: 0.35rem;
+ border-radius: 999px;
+ background: color-mix(in srgb, currentColor 12%, transparent);
+ overflow: hidden;
+}
+
+.sa-train-progress-fill {
+ height: 100%;
+ width: 0%;
+ background: color-mix(in srgb, #4caf50 70%, currentColor);
+ transition: width 0.3s ease;
+}
+
+.sa-train-log {
+ max-height: 14rem;
+ overflow: auto;
+ font-size: 0.72rem;
+ margin: 0.35rem 0 0;
+ padding: 0.45rem;
+ border-radius: 0.35rem;
+ background: color-mix(in srgb, #000 22%, transparent);
+}
+
+.sa-train-models-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.sa-root-training-lock .sa-composer,
+.sa-root-training-lock #sa_btn_send,
+.sa-root-training-lock #sa_btn_build_gen {
+ pointer-events: none;
+ opacity: 0.45;
}
diff --git a/AssistentChatPipeline.cs b/AssistentChatPipeline.cs
index a2eec2d..4170e60 100644
--- a/AssistentChatPipeline.cs
+++ b/AssistentChatPipeline.cs
@@ -185,6 +185,7 @@ public partial class SwarmAssistentExtension
{
AssistentMemory.RetrieveOptions opt = MemoryRetrieveOptions(pid);
hits = await Memory.RetrieveAsync(root, retrieveQuery, opt.TopK, embed, Config.PersonaExtendsChain(pid), opt);
+ hits = FilterHeardHitsIfDisabled(hits);
}
catch (Exception ex)
{
@@ -192,7 +193,7 @@ public partial class SwarmAssistentExtension
}
}
- string enrichedContext = InjectMemoryHits(contextJson, hits);
+ string enrichedContext = InjectMemoryHits(contextJson, hits, pid);
if (!slimDebug)
{
enrichedContext = EnrichPersonaContext(enrichedContext, pid, packName);
@@ -338,24 +339,51 @@ public partial class SwarmAssistentExtension
AssistentMemory.RetrieveOptions MemoryRetrieveOptions(string pid)
{
JObject a = Config.LoadAssistant(pid) ?? new JObject();
+ JObject agent = Config.LoadTrainingAgent();
AssistentMemory.RetrieveOptions opt = new()
{
TopK = a["memory_top_k"]?.Value() ?? 8,
MinScore = a["memory_min_score"]?.Value() ?? 0.32f,
ApplyQuotas = true,
};
- if (a["memory_quotas"] is JObject quotas)
+ Dictionary quotas = AssistentMemory.CopyDefaultQuotas();
+ if (a["memory_quotas"] is JObject qOverrides)
{
- Dictionary d = new(StringComparer.OrdinalIgnoreCase);
- foreach (JProperty p in quotas.Properties())
+ foreach (JProperty p in qOverrides.Properties())
{
- d[p.Name] = p.Value?.Value() ?? 2;
+ quotas[p.Name] = p.Value?.Value() ?? 2;
}
- opt.Quotas = d;
}
+ if (agent["enabled"]?.Value() != false)
+ {
+ quotas["heard"] = agent["heard_quota"]?.Value() ?? 3;
+ }
+ else
+ {
+ quotas.Remove("heard");
+ }
+ opt.Quotas = quotas;
return opt;
}
+ JArray FilterHeardHitsIfDisabled(JArray hits)
+ {
+ if (Config.LoadTrainingAgent()["enabled"]?.Value() != false)
+ {
+ return hits;
+ }
+ JArray filtered = [];
+ foreach (JToken t in hits ?? [])
+ {
+ if (t is JObject ho && string.Equals(ho["kind"]?.ToString(), AssistentMemory.HeardKind, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+ filtered.Add(t);
+ }
+ return filtered;
+ }
+
async Task<(string follow, JArray civitai)> RunToolHop(
Session session,
string root,
@@ -413,6 +441,38 @@ public partial class SwarmAssistentExtension
+ rows.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
null);
}
+ if (tool == "heard_search")
+ {
+ if (Config.LoadTrainingAgent()["enabled"]?.Value() == false)
+ {
+ return ("heard_search disabled in training-agent settings.", null);
+ }
+ string q = patch["memory_query"]?.ToString()?.Trim()
+ ?? patch["search_query"]?.ToString()?.Trim()
+ ?? ExtractMemoryQuery(patch);
+ if (string.IsNullOrWhiteSpace(q) || !hopDone.Add("heard:" + q))
+ {
+ return (null, null);
+ }
+ int topK = Config.LoadTrainingAgent()["heard_quota"]?.Value() ?? 3;
+ JArray rows = await Memory.SearchAsync(root, q, AssistentMemory.HeardKind, topK, embed, chain);
+ JArray examples = [];
+ foreach (JToken t in rows)
+ {
+ if (t is JObject ho)
+ {
+ JObject ex = Memory.BuildHeardExampleFromHit(ho, chain);
+ if (ex is not null)
+ {
+ examples.Add(ex);
+ }
+ }
+ }
+ return (
+ "heard_search — curated dialogue examples the assistant learned (style/reference, not hard rules). Use tone and structure; omit heard_search unless you need more examples.\n```json\n"
+ + examples.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
+ null);
+ }
if (tool == "lookup_tags")
{
string q = ExtractTagQuery(patch);
@@ -599,7 +659,7 @@ public partial class SwarmAssistentExtension
return outRows;
}
- string InjectMemoryHits(string contextJson, JArray hits, JObject exact = null)
+ string InjectMemoryHits(string contextJson, JArray hits, string personaId = null)
{
JObject ctx;
try
@@ -614,7 +674,7 @@ public partial class SwarmAssistentExtension
int hitChars = 240;
try
{
- string pidHit = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? Config?.DefaultPersonaId() ?? "neutral";
+ string pidHit = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? personaId ?? Config?.DefaultPersonaId() ?? "neutral";
hitChars = Config?.LoadAssistant(pidHit)?["memory_hit_chars"]?.Value() ?? 240;
}
catch
@@ -623,7 +683,11 @@ public partial class SwarmAssistentExtension
}
hitChars = Math.Max(80, Math.Min(hitChars, 800));
+ string pid = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? personaId ?? Config?.DefaultPersonaId() ?? "neutral";
+ IEnumerable chain = Config?.PersonaExtendsChain(pid) ?? [];
+
JArray clippedHits = [];
+ JArray heardExamples = [];
foreach (JToken t in hits ?? [])
{
if (t is not JObject ho)
@@ -634,6 +698,15 @@ public partial class SwarmAssistentExtension
{
continue;
}
+ if (string.Equals(ho["kind"]?.ToString(), AssistentMemory.HeardKind, StringComparison.OrdinalIgnoreCase))
+ {
+ JObject ex = Memory?.BuildHeardExampleFromHit(ho, chain);
+ if (ex is not null)
+ {
+ heardExamples.Add(ex);
+ }
+ continue;
+ }
JObject copy = (JObject)ho.DeepClone();
string text = copy["text"]?.ToString() ?? "";
if (text.Length > hitChars)
@@ -644,6 +717,14 @@ public partial class SwarmAssistentExtension
clippedHits.Add(copy);
}
ctx["memory_hits"] = clippedHits;
+ if (heardExamples.Count > 0)
+ {
+ ctx["heard_examples"] = heardExamples;
+ }
+ else
+ {
+ ctx.Remove("heard_examples");
+ }
ctx.Remove("taste_profile");
ctx.Remove("enabled_loras"); // alias of selected_loras — do not double-feed
try
diff --git a/AssistentConfig.cs b/AssistentConfig.cs
index 4777dfa..cce693e 100644
--- a/AssistentConfig.cs
+++ b/AssistentConfig.cs
@@ -1380,6 +1380,40 @@ public sealed class AssistentConfig
}
}
+ public JObject LoadTrainingRunner()
+ => TryReadJson(Path.Combine(_overlayRoot, "training-runner.json")) ?? new JObject();
+
+ public void SaveTrainingRunner(JObject settings)
+ {
+ lock (_lock)
+ {
+ Directory.CreateDirectory(_overlayRoot);
+ string path = Path.Combine(_overlayRoot, "training-runner.json");
+ JObject merged = DeepMerge(LoadTrainingRunner(), settings ?? new JObject());
+ File.WriteAllText(path, merged.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
+ }
+ }
+
+ public JObject LoadTrainingAgent()
+ => TryReadJson(Path.Combine(_overlayRoot, "training-agent.json"))
+ ?? new JObject
+ {
+ ["enabled"] = true,
+ ["auto_link_on_approve"] = true,
+ ["heard_quota"] = 3,
+ };
+
+ public void SaveTrainingAgent(JObject settings)
+ {
+ lock (_lock)
+ {
+ Directory.CreateDirectory(_overlayRoot);
+ string path = Path.Combine(_overlayRoot, "training-agent.json");
+ JObject merged = DeepMerge(LoadTrainingAgent(), settings ?? new JObject());
+ File.WriteAllText(path, merged.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
+ }
+ }
+
public JObject LoadOllamaRoles()
{
return TryReadJson(Path.Combine(_overlayRoot, "ollama-roles.json"))
diff --git a/AssistentHuggingFace.cs b/AssistentHuggingFace.cs
new file mode 100644
index 0000000..e75dd5d
--- /dev/null
+++ b/AssistentHuggingFace.cs
@@ -0,0 +1,483 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using SwarmUI.Accounts;
+using SwarmUI.Utils;
+
+namespace Mrleo1nid.SwarmAssistent;
+
+/// Hugging Face datasets: search, compatibility gate, preview, import.
+public partial class SwarmAssistentExtension
+{
+ static readonly Regex HfRepoIdRe = new(@"^[A-Za-z0-9][A-Za-z0-9._-]{0,95}(/[A-Za-z0-9][A-Za-z0-9._-]{0,95})?$", RegexOptions.Compiled);
+
+ const string HfDatasetsServer = "https://datasets-server.huggingface.co";
+ const string HfHubApi = "https://huggingface.co/api/datasets";
+
+ static string GetHfToken(Session session)
+ => session?.User?.GetGenericData("huggingface_api", "key")?.Trim();
+
+ static HttpRequestMessage HfRequest(string url, Session session)
+ {
+ HttpRequestMessage req = new(HttpMethod.Get, url);
+ string token = GetHfToken(session);
+ if (!string.IsNullOrWhiteSpace(token))
+ {
+ req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
+ }
+ return req;
+ }
+
+ /// Normalize owner/name or HF datasets URL. Returns null when invalid.
+ public static string NormalizeHfDatasetId(string raw)
+ {
+ string s = (raw ?? "").Trim();
+ if (string.IsNullOrWhiteSpace(s))
+ {
+ return null;
+ }
+ if (s.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || s.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
+ {
+ if (!Uri.TryCreate(s, UriKind.Absolute, out Uri uri))
+ {
+ return null;
+ }
+ if (!string.Equals(uri.Host, "huggingface.co", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ string[] parts = uri.AbsolutePath.Trim('/').Split('/');
+ if (parts.Length < 2 || !string.Equals(parts[0], "datasets", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ s = $"{parts[1]}/{parts[2]}";
+ }
+ s = s.Trim().TrimEnd('/');
+ return HfRepoIdRe.IsMatch(s) ? s : null;
+ }
+
+ public async Task AssistentSearchHfDatasets(Session session, string q = null, int limit = 20, bool show_all = false)
+ {
+ int take = Math.Clamp(limit, 1, 50);
+ string search = (q ?? "").Trim();
+ StringBuilder url = new($"{HfHubApi}?limit={take}&full=true");
+ url.Append("&filter=task_categories:text-generation");
+ url.Append("&filter=modality:text");
+ if (!string.IsNullOrWhiteSpace(search))
+ {
+ url.Append("&search=").Append(Uri.EscapeDataString(search));
+ }
+ try
+ {
+ using HttpRequestMessage req = HfRequest(url.ToString(), session);
+ using HttpResponseMessage resp = await HttpClient.SendAsync(req);
+ string body = await resp.Content.ReadAsStringAsync();
+ if (!resp.IsSuccessStatusCode)
+ {
+ return new JObject { ["error"] = $"HF search HTTP {(int)resp.StatusCode}: {Clip(body, 300)}" };
+ }
+ JArray rawList = JArray.Parse(body);
+ JArray results = [];
+ foreach (JToken item in rawList)
+ {
+ if (item is not JObject o)
+ {
+ continue;
+ }
+ string id = o["id"]?.ToString();
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ continue;
+ }
+ JObject check = await CheckHfDatasetInternal(session, id, useCache: true);
+ string gate = check["gate"]?.ToString() ?? "rejected";
+ if (!show_all && gate == "rejected")
+ {
+ continue;
+ }
+ results.Add(new JObject
+ {
+ ["id"] = id,
+ ["title"] = o["id"],
+ ["downloads"] = o["downloads"],
+ ["gate"] = gate,
+ ["reason"] = check["reason"],
+ ["schema"] = check["schema"],
+ });
+ }
+ return new JObject { ["success"] = true, ["results"] = results, ["has_hf_token"] = !string.IsNullOrWhiteSpace(GetHfToken(session)) };
+ }
+ catch (Exception ex)
+ {
+ return new JObject { ["error"] = $"HF search: {ex.Message}" };
+ }
+ }
+
+ public async Task AssistentCheckHfDataset(Session session, string dataset)
+ {
+ string id = NormalizeHfDatasetId(dataset);
+ if (id is null)
+ {
+ return new JObject { ["success"] = false, ["error"] = "Нужен owner/name или ссылка huggingface.co/datasets/…" };
+ }
+ JObject check = await CheckHfDatasetInternal(session, id, useCache: false);
+ check["success"] = check["gate"]?.ToString() != "rejected";
+ check["id"] = id;
+ return check;
+ }
+
+ async Task CheckHfDatasetInternal(Session session, string datasetId, bool useCache)
+ {
+ string cacheKey = $"hf:{datasetId}";
+ if (useCache)
+ {
+ JObject cached = Memory.GetKvObject(KvHfDatasetCache)?[cacheKey] as JObject;
+ if (cached is not null && cached["checked_at"]?.Value() > DateTimeOffset.UtcNow.AddHours(-6).ToUnixTimeMilliseconds())
+ {
+ return cached;
+ }
+ }
+ JObject result = new() { ["id"] = datasetId, ["gate"] = "rejected", ["reason"] = "unknown" };
+ try
+ {
+ using HttpRequestMessage validReq = HfRequest($"{HfDatasetsServer}/is-valid?dataset={Uri.EscapeDataString(datasetId)}", session);
+ using HttpResponseMessage validResp = await HttpClient.SendAsync(validReq);
+ string validBody = await validResp.Content.ReadAsStringAsync();
+ if (!validResp.IsSuccessStatusCode)
+ {
+ result["reason"] = $"is-valid HTTP {(int)validResp.StatusCode}";
+ return CacheHfCheck(cacheKey, result);
+ }
+ JObject valid = JObject.Parse(validBody);
+ bool viewer = valid["viewer"]?.Value() == true;
+ bool preview = valid["preview"]?.Value() == true;
+ if (!viewer && !preview)
+ {
+ result["reason"] = "Набор не читается через datasets (viewer/preview = false)";
+ return CacheHfCheck(cacheKey, result);
+ }
+ using HttpRequestMessage splitReq = HfRequest($"{HfDatasetsServer}/splits?dataset={Uri.EscapeDataString(datasetId)}", session);
+ using HttpResponseMessage splitResp = await HttpClient.SendAsync(splitReq);
+ string splitBody = await splitResp.Content.ReadAsStringAsync();
+ if (!splitResp.IsSuccessStatusCode)
+ {
+ result["reason"] = $"splits HTTP {(int)splitResp.StatusCode}";
+ return CacheHfCheck(cacheKey, result);
+ }
+ JArray splits = JObject.Parse(splitBody)["splits"] as JArray ?? [];
+ if (splits.Count == 0)
+ {
+ result["reason"] = "Нет splits";
+ return CacheHfCheck(cacheKey, result);
+ }
+ JObject first = splits[0] as JObject;
+ string config = first?["config"]?.ToString() ?? "default";
+ string split = first?["split"]?.ToString() ?? "train";
+ using HttpRequestMessage rowsReq = HfRequest($"{HfDatasetsServer}/first-rows?dataset={Uri.EscapeDataString(datasetId)}&config={Uri.EscapeDataString(config)}&split={Uri.EscapeDataString(split)}", session);
+ using HttpResponseMessage rowsResp = await HttpClient.SendAsync(rowsReq);
+ string rowsBody = await rowsResp.Content.ReadAsStringAsync();
+ if (!rowsResp.IsSuccessStatusCode)
+ {
+ result["reason"] = $"first-rows HTTP {(int)rowsResp.StatusCode}: {Clip(rowsBody, 200)}";
+ return CacheHfCheck(cacheKey, result);
+ }
+ JObject rowsData = JObject.Parse(rowsBody);
+ JObject features = rowsData["features"] as JObject;
+ (string gate, string reason, JObject schema) = ClassifyHfFeatures(features);
+ result["gate"] = gate;
+ result["reason"] = reason;
+ result["schema"] = schema;
+ result["config"] = config;
+ result["split"] = split;
+ result["features"] = features;
+ result["sample_rows"] = rowsData["rows"];
+ result["runner_only"] = HasHugeSizeTag(datasetId);
+ return CacheHfCheck(cacheKey, result);
+ }
+ catch (Exception ex)
+ {
+ result["reason"] = ex.Message;
+ return CacheHfCheck(cacheKey, result);
+ }
+ }
+
+ static bool HasHugeSizeTag(string datasetId) => false;
+
+ JObject CacheHfCheck(string cacheKey, JObject result)
+ {
+ result["checked_at"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ try
+ {
+ JObject bag = Memory.GetKvObject(KvHfDatasetCache) ?? new JObject();
+ bag[cacheKey] = result;
+ Memory.SetKvObject(KvHfDatasetCache, bag);
+ }
+ catch (Exception ex)
+ {
+ Logs.Debug($"CacheHfCheck: {ex.Message}");
+ }
+ return result;
+ }
+
+ static (string gate, string reason, JObject schema) ClassifyHfFeatures(JObject features)
+ {
+ if (features is null || !features.Properties().Any())
+ {
+ return ("rejected", "Нет колонок (features пуст)", null);
+ }
+ HashSet names = new(StringComparer.OrdinalIgnoreCase);
+ foreach (JProperty p in features.Properties())
+ {
+ names.Add(p.Name);
+ JToken dtype = p.Value?["dtype"] ?? p.Value?["type"];
+ string dt = dtype?.ToString() ?? "";
+ if (dt.Contains("image", StringComparison.OrdinalIgnoreCase)
+ || dt.Contains("audio", StringComparison.OrdinalIgnoreCase)
+ || dt.Contains("video", StringComparison.OrdinalIgnoreCase))
+ {
+ return ("rejected", $"Мультимодальная колонка {p.Name} ({dt})", null);
+ }
+ }
+ if (names.Contains("chosen") && names.Contains("rejected"))
+ {
+ return ("rejected", "DPO-набор (chosen/rejected) — не для SFT", null);
+ }
+ if (names.Count == 1 && names.Contains("text"))
+ {
+ return ("rejected", "Предобучение (одна колонка text), не диалоги", null);
+ }
+ if (names.Contains("messages"))
+ {
+ return ("ok", "OpenAI messages", new JObject { ["kind"] = "messages" });
+ }
+ if (names.Contains("conversations"))
+ {
+ return ("ok", "ShareGPT conversations", new JObject { ["kind"] = "conversations" });
+ }
+ if (names.Contains("instruction") && names.Contains("output"))
+ {
+ return ("ok", "Alpaca instruction/output", new JObject { ["kind"] = "alpaca" });
+ }
+ if (names.Contains("prompt") && (names.Contains("response") || names.Contains("completion") || names.Contains("answer")))
+ {
+ string respCol = names.Contains("response") ? "response" : names.Contains("completion") ? "completion" : "answer";
+ return ("ok", "prompt/response", new JObject { ["kind"] = "prompt_response", ["response_col"] = respCol });
+ }
+ if (names.Contains("question") && names.Contains("answer"))
+ {
+ return ("ok", "question/answer", new JObject { ["kind"] = "qa" });
+ }
+ List stringCols = [];
+ foreach (JProperty p in features.Properties())
+ {
+ JToken dtype = p.Value?["dtype"] ?? p.Value?["type"];
+ string dt = dtype?.ToString() ?? "";
+ if (dt.Contains("string", StringComparison.OrdinalIgnoreCase) || dt == "value")
+ {
+ stringCols.Add(p.Name);
+ }
+ }
+ if (stringCols.Count >= 2)
+ {
+ return ("mapping", "Нужен ручной маппинг колонок", new JObject
+ {
+ ["kind"] = "custom",
+ ["columns"] = new JArray(stringCols),
+ });
+ }
+ return ("rejected", "Схема не подходит для SFT", null);
+ }
+
+ public async Task AssistentPreviewHfDataset(Session session, string dataset, string config = null, string split = null)
+ {
+ string id = NormalizeHfDatasetId(dataset);
+ if (id is null)
+ {
+ return new JObject { ["error"] = "invalid dataset id" };
+ }
+ JObject check = await CheckHfDatasetInternal(session, id, useCache: true);
+ if (check["gate"]?.ToString() == "rejected")
+ {
+ return new JObject { ["error"] = check["reason"]?.ToString() ?? "rejected", ["check"] = check };
+ }
+ return new JObject { ["success"] = true, ["check"] = check };
+ }
+
+ public async Task AssistentImportHfDataset(Session session, JObject raw)
+ {
+ string dataset = raw?["dataset"]?.ToString();
+ int limit = raw?["limit"]?.Value() ?? 200;
+ JObject mapping = raw?["mapping"] as JObject;
+ string id = NormalizeHfDatasetId(dataset);
+ if (id is null)
+ {
+ return new JObject { ["error"] = "invalid dataset id" };
+ }
+ JObject check = await CheckHfDatasetInternal(session, id, useCache: true);
+ string gate = check["gate"]?.ToString();
+ if (gate == "rejected")
+ {
+ return new JObject { ["error"] = check["reason"]?.ToString() ?? "rejected" };
+ }
+ if (gate == "mapping" && (mapping is null || mapping.Count == 0))
+ {
+ return new JObject { ["error"] = "Нужен маппинг колонок", ["check"] = check };
+ }
+ int take = Math.Clamp(limit, 1, 5000);
+ JArray rows = [];
+ string config = check["config"]?.ToString() ?? "default";
+ string split = check["split"]?.ToString() ?? "train";
+ int offset = 0;
+ while (rows.Count < take)
+ {
+ int chunk = Math.Min(100, take - rows.Count);
+ using HttpRequestMessage rowsReq = HfRequest($"{HfDatasetsServer}/rows?dataset={Uri.EscapeDataString(id)}&config={Uri.EscapeDataString(config)}&split={Uri.EscapeDataString(split)}&offset={offset}&length={chunk}", session);
+ using HttpResponseMessage rowsResp = await HttpClient.SendAsync(rowsReq);
+ string rowsBody = await rowsResp.Content.ReadAsStringAsync();
+ if (!rowsResp.IsSuccessStatusCode)
+ {
+ break;
+ }
+ JObject parsed = JObject.Parse(rowsBody);
+ JArray batch = parsed["rows"] as JArray ?? [];
+ if (batch.Count == 0)
+ {
+ break;
+ }
+ foreach (JToken t in batch)
+ {
+ rows.Add(t);
+ }
+ offset += batch.Count;
+ if (batch.Count < chunk)
+ {
+ break;
+ }
+ }
+ if (rows.Count == 0)
+ {
+ rows = check["sample_rows"] as JArray ?? [];
+ }
+ int imported = 0;
+ foreach (JToken rowTok in rows.Take(take))
+ {
+ if (rowTok is not JObject row)
+ {
+ continue;
+ }
+ JObject rowData = row["row"] as JObject ?? row;
+ JArray messages = ConvertHfRowToMessages(rowData, check["schema"] as JObject, mapping);
+ if (messages is null || messages.Count == 0)
+ {
+ continue;
+ }
+ Memory.UpsertTrainSample(new JObject
+ {
+ ["source"] = "hf",
+ ["hf_repo"] = id,
+ ["messages"] = messages,
+ ["status"] = "draft",
+ });
+ imported++;
+ }
+ if (imported == 0 && check["runner_only"]?.Value() == true)
+ {
+ return new JObject { ["success"] = true, ["imported"] = 0, ["runner_only"] = true, ["id"] = id, ["note"] = "Большой набор — используй HF id в QLoRA-раннере" };
+ }
+ return new JObject { ["success"] = true, ["imported"] = imported, ["id"] = id };
+ }
+
+ static JArray ConvertHfRowToMessages(JObject row, JObject schema, JObject mapping)
+ {
+ string kind = schema?["kind"]?.ToString() ?? mapping?["kind"]?.ToString();
+ if (kind == "messages" && row["messages"] is JArray msgs)
+ {
+ return NormalizeMessagesArray(msgs);
+ }
+ if (kind == "conversations" && row["conversations"] is JArray conv)
+ {
+ JArray outArr = [];
+ foreach (JToken c in conv)
+ {
+ if (c is not JObject co)
+ {
+ continue;
+ }
+ string from = co["from"]?.ToString() ?? "";
+ string val = co["value"]?.ToString() ?? "";
+ string role = from is "human" or "user" ? "user" : from is "gpt" or "assistant" or "chatgpt" ? "assistant" : "user";
+ outArr.Add(new JObject { ["role"] = role, ["content"] = val });
+ }
+ return outArr.Count > 0 ? outArr : null;
+ }
+ if (kind == "alpaca")
+ {
+ string instr = row["instruction"]?.ToString() ?? "";
+ string inp = row["input"]?.ToString() ?? "";
+ string output = row["output"]?.ToString() ?? "";
+ string user = string.IsNullOrWhiteSpace(inp) ? instr : $"{instr}\n{inp}";
+ return new JArray
+ {
+ new JObject { ["role"] = "user", ["content"] = user },
+ new JObject { ["role"] = "assistant", ["content"] = output },
+ };
+ }
+ if (kind == "prompt_response")
+ {
+ string respCol = schema?["response_col"]?.ToString() ?? "response";
+ return new JArray
+ {
+ new JObject { ["role"] = "user", ["content"] = row["prompt"]?.ToString() ?? "" },
+ new JObject { ["role"] = "assistant", ["content"] = row[respCol]?.ToString() ?? "" },
+ };
+ }
+ if (kind == "qa")
+ {
+ return new JArray
+ {
+ new JObject { ["role"] = "user", ["content"] = row["question"]?.ToString() ?? "" },
+ new JObject { ["role"] = "assistant", ["content"] = row["answer"]?.ToString() ?? "" },
+ };
+ }
+ if (kind == "custom" && mapping is not null)
+ {
+ string userCol = mapping["user_col"]?.ToString();
+ string asstCol = mapping["assistant_col"]?.ToString();
+ if (!string.IsNullOrWhiteSpace(userCol) && !string.IsNullOrWhiteSpace(asstCol))
+ {
+ return new JArray
+ {
+ new JObject { ["role"] = "user", ["content"] = row[userCol]?.ToString() ?? "" },
+ new JObject { ["role"] = "assistant", ["content"] = row[asstCol]?.ToString() ?? "" },
+ };
+ }
+ }
+ return null;
+ }
+
+ static JArray NormalizeMessagesArray(JArray msgs)
+ {
+ JArray outArr = [];
+ foreach (JToken m in msgs)
+ {
+ if (m is not JObject mo)
+ {
+ continue;
+ }
+ string role = mo["role"]?.ToString() ?? "user";
+ string content = mo["content"]?.ToString() ?? mo["text"]?.ToString() ?? "";
+ if (string.IsNullOrWhiteSpace(content))
+ {
+ continue;
+ }
+ outArr.Add(new JObject { ["role"] = role, ["content"] = content });
+ }
+ return outArr.Count > 0 ? outArr : null;
+ }
+}
diff --git a/AssistentMemory.Heard.cs b/AssistentMemory.Heard.cs
new file mode 100644
index 0000000..bd462cf
--- /dev/null
+++ b/AssistentMemory.Heard.cs
@@ -0,0 +1,189 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Data.Sqlite;
+using Newtonsoft.Json.Linq;
+using SwarmUI.Utils;
+
+namespace Mrleo1nid.SwarmAssistent;
+
+/// Train samples linked to the agent as retrievable "heard" dialogue examples.
+public sealed partial class AssistentMemory
+{
+ public const string HeardKind = "heard";
+ public const string HeardSource = "train";
+
+ public static string FormatHeardEmbedText(JArray messages, string pack, string persona)
+ {
+ StringBuilder sb = new();
+ if (!string.IsNullOrWhiteSpace(pack))
+ {
+ sb.AppendLine($"pack: {pack.Trim()}");
+ }
+ if (!string.IsNullOrWhiteSpace(persona))
+ {
+ sb.AppendLine($"persona: {persona.Trim()}");
+ }
+ foreach (JToken t in messages ?? [])
+ {
+ if (t is not JObject m)
+ {
+ continue;
+ }
+ string role = m["role"]?.ToString() ?? "user";
+ string content = m["content"]?.ToString() ?? "";
+ if (string.IsNullOrWhiteSpace(content))
+ {
+ continue;
+ }
+ sb.AppendLine($"{role}: {content.Trim()}");
+ }
+ return sb.ToString().Trim();
+ }
+
+ public void SetTrainSampleAgentLinked(string id, bool linked)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ return;
+ }
+ lock (_lock)
+ {
+ EnsureOpen();
+ if (!HasColumn("train_samples", "agent_linked"))
+ {
+ return;
+ }
+ using SqliteCommand cmd = _conn.CreateCommand();
+ cmd.CommandText = "UPDATE train_samples SET agent_linked = $v, updated_at = $u WHERE id = $id";
+ cmd.Parameters.AddWithValue("$v", linked ? 1 : 0);
+ cmd.Parameters.AddWithValue("$u", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
+ cmd.Parameters.AddWithValue("$id", id.Trim());
+ cmd.ExecuteNonQuery();
+ }
+ }
+
+ public int CountAgentLinkedTrainSamples()
+ {
+ lock (_lock)
+ {
+ EnsureOpen();
+ if (!HasColumn("train_samples", "agent_linked"))
+ {
+ return 0;
+ }
+ using SqliteCommand cmd = _conn.CreateCommand();
+ cmd.CommandText = "SELECT COUNT(*) FROM train_samples WHERE agent_linked = 1";
+ return Convert.ToInt32(cmd.ExecuteScalar());
+ }
+ }
+
+ public async Task LinkTrainSampleToAgentAsync(string baseUrl, JObject sample, string embedModel)
+ {
+ if (sample is null)
+ {
+ return false;
+ }
+ string id = sample["id"]?.ToString()?.Trim();
+ JArray messages = sample["messages"] as JArray ?? [];
+ if (string.IsNullOrWhiteSpace(id) || messages.Count == 0)
+ {
+ return false;
+ }
+ string status = sample["status"]?.ToString() ?? "draft";
+ if (!string.Equals(status, "approved", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ string persona = NormalizePersona(sample["persona"]?.ToString());
+ string pack = sample["pack"]?.ToString()?.Trim() ?? "";
+ string text = FormatHeardEmbedText(messages, pack, persona);
+ if (string.IsNullOrWhiteSpace(text))
+ {
+ return false;
+ }
+ JObject meta = new()
+ {
+ ["train_sample_id"] = id,
+ ["pack"] = pack,
+ ["messages"] = messages,
+ ["source_type"] = sample["source"]?.ToString() ?? "manual",
+ };
+ if (!string.IsNullOrWhiteSpace(sample["chat_id"]?.ToString()))
+ {
+ meta["chat_id"] = sample["chat_id"]?.ToString();
+ }
+ float[] vec = await EmbedAsync(baseUrl, embedModel, text);
+ Upsert(HeardKind, id, text, HeardSource, meta, vec, persona);
+ SetTrainSampleAgentLinked(id, true);
+ return true;
+ }
+
+ public void UnlinkTrainSampleFromAgent(JObject sample)
+ {
+ if (sample is null)
+ {
+ return;
+ }
+ string id = sample["id"]?.ToString()?.Trim();
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ return;
+ }
+ string persona = NormalizePersona(sample["persona"]?.ToString());
+ Forget(HeardKind, id, HeardSource, persona);
+ if (persona != SharedPersona)
+ {
+ Forget(HeardKind, id, HeardSource, SharedPersona);
+ }
+ SetTrainSampleAgentLinked(id, false);
+ }
+
+ public JObject BuildHeardExampleFromHit(JObject hit, IEnumerable personaChain = null)
+ {
+ if (hit is null)
+ {
+ return null;
+ }
+ string key = hit["key"]?.ToString() ?? "";
+ if (string.IsNullOrWhiteSpace(key))
+ {
+ return null;
+ }
+ JObject row = Get(HeardKind, key, personaChain);
+ JObject ex = new()
+ {
+ ["id"] = key,
+ ["score"] = hit["score"],
+ ["persona"] = hit["persona"],
+ ["source"] = hit["source"],
+ };
+ JObject meta = null;
+ try
+ {
+ string metaRaw = row?["meta_json"]?.ToString();
+ if (!string.IsNullOrWhiteSpace(metaRaw))
+ {
+ meta = JObject.Parse(metaRaw);
+ }
+ }
+ catch
+ {
+ // ignore
+ }
+ if (meta?["pack"] is not null)
+ {
+ ex["pack"] = meta["pack"];
+ }
+ if (meta?["messages"] is JArray msgs && msgs.Count > 0)
+ {
+ ex["messages"] = msgs;
+ }
+ else
+ {
+ ex["text"] = hit["text"]?.ToString() ?? row?["text"]?.ToString() ?? "";
+ }
+ return ex;
+ }
+}
diff --git a/AssistentMemory.Training.cs b/AssistentMemory.Training.cs
new file mode 100644
index 0000000..08b7518
--- /dev/null
+++ b/AssistentMemory.Training.cs
@@ -0,0 +1,345 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Data.Sqlite;
+using Newtonsoft.Json.Linq;
+using SwarmUI.Utils;
+
+namespace Mrleo1nid.SwarmAssistent;
+
+/// Training datasets, samples, and job metadata in assistent.sqlite.
+public sealed partial class AssistentMemory
+{
+ public const string KvHfDatasetCache = "hf_dataset_cache";
+
+ void EnsureTrainingSchema()
+ {
+ Exec(
+ """
+ CREATE TABLE IF NOT EXISTS train_datasets (
+ id TEXT PRIMARY KEY,
+ title TEXT NOT NULL DEFAULT '',
+ created_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL,
+ meta_json TEXT
+ );
+ CREATE TABLE IF NOT EXISTS train_samples (
+ id TEXT PRIMARY KEY,
+ dataset_id TEXT NOT NULL DEFAULT 'default',
+ source TEXT NOT NULL DEFAULT 'manual',
+ chat_id TEXT,
+ persona TEXT,
+ pack TEXT,
+ hf_repo TEXT,
+ messages_json TEXT NOT NULL DEFAULT '[]',
+ status TEXT NOT NULL DEFAULT 'draft',
+ created_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL,
+ FOREIGN KEY(dataset_id) REFERENCES train_datasets(id) ON DELETE CASCADE
+ );
+ CREATE INDEX IF NOT EXISTS idx_train_samples_status ON train_samples(status);
+ CREATE INDEX IF NOT EXISTS idx_train_samples_dataset ON train_samples(dataset_id);
+ CREATE TABLE IF NOT EXISTS train_jobs (
+ id TEXT PRIMARY KEY,
+ kind TEXT NOT NULL,
+ status TEXT NOT NULL DEFAULT 'pending',
+ config_json TEXT,
+ base_model TEXT,
+ output_name TEXT,
+ log_path TEXT,
+ progress_json TEXT,
+ created_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL,
+ finished_at INTEGER
+ );
+ CREATE INDEX IF NOT EXISTS idx_train_jobs_status ON train_jobs(status);
+ """);
+ if (!HasColumn("train_samples", "agent_linked"))
+ {
+ Exec("ALTER TABLE train_samples ADD COLUMN agent_linked INTEGER NOT NULL DEFAULT 0");
+ }
+ using SqliteCommand cmd = _conn.CreateCommand();
+ cmd.CommandText = "INSERT OR IGNORE INTO train_datasets(id, title, created_at, updated_at) VALUES('default', 'Default', $u, $u)";
+ cmd.Parameters.AddWithValue("$u", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
+ cmd.ExecuteNonQuery();
+ }
+
+ public List ListTrainSamples(string status = null, string persona = null, string datasetId = null, int limit = 200)
+ {
+ lock (_lock)
+ {
+ EnsureOpen();
+ int take = Math.Clamp(limit, 1, 2000);
+ List where = [];
+ if (!string.IsNullOrWhiteSpace(status) && !string.Equals(status, "all", StringComparison.OrdinalIgnoreCase))
+ {
+ where.Add("status = $status");
+ }
+ if (!string.IsNullOrWhiteSpace(persona) && !string.Equals(persona, "all", StringComparison.OrdinalIgnoreCase))
+ {
+ where.Add("persona = $persona");
+ }
+ if (!string.IsNullOrWhiteSpace(datasetId))
+ {
+ where.Add("dataset_id = $ds");
+ }
+ bool hasAgentLinked = HasColumn("train_samples", "agent_linked");
+ string sql = hasAgentLinked
+ ? "SELECT id, dataset_id, source, chat_id, persona, pack, hf_repo, messages_json, status, created_at, updated_at, agent_linked FROM train_samples"
+ : "SELECT id, dataset_id, source, chat_id, persona, pack, hf_repo, messages_json, status, created_at, updated_at FROM train_samples";
+ if (where.Count > 0)
+ {
+ sql += " WHERE " + string.Join(" AND ", where);
+ }
+ sql += " ORDER BY updated_at DESC LIMIT $lim";
+ using SqliteCommand cmd = _conn.CreateCommand();
+ cmd.CommandText = sql;
+ if (where.Any(w => w.Contains("$status")))
+ {
+ cmd.Parameters.AddWithValue("$status", status.Trim());
+ }
+ if (where.Any(w => w.Contains("$persona")))
+ {
+ cmd.Parameters.AddWithValue("$persona", persona.Trim());
+ }
+ if (where.Any(w => w.Contains("$ds")))
+ {
+ cmd.Parameters.AddWithValue("$ds", datasetId.Trim());
+ }
+ cmd.Parameters.AddWithValue("$lim", take);
+ List list = [];
+ using SqliteDataReader r = cmd.ExecuteReader();
+ while (r.Read())
+ {
+ list.Add(ReadTrainSampleRow(r));
+ }
+ return list;
+ }
+ }
+
+ static JObject ReadTrainSampleRow(SqliteDataReader r)
+ {
+ JArray messages = [];
+ try
+ {
+ messages = JArray.Parse(r.GetString(7));
+ }
+ catch
+ {
+ // ignore
+ }
+ bool hasAgentLinked = r.FieldCount > 11;
+ return new JObject
+ {
+ ["id"] = r.GetString(0),
+ ["dataset_id"] = r.GetString(1),
+ ["source"] = r.GetString(2),
+ ["chat_id"] = r.IsDBNull(3) ? null : r.GetString(3),
+ ["persona"] = r.IsDBNull(4) ? null : r.GetString(4),
+ ["pack"] = r.IsDBNull(5) ? null : r.GetString(5),
+ ["hf_repo"] = r.IsDBNull(6) ? null : r.GetString(6),
+ ["messages"] = messages,
+ ["status"] = r.GetString(8),
+ ["createdAt"] = r.GetInt64(9),
+ ["updatedAt"] = r.GetInt64(10),
+ ["agent_linked"] = hasAgentLinked && !r.IsDBNull(11) && r.GetInt64(11) != 0,
+ };
+ }
+
+ public JObject GetTrainSample(string id)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ return null;
+ }
+ lock (_lock)
+ {
+ EnsureOpen();
+ bool hasAgentLinked = HasColumn("train_samples", "agent_linked");
+ string sql = hasAgentLinked
+ ? "SELECT id, dataset_id, source, chat_id, persona, pack, hf_repo, messages_json, status, created_at, updated_at, agent_linked FROM train_samples WHERE id = $id LIMIT 1"
+ : "SELECT id, dataset_id, source, chat_id, persona, pack, hf_repo, messages_json, status, created_at, updated_at FROM train_samples WHERE id = $id LIMIT 1";
+ using SqliteCommand cmd = _conn.CreateCommand();
+ cmd.CommandText = sql;
+ cmd.Parameters.AddWithValue("$id", id.Trim());
+ using SqliteDataReader r = cmd.ExecuteReader();
+ return r.Read() ? ReadTrainSampleRow(r) : null;
+ }
+ }
+
+ public JObject UpsertTrainSample(JObject sample)
+ {
+ lock (_lock)
+ {
+ EnsureOpen();
+ long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ string id = sample["id"]?.ToString()?.Trim();
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ id = $"ts_{now}_{Guid.NewGuid():N}"[..24];
+ }
+ string datasetId = sample["dataset_id"]?.ToString()?.Trim() ?? "default";
+ JArray messages = sample["messages"] as JArray ?? [];
+ using SqliteCommand cmd = _conn.CreateCommand();
+ cmd.CommandText =
+ """
+ INSERT INTO train_samples(id, dataset_id, source, chat_id, persona, pack, hf_repo, messages_json, status, created_at, updated_at)
+ VALUES($id, $ds, $src, $chat, $persona, $pack, $hf, $msg, $status, $c, $u)
+ ON CONFLICT(id) DO UPDATE SET
+ dataset_id = excluded.dataset_id,
+ source = excluded.source,
+ chat_id = excluded.chat_id,
+ persona = excluded.persona,
+ pack = excluded.pack,
+ hf_repo = excluded.hf_repo,
+ messages_json = excluded.messages_json,
+ status = excluded.status,
+ updated_at = excluded.updated_at
+ """;
+ cmd.Parameters.AddWithValue("$id", id);
+ cmd.Parameters.AddWithValue("$ds", datasetId);
+ cmd.Parameters.AddWithValue("$src", sample["source"]?.ToString() ?? "manual");
+ cmd.Parameters.AddWithValue("$chat", (object)sample["chat_id"]?.ToString() ?? DBNull.Value);
+ cmd.Parameters.AddWithValue("$persona", (object)sample["persona"]?.ToString() ?? DBNull.Value);
+ cmd.Parameters.AddWithValue("$pack", (object)sample["pack"]?.ToString() ?? DBNull.Value);
+ cmd.Parameters.AddWithValue("$hf", (object)sample["hf_repo"]?.ToString() ?? DBNull.Value);
+ cmd.Parameters.AddWithValue("$msg", messages.ToString(Newtonsoft.Json.Formatting.None));
+ cmd.Parameters.AddWithValue("$status", sample["status"]?.ToString() ?? "draft");
+ long created = sample["createdAt"]?.Value() ?? now;
+ cmd.Parameters.AddWithValue("$c", created);
+ cmd.Parameters.AddWithValue("$u", now);
+ cmd.ExecuteNonQuery();
+ return new JObject { ["id"] = id, ["updatedAt"] = now };
+ }
+ }
+
+ public bool DeleteTrainSample(string id)
+ {
+ lock (_lock)
+ {
+ EnsureOpen();
+ using SqliteCommand cmd = _conn.CreateCommand();
+ cmd.CommandText = "DELETE FROM train_samples WHERE id = $id";
+ cmd.Parameters.AddWithValue("$id", id ?? "");
+ return cmd.ExecuteNonQuery() > 0;
+ }
+ }
+
+ public int CountTrainSamples(string status = null)
+ {
+ lock (_lock)
+ {
+ EnsureOpen();
+ using SqliteCommand cmd = _conn.CreateCommand();
+ if (string.IsNullOrWhiteSpace(status) || string.Equals(status, "all", StringComparison.OrdinalIgnoreCase))
+ {
+ cmd.CommandText = "SELECT COUNT(*) FROM train_samples";
+ }
+ else
+ {
+ cmd.CommandText = "SELECT COUNT(*) FROM train_samples WHERE status = $s";
+ cmd.Parameters.AddWithValue("$s", status.Trim());
+ }
+ return Convert.ToInt32(cmd.ExecuteScalar());
+ }
+ }
+
+ public JObject SaveTrainJob(JObject job)
+ {
+ lock (_lock)
+ {
+ EnsureOpen();
+ long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ string id = job["id"]?.ToString()?.Trim();
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ id = $"tj_{now}_{Guid.NewGuid():N}"[..24];
+ }
+ using SqliteCommand cmd = _conn.CreateCommand();
+ cmd.CommandText =
+ """
+ INSERT INTO train_jobs(id, kind, status, config_json, base_model, output_name, log_path, progress_json, created_at, updated_at, finished_at)
+ VALUES($id, $kind, $status, $cfg, $base, $out, $log, $prog, $c, $u, $f)
+ ON CONFLICT(id) DO UPDATE SET
+ status = excluded.status,
+ config_json = excluded.config_json,
+ log_path = excluded.log_path,
+ progress_json = excluded.progress_json,
+ updated_at = excluded.updated_at,
+ finished_at = excluded.finished_at
+ """;
+ cmd.Parameters.AddWithValue("$id", id);
+ cmd.Parameters.AddWithValue("$kind", job["kind"]?.ToString() ?? "qlora");
+ cmd.Parameters.AddWithValue("$status", job["status"]?.ToString() ?? "pending");
+ cmd.Parameters.AddWithValue("$cfg", job["config"]?.ToString(Newtonsoft.Json.Formatting.None) ?? job["config_json"]?.ToString() ?? "{}");
+ cmd.Parameters.AddWithValue("$base", (object)job["base_model"]?.ToString() ?? DBNull.Value);
+ cmd.Parameters.AddWithValue("$out", (object)job["output_name"]?.ToString() ?? DBNull.Value);
+ cmd.Parameters.AddWithValue("$log", (object)job["log_path"]?.ToString() ?? DBNull.Value);
+ cmd.Parameters.AddWithValue("$prog", (object)job["progress"]?.ToString(Newtonsoft.Json.Formatting.None) ?? job["progress_json"]?.ToString() ?? DBNull.Value);
+ cmd.Parameters.AddWithValue("$c", job["created_at"]?.Value() ?? job["createdAt"]?.Value() ?? now);
+ cmd.Parameters.AddWithValue("$u", now);
+ cmd.Parameters.AddWithValue("$f", (object)(job["finished_at"]?.Value() ?? job["finishedAt"]?.Value()) ?? DBNull.Value);
+ cmd.ExecuteNonQuery();
+ return new JObject { ["id"] = id };
+ }
+ }
+
+ public JObject GetTrainJob(string id)
+ {
+ lock (_lock)
+ {
+ EnsureOpen();
+ using SqliteCommand cmd = _conn.CreateCommand();
+ cmd.CommandText = "SELECT id, kind, status, config_json, base_model, output_name, log_path, progress_json, created_at, updated_at, finished_at FROM train_jobs WHERE id = $id";
+ cmd.Parameters.AddWithValue("$id", id ?? "");
+ using SqliteDataReader r = cmd.ExecuteReader();
+ if (!r.Read())
+ {
+ return null;
+ }
+ return new JObject
+ {
+ ["id"] = r.GetString(0),
+ ["kind"] = r.GetString(1),
+ ["status"] = r.GetString(2),
+ ["config_json"] = r.IsDBNull(3) ? null : r.GetString(3),
+ ["base_model"] = r.IsDBNull(4) ? null : r.GetString(4),
+ ["output_name"] = r.IsDBNull(5) ? null : r.GetString(5),
+ ["log_path"] = r.IsDBNull(6) ? null : r.GetString(6),
+ ["progress_json"] = r.IsDBNull(7) ? null : r.GetString(7),
+ ["created_at"] = r.GetInt64(8),
+ ["updated_at"] = r.GetInt64(9),
+ ["finished_at"] = r.IsDBNull(10) ? null : r.GetInt64(10),
+ };
+ }
+ }
+
+ public JObject GetActiveTrainJob()
+ {
+ lock (_lock)
+ {
+ EnsureOpen();
+ using SqliteCommand cmd = _conn.CreateCommand();
+ cmd.CommandText = "SELECT id, kind, status, config_json, base_model, output_name, log_path, progress_json, created_at, updated_at, finished_at FROM train_jobs WHERE status IN ('pending','running') ORDER BY updated_at DESC LIMIT 1";
+ using SqliteDataReader r = cmd.ExecuteReader();
+ if (!r.Read())
+ {
+ return null;
+ }
+ return new JObject
+ {
+ ["id"] = r.GetString(0),
+ ["kind"] = r.GetString(1),
+ ["status"] = r.GetString(2),
+ ["config_json"] = r.IsDBNull(3) ? null : r.GetString(3),
+ ["base_model"] = r.IsDBNull(4) ? null : r.GetString(4),
+ ["output_name"] = r.IsDBNull(5) ? null : r.GetString(5),
+ ["log_path"] = r.IsDBNull(6) ? null : r.GetString(6),
+ ["progress_json"] = r.IsDBNull(7) ? null : r.GetString(7),
+ ["created_at"] = r.GetInt64(8),
+ ["updated_at"] = r.GetInt64(9),
+ ["finished_at"] = r.IsDBNull(10) ? null : r.GetInt64(10),
+ };
+ }
+ }
+}
diff --git a/AssistentMemory.cs b/AssistentMemory.cs
index 6604ff9..46597da 100644
--- a/AssistentMemory.cs
+++ b/AssistentMemory.cs
@@ -37,8 +37,12 @@ public sealed partial class AssistentMemory : IDisposable
["note"] = 4,
["model"] = 2,
["aspect"] = 1,
+ ["heard"] = 3,
};
+ public static Dictionary CopyDefaultQuotas()
+ => new(DefaultQuotas, StringComparer.OrdinalIgnoreCase);
+
readonly string _dataRoot;
readonly string _dbPath;
readonly HttpClient _http;
@@ -139,6 +143,14 @@ public sealed partial class AssistentMemory : IDisposable
Logs.Debug($"AssistentMemory store schema: {ex.Message}");
}
try
+ {
+ EnsureTrainingSchema();
+ }
+ catch (Exception ex)
+ {
+ Logs.Debug($"AssistentMemory training schema: {ex.Message}");
+ }
+ try
{
EnsureUserPrefsSchema();
}
@@ -857,7 +869,7 @@ public sealed partial class AssistentMemory : IDisposable
{
EnsureOpen();
using SqliteCommand cmd = _conn.CreateCommand();
- cmd.CommandText = "SELECT kind, key, text, source, persona, updated FROM memories WHERE kind = $kind AND key = $key";
+ cmd.CommandText = "SELECT kind, key, text, source, persona, updated, meta_json FROM memories WHERE kind = $kind AND key = $key";
cmd.Parameters.AddWithValue("$kind", kind);
cmd.Parameters.AddWithValue("$key", key);
using SqliteDataReader reader = cmd.ExecuteReader();
@@ -885,6 +897,7 @@ public sealed partial class AssistentMemory : IDisposable
["scope"] = shared ? "shared" : "personal",
["persona"] = shared ? "shared" : persona,
["updated"] = reader.IsDBNull(5) ? 0 : reader.GetInt64(5),
+ ["meta_json"] = reader.IsDBNull(6) ? null : reader.GetString(6),
};
}
}
diff --git a/AssistentPatch.cs b/AssistentPatch.cs
index 0f3fe1e..9485a51 100644
--- a/AssistentPatch.cs
+++ b/AssistentPatch.cs
@@ -186,6 +186,7 @@ public partial class SwarmAssistentExtension
|| s.Equals("persona_read", StringComparison.OrdinalIgnoreCase)
|| s.Equals("memory_get", StringComparison.OrdinalIgnoreCase)
|| s.Equals("memory_search", StringComparison.OrdinalIgnoreCase)
+ || s.Equals("heard_search", StringComparison.OrdinalIgnoreCase)
|| s.Equals("lookup_tags", StringComparison.OrdinalIgnoreCase)
|| s.Equals("list_inventory", StringComparison.OrdinalIgnoreCase)
|| s.Equals("search_civitai", StringComparison.OrdinalIgnoreCase)
@@ -277,6 +278,11 @@ public partial class SwarmAssistentExtension
{
return "memory_search";
}
+ if ((ActionsContain(patch, "heard_search") || string.Equals(patch["heard_query"]?.ToString(), "1", StringComparison.Ordinal))
+ && !Skip("heard_search"))
+ {
+ return "heard_search";
+ }
if ((ActionsContain(patch, "lookup_tags") || !string.IsNullOrWhiteSpace(patch["tag_query"]?.ToString()))
&& !Skip("lookup_tags"))
{
diff --git a/AssistentPersist.cs b/AssistentPersist.cs
index 1a3e550..5b6cce7 100644
--- a/AssistentPersist.cs
+++ b/AssistentPersist.cs
@@ -21,7 +21,7 @@ public partial class SwarmAssistentExtension
static readonly string[] UiStateKeys =
[
"pack", "persona", "auto_vision", "auto_apply", "auto_generate", "auto_critique",
- "auto_download", "pane_width", "embed_model", "base_url", "model", "view", "board_tab", "park_llm",
+ "auto_download", "pane_width", "embed_model", "base_url", "model", "view", "board_tab", "park_llm", "chats_drawer",
];
static string SafeChatId(string id)
diff --git a/AssistentTraining.Agent.cs b/AssistentTraining.Agent.cs
new file mode 100644
index 0000000..21418f4
--- /dev/null
+++ b/AssistentTraining.Agent.cs
@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using SwarmUI.Accounts;
+using SwarmUI.Utils;
+
+namespace Mrleo1nid.SwarmAssistent;
+
+/// Link training dataset samples to the live agent as retrievable "heard" examples.
+public partial class SwarmAssistentExtension
+{
+ static string MemoryEmbedForTraining(string personaId = null)
+ {
+ string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId();
+ return Config.LoadSettings()["embed_model"]?.ToString()
+ ?? Config.LoadAssistant(pid)["embed_model"]?.ToString()
+ ?? "nomic-embed-text";
+ }
+
+ static string MemoryBaseForTraining(JObject raw = null)
+ => MemoryBaseUrl(raw?["base_url"]?.ToString());
+
+ public async Task AssistentGetDatasetAgentSettings(Session session)
+ {
+ await Task.CompletedTask;
+ JObject settings = Config.LoadTrainingAgent();
+ return new JObject
+ {
+ ["success"] = true,
+ ["settings"] = settings,
+ ["linked"] = Memory.CountAgentLinkedTrainSamples(),
+ ["approved"] = Memory.CountTrainSamples("approved"),
+ };
+ }
+
+ public async Task AssistentSaveDatasetAgentSettings(Session session, JObject settings)
+ {
+ await Task.CompletedTask;
+ if (settings is null)
+ {
+ return new JObject { ["error"] = "settings required" };
+ }
+ Config.SaveTrainingAgent(settings);
+ return new JObject { ["success"] = true, ["settings"] = Config.LoadTrainingAgent() };
+ }
+
+ public async Task AssistentLinkTrainSampleToAgent(Session session, string id, JObject raw = null)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ return new JObject { ["error"] = "id required" };
+ }
+ JObject sample = Memory.GetTrainSample(id.Trim());
+ if (sample is null)
+ {
+ return new JObject { ["error"] = "sample not found" };
+ }
+ if (!string.Equals(sample["status"]?.ToString(), "approved", StringComparison.OrdinalIgnoreCase))
+ {
+ return new JObject { ["error"] = "only approved samples can be linked to the agent" };
+ }
+ try
+ {
+ string embed = MemoryEmbedForTraining(sample["persona"]?.ToString());
+ bool ok = await Memory.LinkTrainSampleToAgentAsync(MemoryBaseForTraining(raw), sample, embed);
+ return new JObject { ["success"] = ok, ["linked"] = Memory.CountAgentLinkedTrainSamples() };
+ }
+ catch (Exception ex)
+ {
+ return new JObject { ["error"] = ex.Message };
+ }
+ }
+
+ public async Task AssistentUnlinkTrainSampleFromAgent(Session session, string id)
+ {
+ await Task.CompletedTask;
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ return new JObject { ["error"] = "id required" };
+ }
+ JObject sample = Memory.GetTrainSample(id.Trim());
+ if (sample is null)
+ {
+ return new JObject { ["error"] = "sample not found" };
+ }
+ Memory.UnlinkTrainSampleFromAgent(sample);
+ return new JObject { ["success"] = true, ["linked"] = Memory.CountAgentLinkedTrainSamples() };
+ }
+
+ public async Task AssistentSyncDatasetToAgent(Session session, JObject raw = null)
+ {
+ bool approvedOnly = raw?["approved_only"]?.Value() ?? true;
+ bool relink = raw?["relink"]?.Value() ?? false;
+ string persona = raw?["persona"]?.ToString();
+ string status = approvedOnly ? "approved" : "all";
+ List samples = Memory.ListTrainSamples(status, persona, null, 2000);
+ string baseUrl = MemoryBaseForTraining(raw);
+ int linked = 0;
+ int skipped = 0;
+ List errors = [];
+ foreach (JObject sample in samples)
+ {
+ bool already = sample["agent_linked"]?.Value() ?? false;
+ if (already && !relink)
+ {
+ skipped++;
+ continue;
+ }
+ if (!string.Equals(sample["status"]?.ToString(), "approved", StringComparison.OrdinalIgnoreCase))
+ {
+ skipped++;
+ continue;
+ }
+ try
+ {
+ string embed = MemoryEmbedForTraining(sample["persona"]?.ToString());
+ if (await Memory.LinkTrainSampleToAgentAsync(baseUrl, sample, embed))
+ {
+ linked++;
+ }
+ else
+ {
+ skipped++;
+ }
+ }
+ catch (Exception ex)
+ {
+ errors.Add($"{sample["id"]}: {ex.Message}");
+ }
+ }
+ return new JObject
+ {
+ ["success"] = true,
+ ["linked_now"] = linked,
+ ["skipped"] = skipped,
+ ["total_linked"] = Memory.CountAgentLinkedTrainSamples(),
+ ["errors"] = new JArray(errors.Take(8)),
+ };
+ }
+
+ async Task TryAutoLinkTrainSample(Session session, JObject sample, JObject raw = null)
+ {
+ if (sample is null || Memory is null)
+ {
+ return;
+ }
+ JObject agent = Config.LoadTrainingAgent();
+ if (agent["enabled"]?.Value() == false)
+ {
+ return;
+ }
+ if (agent["auto_link_on_approve"]?.Value() == false)
+ {
+ return;
+ }
+ string status = sample["status"]?.ToString() ?? "";
+ if (!string.Equals(status, "approved", StringComparison.OrdinalIgnoreCase))
+ {
+ Memory.UnlinkTrainSampleFromAgent(sample);
+ return;
+ }
+ try
+ {
+ string embed = MemoryEmbedForTraining(sample["persona"]?.ToString());
+ await Memory.LinkTrainSampleToAgentAsync(MemoryBaseForTraining(raw), sample, embed);
+ }
+ catch (Exception ex)
+ {
+ Logs.Debug($"TryAutoLinkTrainSample: {ex.Message}");
+ }
+ }
+}
diff --git a/AssistentTraining.cs b/AssistentTraining.cs
new file mode 100644
index 0000000..38feac1
--- /dev/null
+++ b/AssistentTraining.cs
@@ -0,0 +1,443 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using SwarmUI.Accounts;
+using SwarmUI.Utils;
+
+namespace Mrleo1nid.SwarmAssistent;
+
+/// Training samples, dataset import/export, Ollama Modelfile builder.
+public partial class SwarmAssistentExtension
+{
+ static string TrainingRoot()
+ {
+ string root = Path.Combine(DataRoot(), "Assistent", "training");
+ Directory.CreateDirectory(root);
+ Directory.CreateDirectory(Path.Combine(root, "datasets"));
+ Directory.CreateDirectory(Path.Combine(root, "jobs"));
+ Directory.CreateDirectory(Path.Combine(root, "adapters"));
+ return root;
+ }
+
+ public async Task AssistentListTrainSamples(Session session, string status = null, string persona = null, int limit = 200)
+ {
+ await Task.CompletedTask;
+ try
+ {
+ List list = Memory.ListTrainSamples(status, persona, null, limit);
+ return new JObject
+ {
+ ["success"] = true,
+ ["samples"] = new JArray(list),
+ ["approved"] = Memory.CountTrainSamples("approved"),
+ ["total"] = Memory.CountTrainSamples(null),
+ };
+ }
+ catch (Exception ex)
+ {
+ return new JObject { ["error"] = ex.Message };
+ }
+ }
+
+ public async Task AssistentUpsertTrainSample(Session session, JObject raw)
+ {
+ await Task.CompletedTask;
+ if (raw is null)
+ {
+ return new JObject { ["error"] = "body required" };
+ }
+ try
+ {
+ JObject saved = Memory.UpsertTrainSample(raw);
+ JObject full = Memory.GetTrainSample(saved["id"]?.ToString()) ?? raw;
+ await TryAutoLinkTrainSample(session, full, raw);
+ return new JObject { ["success"] = true, ["sample"] = full };
+ }
+ catch (Exception ex)
+ {
+ return new JObject { ["error"] = ex.Message };
+ }
+ }
+
+ public async Task AssistentDeleteTrainSample(Session session, string id)
+ {
+ await Task.CompletedTask;
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ return new JObject { ["error"] = "id required" };
+ }
+ JObject sample = Memory.GetTrainSample(id.Trim());
+ bool ok = Memory.DeleteTrainSample(id.Trim());
+ if (ok && sample is not null)
+ {
+ Memory.UnlinkTrainSampleFromAgent(sample);
+ }
+ return new JObject { ["success"] = true, ["deleted"] = ok };
+ }
+
+ public async Task AssistentBuildDatasetFromChats(Session session, bool approved_only = false)
+ {
+ await Task.CompletedTask;
+ try
+ {
+ List chats = Memory.ListChats(withMessages: true, limit: AssistentMemory.MaxChatsStored);
+ int added = 0;
+ foreach (JObject chat in chats)
+ {
+ JArray messages = chat["messages"] as JArray ?? [];
+ for (int i = 0; i < messages.Count - 1; i++)
+ {
+ if (messages[i] is not JObject u || messages[i + 1] is not JObject a)
+ {
+ continue;
+ }
+ if (!string.Equals(u["role"]?.ToString(), "user", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+ if (!string.Equals(a["role"]?.ToString(), "assistant", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+ Memory.UpsertTrainSample(new JObject
+ {
+ ["source"] = "chat",
+ ["chat_id"] = chat["id"],
+ ["persona"] = a["persona"] ?? u["persona"],
+ ["pack"] = a["pack"] ?? u["pack"],
+ ["status"] = approved_only ? "approved" : "draft",
+ ["messages"] = new JArray { u.DeepClone(), a.DeepClone() },
+ });
+ added++;
+ }
+ }
+ return new JObject { ["success"] = true, ["added"] = added };
+ }
+ catch (Exception ex)
+ {
+ return new JObject { ["error"] = ex.Message };
+ }
+ }
+
+ public async Task AssistentImportDataset(Session session, JObject raw)
+ {
+ await Task.CompletedTask;
+ string content = raw?["content"]?.ToString();
+ string format = raw?["format"]?.ToString() ?? "auto";
+ if (string.IsNullOrWhiteSpace(content))
+ {
+ return new JObject { ["error"] = "content required" };
+ }
+ try
+ {
+ int imported = 0;
+ string fmt = (format ?? "auto").Trim().ToLowerInvariant();
+ List records = ParseDatasetContent(content, fmt);
+ foreach (JObject rec in records)
+ {
+ JArray messages = rec["messages"] as JArray;
+ if (messages is null || messages.Count == 0)
+ {
+ continue;
+ }
+ Memory.UpsertTrainSample(new JObject
+ {
+ ["source"] = "import",
+ ["messages"] = messages,
+ ["status"] = "draft",
+ });
+ imported++;
+ }
+ return new JObject { ["success"] = true, ["imported"] = imported };
+ }
+ catch (Exception ex)
+ {
+ return new JObject { ["error"] = ex.Message };
+ }
+ }
+
+ static List ParseDatasetContent(string content, string format)
+ {
+ List list = [];
+ string trimmed = content.Trim();
+ if (trimmed.StartsWith('['))
+ {
+ JArray arr = JArray.Parse(trimmed);
+ foreach (JToken t in arr)
+ {
+ if (t is JObject o)
+ {
+ JArray msgs = ExtractMessagesFromRecord(o);
+ if (msgs != null)
+ {
+ list.Add(new JObject { ["messages"] = msgs });
+ }
+ }
+ }
+ return list;
+ }
+ if (format == "csv" || LooksLikeCsv(trimmed))
+ {
+ return ParseCsvDataset(trimmed);
+ }
+ foreach (string line in trimmed.Split('\n'))
+ {
+ string ln = line.Trim();
+ if (string.IsNullOrWhiteSpace(ln))
+ {
+ continue;
+ }
+ try
+ {
+ JObject o = JObject.Parse(ln);
+ JArray msgs = ExtractMessagesFromRecord(o);
+ if (msgs != null)
+ {
+ list.Add(new JObject { ["messages"] = msgs });
+ }
+ }
+ catch
+ {
+ // skip bad line
+ }
+ }
+ return list;
+ }
+
+ static bool LooksLikeCsv(string s) => s.Contains(',') && s.Contains('\n') && !s.TrimStart().StartsWith('{');
+
+ static List ParseCsvDataset(string csv)
+ {
+ List list = [];
+ string[] lines = csv.Split('\n').Select(l => l.Trim()).Where(l => l.Length > 0).ToArray();
+ if (lines.Length < 2)
+ {
+ return list;
+ }
+ string[] headers = lines[0].Split(',').Select(h => h.Trim().Trim('"')).ToArray();
+ int promptIdx = Array.FindIndex(headers, h => h.Equals("prompt", StringComparison.OrdinalIgnoreCase) || h.Equals("question", StringComparison.OrdinalIgnoreCase) || h.Equals("instruction", StringComparison.OrdinalIgnoreCase));
+ int respIdx = Array.FindIndex(headers, h => h.Equals("response", StringComparison.OrdinalIgnoreCase) || h.Equals("answer", StringComparison.OrdinalIgnoreCase) || h.Equals("output", StringComparison.OrdinalIgnoreCase) || h.Equals("completion", StringComparison.OrdinalIgnoreCase));
+ if (promptIdx < 0 || respIdx < 0)
+ {
+ return list;
+ }
+ for (int i = 1; i < lines.Length; i++)
+ {
+ string[] cols = SplitCsvLine(lines[i]);
+ if (cols.Length <= Math.Max(promptIdx, respIdx))
+ {
+ continue;
+ }
+ list.Add(new JObject
+ {
+ ["messages"] = new JArray
+ {
+ new JObject { ["role"] = "user", ["content"] = cols[promptIdx] },
+ new JObject { ["role"] = "assistant", ["content"] = cols[respIdx] },
+ },
+ });
+ }
+ return list;
+ }
+
+ static string[] SplitCsvLine(string line)
+ {
+ List parts = [];
+ StringBuilder cur = new();
+ bool inQ = false;
+ foreach (char c in line)
+ {
+ if (c == '"')
+ {
+ inQ = !inQ;
+ continue;
+ }
+ if (c == ',' && !inQ)
+ {
+ parts.Add(cur.ToString().Trim());
+ cur.Clear();
+ continue;
+ }
+ cur.Append(c);
+ }
+ parts.Add(cur.ToString().Trim());
+ return parts.ToArray();
+ }
+
+ static JArray ExtractMessagesFromRecord(JObject o)
+ {
+ if (o["messages"] is JArray msgs)
+ {
+ return NormalizeMessagesArray(msgs);
+ }
+ if (o["conversations"] is JArray conv)
+ {
+ return ConvertHfRowToMessages(new JObject { ["conversations"] = conv }, new JObject { ["kind"] = "conversations" }, null);
+ }
+ if (o["instruction"] != null && o["output"] != null)
+ {
+ return ConvertHfRowToMessages(o, new JObject { ["kind"] = "alpaca" }, null);
+ }
+ if (o["prompt"] != null && (o["response"] != null || o["completion"] != null))
+ {
+ return ConvertHfRowToMessages(o, new JObject { ["kind"] = "prompt_response", ["response_col"] = o["response"] != null ? "response" : "completion" }, null);
+ }
+ return null;
+ }
+
+ public async Task AssistentExportDataset(Session session, string status = "approved", string format = "jsonl")
+ {
+ await Task.CompletedTask;
+ try
+ {
+ List samples = Memory.ListTrainSamples(status, null, null, 5000);
+ StringBuilder sb = new();
+ foreach (JObject s in samples)
+ {
+ JArray messages = s["messages"] as JArray ?? [];
+ if (messages.Count == 0)
+ {
+ continue;
+ }
+ if (string.Equals(format, "sharegpt", StringComparison.OrdinalIgnoreCase))
+ {
+ JArray conv = [];
+ foreach (JToken m in messages)
+ {
+ if (m is not JObject mo)
+ {
+ continue;
+ }
+ string role = mo["role"]?.ToString() ?? "user";
+ conv.Add(new JObject
+ {
+ ["from"] = role == "assistant" ? "gpt" : "human",
+ ["value"] = mo["content"]?.ToString() ?? "",
+ });
+ }
+ sb.AppendLine(new JObject { ["conversations"] = conv }.ToString(Newtonsoft.Json.Formatting.None));
+ }
+ else
+ {
+ sb.AppendLine(new JObject { ["messages"] = messages }.ToString(Newtonsoft.Json.Formatting.None));
+ }
+ }
+ string path = Path.Combine(TrainingRoot(), "datasets", $"export_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.jsonl");
+ await File.WriteAllTextAsync(path, sb.ToString(), Encoding.UTF8);
+ return new JObject { ["success"] = true, ["path"] = path, ["count"] = samples.Count, ["content"] = sb.ToString() };
+ }
+ catch (Exception ex)
+ {
+ return new JObject { ["error"] = ex.Message };
+ }
+ }
+
+ public async Task AssistentCreateOllamaModel(Session session, JObject raw)
+ {
+ if (raw is null)
+ {
+ return new JObject { ["error"] = "body required" };
+ }
+ string baseUrl = NormalizeBaseUrl(raw["base_url"]?.ToString());
+ string baseModel = raw["base_model"]?.ToString()?.Trim();
+ string name = raw["name"]?.ToString()?.Trim();
+ string system = raw["system"]?.ToString() ?? "";
+ int shots = raw["shots"]?.Value() ?? 8;
+ if (string.IsNullOrWhiteSpace(baseModel) || string.IsNullOrWhiteSpace(name))
+ {
+ return new JObject { ["error"] = "base_model and name required" };
+ }
+ if (string.IsNullOrWhiteSpace(system))
+ {
+ string persona = AssistentConfig.SafeId(raw["persona"]?.ToString()) ?? Config.DefaultPersonaId();
+ system = Config.LoadCorePrompt(persona) + "\n\n" + Config.RenderIdentityBlock(persona, includeAllShelves: true);
+ }
+ StringBuilder mf = new();
+ mf.AppendLine($"FROM {baseModel}");
+ mf.AppendLine($"SYSTEM \"\"\"{system}\"\"\"");
+ List samples = Memory.ListTrainSamples("approved", null, null, Math.Clamp(shots, 0, 32));
+ foreach (JObject s in samples.Take(shots))
+ {
+ JArray messages = s["messages"] as JArray ?? [];
+ foreach (JToken m in messages)
+ {
+ if (m is not JObject mo)
+ {
+ continue;
+ }
+ string role = mo["role"]?.ToString() ?? "user";
+ string content = mo["content"]?.ToString() ?? "";
+ if (string.IsNullOrWhiteSpace(content))
+ {
+ continue;
+ }
+ mf.AppendLine($"MESSAGE {role} \"\"\"{content.Replace("\"\"\"", "\"\"\"\"\"\"\"")}\"\"\"");
+ }
+ }
+ if (raw["num_ctx"] != null)
+ {
+ mf.AppendLine($"PARAMETER num_ctx {raw["num_ctx"]}");
+ }
+ if (raw["temperature"] != null)
+ {
+ mf.AppendLine($"PARAMETER temperature {raw["temperature"]}");
+ }
+ string modelfilePath = Path.Combine(TrainingRoot(), "jobs", $"modelfile_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.Modelfile");
+ Directory.CreateDirectory(Path.GetDirectoryName(modelfilePath)!);
+ await File.WriteAllTextAsync(modelfilePath, mf.ToString(), Encoding.UTF8);
+ try
+ {
+ JObject payload = new()
+ {
+ ["name"] = name,
+ ["modelfile"] = mf.ToString(),
+ ["stream"] = false,
+ };
+ using StringContent content = new(payload.ToString(Newtonsoft.Json.Formatting.None), Encoding.UTF8, "application/json");
+ using HttpResponseMessage resp = await HttpClient.PostAsync($"{baseUrl}/api/create", content);
+ string body = await resp.Content.ReadAsStringAsync();
+ if (!resp.IsSuccessStatusCode)
+ {
+ return new JObject { ["error"] = $"Ollama create HTTP {(int)resp.StatusCode}: {Clip(body, 400)}", ["modelfile_path"] = modelfilePath };
+ }
+ return new JObject { ["success"] = true, ["name"] = name, ["modelfile_path"] = modelfilePath, ["ollama"] = body };
+ }
+ catch (Exception ex)
+ {
+ return new JObject { ["error"] = ex.Message, ["modelfile_path"] = modelfilePath };
+ }
+ }
+
+ public async Task AssistentGetTrainJob(Session session, string id = null)
+ {
+ await Task.CompletedTask;
+ JObject job = string.IsNullOrWhiteSpace(id) ? Memory.GetActiveTrainJob() : Memory.GetTrainJob(id);
+ return new JObject
+ {
+ ["success"] = true,
+ ["job"] = job,
+ ["training_active"] = TrainingJobManager.IsRunning,
+ };
+ }
+
+ public async Task AssistentSaveRunnerSettings(Session session, JObject settings)
+ {
+ await Task.CompletedTask;
+ if (settings is null)
+ {
+ return new JObject { ["error"] = "settings required" };
+ }
+ Config.SaveTrainingRunner(settings);
+ return new JObject { ["success"] = true };
+ }
+
+ public async Task AssistentGetRunnerSettings(Session session)
+ {
+ await Task.CompletedTask;
+ return new JObject { ["success"] = true, ["settings"] = Config.LoadTrainingRunner() };
+ }
+}
diff --git a/AssistentTrainingJobs.cs b/AssistentTrainingJobs.cs
new file mode 100644
index 0000000..0b0ae18
--- /dev/null
+++ b/AssistentTrainingJobs.cs
@@ -0,0 +1,417 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using SwarmUI.Accounts;
+using SwarmUI.Utils;
+using SwarmUI.WebAPI;
+
+namespace Mrleo1nid.SwarmAssistent;
+
+/// QLoRA training job runner with VRAM lock and progress streaming.
+public partial class SwarmAssistentExtension
+{
+ static readonly TrainingJobManager TrainingJobManager = new();
+
+ public async Task AssistentStartTrainJob(Session session, JObject raw)
+ {
+ if (raw is null)
+ {
+ return new JObject { ["error"] = "body required" };
+ }
+ if (TrainingJobManager.IsRunning)
+ {
+ return new JObject { ["error"] = "Тренировка уже идёт" };
+ }
+ string hfBase = raw["base_model"]?.ToString()?.Trim();
+ string outputName = raw["output_name"]?.ToString()?.Trim();
+ if (string.IsNullOrWhiteSpace(hfBase) || string.IsNullOrWhiteSpace(outputName))
+ {
+ return new JObject { ["error"] = "base_model and output_name required" };
+ }
+ if (!string.IsNullOrWhiteSpace(raw["hf_dataset"]?.ToString()))
+ {
+ string dsId = NormalizeHfDatasetId(raw["hf_dataset"]?.ToString());
+ if (dsId is null)
+ {
+ return new JObject { ["error"] = "invalid hf_dataset id" };
+ }
+ JObject check = await CheckHfDatasetInternal(session, dsId, useCache: true);
+ if (check["gate"]?.ToString() == "rejected")
+ {
+ return new JObject { ["error"] = check["reason"]?.ToString() ?? "hf dataset rejected" };
+ }
+ raw["hf_dataset"] = dsId;
+ }
+ JObject runner = Config.LoadTrainingRunner();
+ string python = runner["python"]?.ToString()?.Trim();
+ if (string.IsNullOrWhiteSpace(python))
+ {
+ python = "python";
+ }
+ string kind = runner["kind"]?.ToString()?.Trim();
+ if (string.IsNullOrWhiteSpace(kind))
+ {
+ return new JObject { ["error"] = "QLoRA-раннер не настроен (Настройки → Модели)" };
+ }
+ string baseUrl = NormalizeBaseUrl(raw["base_url"]?.ToString());
+ string chatModel = raw["chat_model"]?.ToString()?.Trim();
+ if (!string.IsNullOrWhiteSpace(chatModel))
+ {
+ await AssistentParkLlm(session, baseUrl, chatModel);
+ }
+ JObject export = await AssistentExportDataset(session, "approved", "jsonl");
+ string datasetPath = export["path"]?.ToString();
+ if (string.IsNullOrWhiteSpace(datasetPath) || !File.Exists(datasetPath))
+ {
+ return new JObject { ["error"] = "Нет одобренных примеров для тренировки" };
+ }
+ long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ string jobId = $"tj_{now}";
+ string jobDir = Path.Combine(TrainingRoot(), "jobs", jobId);
+ Directory.CreateDirectory(jobDir);
+ string configPath = Path.Combine(jobDir, "config.json");
+ string logPath = Path.Combine(jobDir, "log.txt");
+ JObject jobConfig = new()
+ {
+ ["base_model"] = hfBase,
+ ["output_name"] = outputName,
+ ["dataset_path"] = datasetPath,
+ ["hf_dataset"] = raw["hf_dataset"],
+ ["rank"] = raw["rank"] ?? 16,
+ ["alpha"] = raw["alpha"] ?? 32,
+ ["lr"] = raw["lr"] ?? 0.0002,
+ ["epochs"] = raw["epochs"] ?? 3,
+ ["seq_len"] = raw["seq_len"] ?? 2048,
+ ["four_bit"] = raw["four_bit"] ?? true,
+ ["adapter_dir"] = Path.Combine(TrainingRoot(), "adapters", outputName),
+ };
+ await File.WriteAllTextAsync(configPath, jobConfig.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
+ Memory.SaveTrainJob(new JObject
+ {
+ ["id"] = jobId,
+ ["kind"] = "qlora",
+ ["status"] = "running",
+ ["config"] = jobConfig,
+ ["base_model"] = hfBase,
+ ["output_name"] = outputName,
+ ["log_path"] = logPath,
+ ["created_at"] = now,
+ });
+ string cmdLine = BuildRunnerCommand(runner, configPath, logPath, jobDir);
+ bool started = TrainingJobManager.Start(this, session, jobId, cmdLine, logPath, baseUrl, chatModel, GetHfToken(session));
+ if (!started)
+ {
+ Memory.SaveTrainJob(new JObject { ["id"] = jobId, ["status"] = "failed", ["progress"] = new JObject { ["error"] = "process start failed" } });
+ return new JObject { ["error"] = "Не удалось запустить процесс тренировки" };
+ }
+ return new JObject { ["success"] = true, ["job_id"] = jobId, ["log_path"] = logPath };
+ }
+
+ static string BuildRunnerCommand(JObject runner, string configPath, string logPath, string workDir)
+ {
+ string python = runner["python"]?.ToString()?.Trim();
+ if (string.IsNullOrWhiteSpace(python))
+ {
+ python = "python";
+ }
+ string kind = runner["kind"]?.ToString()?.Trim() ?? "custom";
+ string custom = runner["cmd"]?.ToString()?.Trim();
+ string scriptPath = Path.Combine(FilePath, "scripts", "train_qlora.py");
+ if (kind == "custom" && !string.IsNullOrWhiteSpace(custom))
+ {
+ return custom
+ .Replace("{python}", python, StringComparison.OrdinalIgnoreCase)
+ .Replace("{config}", configPath, StringComparison.OrdinalIgnoreCase)
+ .Replace("{log}", logPath, StringComparison.OrdinalIgnoreCase)
+ .Replace("{workdir}", workDir, StringComparison.OrdinalIgnoreCase);
+ }
+ return $"\"{python}\" \"{scriptPath}\" --config \"{configPath}\" --log \"{logPath}\"";
+ }
+
+ public async Task AssistentCancelTrainJob(Session session, string id = null)
+ {
+ await Task.CompletedTask;
+ TrainingJobManager.Cancel();
+ string jobId = id ?? TrainingJobManager.CurrentJobId;
+ if (!string.IsNullOrWhiteSpace(jobId))
+ {
+ Memory.SaveTrainJob(new JObject
+ {
+ ["id"] = jobId,
+ ["status"] = "cancelled",
+ ["finished_at"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
+ });
+ }
+ return new JObject { ["success"] = true, ["cancelled"] = true };
+ }
+
+ public async Task AssistentTrainWS(Session session, WebSocket ws, JObject raw)
+ {
+ await Task.CompletedTask;
+ try
+ {
+ while (TrainingJobManager.IsRunning && ws.State == System.Net.WebSockets.WebSocketState.Open)
+ {
+ JObject progress = TrainingJobManager.GetProgress();
+ string msg = progress.ToString(Newtonsoft.Json.Formatting.None);
+ await ws.SendAsync(Encoding.UTF8.GetBytes(msg), System.Net.WebSockets.WebSocketMessageType.Text, true, CancellationToken.None);
+ await Task.Delay(800);
+ }
+ JObject final = TrainingJobManager.GetProgress();
+ final["done"] = true;
+ await ws.SendAsync(Encoding.UTF8.GetBytes(final.ToString(Newtonsoft.Json.Formatting.None)), System.Net.WebSockets.WebSocketMessageType.Text, true, CancellationToken.None);
+ }
+ catch (Exception ex)
+ {
+ Logs.Debug($"AssistentTrainWS: {ex.Message}");
+ }
+ return new JObject { ["success"] = true };
+ }
+
+ internal async Task FinishTrainJobAsync(string jobId, bool success, string logPath, Session session, string baseUrl, string chatModel, string adapterDir, string outputName, string ggufScript)
+ {
+ long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ Memory.SaveTrainJob(new JObject
+ {
+ ["id"] = jobId,
+ ["status"] = success ? "completed" : "failed",
+ ["finished_at"] = now,
+ ["progress"] = TrainingJobManager.GetProgress(),
+ });
+ if (success && Directory.Exists(adapterDir))
+ {
+ try
+ {
+ await RegisterAdapterInOllama(session, baseUrl, outputName, adapterDir, ggufScript);
+ }
+ catch (Exception ex)
+ {
+ Logs.Debug($"RegisterAdapter: {ex.Message}");
+ }
+ }
+ if (!string.IsNullOrWhiteSpace(chatModel))
+ {
+ await AssistentWarmLlm(session, baseUrl, chatModel);
+ }
+ TrainingJobManager.ClearRunning();
+ }
+
+ async Task RegisterAdapterInOllama(Session session, string baseUrl, string outputName, string adapterDir, string ggufScript)
+ {
+ string adapterFile = Directory.GetFiles(adapterDir, "*.gguf").FirstOrDefault()
+ ?? Directory.GetFiles(adapterDir, "adapter_model.safetensors").FirstOrDefault();
+ if (string.IsNullOrWhiteSpace(adapterFile))
+ {
+ return;
+ }
+ StringBuilder mf = new();
+ JObject job = Memory.GetTrainJob(TrainingJobManager.CurrentJobId ?? "");
+ string baseModel = job?["base_model"]?.ToString() ?? "unknown";
+ mf.AppendLine($"FROM {baseModel}");
+ mf.AppendLine($"ADAPTER {adapterFile.Replace("\\", "/")}");
+ JObject payload = new()
+ {
+ ["name"] = outputName,
+ ["modelfile"] = mf.ToString(),
+ ["stream"] = false,
+ };
+ using StringContent content = new(payload.ToString(Newtonsoft.Json.Formatting.None), Encoding.UTF8, "application/json");
+ using HttpResponseMessage resp = await HttpClient.PostAsync($"{NormalizeBaseUrl(baseUrl)}/api/create", content);
+ _ = await resp.Content.ReadAsStringAsync();
+ }
+}
+
+sealed class TrainingJobManager
+{
+ static readonly Regex LossRe = new(@"loss[:\s]+([0-9.]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+ static readonly Regex StepRe = new(@"(\d+)\s*/\s*(\d+)", RegexOptions.Compiled);
+
+ Process _process;
+ readonly object _lock = new();
+ JObject _progress = new() { ["status"] = "idle" };
+ string _logPath;
+ SwarmAssistentExtension _ext;
+ Session _session;
+ string _jobId;
+ string _baseUrl;
+ string _chatModel;
+
+ public bool IsRunning { get; private set; }
+ public string CurrentJobId => _jobId;
+
+ public bool Start(SwarmAssistentExtension ext, Session session, string jobId, string commandLine, string logPath, string baseUrl, string chatModel, string hfToken)
+ {
+ lock (_lock)
+ {
+ if (IsRunning)
+ {
+ return false;
+ }
+ _ext = ext;
+ _session = session;
+ _jobId = jobId;
+ _logPath = logPath;
+ _baseUrl = baseUrl;
+ _chatModel = chatModel;
+ _progress = new JObject { ["status"] = "running", ["step"] = 0, ["loss"] = null, ["log"] = "" };
+ try
+ {
+ ProcessStartInfo psi = new()
+ {
+ FileName = "cmd.exe",
+ Arguments = $"/c {commandLine}",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ WorkingDirectory = Path.GetDirectoryName(logPath) ?? Environment.CurrentDirectory,
+ };
+ if (!string.IsNullOrWhiteSpace(hfToken))
+ {
+ psi.Environment["HF_TOKEN"] = hfToken;
+ }
+ _process = new Process { StartInfo = psi, EnableRaisingEvents = true };
+ _process.OutputDataReceived += (_, e) => AppendLog(e.Data);
+ _process.ErrorDataReceived += (_, e) => AppendLog(e.Data);
+ _process.Exited += async (_, _) => await OnExited();
+ _process.Start();
+ _process.BeginOutputReadLine();
+ _process.BeginErrorReadLine();
+ IsRunning = true;
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _progress["error"] = ex.Message;
+ IsRunning = false;
+ return false;
+ }
+ }
+ }
+
+ void AppendLog(string line)
+ {
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ return;
+ }
+ lock (_lock)
+ {
+ try
+ {
+ File.AppendAllText(_logPath, line + Environment.NewLine);
+ }
+ catch
+ {
+ // ignore
+ }
+ string prev = _progress["log"]?.ToString() ?? "";
+ string combined = (prev + line + "\n");
+ if (combined.Length > 12000)
+ {
+ combined = combined[^12000..];
+ }
+ _progress["log"] = combined;
+ Match lossM = LossRe.Match(line);
+ if (lossM.Success)
+ {
+ _progress["loss"] = lossM.Groups[1].Value;
+ }
+ Match stepM = StepRe.Match(line);
+ if (stepM.Success)
+ {
+ _progress["step"] = int.Parse(stepM.Groups[1].Value);
+ _progress["total_steps"] = int.Parse(stepM.Groups[2].Value);
+ int total = int.Parse(stepM.Groups[2].Value);
+ int step = int.Parse(stepM.Groups[1].Value);
+ _progress["percent"] = total > 0 ? (int)(100.0 * step / total) : 0;
+ }
+ try
+ {
+ _ext?.Memory?.SaveTrainJob(new JObject
+ {
+ ["id"] = _jobId,
+ ["status"] = "running",
+ ["progress"] = _progress,
+ });
+ }
+ catch
+ {
+ // ignore
+ }
+ }
+ }
+
+ async Task OnExited()
+ {
+ bool ok = false;
+ string adapterDir = "";
+ string outputName = "";
+ lock (_lock)
+ {
+ ok = _process?.ExitCode == 0;
+ IsRunning = false;
+ _progress["status"] = ok ? "completed" : "failed";
+ _progress["exit_code"] = _process?.ExitCode;
+ }
+ if (_ext != null)
+ {
+ JObject job = _ext.Memory.GetTrainJob(_jobId);
+ try
+ {
+ JObject cfg = JObject.Parse(job?["config_json"]?.ToString() ?? "{}");
+ adapterDir = cfg["adapter_dir"]?.ToString() ?? "";
+ outputName = cfg["output_name"]?.ToString() ?? job?["output_name"]?.ToString() ?? "";
+ }
+ catch
+ {
+ // ignore
+ }
+ JObject runner = _ext.Config.LoadTrainingRunner();
+ await _ext.FinishTrainJobAsync(_jobId, ok, _logPath, _session, _baseUrl, _chatModel, adapterDir, outputName, runner["gguf_script"]?.ToString());
+ }
+ }
+
+ public void Cancel()
+ {
+ lock (_lock)
+ {
+ try
+ {
+ if (_process != null && !_process.HasExited)
+ {
+ _process.Kill(entireProcessTree: true);
+ }
+ }
+ catch
+ {
+ // ignore
+ }
+ IsRunning = false;
+ _progress["status"] = "cancelled";
+ }
+ }
+
+ public JObject GetProgress()
+ {
+ lock (_lock)
+ {
+ return (JObject)_progress.DeepClone();
+ }
+ }
+
+ public void ClearRunning()
+ {
+ lock (_lock)
+ {
+ IsRunning = false;
+ }
+ }
+}
diff --git a/README.md b/README.md
index 1d6a14f..9a1e26a 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,10 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat +
**Turn model:** one user message is one *turn*. A turn may fan out into nested LLM *hops* — Krea prompt prep, empty-patch retry, vision, auto-critique. Hops share one `HOP_BUDGET`, never re-read the user's text (their prompt is client-authored), and pass the busy gate that blocks new user sends. What a reply does to generation state is decided once, in `resolveTurnIntent`: the model's `actions:["generate"]` / `look_at` win, RU intent heuristics only back it up when the model forgets, and an explicit «запомни, не генерируй» vetoes both.
+**Version 0.12.1** — **Услышанное → агент**: одобренные примеры датасета сразу попадают в vector memory (`kind=heard`) и в контекст чата как `heard_examples` (без QLoRA). На вкладке «Датасет»: авто-подключение при одобрении, синхронизация всех, per-sample 🔗. Агент может запросить `heard_search`. Настройки: `training-agent.json`.
+
+**Version 0.12.0** — App-level tabs (Чат / Карточки / **Обучение** / Настройки), боковая панель истории чатов, вкладка обучения LLM: курирование диалогов, импорт JSONL/CSV, Hugging Face datasets (фильтр совместимости), быстрый Ollama Modelfile, опциональный QLoRA-раннер с локаутом VRAM. HF token из SwarmUI User Settings (`huggingface_api`).
+
**Version 0.11.9** — Distilled client: esbuild bundle (`Assets/assistent.bundle.js`), unified patch keys (`Config/_base/patch-keys.json`), taste stack removed (UserPrefs only), chat storage merge + all-chats disk save, `write_prompt` → alias of `ordinary`. Builds on prior 0.11.9 turn-intent work.
**Version 0.11.9** — One turn, one decision. Nested hops (Krea prep, empty-patch retry, vision, critique) share a `turnHops` budget and pass the busy gate — Krea prep and the empty-patch retry were silently no-ops since 0.10.22/0.11.2. Generate / `look_at` are decided in a single `resolveTurnIntent`; `ensureGenerateAction`, `shouldHonorLookAt` and the `wantsGen`/`willGen`/`suppressGen` tangle are gone. Builds on 0.11.8.
@@ -204,6 +208,14 @@ Patch fence keys: single source `Config/_base/patch-keys.json` → C# + client v
| `AssistentListChats` / `AssistentGetChat` / `AssistentSaveChat` / `AssistentDeleteChat` | sqlite `chats` (optional `q` FTS) |
| `AssistentGetUiState` / `AssistentSaveUiState` | sqlite `kv.ui_state` |
| `AssistentParkLlm` / `AssistentWarmLlm` | Unload / reload the chat model in VRAM |
+| `AssistentListTrainSamples` / `AssistentUpsertTrainSample` / `AssistentDeleteTrainSample` | Training samples in sqlite |
+| `AssistentBuildDatasetFromChats` / `AssistentImportDataset` / `AssistentExportDataset` | Dataset from chats / file import / JSONL export |
+| `AssistentCreateOllamaModel` | Build Ollama model from Modelfile (SYSTEM + few-shot) |
+| `AssistentSearchHfDatasets` / `AssistentCheckHfDataset` / `AssistentPreviewHfDataset` / `AssistentImportHfDataset` | Hugging Face datasets (gated by schema) |
+| `AssistentStartTrainJob` / `AssistentCancelTrainJob` / `AssistentGetTrainJob` / `AssistentTrainWS` | QLoRA runner + progress |
+| `AssistentGetRunnerSettings` / `AssistentSaveRunnerSettings` | Python runner config overlay |
+| `AssistentGetDatasetAgentSettings` / `AssistentSaveDatasetAgentSettings` | «Услышанное» → agent RAG (`training-agent.json`) |
+| `AssistentLinkTrainSampleToAgent` / `AssistentUnlinkTrainSampleFromAgent` / `AssistentSyncDatasetToAgent` | Embed approved samples as `heard` memory |
## License
diff --git a/SwarmAssistentExtension.cs b/SwarmAssistentExtension.cs
index ccc8718..e7154ab 100644
--- a/SwarmAssistentExtension.cs
+++ b/SwarmAssistentExtension.cs
@@ -33,8 +33,8 @@ public partial class SwarmAssistentExtension : Extension
ExtensionAuthor = "mrleo1nid";
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
License = "MIT";
- Version = "0.11.9";
- Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"];
+ Version = "0.12.1";
+ Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard"];
}
public override void OnInit()
@@ -82,7 +82,29 @@ public partial class SwarmAssistentExtension : Extension
API.RegisterAPICall(AssistentForgetUserPref, true, PermUse);
API.RegisterAPICall(AssistentClearUserPrefs, true, PermUse);
API.RegisterAPICall(AssistentClearMemory, true, PermUse);
- Logs.Init("Swarm Assistent extension loaded (settings panel + user prefs + craft memory)");
+ API.RegisterAPICall(AssistentListTrainSamples, false, PermUse);
+ API.RegisterAPICall(AssistentUpsertTrainSample, true, PermUse);
+ API.RegisterAPICall(AssistentDeleteTrainSample, true, PermUse);
+ API.RegisterAPICall(AssistentBuildDatasetFromChats, false, PermUse);
+ API.RegisterAPICall(AssistentImportDataset, true, PermUse);
+ API.RegisterAPICall(AssistentExportDataset, false, PermUse);
+ API.RegisterAPICall(AssistentCreateOllamaModel, true, PermUse);
+ API.RegisterAPICall(AssistentSearchHfDatasets, false, PermUse);
+ API.RegisterAPICall(AssistentCheckHfDataset, false, PermUse);
+ API.RegisterAPICall(AssistentPreviewHfDataset, false, PermUse);
+ API.RegisterAPICall(AssistentImportHfDataset, true, PermUse);
+ API.RegisterAPICall(AssistentStartTrainJob, true, PermUse);
+ API.RegisterAPICall(AssistentCancelTrainJob, true, PermUse);
+ API.RegisterAPICall(AssistentGetTrainJob, false, PermUse);
+ API.RegisterAPICall(AssistentTrainWS, true, PermUse);
+ API.RegisterAPICall(AssistentSaveRunnerSettings, true, PermUse);
+ API.RegisterAPICall(AssistentGetRunnerSettings, false, PermUse);
+ API.RegisterAPICall(AssistentGetDatasetAgentSettings, false, PermUse);
+ API.RegisterAPICall(AssistentSaveDatasetAgentSettings, true, PermUse);
+ API.RegisterAPICall(AssistentLinkTrainSampleToAgent, true, PermUse);
+ API.RegisterAPICall(AssistentUnlinkTrainSampleFromAgent, true, PermUse);
+ API.RegisterAPICall(AssistentSyncDatasetToAgent, true, PermUse);
+ Logs.Init("Swarm Assistent extension loaded (0.12.1 heard dataset → agent)");
}
int CfgInt(string key, int fallback)
diff --git a/Tabs/Text2Image/Assistent.html b/Tabs/Text2Image/Assistent.html
index 30d06b2..221fe25 100644
--- a/Tabs/Text2Image/Assistent.html
+++ b/Tabs/Text2Image/Assistent.html
@@ -2,161 +2,290 @@
Swarm Assistent только для моделей Krea 2. Выбери checkpoint Krea 2, чтобы включить чат.
-
-