From 6d8aaefc383ab316058938a4f118aa6585584102 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sat, 22 Aug 2026 00:06:11 +0300 Subject: [PATCH] Ship Assistent 0.7.6: multi-chat history, UI polish, and race fixes. Persist chats with Generate param snapshots, always open a new chat on start, and abort in-flight work on clear/switch so replies cannot land in the wrong chat. Co-authored-by: Cursor --- Assets/assistent.css | 297 ++++++++- Assets/assistent.js | 1058 ++++++++++++++++++++++++++++---- Config/_base/ui.json | 4 +- README.md | 2 +- SwarmAssistentExtension.cs | 2 +- Tabs/Text2Image/Assistent.html | 37 +- 6 files changed, 1260 insertions(+), 140 deletions(-) diff --git a/Assets/assistent.css b/Assets/assistent.css index bb852ca..32644dc 100644 --- a/Assets/assistent.css +++ b/Assets/assistent.css @@ -335,6 +335,83 @@ align-items: center; } +.sa-live-params { + font-size: 0.75rem; + opacity: 0.78; + padding: 0.2rem 0.15rem 0.35rem; + letter-spacing: 0.01em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-variant-numeric: tabular-nums; +} + +.sa-more-wrap { + position: relative; + display: inline-flex; + align-items: center; + gap: 0.15rem; +} + +.sa-more-menu { + position: absolute; + z-index: 40; + bottom: calc(100% + 0.25rem); + left: 0; + min-width: 11rem; + display: flex; + flex-direction: column; + gap: 0.15rem; + padding: 0.35rem; + border-radius: 0.45rem; + border: 1px solid color-mix(in srgb, currentColor 28%, transparent); + background: color-mix(in srgb, #1a1a1a 92%, currentColor); + box-shadow: 0 8px 24px color-mix(in srgb, #000 45%, transparent); +} + +#sa_clear_more_wrap .sa-more-menu { + left: auto; + right: 0; + bottom: auto; + top: calc(100% + 0.25rem); +} + +.sa-more-item { + appearance: none; + border: 0; + background: transparent; + color: inherit; + text-align: left; + padding: 0.4rem 0.55rem; + border-radius: 0.3rem; + cursor: pointer; + font-size: 0.85rem; +} + +.sa-more-item:hover { + background: color-mix(in srgb, currentColor 12%, transparent); +} + +.sa-mode-badge { + display: inline-flex; + align-items: center; + padding: 0.12rem 0.45rem; + border-radius: 999px; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.03em; + text-transform: lowercase; + border: 1px solid color-mix(in srgb, currentColor 30%, transparent); + background: color-mix(in srgb, currentColor 8%, transparent); + opacity: 0.92; + flex: 0 0 auto; +} + +.sa-mode-badge.sa-mode-hot { + border-color: color-mix(in srgb, #f5a623 55%, currentColor); + background: color-mix(in srgb, #f5a623 18%, transparent); +} + .sa-chat-pane { flex: 1 1 auto; display: flex; @@ -348,6 +425,7 @@ } .sa-chat-header { + position: relative; display: flex; align-items: center; justify-content: space-between; @@ -355,6 +433,7 @@ padding: 0.5rem 0.75rem; border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent); background: color-mix(in srgb, currentColor 5%, transparent); + flex-wrap: wrap; } .sa-chat-title { @@ -363,6 +442,144 @@ display: flex; align-items: center; gap: 0.45rem; + flex-wrap: wrap; +} + +.sa-sessions-bar { + display: inline-flex; + align-items: center; + gap: 0.3rem; + margin-left: 0.15rem; +} + +.sa-sessions-toggle { + font-size: 0.78rem !important; + padding: 0.18rem 0.5rem !important; +} + +.sa-session-label { + font-size: 0.78rem; + font-weight: 500; + opacity: 0.75; + max-width: 10rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + border-radius: 0.3rem; + padding: 0.1rem 0.35rem; +} + +.sa-session-label:hover { + opacity: 1; + 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-head { + font-size: 0.78rem; + font-weight: 650; + opacity: 0.85; + letter-spacing: 0.03em; +} + +.sa-chats-panel-hint { + font-size: 0.72rem; + opacity: 0.65; + line-height: 1.35; +} + +.sa-chats-list { + overflow: auto; + display: flex; + flex-direction: column; + gap: 0.25rem; + min-height: 0; +} + +.sa-chats-empty { + font-size: 0.82rem; + opacity: 0.65; + padding: 0.5rem 0.25rem; +} + +.sa-chat-row { + display: flex; + align-items: stretch; + gap: 0.2rem; + border-radius: 0.4rem; + border: 1px solid transparent; + background: color-mix(in srgb, currentColor 5%, transparent); +} + +.sa-chat-row-active { + border-color: color-mix(in srgb, #6cf 45%, currentColor); + background: color-mix(in srgb, #6cf 12%, transparent); +} + +.sa-chat-row-main { + appearance: none; + border: 0; + background: transparent; + color: inherit; + text-align: left; + flex: 1; + min-width: 0; + padding: 0.4rem 0.5rem; + cursor: pointer; + display: flex; + flex-direction: column; + gap: 0.12rem; +} + +.sa-chat-row-title { + font-size: 0.85rem; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sa-chat-row-meta { + font-size: 0.72rem; + opacity: 0.7; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sa-chat-row-del { + appearance: none; + border: 0; + background: transparent; + color: inherit; + opacity: 0.55; + cursor: pointer; + padding: 0 0.55rem; + font-size: 1.1rem; + line-height: 1; +} + +.sa-chat-row-del:hover { + opacity: 1; + color: #f66; } .sa-live-dot { @@ -818,9 +1035,18 @@ .sa-chips { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; gap: 0.35rem; align-items: center; + overflow-x: auto; + overflow-y: hidden; + max-width: 100%; + padding-bottom: 0.15rem; + scrollbar-width: thin; +} + +.sa-chips::-webkit-scrollbar { + height: 4px; } .sa-chip { @@ -835,6 +1061,7 @@ letter-spacing: 0.02em; cursor: pointer; line-height: 1.3; + flex: 0 0 auto; } .sa-chip:hover { @@ -1080,14 +1307,14 @@ .sa-card-row { display: flex; - gap: 0.45rem; - align-items: flex-start; + gap: 0.55rem; + align-items: center; } .sa-card-row-thumb { - width: 2.4rem; - height: 2.4rem; - border-radius: 0.3rem; + width: 3.6rem; + height: 3.6rem; + border-radius: 0.35rem; object-fit: cover; flex: 0 0 auto; background: color-mix(in srgb, currentColor 8%, transparent); @@ -1099,12 +1326,68 @@ flex: 1; } +.sa-card-row-name { + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sa-card-row-chat { + flex: 0 0 auto; + padding: 0.2rem 0.45rem !important; + font-size: 0.85rem; +} + +.sa-btn-gen.sa-has-patch { + border-color: color-mix(in srgb, #3fb950 55%, currentColor); + box-shadow: inset 0 0 0 1px color-mix(in srgb, #3fb950 25%, transparent); +} + +.sa-patch-details { + margin: 0.35rem 0 0.15rem; +} + +.sa-patch-details > summary { + cursor: pointer; + font-size: 0.82rem; + opacity: 0.88; + user-select: none; + list-style: none; +} + +.sa-patch-details > summary::-webkit-details-marker { + display: none; +} + +.sa-patch-details > summary::before { + content: '▸ '; + opacity: 0.7; +} + +.sa-patch-details[open] > summary::before { + content: '▾ '; +} + +.sa-patch-details pre { + margin-top: 0.35rem; + max-height: 14rem; + overflow: auto; +} + +.sa-patch-auto { + opacity: 0.95; +} + .sa-lora-chips { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; gap: 0.3rem; align-items: center; min-height: 0; + overflow-x: auto; + max-width: 100%; + scrollbar-width: thin; } .sa-lora-chip { diff --git a/Assets/assistent.js b/Assets/assistent.js index 18abb9c..8ffc9af 100644 --- a/Assets/assistent.js +++ b/Assets/assistent.js @@ -1,6 +1,6 @@ /** * Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API). - * v0.7.3: Silent apply+gen when user asks to generate; composer «Собрать + Gen» button. + * v0.7.6: Cheap-bug pass — abort on clear/new, busy until side-effects, interrupt cleans stream. */ (function () { const LS_BASE = 'swarm_assistent_base_url'; @@ -18,7 +18,10 @@ const LS_WELCOMED = 'swarm_assistent_welcomed'; const LS_TASTE = 'swarm_assistent_taste'; const LS_HISTORY = 'swarm_assistent_history'; + const LS_CHATS = 'swarm_assistent_chats_v1'; const LS_BOARD_TAB = 'swarm_assistent_board_tab'; + const MAX_CHATS = 40; + const MAX_CHAT_MSGS = 24; const TAB_BUTTON_ID = 'maintab_assistent'; const GEN_ID = 'generate'; const MAX_REF_SLOTS = 4; @@ -66,6 +69,8 @@ let HELP_TEXT = `Slash-команды (без LLM): /help — этот список +/new — новый чат (текущий сохранится в Историю) +/history — открыть список чатов /debug — сводка UI/Exact (без LLM) /debug ask — то же + короткий ответ модели /why — сразу /debug ask @@ -80,10 +85,13 @@ /civitai — поиск LoRA (Confirm в чате) /inventory — rescan моделей + обновить список LoRA -Чипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.`; +Чипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW. +При старте всегда новый чат; смена чата в Истории восстанавливает параметры.`; let SLASH_COMMANDS = [ { cmd: '/help', hint: 'список команд' }, + { cmd: '/new', hint: 'новый чат' }, + { cmd: '/history', hint: 'история чатов' }, { cmd: '/debug', hint: 'сводка · ask = с LLM' }, { cmd: '/why', hint: 'debug + пояснение LLM' }, { cmd: '/gen', hint: 'Generate сейчас' }, @@ -142,6 +150,10 @@ cardsSelection: null, cardsBusy: false, pendingPersonaNote: null, + chats: [], + activeChatId: null, + restoringChat: false, + chatsPanelOpen: false, slashIndex: 0, }; @@ -204,6 +216,7 @@ streaming: 'Writing…', generating: 'Generating image…', applying: 'Applying patch…', + silent_gen: 'Применяю патч → Generate…', refining: 'Civitai search done — refining…', }; const text = labels[state.busyPhase] || 'Working…'; @@ -466,14 +479,62 @@ function rememberLastPatch(patch) { if (patch && typeof patch === 'object' && !isCardObject(patch)) { state.lastPatch = patch; - const btn = $('sa_btn_build_gen'); - if (btn) { - btn.disabled = false; - btn.title = 'Применить последний патч Assistent и Generate'; - } + syncBuildGenButton(); } } + function syncBuildGenButton() { + const btn = $('sa_btn_build_gen'); + if (!btn) { + return; + } + if (state.lastPatch) { + const keys = Object.keys(state.lastPatch).filter((k) => state.lastPatch[k] != null).slice(0, 6); + btn.title = `Есть патч Assistent (${keys.join(', ') || '…'}) → Apply + Generate`; + btn.classList.add('sa-has-patch'); + } else { + btn.title = 'Нет патча — Generate с текущим промптом SwarmUI'; + btn.classList.remove('sa-has-patch'); + } + } + + function syncModeBadge() { + const badge = $('sa_mode_badge'); + const pack = $('sa_pack')?.value || 'write_prompt'; + if (!badge) { + return; + } + const shortMap = { + write_prompt: 'write', + critique_image: 'critique', + compose_scene: 'compose', + fix_params: 'params', + inpaint_edit: 'inpaint', + describe_ref: 'describe', + catalog_card: 'card', + }; + const short = shortMap[pack] || pack.replace(/_/g, ' ').slice(0, 12); + badge.textContent = short; + badge.dataset.pack = pack; + badge.title = `Режим: ${pack}`; + badge.classList.toggle('sa-mode-hot', pack === 'critique_image' || pack === 'inpaint_edit'); + } + + function syncLiveParamsBar() { + const el = $('sa_live_params'); + if (!el) { + return; + } + const w = parseInt(val('input_width') || '0', 10) || null; + const h = parseInt(val('input_height') || '0', 10) || null; + const aspect = guessAspectFromSize(w, h) || '—'; + const steps = val('input_steps') || '—'; + const cfg = val('input_cfgscale') || val('input_cfg') || '—'; + const seed = val('input_seed') || '—'; + const profile = detectKreaProfileName(); + el.textContent = `${aspect} · ${w || '?'}×${h || '?'} · steps ${steps} · cfg ${cfg} · ${profile} · seed ${seed}`; + } + function applyAspectTableFrom(obj) { if (!obj || typeof obj !== 'object') { return false; @@ -530,7 +591,7 @@ } function rememberSessionExact(partial) { - if (!partial || typeof partial !== 'object') { + if (state.restoringChat || !partial || typeof partial !== 'object') { return; } const keys = ['steps', 'cfg', 'sigma_shift', 'aspect', 'width', 'height', 'images', 'batch', 'seed', 'sampler', 'scheduler']; @@ -590,6 +651,9 @@ } function shouldSkipSessionRollback(key, patchValue) { + if (state.restoringChat) { + return false; + } if (state.lastUserParamIntent) { return false; } @@ -947,6 +1011,29 @@ }); } + function mountPatchBlock(host, patch, { silent = false } = {}) { + if (!host || !patch) { + return; + } + rememberLastPatch(patch); + const wrap = document.createElement('div'); + wrap.className = 'sa-patch' + (silent ? ' sa-patch-auto' : ''); + const details = document.createElement('details'); + details.className = 'sa-patch-details'; + const summary = document.createElement('summary'); + const keys = Object.keys(patch).filter((k) => patch[k] != null && k !== 'notes' && k !== 'actions'); + summary.textContent = silent + ? `Патч применён · ${keys.slice(0, 6).join(', ') || 'generate'}` + : `JSON патч · ${keys.slice(0, 8).join(', ') || '…'}`; + const pre = document.createElement('pre'); + pre.textContent = JSON.stringify(patch, null, 2); + details.appendChild(summary); + details.appendChild(pre); + wrap.appendChild(details); + mountPatchActions(wrap, patch, { silent }); + host.appendChild(wrap); + } + function mountPatchActions(parent, patch, { silent = false } = {}) { if (!parent || !patch) { return; @@ -958,7 +1045,11 @@ if (silent) { const note = document.createElement('div'); note.className = 'sa-patch-actions sa-patch-silent sa-patch-current'; - note.textContent = 'Применено автоматически · Generate…'; + const willGen = Array.isArray(patch.actions) && patch.actions.map(String).includes('generate') + || !!state.pendingSilentGen; + note.textContent = willGen + ? 'Применено автоматически · Generate…' + : 'Применено автоматически'; host.appendChild(note); return; } @@ -985,6 +1076,7 @@ if (isGenerateUnavailable()) { return; } + startBusyUi('silent_gen'); await applyPatch(patch, 'all'); await runGenerateFromPatch({ ...patch, actions: ['generate'] }, { force: true }); }); @@ -994,18 +1086,24 @@ } async function buildCurrentAndGenerate() { - if (state.busy || state.generating || isGenerateUnavailable()) { + if (state.busy || state.generating) { setStatus('Занято — подожди или нажми Стоп'); return; } + if (isGenerateUnavailable()) { + setStatus('Generate недоступен — дождись SwarmUI'); + return; + } const patch = state.lastPatch; if (patch) { + startBusyUi('silent_gen'); setStatus('Собираю патч → Generate…'); await applyPatch(patch, 'all'); + syncLiveParamsBar(); await runGenerateFromPatch({ ...patch, actions: ['generate'] }, { force: true }); return; } - // No Assistent patch yet — generate with whatever is already in SwarmUI. + startBusyUi('generating'); setStatus('Generate с текущим промптом…'); await runGenerateFromPatch({ actions: ['generate'] }, { force: true }); } @@ -1238,22 +1336,248 @@ box.scrollTop = box.scrollHeight; } - function persistHistory() { - try { - const slim = (state.history || []) - .filter((m) => m && (m.role === 'user' || m.role === 'assistant') && !m.systemish) - .slice(-24) - .map((m) => ({ - role: m.role, - content: String(m.content || '').slice(0, 4000), - persona: m.persona || undefined, - pack: m.pack || undefined, - })); - localStorage.setItem(LS_HISTORY, JSON.stringify(slim)); - } catch (e) { /* ignore */ } + function chatUid() { + return `c_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; } - function restoreHistory() { + function slimHistoryMessages(list) { + return (list || []) + .filter((m) => m && (m.role === 'user' || m.role === 'assistant') && !m.systemish) + .slice(-MAX_CHAT_MSGS) + .map((m) => ({ + role: m.role, + content: String(m.content || '').slice(0, 4000), + persona: m.persona || undefined, + pack: m.pack || undefined, + })); + } + + function titleFromMessages(messages) { + const u = (messages || []).find((m) => m.role === 'user' && m.content); + const t = String(u?.content || '').replace(/\s+/g, ' ').trim(); + return t ? t.slice(0, 52) : 'Новый чат'; + } + + function snapshotChatParams() { + let loras = []; + try { + if (typeof loraHelper !== 'undefined' && loraHelper && Array.isArray(loraHelper.selected)) { + loras = loraHelper.selected.map((l) => ({ + name: l.name || l, + weight: (loraHelper.loraWeightPref && loraHelper.loraWeightPref[l.name || l]) || 1, + })); + } + } catch (e) { /* ignore */ } + let lastPatch = null; + try { + lastPatch = state.lastPatch ? JSON.parse(JSON.stringify(state.lastPatch)) : null; + } catch (e) { + lastPatch = null; + } + let sessionExact = {}; + try { + sessionExact = state.sessionExact && typeof state.sessionExact === 'object' + ? JSON.parse(JSON.stringify(state.sessionExact)) + : {}; + } catch (e) { + sessionExact = {}; + } + return { + prompt: val('alt_prompt_textbox') || val('input_prompt') || '', + negative: val('input_negativeprompt') || val('alt_negativeprompt_textbox') || '', + width: parseInt(val('input_width') || '0', 10) || null, + height: parseInt(val('input_height') || '0', 10) || null, + steps: parseInt(val('input_steps') || '0', 10) || null, + cfg: parseFloat(val('input_cfgscale') || val('input_cfg') || '') || null, + sigma_shift: parseFloat(val('input_sigmashift') || '') || null, + seed: val('input_seed') || null, + sampler: val('input_sampler') || null, + scheduler: val('input_scheduler') || null, + batch: parseInt(val('input_images') || val('input_batchsize') || '0', 10) || null, + loras, + persona: $('sa_persona')?.value || 'neutral', + pack: $('sa_pack')?.value || 'write_prompt', + sessionExact, + lastPatch, + }; + } + + async function restoreChatParams(params) { + state.restoringChat = true; + try { + // Always reset chat-scoped state so previous chat cannot leak. + state.sessionExact = {}; + state.lastPatch = null; + state.lastUserParamIntent = false; + + if (!params || typeof params !== 'object') { + syncBuildGenButton(); + syncLiveParamsBar(); + syncModeBadge(); + return { restored: false }; + } + + const promptBox = document.getElementById('alt_prompt_textbox') || document.getElementById('input_prompt'); + if (promptBox) { + promptBox.value = params.prompt != null ? String(params.prompt) : ''; + promptBox.dispatchEvent(new Event('input', { bubbles: true })); + promptBox.dispatchEvent(new Event('change', { bubbles: true })); + } + setVal('input_negativeprompt', params.negative != null ? String(params.negative) : ''); + // Force-write numerics when present so prior chat values cannot stick. + if (params.width != null) { + setVal('input_width', String(params.width)); + } + if (params.height != null) { + setVal('input_height', String(params.height)); + } + if (params.steps != null) { + setVal('input_steps', String(params.steps)); + } + if (params.cfg != null) { + if (document.getElementById('input_cfgscale')) { + setVal('input_cfgscale', String(params.cfg)); + } else { + setVal('input_cfg', String(params.cfg)); + } + } + if (params.sigma_shift != null) { + setVal('input_sigmashift', String(params.sigma_shift)); + } + if (params.seed != null && params.seed !== '') { + setVal('input_seed', String(params.seed)); + } + if (params.sampler) { + setVal('input_sampler', String(params.sampler)); + } + if (params.scheduler) { + setVal('input_scheduler', String(params.scheduler)); + } + if (params.batch != null) { + if (document.getElementById('input_images')) { + setVal('input_images', String(params.batch)); + } else if (document.getElementById('input_batchsize')) { + setVal('input_batchsize', String(params.batch)); + } + } + + const loras = Array.isArray(params.loras) ? params.loras : []; + await applyPatch({ loras }, 'loras'); + + if (params.pack) { + setPackValue(params.pack, { flash: false }); + } + if (params.persona) { + await applyPersonaForChat(params.persona, { quiet: true }); + } + state.sessionExact = params.sessionExact && typeof params.sessionExact === 'object' + ? { ...params.sessionExact } + : {}; + state.lastPatch = params.lastPatch || null; + syncBuildGenButton(); + syncLiveParamsBar(); + syncModeBadge(); + renderLoraChips(); + syncChipHighlight(); + return { restored: true }; + } finally { + state.restoringChat = false; + } + } + + function applyPersonaForChat(personaId, { quiet = false } = {}) { + const id = String(personaId || 'neutral').trim() || 'neutral'; + return new Promise((resolve) => { + const sel = $('sa_persona'); + if (sel && [...sel.options].some((o) => o.value === id)) { + sel.value = id; + } + if (!quiet) { + onPersonaChanged(); + resolve(); + return; + } + // Quiet: reload persona config/exact without chat spam or wiping sessionExact. + saveSettings(); + if (typeof genericRequest !== 'function') { + resolve(); + return; + } + const packKeep = $('sa_pack')?.value; + genericRequest( + 'AssistentGetConfig', + { persona: id }, + (data) => { + applyConfigPayload(data, { applyDefaults: false }); + if (sel && [...sel.options].some((o) => o.value === id)) { + sel.value = id; + } + if (packKeep) { + setPackValue(packKeep, { flash: false }); + } + resolve(); + }, + 0, + () => resolve(), + ); + }); + } + + function persistChatsStore() { + try { + const chats = (state.chats || []) + .filter((c) => c && c.id && (c.id === state.activeChatId || (c.messages || []).length > 0)) + .slice() + .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)) + .slice(0, MAX_CHATS) + .map((c) => ({ + id: c.id, + title: c.title || 'Новый чат', + createdAt: c.createdAt || Date.now(), + updatedAt: c.updatedAt || Date.now(), + messages: slimHistoryMessages(c.messages || []), + params: c.params || null, + })); + state.chats = chats; + localStorage.setItem(LS_CHATS, JSON.stringify({ version: 1, chats })); + } catch (e) { + console.warn('Assistent: persist chats failed', e); + try { + // Quota fallback: keep fewer / shorter chats. + const slim = (state.chats || []) + .filter((c) => c && c.id && (c.id === state.activeChatId || (c.messages || []).length > 0)) + .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)) + .slice(0, 12) + .map((c) => ({ + ...c, + messages: slimHistoryMessages(c.messages).slice(-12).map((m) => ({ + ...m, + content: String(m.content || '').slice(0, 1500), + })), + })); + state.chats = slim; + localStorage.setItem(LS_CHATS, JSON.stringify({ version: 1, chats: slim })); + } catch (e2) { + console.warn('Assistent: chats quota fallback failed', e2); + } + } + } + + function loadChatsStore() { + state.chats = []; + try { + const raw = localStorage.getItem(LS_CHATS); + if (raw) { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed?.chats)) { + state.chats = parsed.chats.filter((c) => c && c.id); + } + } + } catch (e) { /* ignore */ } + migrateLegacyHistoryIntoChats(); + } + + function migrateLegacyHistoryIntoChats() { try { const raw = localStorage.getItem(LS_HISTORY); if (!raw) { @@ -1261,39 +1585,364 @@ } const parsed = JSON.parse(raw); if (!Array.isArray(parsed) || !parsed.length) { + localStorage.removeItem(LS_HISTORY); return; } - state.history = parsed - .filter((m) => m && (m.role === 'user' || m.role === 'assistant') && m.content) - .slice(-24) - .map((m) => ({ - role: m.role, - content: String(m.content), - persona: m.persona, + const messages = slimHistoryMessages(parsed); + if (!messages.length) { + localStorage.removeItem(LS_HISTORY); + return; + } + const already = state.chats.some((c) => + (c.messages || []).length === messages.length + && (c.messages[0]?.content || '') === (messages[0]?.content || '')); + if (!already) { + state.chats.unshift({ + id: chatUid(), + title: titleFromMessages(messages), + createdAt: Date.now() - 1000, + updatedAt: Date.now() - 1000, + messages, + params: null, + }); + persistChatsStore(); + } + localStorage.removeItem(LS_HISTORY); + } catch (e) { + try { localStorage.removeItem(LS_HISTORY); } catch (e2) { /* ignore */ } + } + } + + function findChat(id) { + return (state.chats || []).find((c) => c.id === id) || null; + } + + function saveActiveChatToStore({ dropEmpty = false } = {}) { + if (!state.activeChatId || state.restoringChat) { + return; + } + const chat = findChat(state.activeChatId); + if (!chat) { + return; + } + chat.messages = slimHistoryMessages(state.history); + chat.params = snapshotChatParams(); + chat.updatedAt = Date.now(); + chat.title = titleFromMessages(chat.messages); + if (dropEmpty && !chat.messages.length) { + state.chats = state.chats.filter((c) => c.id !== chat.id); + if (state.activeChatId === chat.id) { + state.activeChatId = null; + } + } + persistChatsStore(); + } + + function resetMessagesUi(emptyHint) { + const box = $('sa_messages'); + if (!box) { + return; + } + box.innerHTML = ''; + const empty = document.createElement('div'); + empty.className = 'sa-chat-empty'; + empty.id = 'sa_chat_empty'; + empty.innerHTML = emptyHint + || '
Новый чат
Параметры Generate остаются как сейчас.
+ — ещё один чат · История — вернуться к прошлому (с его параметрами).
'; + box.appendChild(empty); + } + + function renderHistoryIntoUi(messages) { + const box = $('sa_messages'); + if (!box) { + return; + } + box.innerHTML = ''; + const list = slimHistoryMessages(messages); + if (!list.length) { + resetMessagesUi(); + return; + } + for (const m of list) { + if (m.role === 'user') { + appendMessage('user', m.content, null, null, { historical: true }); + } else { + appendMessage('assistant', m.content, null, null, { + persona: m.persona ? { id: m.persona, title: m.persona } : null, pack: m.pack, - })); - const box = $('sa_messages'); - if (!box || !state.history.length) { - return; + historical: true, + }); } - hideChatEmpty(); - for (const m of state.history) { - if (m.role === 'user') { - appendMessage('user', m.content); - } else { - appendMessage('assistant', m.content, null, null, { - persona: m.persona ? { id: m.persona, title: m.persona } : null, - pack: m.pack, - }); - } + } + } + + function updateSessionLabel() { + const el = $('sa_session_label'); + if (!el) { + return; + } + const chat = findChat(state.activeChatId); + el.textContent = chat?.title || 'Новый чат'; + el.title = (chat?.title || 'Новый чат') + ' — клик: История'; + } + + function savedChatsCount() { + return (state.chats || []).filter((c) => (c.messages || []).length > 0).length; + } + + function syncHistoryBadge() { + const btn = $('sa_btn_chats'); + if (!btn) { + return; + } + const n = savedChatsCount(); + btn.textContent = n > 0 ? `История (${n})` : 'История'; + btn.title = n > 0 + ? `Сохранённых чатов: ${n}. Переключение восстанавливает параметры.` + : 'История чатов (пока пусто)'; + } + + function formatChatWhen(ts) { + if (!ts) { + return ''; + } + try { + return new Date(ts).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); + } catch (e) { + return ''; + } + } + + function renderChatsList() { + const root = $('sa_chats_list'); + if (!root) { + return; + } + root.innerHTML = ''; + syncHistoryBadge(); + const chats = (state.chats || []) + .slice() + .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)) + .filter((c) => (c.messages || []).length > 0); + if (!chats.length) { + root.innerHTML = '
Пока пусто. Напиши что-нибудь в чат — он появится здесь. Переключение вернёт и сообщения, и параметры Generate.
'; + return; + } + for (const c of chats) { + const row = document.createElement('div'); + row.className = 'sa-chat-row' + (c.id === state.activeChatId ? ' sa-chat-row-active' : ''); + row.dataset.id = c.id; + const n = (c.messages || []).length; + const bits = []; + if (c.params?.width && c.params?.height) { + bits.push(`${c.params.width}×${c.params.height}`); } - } catch (e) { /* ignore */ } + if (c.params?.steps != null) { + bits.push(`steps ${c.params.steps}`); + } + if (c.params?.cfg != null) { + bits.push(`cfg ${c.params.cfg}`); + } + if (Array.isArray(c.params?.loras) && c.params.loras.length) { + bits.push(`LoRA ${c.params.loras.length}`); + } + const noParams = !c.params ? ' · без снимка params' : ''; + row.innerHTML = ``; + root.appendChild(row); + } + } + + function setChatsPanelOpen(open) { + state.chatsPanelOpen = !!open; + const panel = $('sa_chats_panel'); + const btn = $('sa_btn_chats'); + if (panel) { + panel.hidden = !state.chatsPanelOpen; + } + btn?.setAttribute('aria-expanded', state.chatsPanelOpen ? 'true' : 'false'); + if (state.chatsPanelOpen) { + saveActiveChatToStore(); + renderChatsList(); + } + } + + async function startNewChat({ saveCurrent = true, force = false } = {}) { + if (!force && (state.busy || state.generating)) { + setStatus('Занято — дождись конца ответа или Стоп'); + return; + } + if (force) { + // Drop in-flight reply so it cannot land in the new chat. + abortInFlightWork({ status: '' }); + } + setChatsPanelOpen(false); + if (saveCurrent) { + saveActiveChatToStore({ dropEmpty: true }); + } + state.sessionExact = {}; + state.lastUserParamIntent = false; + state.pendingSilentGen = false; + state.lastPatch = null; + const chat = { + id: chatUid(), + title: 'Новый чат', + createdAt: Date.now(), + updatedAt: Date.now(), + messages: [], + params: snapshotChatParams(), + }; + state.chats.unshift(chat); + state.activeChatId = chat.id; + state.history = []; + state.critiqueHopUsed = false; + state.visionHopUsed = false; + state.packUserTouched = false; + state.pendingPersonaNote = null; + if (state.streamEl) { + try { state.streamEl.remove(); } catch (e) { /* ignore */ } + state.streamEl = null; + } + syncBuildGenButton(); + resetMessagesUi(); + persistChatsStore(); + updateSessionLabel(); + syncHistoryBadge(); + renderChatsList(); + setStatus('Новый чат — параметры Generate как сейчас'); + maybeWelcome(); + } + + async function switchToChat(id) { + if (!id || id === state.activeChatId) { + setChatsPanelOpen(false); + return; + } + if (state.busy || state.generating) { + setStatus('Занято — нельзя сменить чат сейчас'); + return; + } + saveActiveChatToStore({ dropEmpty: true }); + const chat = findChat(id); + if (!chat) { + setStatus('Чат не найден'); + return; + } + state.activeChatId = chat.id; + state.history = slimHistoryMessages(chat.messages); + state.critiqueHopUsed = false; + state.visionHopUsed = false; + state.packUserTouched = false; + state.pendingPersonaNote = null; + state.pendingSilentGen = false; + state.lastUserParamIntent = false; + if (state.streamEl) { + try { state.streamEl.remove(); } catch (e) { /* ignore */ } + state.streamEl = null; + } + renderHistoryIntoUi(state.history); + const result = await restoreChatParams(chat.params); + updateSessionLabel(); + syncHistoryBadge(); + renderChatsList(); + setChatsPanelOpen(false); + setView('chat'); + if (result?.restored) { + setStatus(`Чат «${chat.title}» · параметры восстановлены`); + } else { + setStatus(`Чат «${chat.title}» · снимок параметров отсутствует — Generate не менялся`); + } + } + + function deleteChat(id) { + if (!id) { + return; + } + const wasActive = id === state.activeChatId; + state.chats = state.chats.filter((c) => c.id !== id); + if (wasActive) { + state.activeChatId = null; + } + persistChatsStore(); + syncHistoryBadge(); + if (wasActive) { + startNewChat({ saveCurrent: false, force: true }); + } else { + renderChatsList(); + } + } + + function initChatSessions() { + loadChatsStore(); + // Always open a fresh chat on startup; past chats stay in History. + startNewChat({ saveCurrent: false, force: true }); + syncHistoryBadge(); + } + + function persistHistory() { + if (state.restoringChat) { + return; + } + if (!state.activeChatId) { + const chat = { + id: chatUid(), + title: 'Новый чат', + createdAt: Date.now(), + updatedAt: Date.now(), + messages: [], + params: snapshotChatParams(), + }; + state.chats.unshift(chat); + state.activeChatId = chat.id; + } + const chat = findChat(state.activeChatId); + if (!chat) { + return; + } + chat.messages = slimHistoryMessages(state.history); + chat.params = snapshotChatParams(); + chat.updatedAt = Date.now(); + chat.title = titleFromMessages(chat.messages); + persistChatsStore(); + updateSessionLabel(); + syncHistoryBadge(); + } + + function restoreHistory() { + // Replaced by initChatSessions — kept as no-op for safety. } function clearPersistedHistory() { - try { - localStorage.removeItem(LS_HISTORY); - } catch (e) { /* ignore */ } + if (state.activeChatId) { + const chat = findChat(state.activeChatId); + if (chat) { + chat.messages = []; + chat.title = 'Новый чат'; + chat.params = snapshotChatParams(); + chat.updatedAt = Date.now(); + } + persistChatsStore(); + } + updateSessionLabel(); + renderChatsList(); + } + + function clearChatHistory() { + abortInFlightWork({ status: '' }); + state.history = []; + state.critiqueHopUsed = false; + state.visionHopUsed = false; + state.packUserTouched = false; + state.pendingPersonaNote = null; + state.sessionExact = {}; + state.lastUserParamIntent = false; + state.pendingSilentGen = false; + state.lastPatch = null; + syncBuildGenButton(); + clearPersistedHistory(); + resetMessagesUi('
Чат очищен
Сообщения сброшены. Параметры Generate на месте. + — новый чат в Историю, История — прошлые диалоги.
'); + setStatus('Чат очищен'); + updateSessionLabel(); + syncHistoryBadge(); } function hideSlashMenu() { @@ -2111,6 +2760,37 @@ return true; } + function clearPatchBlocksOnly() { + document.querySelectorAll('#sa_messages .sa-patch, #sa_messages .sa-patch-stale, #sa_messages .sa-civitai-list').forEach((el) => el.remove()); + state.lastPatch = null; + syncBuildGenButton(); + setStatus('Патчи убраны из чата'); + } + + function toggleMoreMenu(menuId, btnId) { + const menu = $(menuId); + const btn = $(btnId); + if (!menu) { + return; + } + const open = menu.hidden; + document.querySelectorAll('.sa-more-menu').forEach((m) => { + m.hidden = true; + }); + document.querySelectorAll('#sa_btn_board_more, #sa_btn_clear_more').forEach((b) => b.setAttribute('aria-expanded', 'false')); + if (open) { + menu.hidden = false; + btn?.setAttribute('aria-expanded', 'true'); + } + } + + function closeAllMoreMenus() { + document.querySelectorAll('.sa-more-menu').forEach((m) => { + m.hidden = true; + }); + document.querySelectorAll('#sa_btn_board_more, #sa_btn_clear_more').forEach((b) => b.setAttribute('aria-expanded', 'false')); + } + function setPackValue(packName, { flash, user } = {}) { const pack = $('sa_pack'); if (!pack || !packName) { @@ -2131,6 +2811,7 @@ pack.classList.add('sa-pack-flash'); setTimeout(() => pack.classList.remove('sa-pack-flash'), 900); } + syncModeBadge(); return true; } @@ -2452,7 +3133,19 @@ } } syncChipHighlight(); - setStatus('Applied patch'); + syncLiveParamsBar(); + syncBuildGenButton(); + if (state.activeChatId && !state.restoringChat) { + const chat = findChat(state.activeChatId); + if (chat) { + chat.params = snapshotChatParams(); + chat.updatedAt = Date.now(); + persistChatsStore(); + } + } + if (!state.restoringChat) { + setStatus('Applied patch'); + } } function triggerGenerate() { @@ -2488,6 +3181,41 @@ return state.chatEpoch; } + function clearInFlightUi({ status } = {}) { + state.busy = false; + state.generating = false; + state.pendingSilentGen = false; + if (state.streamEl) { + try { state.streamEl.remove(); } catch (e) { /* ignore */ } + state.streamEl = null; + state.streamMeta = null; + } + setInterruptVisible(false); + syncGenerateBusy(); + syncPatchActionAvailability(); + if (status != null) { + stopBusyUi(status); + } else { + stopBusyUi(''); + } + } + + /** Invalidate in-flight Assistent WS/wait; optionally also interrupt Swarm Generate. */ + function abortInFlightWork({ status, interruptSwarm = false } = {}) { + bumpChatEpoch(); + cancelWaitForNewImage(); + if (interruptSwarm) { + try { + if (typeof doInterrupt === 'function') { + doInterrupt(false); + } else if (typeof genericRequest === 'function') { + genericRequest('InterruptAll', { other_sessions: false }, () => {}, 0, () => {}); + } + } catch (e) { /* ignore */ } + } + clearInFlightUi({ status: status != null ? status : '' }); + } + function doInterruptNow() { bumpChatEpoch(); cancelWaitForNewImage(); @@ -2659,16 +3387,10 @@ } else { div.textContent = prose || text || ''; } - if (finalPatch) { - rememberLastPatch(finalPatch); - const wrap = document.createElement('div'); - wrap.className = 'sa-patch'; - const pre = document.createElement('pre'); - pre.textContent = JSON.stringify(finalPatch, null, 2); - wrap.appendChild(pre); + // Historical replay: show prose only — no Apply strip (lastPatch comes from chat.params). + if (finalPatch && !(meta && meta.historical)) { const silent = !!(meta && meta.silentPatch); - mountPatchActions(wrap, finalPatch, { silent }); - div.appendChild(wrap); + mountPatchBlock(div, finalPatch, { silent }); } if (civitaiResults && civitaiResults.length) { div.appendChild(buildCivitaiCards(civitaiResults)); @@ -2740,15 +3462,8 @@ setAssistantBody(el, prose || fullReply || ''); el.querySelectorAll('.sa-patch, .sa-civitai-list').forEach((n) => n.remove()); if (patch && !isCardObject(patch) && !(card && !patch.prompt && !patch.actions && !patch.loras)) { - rememberLastPatch(patch); - const wrap = document.createElement('div'); - wrap.className = 'sa-patch'; - const pre = document.createElement('pre'); - pre.textContent = JSON.stringify(patch, null, 2); - wrap.appendChild(pre); const silent = !!(meta && meta.silentPatch) || !!state.pendingSilentGen; - mountPatchActions(wrap, patch, { silent }); - el.appendChild(wrap); + mountPatchBlock(el, patch, { silent }); } else if (card) { const wrap = document.createElement('div'); wrap.className = 'sa-patch sa-card-json-preview'; @@ -3654,12 +4369,35 @@ if (row.trigger) { metaBits.push(String(row.trigger).slice(0, 40)); } - btn.innerHTML = `${thumb}
${escapeHtml(row.kind)}
${escapeHtml(row.title || row.name)}
${escapeHtml(metaBits.join(' · '))}
`; - btn.addEventListener('click', () => selectCardModel(row)); + btn.innerHTML = `${thumb}
${escapeHtml(row.kind)}
${escapeHtml(row.title || row.name)}
${escapeHtml(metaBits.join(' · '))}
`; + btn.addEventListener('click', (e) => { + if (e.target?.closest?.('[data-chat]')) { + e.preventDefault(); + e.stopPropagation(); + sendCardToChat(row); + return; + } + selectCardModel(row); + }); root.appendChild(btn); } } + function sendCardToChat(row) { + if (!row?.name) { + return; + } + selectCardModel(row); + setView('chat'); + const kind = row.kind === 'checkpoint' ? 'checkpoint' : 'LoRA'; + const triggers = row.trigger ? ` Triggers: ${row.trigger}.` : ''; + if ($('sa_input')) { + $('sa_input').value = `Используй ${kind} «${row.name}».${triggers} Учти карточку/triggers и предложи патч.`; + $('sa_input').focus(); + } + setStatus(`В чат → ${row.name}`); + } + function wireCardForm() { const sync = () => { if ($('sa_card_show_json')?.checked) { @@ -4298,7 +5036,13 @@ || (Array.isArray(patch?.actions) && patch.actions.map(String).includes('generate'))); const doApply = !!(patch && (wantsGen || $('sa_auto_apply')?.checked)); if (doApply) { + if (wantsGen) { + startBusyUi('silent_gen'); + } else { + setBusyPhase('applying'); + } await applyPatch(patch, 'all'); + syncLiveParamsBar(); updateTasteFromPatch(patch, opts.userText || ''); if (!fromAutoCritique && (wantsGen || $('sa_auto_generate')?.checked)) { const src = await runGenerateFromPatch( @@ -4308,6 +5052,8 @@ if (src) { await maybeAutoCritique(src); } + } else if (!state.generating) { + stopBusyUi(wantsGen ? 'Применено' : ''); } } state.pendingSilentGen = false; @@ -4463,6 +5209,15 @@ setStatus('/help'); return true; } + if (cmd === 'new' || cmd === 'newchat') { + await startNewChat({ saveCurrent: true }); + return true; + } + if (cmd === 'history' || cmd === 'chats' || cmd === 'sessions') { + setChatsPanelOpen(true); + setStatus('/history'); + return true; + } if (cmd === 'debug' || cmd === 'dbg' || cmd === 'why') { const dump = buildDebugSummary(); appendSystemNote(dump); @@ -4568,12 +5323,7 @@ } if (cmd === 'interrupt' || cmd === 'stop') { doInterruptNow(); - state.busy = false; - state.generating = false; - setInterruptVisible(false); - syncGenerateBusy(); - syncPatchActionAvailability(); - stopBusyUi('Interrupted'); + clearInFlightUi({ status: 'Прервано' }); return true; } if (cmd === 'aspect') { @@ -4678,7 +5428,7 @@ } async function sendChat(opts = {}) { - if (state.busy && !opts.fromVisionHop && !opts.fromAutoCritique) { + if ((state.busy || state.generating) && !opts.fromVisionHop && !opts.fromAutoCritique) { return; } const rawInput = ($('sa_input')?.value || '').trim(); @@ -4850,18 +5600,27 @@ if (chatEpoch !== state.chatEpoch) { return; } - state.busy = false; - setInterruptVisible(state.generating); const prose = extractPatch(reply).prose || reply; state.history.push({ role: 'assistant', content: prose, persona, pack }); persistHistory(); - stopBusyUi('Готово'); - await handleReplySideEffects(reply, civitaiResults, { - ...opts, - userText: text, - userWantsGenerate: !!state.pendingSilentGen || userAsksGenerate(text), - attachedSlotIds: visionSlots.map((s) => s.id), - }); + setBusyPhase(state.pendingSilentGen || userAsksGenerate(text) ? 'silent_gen' : 'thinking'); + try { + await handleReplySideEffects(reply, civitaiResults, { + ...opts, + userText: text, + userWantsGenerate: !!state.pendingSilentGen || userAsksGenerate(text), + attachedSlotIds: visionSlots.map((s) => s.id), + }); + } finally { + if (chatEpoch !== state.chatEpoch) { + return; + } + state.busy = false; + setInterruptVisible(state.generating); + if (!state.generating) { + stopBusyUi('Готово'); + } + } }; const finishErr = (msg) => { @@ -5134,8 +5893,7 @@ if (wantsAutoVision()) { refreshImagePreview(); } - restoreHistory(); - maybeWelcome(); + initChatSessions(); loadConfig(localStorage.getItem(LS_PERSONA) || 'neutral', () => { refreshModels(); refreshInventory(() => { @@ -5149,6 +5907,40 @@ wireSlashInput(); wireCardForm(); + $('sa_btn_new_chat')?.addEventListener('click', () => startNewChat({ saveCurrent: true })); + $('sa_btn_chats')?.addEventListener('click', (e) => { + e.stopPropagation(); + setChatsPanelOpen(!state.chatsPanelOpen); + }); + $('sa_session_label')?.addEventListener('click', (e) => { + e.stopPropagation(); + setChatsPanelOpen(!state.chatsPanelOpen); + }); + $('sa_session_label')?.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setChatsPanelOpen(!state.chatsPanelOpen); + } + }); + $('sa_chats_panel')?.addEventListener('click', (e) => e.stopPropagation()); + $('sa_chats_list')?.addEventListener('click', (e) => { + const row = e.target.closest('.sa-chat-row'); + if (!row) { + return; + } + const id = row.dataset.id; + if (e.target.closest('[data-del]')) { + e.preventDefault(); + if (window.confirm('Удалить этот чат из истории?')) { + deleteChat(id); + } + return; + } + if (e.target.closest('[data-open]')) { + switchToChat(id); + } + }); + $('sa_tab_chat')?.addEventListener('click', () => setView('chat')); $('sa_tab_cards')?.addEventListener('click', () => setView('cards')); $('sa_board_tab_gen')?.addEventListener('click', () => setBoardTab('generate')); @@ -5181,6 +5973,7 @@ }); $('sa_btn_use_current')?.addEventListener('click', () => snapshotGenerateToRef()); $('sa_btn_as_init')?.addEventListener('click', async () => { + closeAllMoreMenus(); const src = selectedSrc() || findCurrentGenerateSrc(); if (!src) { setStatus('Нет изображения для Init'); @@ -5189,61 +5982,68 @@ await setInitFromSrc(src); const pack = $('sa_pack'); if (pack && pack.value === 'write_prompt') { - pack.value = 'inpaint_edit'; - saveSettings(); + setPackValue('inpaint_edit', { flash: true }); } }); $('sa_btn_as_mask')?.addEventListener('click', async () => { + closeAllMoreMenus(); const src = selectedSrc(); if (!src) { setStatus('Выбери окно с маской'); return; } await setMaskFromSrc(src); - const pack = $('sa_pack'); - if (pack) { - pack.value = 'inpaint_edit'; - saveSettings(); - } + setPackValue('inpaint_edit', { flash: true }); + }); + $('sa_btn_clear_init')?.addEventListener('click', () => { + clearInitAndMask(); + closeAllMoreMenus(); }); - $('sa_btn_clear_init')?.addEventListener('click', clearInitAndMask); $('sa_btn_clear_image')?.addEventListener('click', () => clearSlot(state.selectedSlotId)); + $('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', () => { doInterruptNow(); - state.busy = false; - state.generating = false; - setInterruptVisible(false); - syncGenerateBusy(); - syncPatchActionAvailability(); - if (state.streamEl?.classList.contains('sa-typing')) { - state.streamEl.remove(); - state.streamEl = null; - } - stopBusyUi('Прервано'); + clearInFlightUi({ status: 'Прервано' }); }); $('sa_btn_clear')?.addEventListener('click', () => { - state.history = []; - state.critiqueHopUsed = false; - state.visionHopUsed = false; - state.packUserTouched = false; - state.pendingPersonaNote = null; - state.sessionExact = {}; - state.lastUserParamIntent = false; - clearPersistedHistory(); - const box = $('sa_messages'); - if (box) { - box.innerHTML = ''; - const empty = document.createElement('div'); - empty.className = 'sa-chat-empty'; - empty.id = 'sa_chat_empty'; - empty.innerHTML = '
Совместная работа с Krea 2
Напиши промпт, кинь refs, чипсы aspect или /help.
'; - box.appendChild(empty); + if (window.confirm('Очистить весь чат Assistent?')) { + clearChatHistory(); } - stopBusyUi(''); - setStatus(''); }); + $('sa_btn_clear_more')?.addEventListener('click', (e) => { + e.stopPropagation(); + toggleMoreMenu('sa_clear_more_menu', 'sa_btn_clear_more'); + }); + $('sa_btn_clear_confirm')?.addEventListener('click', () => { + closeAllMoreMenus(); + if (window.confirm('Очистить весь чат Assistent?')) { + clearChatHistory(); + } + }); + $('sa_btn_clear_patches')?.addEventListener('click', () => { + closeAllMoreMenus(); + clearPatchBlocksOnly(); + }); + $('sa_btn_card_to_chat')?.addEventListener('click', () => { + if (state.cardsSelection) { + sendCardToChat(state.cardsSelection); + } else { + setCardStatus('Сначала выбери модель в списке'); + } + }); + document.addEventListener('click', () => { + if (state.chatsPanelOpen) { + setChatsPanelOpen(false); + } + 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', saveSettings); $('sa_embed_model')?.addEventListener('change', () => { @@ -5253,6 +6053,7 @@ $('sa_pack')?.addEventListener('change', () => { state.packUserTouched = true; saveSettings(); + syncModeBadge(); }); $('sa_chips')?.addEventListener('click', async (e) => { const btn = e.target.closest('.sa-chip'); @@ -5296,9 +6097,18 @@ syncChipHighlight(); setInterval(syncChipHighlight, 2500); setInterval(renderLoraChips, 4000); + syncLiveParamsBar(); + setInterval(syncLiveParamsBar, 1200); + syncModeBadge(); + syncBuildGenButton(); setInterval(updateGate, 2000); setInterval(syncGenerateSlot, 700); + window.addEventListener('beforeunload', () => { + try { + saveActiveChatToStore({ dropEmpty: true }); + } catch (e) { /* ignore */ } + }); setInterval(() => { if (!state.busy) { const tabOn = !!document.getElementById(TAB_BUTTON_ID)?.classList.contains('tab-button-selected') diff --git a/Config/_base/ui.json b/Config/_base/ui.json index 6a82be2..48b5ce8 100644 --- a/Config/_base/ui.json +++ b/Config/_base/ui.json @@ -1,6 +1,6 @@ { "welcome_html": "
Assistent · Krea 2
  • Generate слева — живой просмотр. В чат сам не уходит.
  • Refs — референсы на отдельной вкладке: drop / paste / Снимок gen.
  • Галочка vision на окне — отправить кадр модели.
  • Чипсы aspect / seed / Vary / Turbo·RAW. В чате: /help.
  • Кнопки патча только у последнего предложения.
Напиши, что сгенерировать — или кинь референс и попроси правку.", - "help_text": "Slash-команды (без LLM):\n/help — этот список\n/debug — сводка UI/Exact\n/debug ask · /why — сводка + короткий ответ модели\n/gen — Generate сейчас\n/look generate|refN — прикрепить окно к vision\n/init /mask /clear — Init / Mask / Clear Init\n/interrupt — остановить генерацию\n/aspect 16:9 — размер из таблицы 1K\n/seed lock|random — зафиксировать или рандомизировать seed\n/vary — новый seed, тот же промпт\n/pack write|critique|compose|params|inpaint|describe|card\n/civitai — поиск LoRA (Confirm в чате)\n/inventory — rescan моделей + обновить список LoRA\n\nЧипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.", + "help_text": "Slash-команды (без LLM):\n/help — этот список\n/new — новый чат\n/history — список чатов\n/debug — сводка UI/Exact\n/debug ask · /why — сводка + короткий ответ модели\n/gen — Generate сейчас\n/look generate|refN — прикрепить окно к vision\n/init /mask /clear — Init / Mask / Clear Init\n/interrupt — остановить генерацию\n/aspect 16:9 — размер из таблицы 1K\n/seed lock|random — зафиксировать или рандомизировать seed\n/vary — новый seed, тот же промпт\n/pack write|critique|compose|params|inpaint|describe|card\n/civitai — поиск LoRA (Confirm в чате)\n/inventory — rescan моделей + обновить список LoRA\n\nЧипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.\nПри старте всегда новый чат; смена чата восстанавливает параметры.", "chips": [ { "label": "1:1", "action": "aspect", "value": "1:1", "title": "1024×1024" }, { "label": "4:5", "action": "aspect", "value": "4:5", "title": "928×1152" }, @@ -17,6 +17,8 @@ ], "slash": [ { "cmd": "/help", "hint": "список команд", "action": "help" }, + { "cmd": "/new", "hint": "новый чат", "action": "new" }, + { "cmd": "/history", "hint": "история чатов", "action": "history" }, { "cmd": "/debug", "hint": "сводка · ask = с LLM", "action": "debug" }, { "cmd": "/why", "hint": "debug + пояснение LLM", "action": "why" }, { "cmd": "/gen", "hint": "Generate сейчас", "action": "gen" }, diff --git a/README.md b/README.md index 5fe8f21..cbdecc4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + board (Generate | Refs tabs), LoRA chips, **persona presets** (`Config/personas/`), **vector memory**, model cards with Civitai fetch, img2img/inpaint, slash commands, auto Generate. -**Version 0.7.3** — Exact KV; silent apply+Generate when user asks; composer **Собрать + Gen**; block model-card previews; critique needs vision + JSON. +**Version 0.7.6** — Bugfix: clear/new abort in-flight chat; Send blocked while generating; Interrupt always drops stream bubble; patch-clear resets lastPatch. ## Layout diff --git a/SwarmAssistentExtension.cs b/SwarmAssistentExtension.cs index f066614..11c0640 100644 --- a/SwarmAssistentExtension.cs +++ b/SwarmAssistentExtension.cs @@ -49,7 +49,7 @@ public class SwarmAssistentExtension : Extension ExtensionAuthor = "mrleo1nid"; Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop."; License = "MIT"; - Version = "0.7.3"; + Version = "0.7.6"; Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"]; } diff --git a/Tabs/Text2Image/Assistent.html b/Tabs/Text2Image/Assistent.html index 6248d47..54e6841 100644 --- a/Tabs/Text2Image/Assistent.html +++ b/Tabs/Text2Image/Assistent.html @@ -16,12 +16,18 @@
+
- - - +
+ + +
@@ -30,11 +36,21 @@
Assistent +
+ + + Новый чат +
+
+ write @@ -100,9 +117,16 @@
- + - +
+ + + +
@@ -139,8 +163,9 @@
+ - +