Ship Assistent 0.10.18: settings as a peer tab, stream/VRAM chat fixes.
Move settings beside Chat/Cards; stop fence ramble, sticky scroll, horny echo filter, and force-warm after Generate. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+24
-5
@@ -731,11 +731,17 @@
|
||||
.sa-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
padding: 0.7rem 0.75rem;
|
||||
border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent);
|
||||
background: color-mix(in srgb, currentColor 5%, transparent);
|
||||
max-height: 50vh;
|
||||
gap: 0.55rem;
|
||||
padding: 0.75rem 0.85rem 1rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-bottom: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#sa_view_settings {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sa-settings-head {
|
||||
@@ -743,12 +749,14 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sa-settings-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sa-stab {
|
||||
@@ -1164,6 +1172,12 @@
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.sa-msg-body.sa-prose-live {
|
||||
display: block;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.sa-prose-h {
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.01em;
|
||||
@@ -1485,6 +1499,11 @@
|
||||
background: color-mix(in srgb, currentColor 12%, transparent);
|
||||
}
|
||||
|
||||
#sa_btn_settings.sa-subtab-active {
|
||||
opacity: 1;
|
||||
background: color-mix(in srgb, currentColor 12%, transparent);
|
||||
}
|
||||
|
||||
.sa-view {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
+220
-71
@@ -128,6 +128,7 @@
|
||||
exact: null,
|
||||
sessionExact: {},
|
||||
lastUserParamIntent: false,
|
||||
lastUserControlIntent: false,
|
||||
lastPatch: null,
|
||||
pendingSilentGen: false,
|
||||
enabledSkills: [],
|
||||
@@ -147,6 +148,7 @@
|
||||
streamEl: null,
|
||||
streamMeta: null,
|
||||
streamText: '',
|
||||
streamFenceDone: false,
|
||||
critiqueHopUsed: false,
|
||||
visionHopUsed: false,
|
||||
lastSystemChars: 0,
|
||||
@@ -215,6 +217,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
let scrollMessagesRaf = 0;
|
||||
|
||||
/** Autoscroll only when already near the bottom — otherwise streaming fights the user's scroll (shakes). */
|
||||
function messagesNearBottom(thresholdPx = 96) {
|
||||
const box = $('sa_messages');
|
||||
if (!box) {
|
||||
return true;
|
||||
}
|
||||
return (box.scrollHeight - box.scrollTop - box.clientHeight) <= thresholdPx;
|
||||
}
|
||||
|
||||
function scrollMessagesToBottom({ force = false } = {}) {
|
||||
const box = $('sa_messages');
|
||||
if (!box) {
|
||||
return;
|
||||
}
|
||||
if (!force && !messagesNearBottom()) {
|
||||
return;
|
||||
}
|
||||
if (scrollMessagesRaf) {
|
||||
return;
|
||||
}
|
||||
scrollMessagesRaf = requestAnimationFrame(() => {
|
||||
scrollMessagesRaf = 0;
|
||||
const el = $('sa_messages');
|
||||
if (el && (force || messagesNearBottom(120))) {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showChatEmptyIfIdle() {
|
||||
const box = $('sa_messages');
|
||||
const empty = $('sa_chat_empty');
|
||||
@@ -574,7 +607,7 @@
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
function setAssistantBody(div, text) {
|
||||
function setAssistantBody(div, text, { live = false } = {}) {
|
||||
if (!div) {
|
||||
return;
|
||||
}
|
||||
@@ -585,7 +618,15 @@
|
||||
div.appendChild(body);
|
||||
}
|
||||
const raw = text || '';
|
||||
body.classList.add('sa-prose');
|
||||
// While streaming, plain text — full HTML reformat every token reflows and shakes scroll.
|
||||
if (live) {
|
||||
body.classList.add('sa-prose', 'sa-prose-live');
|
||||
body.classList.remove('sa-prose-rich');
|
||||
body.textContent = raw;
|
||||
return;
|
||||
}
|
||||
body.classList.add('sa-prose', 'sa-prose-rich');
|
||||
body.classList.remove('sa-prose-live');
|
||||
const html = formatAssistantProseHtml(raw);
|
||||
if (html) {
|
||||
body.innerHTML = html;
|
||||
@@ -674,6 +715,56 @@
|
||||
|| /смени\s+(размер|aspect|соотношен)/i.test(t);
|
||||
}
|
||||
|
||||
function userTextMentionsControls(text) {
|
||||
const t = String(text || '');
|
||||
if (/\b(horny|controls?|preference[_\s-]?bias)\b/i.test(t)) {
|
||||
return true;
|
||||
}
|
||||
return cyrTokenRe('хорни|остынь|вкус|слайдер').test(t)
|
||||
|| /\/\s*(остынь|ostyn|horny-game)/i.test(t);
|
||||
}
|
||||
|
||||
function patchLooksLikeGeneration(patch) {
|
||||
if (!patch || typeof patch !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (patch.prompt != null || patch.loras || patch.aspect != null
|
||||
|| patch.width != null || patch.height != null || patch.steps != null
|
||||
|| patch.cfg != null || patch.seed != null) {
|
||||
return true;
|
||||
}
|
||||
return Array.isArray(patch.actions) && patch.actions.map(String).includes('generate');
|
||||
}
|
||||
|
||||
/** Drop Generate-patch echo of schema defaults that would wipe a user-tuned slider. */
|
||||
function filterControlPatch(incoming, patch) {
|
||||
const schema = state.config?.controls || {};
|
||||
const out = {};
|
||||
if (!incoming || typeof incoming !== 'object') {
|
||||
return out;
|
||||
}
|
||||
const likeGen = patchLooksLikeGeneration(patch) && !state.lastUserControlIntent;
|
||||
for (const [id, raw] of Object.entries(incoming)) {
|
||||
if (!schema[id]) {
|
||||
continue;
|
||||
}
|
||||
const n = Number(raw);
|
||||
if (!Number.isFinite(n)) {
|
||||
continue;
|
||||
}
|
||||
const def = Number(schema[id]?.default);
|
||||
const cur = getControlValue(id, Number.isFinite(def) ? def : n);
|
||||
if (Math.abs(n - cur) < 0.0005) {
|
||||
continue;
|
||||
}
|
||||
if (likeGen && Number.isFinite(def) && Math.abs(n - def) < 0.0005 && Math.abs(cur - def) > 0.0005) {
|
||||
continue;
|
||||
}
|
||||
out[id] = n;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function userTextMentionsParams(text) {
|
||||
const t = String(text || '');
|
||||
if (parseAspectFromUserText(t)) {
|
||||
@@ -1597,7 +1688,7 @@
|
||||
div.className = 'sa-msg assistant sa-welcome';
|
||||
div.innerHTML = WELCOME_HTML;
|
||||
box.appendChild(div);
|
||||
box.scrollTop = box.scrollHeight;
|
||||
scrollMessagesToBottom({ force: true });
|
||||
}
|
||||
|
||||
function chatUid() {
|
||||
@@ -2411,8 +2502,7 @@
|
||||
fillEmptyParamsFromExact();
|
||||
renderPersonaControls(data?.controls || {}, data?.control_values || data?.exact?.controls || {});
|
||||
syncPersonaDeleteButton(data?.persona_source || data?.personas?.find((p) => p.id === id)?.source);
|
||||
const settings = $('sa_settings');
|
||||
if (settings && !settings.hidden) {
|
||||
if (state.view === 'settings') {
|
||||
if (state.settingsTab === 'user') {
|
||||
refreshUserPrefs();
|
||||
}
|
||||
@@ -3598,18 +3688,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Persona Exact controls (model or user patch). Ignore persona_delete.
|
||||
// Persona Exact controls (model or user patch). Ignore default-echo inside Generate patches.
|
||||
if (patch.controls && typeof patch.controls === 'object' && !Array.isArray(patch.controls)) {
|
||||
const schema = state.config?.controls || {};
|
||||
const next = { ...(state.config?.control_values || state.exact?.controls || {}) };
|
||||
for (const [k, v] of Object.entries(patch.controls)) {
|
||||
if (schema[k]) {
|
||||
next[k] = v;
|
||||
}
|
||||
}
|
||||
savePersonaControls(next);
|
||||
const filtered = filterControlPatch(patch.controls, patch);
|
||||
if (Object.keys(filtered).length) {
|
||||
const next = { ...(state.config?.control_values || state.exact?.controls || {}), ...filtered };
|
||||
savePersonaControls(filtered);
|
||||
renderPersonaControls(schema, next);
|
||||
}
|
||||
}
|
||||
|
||||
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
|
||||
const wantSwitch = acts.includes('persona_switch')
|
||||
@@ -3728,11 +3816,15 @@
|
||||
});
|
||||
}
|
||||
|
||||
/** Re-load chat model after park. Returns a Promise (await after Generate so the next Send is warm). */
|
||||
function warmLlm() {
|
||||
/** Re-load chat model into VRAM. force=true after Generate even without park — Krea often evicts Ollama. */
|
||||
function warmLlm({ force = false } = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const model = $('sa_model')?.value;
|
||||
if (!model || !state.llmParked || typeof genericRequest !== 'function') {
|
||||
if (!model || typeof genericRequest !== 'function') {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
if (!force && !state.llmParked) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
@@ -3749,7 +3841,7 @@
|
||||
}
|
||||
resolve(!!ok);
|
||||
};
|
||||
// VL 7B cold-load can exceed a minute — don't time out the flag early.
|
||||
// VL cold-load can exceed a minute — don't time out the flag early.
|
||||
setTimeout(() => finish(false), 180000);
|
||||
genericRequest('AssistentWarmLlm', { baseUrl, model }, () => finish(true), 0, () => finish(false));
|
||||
});
|
||||
@@ -3864,12 +3956,15 @@
|
||||
const src = await waitForNewImage(prev);
|
||||
state.generating = false;
|
||||
setInterruptVisible(state.busy);
|
||||
// Auto-critique / next Send load the model themselves — skip warm if critique will run.
|
||||
// Krea Generate almost always evicts the VL chat weights from VRAM — even when Park LLM is off.
|
||||
state.expectColdLoad = true;
|
||||
const paneVisible = !!document.getElementById('swarm_assistent_root')?.offsetParent;
|
||||
if (state.llmParked && state.view === 'chat' && paneVisible && !$('sa_auto_critique')?.checked) {
|
||||
const willAutoCritique = !!$('sa_auto_critique')?.checked;
|
||||
// Auto-critique's own chat request will cold-load; otherwise preload before the user types.
|
||||
if (state.view === 'chat' && paneVisible && !willAutoCritique) {
|
||||
startBusyUi('warming');
|
||||
setStatus('Возвращаю LLM в GPU…');
|
||||
await warmLlm();
|
||||
await warmLlm({ force: true });
|
||||
}
|
||||
if (!state.busy) {
|
||||
stopBusyUi(src ? 'Generate готов' : 'Generate завершён (новое изображение не найдено)');
|
||||
@@ -4053,7 +4148,7 @@
|
||||
div.appendChild(buildCivitaiCards(civitaiResults));
|
||||
}
|
||||
box.appendChild(div);
|
||||
box.scrollTop = box.scrollHeight;
|
||||
scrollMessagesToBottom({ force: true });
|
||||
return div;
|
||||
}
|
||||
|
||||
@@ -4071,13 +4166,42 @@
|
||||
body.innerHTML = '<span class="sa-dots" aria-hidden="true"><i></i><i></i><i></i></span><span class="sa-typing-label">Waiting for the model…</span>';
|
||||
div.appendChild(body);
|
||||
box.appendChild(div);
|
||||
box.scrollTop = box.scrollHeight;
|
||||
scrollMessagesToBottom({ force: true });
|
||||
state.streamEl = div;
|
||||
state.streamMeta = meta || null;
|
||||
state.streamFenceDone = false;
|
||||
return div;
|
||||
}
|
||||
|
||||
function streamHasClosedPatchFence(text) {
|
||||
const t = String(text || '');
|
||||
if (!/```[\s\S]*```/.test(t)) {
|
||||
return false;
|
||||
}
|
||||
const { patch } = extractPatch(t);
|
||||
return !!patch;
|
||||
}
|
||||
|
||||
function trimToClosedPatchFence(text) {
|
||||
const t = String(text || '');
|
||||
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||||
let match;
|
||||
let lastEnd = -1;
|
||||
while ((match = re.exec(t)) !== null) {
|
||||
try {
|
||||
const obj = JSON.parse(match[1].trim());
|
||||
if (isPatchObject(obj) || isCardObject(obj)) {
|
||||
lastEnd = match.index + match[0].length;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
return lastEnd > 0 ? t.slice(0, lastEnd).trimEnd() : t;
|
||||
}
|
||||
|
||||
function appendStreamDelta(delta) {
|
||||
if (state.streamFenceDone) {
|
||||
return;
|
||||
}
|
||||
if (!state.streamEl) {
|
||||
beginStreamMessage(state.streamMeta || undefined);
|
||||
}
|
||||
@@ -4085,7 +4209,7 @@
|
||||
if (state.streamEl.classList.contains('sa-typing')) {
|
||||
state.streamEl.classList.remove('sa-typing');
|
||||
state.streamText = '';
|
||||
setAssistantBody(state.streamEl, '');
|
||||
setAssistantBody(state.streamEl, '', { live: true });
|
||||
}
|
||||
state.gotDelta = true;
|
||||
state.expectColdLoad = false;
|
||||
@@ -4093,11 +4217,12 @@
|
||||
setBusyPhase('streaming');
|
||||
}
|
||||
state.streamText = (state.streamText || '') + (delta || '');
|
||||
setAssistantBody(state.streamEl, state.streamText);
|
||||
const box = $('sa_messages');
|
||||
if (box) {
|
||||
box.scrollTop = box.scrollHeight;
|
||||
if (streamHasClosedPatchFence(state.streamText)) {
|
||||
state.streamText = trimToClosedPatchFence(state.streamText);
|
||||
state.streamFenceDone = true;
|
||||
}
|
||||
setAssistantBody(state.streamEl, state.streamText, { live: true });
|
||||
scrollMessagesToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4107,6 +4232,7 @@
|
||||
state.streamEl = null;
|
||||
state.streamMeta = null;
|
||||
state.streamText = '';
|
||||
state.streamFenceDone = false;
|
||||
if (!el) {
|
||||
appendMessage('assistant', fullReply, null, civitaiResults, meta || undefined);
|
||||
return;
|
||||
@@ -4131,10 +4257,7 @@
|
||||
if (civitaiResults && civitaiResults.length) {
|
||||
el.appendChild(buildCivitaiCards(civitaiResults));
|
||||
}
|
||||
const box = $('sa_messages');
|
||||
if (box) {
|
||||
box.scrollTop = box.scrollHeight;
|
||||
}
|
||||
scrollMessagesToBottom();
|
||||
}
|
||||
|
||||
function buildCivitaiCards(results) {
|
||||
@@ -4517,7 +4640,7 @@
|
||||
if (paneW) {
|
||||
document.documentElement.style.setProperty('--sa-image-width', paneW);
|
||||
}
|
||||
if (view === 'cards' || view === 'chat') {
|
||||
if (view === 'cards' || view === 'chat' || view === 'settings') {
|
||||
state.view = view;
|
||||
}
|
||||
const boardTab = localStorage.getItem(LS_BOARD_TAB);
|
||||
@@ -4577,7 +4700,7 @@
|
||||
fill(LS_PACK, ui.pack, (v) => { if ($('sa_pack')) { $('sa_pack').value = v; } });
|
||||
fill(LS_PERSONA, ui.persona, (v) => { if ($('sa_persona')) { $('sa_persona').value = v; } });
|
||||
fill(LS_PANE_WIDTH, ui.pane_width, (v) => document.documentElement.style.setProperty('--sa-image-width', v));
|
||||
if (ui.view === 'cards' || ui.view === 'chat') {
|
||||
if (ui.view === 'cards' || ui.view === 'chat' || ui.view === 'settings') {
|
||||
fill(LS_VIEW, ui.view, (v) => { state.view = v; });
|
||||
}
|
||||
if (ui.board_tab === 'refs' || ui.board_tab === 'generate') {
|
||||
@@ -4817,16 +4940,26 @@
|
||||
controlsPointerDown = true;
|
||||
});
|
||||
const endPointer = () => {
|
||||
const v = Number(input.value);
|
||||
applyLocal(v);
|
||||
controlsPointerDown = false;
|
||||
// Discard mid-drag rebuilds that carried stale server defaults — keep local values.
|
||||
if (pendingControlsRender) {
|
||||
const pending = pendingControlsRender;
|
||||
const schema = pendingControlsRender.schema;
|
||||
pendingControlsRender = null;
|
||||
renderPersonaControls(pending.schema, pending.values);
|
||||
renderPersonaControls(
|
||||
schema,
|
||||
state.config?.control_values || state.exact?.controls || {},
|
||||
);
|
||||
}
|
||||
if (controlSaveTimer) {
|
||||
clearTimeout(controlSaveTimer);
|
||||
}
|
||||
controlSaveTimer = setTimeout(() => savePersonaControls({ [id]: v }), 50);
|
||||
};
|
||||
input.addEventListener('pointerup', endPointer);
|
||||
input.addEventListener('pointercancel', endPointer);
|
||||
// Live label while dragging; persist only on release (change) so mid-drag saves cannot snap back.
|
||||
// Live label while dragging; also persist on change for keyboard tweaks.
|
||||
input.addEventListener('input', () => {
|
||||
applyLocal(Number(input.value));
|
||||
});
|
||||
@@ -4857,6 +4990,7 @@
|
||||
setStatus('/остынь только для Leonid');
|
||||
return;
|
||||
}
|
||||
state.lastUserControlIntent = true;
|
||||
const schema = state.config?.controls || {};
|
||||
if (!schema.horny) {
|
||||
setStatus('У этой личности нет слайдера Хорни');
|
||||
@@ -4889,6 +5023,7 @@
|
||||
return;
|
||||
}
|
||||
const cur = getControlValue('horny', 35);
|
||||
state.lastUserControlIntent = true;
|
||||
await sendChat({
|
||||
skipSlash: true,
|
||||
skipAutoPack: true,
|
||||
@@ -5549,22 +5684,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function openSettings(tab) {
|
||||
const s = $('sa_settings');
|
||||
if (!s) {
|
||||
return;
|
||||
}
|
||||
s.hidden = false;
|
||||
setSettingsTab(tab || state.settingsTab || 'behavior');
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
const s = $('sa_settings');
|
||||
if (s) {
|
||||
s.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function fillKnobsFromConfig(data) {
|
||||
const asst = data?.assistant || state.config?.assistant || {};
|
||||
const exact = data?.exact || state.config?.exact || state.exact || {};
|
||||
@@ -6133,26 +6252,51 @@
|
||||
}
|
||||
|
||||
function setView(view) {
|
||||
state.view = view === 'cards' ? 'cards' : 'chat';
|
||||
if (view === 'cards') {
|
||||
state.view = 'cards';
|
||||
} else if (view === 'settings') {
|
||||
state.view = 'settings';
|
||||
} else {
|
||||
state.view = 'chat';
|
||||
}
|
||||
const chat = $('sa_view_chat');
|
||||
const cards = $('sa_view_cards');
|
||||
const settings = $('sa_view_settings');
|
||||
if (chat) {
|
||||
chat.hidden = state.view !== 'chat';
|
||||
}
|
||||
if (cards) {
|
||||
cards.hidden = state.view !== 'cards';
|
||||
}
|
||||
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');
|
||||
saveSettings();
|
||||
if (state.view === 'cards') {
|
||||
renderCardsList();
|
||||
} else if (state.llmParked && !state.generating) {
|
||||
// Back in the chat — bring the model home before the user hits Send.
|
||||
warmLlm();
|
||||
} else if (state.view === 'settings') {
|
||||
setSettingsTab(state.settingsTab || 'behavior');
|
||||
} else if ((state.llmParked || state.expectColdLoad) && !state.generating) {
|
||||
// Back in the chat — bring the model home (Krea may have evicted it).
|
||||
warmLlm({ force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function openSettings(tab) {
|
||||
if (tab) {
|
||||
state.settingsTab = tab;
|
||||
}
|
||||
setView('settings');
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
setView('chat');
|
||||
}
|
||||
|
||||
function refreshPersonas() {
|
||||
loadConfig($('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral', (data) => {
|
||||
if (data?.personas) {
|
||||
@@ -7050,7 +7194,7 @@
|
||||
div.className = 'sa-msg assistant sa-system-note';
|
||||
div.textContent = text;
|
||||
box.appendChild(div);
|
||||
box.scrollTop = box.scrollHeight;
|
||||
scrollMessagesToBottom();
|
||||
}
|
||||
|
||||
function clipDebug(s, max) {
|
||||
@@ -7423,6 +7567,7 @@
|
||||
}
|
||||
if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.fromDebug) {
|
||||
state.lastUserParamIntent = userTextMentionsParams(text);
|
||||
state.lastUserControlIntent = userTextMentionsControls(text);
|
||||
state.pendingSilentGen = userAsksGenerate(text) || isSameButAspectRequest(text);
|
||||
}
|
||||
|
||||
@@ -7491,13 +7636,21 @@
|
||||
|
||||
const chatEpoch = bumpChatEpoch();
|
||||
state.busy = true;
|
||||
// This request will keep_alive 15m; clear parked only after we know load started.
|
||||
// Keep expectColdLoad so the UI can show a real GPU-load message if we just parked.
|
||||
if (!state.llmParked) {
|
||||
state.expectColdLoad = false;
|
||||
}
|
||||
// If Krea just ran, expectColdLoad stays true until warm / first token — don't clear it here.
|
||||
state.llmParked = false;
|
||||
setInterruptVisible(true);
|
||||
if (state.expectColdLoad && !opts.fromVisionHop && !opts.fromAutoCritique) {
|
||||
startBusyUi('warming');
|
||||
setStatus('Возвращаю LLM в GPU…');
|
||||
try {
|
||||
await warmLlm({ force: true });
|
||||
} catch (e) {
|
||||
console.warn('Assistent warm before send', e);
|
||||
}
|
||||
if (chatEpoch !== state.chatEpoch) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
startBusyUi(state.expectColdLoad ? 'loading' : 'thinking');
|
||||
saveSettings();
|
||||
|
||||
@@ -8015,6 +8168,7 @@
|
||||
|
||||
$('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);
|
||||
@@ -8025,15 +8179,11 @@
|
||||
$('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', () => {
|
||||
const s = $('sa_settings');
|
||||
if (s) {
|
||||
if (s.hidden) {
|
||||
openSettings(state.settingsTab || 'behavior');
|
||||
} else {
|
||||
if (state.view === 'settings') {
|
||||
closeSettings();
|
||||
}
|
||||
} else {
|
||||
openSettings(state.settingsTab || 'behavior');
|
||||
}
|
||||
});
|
||||
$('sa_settings_close')?.addEventListener('click', () => closeSettings());
|
||||
@@ -8105,8 +8255,7 @@
|
||||
return;
|
||||
}
|
||||
let closed = false;
|
||||
const settings = $('sa_settings');
|
||||
if (settings && !settings.hidden) {
|
||||
if (state.view === 'settings') {
|
||||
closeSettings();
|
||||
closed = true;
|
||||
}
|
||||
|
||||
@@ -1003,13 +1003,25 @@ public partial class SwarmAssistentExtension
|
||||
patch["_persona_written"] = target;
|
||||
}
|
||||
}
|
||||
// Control values from model patch (Exact).
|
||||
// Control values from model patch (Exact). Ignore default-echo inside Generate patches.
|
||||
if (patch["controls"] is JObject ctrlVals)
|
||||
{
|
||||
string ctrlPid = AssistentConfig.SafeId(patch["persona"]?.ToString()) ?? personaId;
|
||||
Config.SaveControlValues(ctrlPid, ctrlVals);
|
||||
JObject schema = Config.LoadControlsSchema(ctrlPid);
|
||||
JObject current = Config.LoadControlValues(ctrlPid);
|
||||
JObject filtered = AssistentConfig.FilterEchoedControlDefaults(
|
||||
schema, current, ctrlVals, AssistentConfig.PatchLooksLikeGeneration(patch));
|
||||
if (filtered.Count > 0)
|
||||
{
|
||||
Config.SaveControlValues(ctrlPid, filtered);
|
||||
patch["controls"] = filtered;
|
||||
patch["_controls_saved"] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
patch.Remove("controls");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -466,6 +466,81 @@ public sealed class AssistentConfig
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drop control keys that merely restate schema defaults while the user already has a
|
||||
/// different saved value — models often echo Exact defaults inside Generate patches and
|
||||
/// that was resetting Хорни / Вкус after every reply.
|
||||
/// Keep intentional changes: non-default values, or default when it matches current, or
|
||||
/// when the patch is controls-only (e.g. /horny-game).
|
||||
/// </summary>
|
||||
public static JObject FilterEchoedControlDefaults(JObject schema, JObject current, JObject incoming, bool patchLooksLikeGen)
|
||||
{
|
||||
JObject result = new();
|
||||
if (schema is null || incoming is null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
current ??= new JObject();
|
||||
foreach (JProperty prop in incoming.Properties())
|
||||
{
|
||||
if (schema[prop.Name] is not JObject def)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!double.TryParse(prop.Value?.ToString(), System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out double incomingVal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
double defVal = def["default"]?.Value<double?>() ?? double.NaN;
|
||||
double curVal = double.NaN;
|
||||
if (current[prop.Name] != null)
|
||||
{
|
||||
double.TryParse(current[prop.Name]?.ToString(), System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out curVal);
|
||||
}
|
||||
if (!double.IsNaN(curVal) && Math.Abs(incomingVal - curVal) < 0.0005)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (patchLooksLikeGen
|
||||
&& !double.IsNaN(defVal)
|
||||
&& Math.Abs(incomingVal - defVal) < 0.0005
|
||||
&& !double.IsNaN(curVal)
|
||||
&& Math.Abs(curVal - defVal) > 0.0005)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
result[prop.Name] = Math.Round(incomingVal, 4);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static bool PatchLooksLikeGeneration(JObject patch)
|
||||
{
|
||||
if (patch is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (patch["prompt"] != null || patch["loras"] != null || patch["aspect"] != null
|
||||
|| patch["width"] != null || patch["height"] != null || patch["steps"] != null
|
||||
|| patch["cfg"] != null || patch["seed"] != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (patch["actions"] is JArray acts)
|
||||
{
|
||||
foreach (JToken a in acts)
|
||||
{
|
||||
if (string.Equals(a?.ToString(), "generate", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public string RenderControlsBlock(string personaId)
|
||||
{
|
||||
string id = SafeId(personaId) ?? "neutral";
|
||||
|
||||
@@ -192,6 +192,10 @@ public partial class SwarmAssistentExtension
|
||||
string body = await resp.Content.ReadAsStringAsync();
|
||||
JObject parsed = JObject.Parse(body);
|
||||
string reply = parsed["message"]?["content"]?.ToString() ?? parsed["response"]?.ToString() ?? "";
|
||||
if (TryTruncateAtCompleteFence(reply, out string cut))
|
||||
{
|
||||
reply = cut;
|
||||
}
|
||||
return (reply, parsed);
|
||||
}
|
||||
StringBuilder full = new();
|
||||
@@ -215,6 +219,23 @@ public partial class SwarmAssistentExtension
|
||||
if (!string.IsNullOrEmpty(delta))
|
||||
{
|
||||
full.Append(delta);
|
||||
// Closed ```json``` patch → stop reading. Models otherwise ramble («Готово!», 2nd aspect…).
|
||||
if (TryTruncateAtCompleteFence(full.ToString(), out string cut))
|
||||
{
|
||||
string extra = full.Length > cut.Length ? full.ToString(cut.Length, full.Length - cut.Length) : "";
|
||||
full.Clear();
|
||||
full.Append(cut);
|
||||
if (onDelta is not null)
|
||||
{
|
||||
// Only forward the part of this delta that stays inside the fence.
|
||||
int keep = delta.Length - extra.Length;
|
||||
if (keep > 0)
|
||||
{
|
||||
await onDelta(delta.Substring(0, keep));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (onDelta is not null)
|
||||
{
|
||||
await onDelta(delta);
|
||||
|
||||
@@ -106,6 +106,51 @@ public partial class SwarmAssistentExtension
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the reply already contains a closed fenced patch/card, cut everything after it.
|
||||
/// Models often keep writing («Готово!», second aspect, …) and the stream never feels done.
|
||||
/// </summary>
|
||||
static bool TryTruncateAtCompleteFence(string reply, out string truncated)
|
||||
{
|
||||
truncated = reply ?? "";
|
||||
if (string.IsNullOrWhiteSpace(reply))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
MatchCollection matches = JsonFenceRe.Matches(reply);
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < matches.Count; i++)
|
||||
{
|
||||
Match match = matches[i];
|
||||
string raw = match.Groups[1].Value.Trim();
|
||||
try
|
||||
{
|
||||
JObject obj = JObject.Parse(raw);
|
||||
if (obj is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
bool usable = LooksLikeCardObject(obj)
|
||||
|| Array.Exists(PatchKeys, k => obj[k] is not null);
|
||||
if (!usable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
truncated = reply.Substring(0, match.Index + match.Length).TrimEnd();
|
||||
// Only treat as complete if the fence actually closed (regex requires ```).
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// incomplete / invalid json inside fence
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static string ExtractSearchQuery(JObject patch)
|
||||
{
|
||||
if (patch is null)
|
||||
|
||||
@@ -18,7 +18,7 @@ When instructions conflict, apply this order (highest wins):
|
||||
|
||||
Exact = defaults encyclopedia. About the user = human taste. RAG = soft craft notes. Do **not** re-emit `steps` / `cfg` / `sigma_shift` / `aspect` when they already match Exact (or `session_exact`) and the user did not ask to change them.
|
||||
|
||||
Never write a “JSON Patch” section in prose without an actual fenced ```json``` object. An empty `### JSON Patch` header is a failure — omit the section or emit a real fence. Keep prose short (a few lines). Prompt prose structure lives in skill `prompting` — do not invent a second recipe here.
|
||||
Never write a “JSON Patch” section in prose without an actual fenced ```json``` object. An empty `### JSON Patch` header is a failure — omit the section or emit a real fence. Keep prose short (a few lines). **After the closing ``` of the JSON fence, STOP** — no «Готово!», no second aspect, no “сейчас сгенерирую оба”. One turn = one patch (one aspect). Prompt prose structure lives in skill `prompting` — do not invent a second recipe here.
|
||||
|
||||
## Live context
|
||||
|
||||
@@ -55,6 +55,7 @@ Never write a “JSON Patch” section in prose without an actual fenced ```json
|
||||
### Patch rules
|
||||
|
||||
- Omit unchanged keys. Prefer omitting Exact-matching `steps`/`cfg`/`sigma_shift`/`aspect`.
|
||||
- Prefer omitting Exact-matching **`controls`** (e.g. do not re-emit `"horny": 35` / `"preference_bias": 0.35` when unchanged) — echoing defaults in a Generate patch resets the UI sliders.
|
||||
- `loras` replaces the full intended set for Apply. Prefer `aspect` over raw width/height.
|
||||
- Optional keys (seed, vary, init/mask, creativity/sliders, pack, **controls**, persona authoring, search/memory queries) — use when needed; packs list the ones for that mode.
|
||||
- **`controls`** — only keys declared in this persona's `controls.json` (Exact). Clamp to min/max. Example: `"controls": { "horny": 55 }`. Do not invent control ids.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"Craft first: triggers, aspect, Turbo — horny never replaces technique",
|
||||
"Scale appearance/outfits by controls.preference_bias (−1…1)",
|
||||
"Scale sexual tone + roleplay fetishes by controls.horny (0…100)",
|
||||
"Never re-emit controls.horny / preference_bias in a Generate patch unless the user asked to change them — echoing defaults resets the UI",
|
||||
"Explicit user look/outfit/plot beats personal taste",
|
||||
"«девушка которая тебе нравится» = YOUR (Leonid) taste, not the user's — unless they said otherwise",
|
||||
"High horny / NSFW plot / craft detail: persona_read shelves [\"roleplay\"] (and humor/craft if needed) — they are not always-on",
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + board (Generate | Refs tabs), LoRA chips, **persona presets** (`Config/personas/`), **About the user** prefs + craft vector memory, model cards with Civitai fetch, img2img/inpaint, slash commands, auto Generate.
|
||||
|
||||
**Version 0.10.13** — Assistant prose renders `### Critique` as **Критика** (and similar headings); empty JSON Patch headers hidden. Builds on 0.10.12 aspect/critique fixes.
|
||||
**Version 0.10.18** — Settings is a full subtab (Чат | Карточки | Настройки), not a header overlay. Builds on 0.10.17 post-Generate warm.
|
||||
|
||||
## Layout
|
||||
|
||||
- **Left — Board tabs:** **Generate** (full-height live view) | **Refs** (reference grid + badge `N · vision M`); **Посмотри результат** attaches the finished frame and asks for a verdict
|
||||
- **Splitter:** drag to resize panes
|
||||
- **Right:** Chat | Cards; persona / pack / Ollama chat model; **Ollama health** badge; ⚙ settings panel (6 tabs)
|
||||
- **Right:** Chat | Cards | Settings; persona / pack / Ollama chat model; **Ollama health** badge
|
||||
- **Chips / slash:** loaded from `Config/_base/ui.json` (persona can override)
|
||||
|
||||
## Config (bundled + overlay)
|
||||
@@ -74,8 +74,8 @@ Two layers in `memory/assistent.sqlite` (`persona` column; empty = shared):
|
||||
- `settings.json` and persona overlays stay files (layered merge + git). `.assistent.json` cards stay next to weights.
|
||||
## VRAM handover
|
||||
|
||||
- Before Generate the chat model is unloaded only if **Park LLM** is enabled (`keep_alive: 0`) — default off so VL chat stays warm; enable when Generate OOMs
|
||||
- When parked, after Generate the model is warmed again (`keep_alive 15m`) before the UI goes idle
|
||||
- Before Generate the chat model is unloaded only if **Park LLM** is enabled (`keep_alive: 0`) — default off
|
||||
- After Generate the chat model is **always** force-warmed (`AssistentWarmLlm`) — Krea still often evicts VL from VRAM even without park
|
||||
- Embed / memory models are never parked — reloading them would stall every retrieve
|
||||
|
||||
## UX
|
||||
|
||||
@@ -36,7 +36,7 @@ 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.10.13";
|
||||
Version = "0.10.18";
|
||||
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"];
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
<div class="sa-subtabs" role="tablist">
|
||||
<button type="button" class="sa-subtab sa-subtab-active" data-view="chat" id="sa_tab_chat">Чат</button>
|
||||
<button type="button" class="sa-subtab" data-view="cards" id="sa_tab_cards">Карточки</button>
|
||||
<button type="button" class="sa-subtab" data-view="settings" id="sa_tab_settings">Настройки</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sa-chats-panel" id="sa_chats_panel" hidden>
|
||||
@@ -73,10 +74,88 @@
|
||||
<button type="button" class="basic-button sa-icon-btn" id="sa_btn_settings" title="Настройки" aria-label="Настройки">⚙</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="sa-settings" id="sa_settings" hidden>
|
||||
<div class="sa-view" id="sa_view_chat">
|
||||
<div class="sa-messages" id="sa_messages">
|
||||
<div class="sa-chat-empty" id="sa_chat_empty">
|
||||
<div class="sa-chat-empty-title">Совместная работа с Krea 2</div>
|
||||
<div class="sa-chat-empty-hint">Напиши промпт, кинь refs, выбери персону или открой <em>Карточки</em> для LoRA.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sa-livebar" id="sa_livebar" hidden>
|
||||
<span class="sa-spinner" aria-hidden="true"></span>
|
||||
<span class="sa-livebar-text" id="sa_livebar_text">Работаю…</span>
|
||||
<span class="sa-elapsed" id="sa_elapsed"></span>
|
||||
</div>
|
||||
<div class="sa-composer" id="sa_composer">
|
||||
<div class="sa-chips" id="sa_chips" role="toolbar" aria-label="Быстрые параметры"></div>
|
||||
<div class="sa-lora-chips" id="sa_lora_chips" role="toolbar" aria-label="Активные LoRA"></div>
|
||||
<div class="sa-slash-wrap">
|
||||
<textarea id="sa_input" rows="3" placeholder="Промпт, img2img, критика… Enter = отправить · /help = команды"></textarea>
|
||||
<div class="sa-slash-menu" id="sa_slash_menu" hidden role="listbox"></div>
|
||||
</div>
|
||||
<div class="sa-composer-actions">
|
||||
<button type="button" class="basic-button sa-primary" id="sa_btn_send">Отправить</button>
|
||||
<button type="button" class="basic-button sa-btn-gen" id="sa_btn_build_gen" title="Нет патча — Generate с текущим промптом SwarmUI">Собрать + Gen</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_interrupt" title="Прервать генерацию / чат" hidden>Стоп</button>
|
||||
<div class="sa-more-wrap" id="sa_clear_more_wrap">
|
||||
<button type="button" class="basic-button" id="sa_btn_clear" title="Очистить чат">Очистить</button>
|
||||
<button type="button" class="basic-button sa-icon-btn" id="sa_btn_clear_more" title="Ещё варианты очистки" aria-expanded="false">▾</button>
|
||||
<div class="sa-more-menu" id="sa_clear_more_menu" hidden role="menu">
|
||||
<button type="button" class="sa-more-item" id="sa_btn_clear_confirm" role="menuitem">Очистить весь чат…</button>
|
||||
<button type="button" class="sa-more-item" id="sa_btn_clear_patches" role="menuitem">Убрать только патчи</button>
|
||||
</div>
|
||||
</div>
|
||||
<span class="sa-status" id="sa_status"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sa-view" id="sa_view_cards" hidden>
|
||||
<div class="sa-cards-layout">
|
||||
<div class="sa-cards-list-pane">
|
||||
<div class="sa-cards-filter">
|
||||
<select id="sa_cards_kind" class="sa-select">
|
||||
<option value="all">Все</option>
|
||||
<option value="checkpoint">Checkpoints</option>
|
||||
<option value="lora">LoRAs</option>
|
||||
</select>
|
||||
<button type="button" class="basic-button" id="sa_btn_cards_refresh">Обновить</button>
|
||||
</div>
|
||||
<div class="sa-cards-list" id="sa_cards_list"></div>
|
||||
</div>
|
||||
<div class="sa-cards-editor-pane">
|
||||
<div class="sa-cards-editor-head">
|
||||
<strong id="sa_card_title">Выбери модель</strong>
|
||||
<span class="sa-card-badge" id="sa_card_badge" hidden></span>
|
||||
</div>
|
||||
<div class="sa-card-previews" id="sa_card_previews" hidden></div>
|
||||
<div class="sa-card-form" id="sa_card_form">
|
||||
<label>Triggers <input type="text" id="sa_card_triggers" placeholder="слово1, слово2" /></label>
|
||||
<label>Weight <input type="number" id="sa_card_weight" step="0.05" min="0" max="2" value="0.8" /></label>
|
||||
<label>When <textarea id="sa_card_when" rows="2" placeholder="Когда использовать"></textarea></label>
|
||||
<label>Avoid <textarea id="sa_card_avoid" rows="2" placeholder="Чего избегать"></textarea></label>
|
||||
<label>Prompt hint <textarea id="sa_card_hint" rows="2" placeholder="Подсказка для промпта"></textarea></label>
|
||||
<label>Notes <textarea id="sa_card_notes" rows="2" placeholder="Заметки"></textarea></label>
|
||||
<label>Civitai URL <input type="text" id="sa_card_url" placeholder="https://civitai…" /></label>
|
||||
<label class="sa-card-json-toggle"><input type="checkbox" id="sa_card_show_json" /> Показать JSON</label>
|
||||
<textarea id="sa_card_json" rows="8" hidden placeholder="Card JSON…" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="sa-cards-actions">
|
||||
<button type="button" class="basic-button sa-primary" id="sa_btn_card_to_chat" title="В чат: используй эту модель/LoRA">В чат</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_card_meta">Загрузить Civitai meta</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_card_generate">Сгенерировать с Assistent</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_card_save">Сохранить карточку</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_card_wanted" title="В очередь на следующий up">В wanted</button>
|
||||
<span class="sa-status" id="sa_card_status"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sa-view" id="sa_view_settings" hidden>
|
||||
<div class="sa-settings" id="sa_settings">
|
||||
<div class="sa-settings-head">
|
||||
<strong>Настройки</strong>
|
||||
<button type="button" class="basic-button sa-icon-btn" id="sa_settings_close" title="Закрыть" aria-label="Закрыть">✕</button>
|
||||
<button type="button" class="basic-button" id="sa_settings_close" title="К чату">← Чат</button>
|
||||
</div>
|
||||
<div class="sa-settings-tabs" role="tablist" aria-label="Разделы настроек">
|
||||
<button type="button" class="sa-stab sa-stab-active" data-stab="behavior" role="tab" aria-selected="true">Поведение</button>
|
||||
@@ -215,83 +294,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sa-view" id="sa_view_chat">
|
||||
<div class="sa-messages" id="sa_messages">
|
||||
<div class="sa-chat-empty" id="sa_chat_empty">
|
||||
<div class="sa-chat-empty-title">Совместная работа с Krea 2</div>
|
||||
<div class="sa-chat-empty-hint">Напиши промпт, кинь refs, выбери персону или открой <em>Карточки</em> для LoRA.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sa-livebar" id="sa_livebar" hidden>
|
||||
<span class="sa-spinner" aria-hidden="true"></span>
|
||||
<span class="sa-livebar-text" id="sa_livebar_text">Работаю…</span>
|
||||
<span class="sa-elapsed" id="sa_elapsed"></span>
|
||||
</div>
|
||||
<div class="sa-composer" id="sa_composer">
|
||||
<div class="sa-chips" id="sa_chips" role="toolbar" aria-label="Быстрые параметры"></div>
|
||||
<div class="sa-lora-chips" id="sa_lora_chips" role="toolbar" aria-label="Активные LoRA"></div>
|
||||
<div class="sa-slash-wrap">
|
||||
<textarea id="sa_input" rows="3" placeholder="Промпт, img2img, критика… Enter = отправить · /help = команды"></textarea>
|
||||
<div class="sa-slash-menu" id="sa_slash_menu" hidden role="listbox"></div>
|
||||
</div>
|
||||
<div class="sa-composer-actions">
|
||||
<button type="button" class="basic-button sa-primary" id="sa_btn_send">Отправить</button>
|
||||
<button type="button" class="basic-button sa-btn-gen" id="sa_btn_build_gen" title="Нет патча — Generate с текущим промптом SwarmUI">Собрать + Gen</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_interrupt" title="Прервать генерацию / чат" hidden>Стоп</button>
|
||||
<div class="sa-more-wrap" id="sa_clear_more_wrap">
|
||||
<button type="button" class="basic-button" id="sa_btn_clear" title="Очистить чат">Очистить</button>
|
||||
<button type="button" class="basic-button sa-icon-btn" id="sa_btn_clear_more" title="Ещё варианты очистки" aria-expanded="false">▾</button>
|
||||
<div class="sa-more-menu" id="sa_clear_more_menu" hidden role="menu">
|
||||
<button type="button" class="sa-more-item" id="sa_btn_clear_confirm" role="menuitem">Очистить весь чат…</button>
|
||||
<button type="button" class="sa-more-item" id="sa_btn_clear_patches" role="menuitem">Убрать только патчи</button>
|
||||
</div>
|
||||
</div>
|
||||
<span class="sa-status" id="sa_status"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sa-view" id="sa_view_cards" hidden>
|
||||
<div class="sa-cards-layout">
|
||||
<div class="sa-cards-list-pane">
|
||||
<div class="sa-cards-filter">
|
||||
<select id="sa_cards_kind" class="sa-select">
|
||||
<option value="all">Все</option>
|
||||
<option value="checkpoint">Checkpoints</option>
|
||||
<option value="lora">LoRAs</option>
|
||||
</select>
|
||||
<button type="button" class="basic-button" id="sa_btn_cards_refresh">Обновить</button>
|
||||
</div>
|
||||
<div class="sa-cards-list" id="sa_cards_list"></div>
|
||||
</div>
|
||||
<div class="sa-cards-editor-pane">
|
||||
<div class="sa-cards-editor-head">
|
||||
<strong id="sa_card_title">Выбери модель</strong>
|
||||
<span class="sa-card-badge" id="sa_card_badge" hidden></span>
|
||||
</div>
|
||||
<div class="sa-card-previews" id="sa_card_previews" hidden></div>
|
||||
<div class="sa-card-form" id="sa_card_form">
|
||||
<label>Triggers <input type="text" id="sa_card_triggers" placeholder="слово1, слово2" /></label>
|
||||
<label>Weight <input type="number" id="sa_card_weight" step="0.05" min="0" max="2" value="0.8" /></label>
|
||||
<label>When <textarea id="sa_card_when" rows="2" placeholder="Когда использовать"></textarea></label>
|
||||
<label>Avoid <textarea id="sa_card_avoid" rows="2" placeholder="Чего избегать"></textarea></label>
|
||||
<label>Prompt hint <textarea id="sa_card_hint" rows="2" placeholder="Подсказка для промпта"></textarea></label>
|
||||
<label>Notes <textarea id="sa_card_notes" rows="2" placeholder="Заметки"></textarea></label>
|
||||
<label>Civitai URL <input type="text" id="sa_card_url" placeholder="https://civitai…" /></label>
|
||||
<label class="sa-card-json-toggle"><input type="checkbox" id="sa_card_show_json" /> Показать JSON</label>
|
||||
<textarea id="sa_card_json" rows="8" hidden placeholder="Card JSON…" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="sa-cards-actions">
|
||||
<button type="button" class="basic-button sa-primary" id="sa_btn_card_to_chat" title="В чат: используй эту модель/LoRA">В чат</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_card_meta">Загрузить Civitai meta</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_card_generate">Сгенерировать с Assistent</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_card_save">Сохранить карточку</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_card_wanted" title="В очередь на следующий up">В wanted</button>
|
||||
<span class="sa-status" id="sa_card_status"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user