Ship Assistent 0.10.8 cheap-bug sweep for RU intent, VRAM park, and identity.
Silent Generate, pack selection, and loading UI no longer trip on Cyrillic or fake GPU loads; persona title is the assistant, not the user. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+240
-56
@@ -14,6 +14,8 @@
|
|||||||
const LS_AUTO_GENERATE = 'swarm_assistent_auto_generate';
|
const LS_AUTO_GENERATE = 'swarm_assistent_auto_generate';
|
||||||
const LS_AUTO_CRITIQUE = 'swarm_assistent_auto_critique';
|
const LS_AUTO_CRITIQUE = 'swarm_assistent_auto_critique';
|
||||||
const LS_AUTO_DOWNLOAD = 'swarm_assistent_auto_download';
|
const LS_AUTO_DOWNLOAD = 'swarm_assistent_auto_download';
|
||||||
|
/** When '1', unload chat LLM before Generate (frees VRAM; VL reload can take 1–2 min). Default off. */
|
||||||
|
const LS_PARK_LLM = 'swarm_assistent_park_llm';
|
||||||
const LS_PANE_WIDTH = 'swarm_assistent_pane_width';
|
const LS_PANE_WIDTH = 'swarm_assistent_pane_width';
|
||||||
const LS_WELCOMED = 'swarm_assistent_welcomed';
|
const LS_WELCOMED = 'swarm_assistent_welcomed';
|
||||||
const LS_TASTE = 'swarm_assistent_taste';
|
const LS_TASTE = 'swarm_assistent_taste';
|
||||||
@@ -163,6 +165,7 @@
|
|||||||
chatsSearchHits: null,
|
chatsSearchHits: null,
|
||||||
slashIndex: 0,
|
slashIndex: 0,
|
||||||
llmParked: false,
|
llmParked: false,
|
||||||
|
expectColdLoad: false,
|
||||||
memoryRows: [],
|
memoryRows: [],
|
||||||
userPrefs: [],
|
userPrefs: [],
|
||||||
settingsTab: 'behavior',
|
settingsTab: 'behavior',
|
||||||
@@ -224,14 +227,17 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const elapsed = Date.now() - (state.busyStarted || Date.now());
|
const elapsed = Date.now() - (state.busyStarted || Date.now());
|
||||||
|
// Only claim "Loading into GPU" when the model was parked / known cold.
|
||||||
|
// Otherwise a slow first token on an already-resident VL looks like a 2‑min reload.
|
||||||
if (!state.gotDelta && (state.busyPhase === 'thinking' || state.busyPhase === 'waiting') && elapsed > 1600) {
|
if (!state.gotDelta && (state.busyPhase === 'thinking' || state.busyPhase === 'waiting') && elapsed > 1600) {
|
||||||
state.busyPhase = 'loading';
|
state.busyPhase = state.llmParked || state.expectColdLoad ? 'loading' : 'waiting';
|
||||||
}
|
}
|
||||||
const model = modelShort($('sa_model')?.value);
|
const model = modelShort($('sa_model')?.value);
|
||||||
const labels = {
|
const labels = {
|
||||||
encoding: 'Encoding image…',
|
encoding: 'Encoding image…',
|
||||||
waiting: 'Waiting for Ollama…',
|
waiting: 'Жду Ollama / первый токен…',
|
||||||
loading: `Loading ${model} into GPU… first load can take a minute`,
|
loading: `Загружаю ${model} в GPU… обычно 30–120 с после park`,
|
||||||
|
warming: `Возвращаю ${model} в GPU…`,
|
||||||
thinking: 'Thinking…',
|
thinking: 'Thinking…',
|
||||||
streaming: 'Writing…',
|
streaming: 'Writing…',
|
||||||
generating: 'Generating image…',
|
generating: 'Generating image…',
|
||||||
@@ -482,19 +488,42 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** JS \\b/\\w are ASCII-only — use this for RU tokens. `alts` = regex alternatives without outer parens. */
|
||||||
|
function cyrTokenRe(alts) {
|
||||||
|
const boundary = '(^|[^0-9A-Za-z_А-Яа-яЁё])';
|
||||||
|
const end = '(?=$|[^0-9A-Za-z_А-Яа-яЁё])';
|
||||||
|
return new RegExp(`${boundary}(?:${alts})${end}`, 'i');
|
||||||
|
}
|
||||||
|
|
||||||
function userTextMentionsParams(text) {
|
function userTextMentionsParams(text) {
|
||||||
return /\b(steps?|cfg|seed|sigma|размер|aspect|ширин|высот|resolution|batch|турбо|turbo|raw)\b/i.test(String(text || ''));
|
const t = String(text || '');
|
||||||
|
if (/\b(steps?|cfg|seed|sigma|aspect|resolution|batch|turbo|raw)\b/i.test(t)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return cyrTokenRe('размер|ширин[а-яё]*|высот[а-яё]*|турбо').test(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
function userAsksGenerate(text) {
|
function userAsksGenerate(text) {
|
||||||
const t = String(text || '');
|
const t = String(text || '').trim();
|
||||||
if (!t.trim()) {
|
if (!t) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (/^(gen|generate|go|рисуй|давай|ещё|еще)\s*[!.…]*$/i.test(t.trim())) {
|
// Short imperatives only — do NOT treat bare «давай» as Generate (false positive on chat).
|
||||||
|
if (/^(gen|generate|go|рисуй|нарисуй)([!.…\s]|$)/i.test(t)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return /\b(сгенерируй|сгенерировать|генерируй|generate|нарисуй|перегенерируй|перерисуй|сделай\s+(картинк|изображен|фото)|run\s+generat|\/gen)\b/i.test(t);
|
if (/^(ещё|еще)(\s+раз)?([!.…\s]|$)/i.test(t)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Do NOT use \b or \w — ASCII-only in JS; breaks «сделай картинку».
|
||||||
|
const letter = '[0-9A-Za-z_А-Яа-яЁё]';
|
||||||
|
const stem = `${letter}*`;
|
||||||
|
return cyrTokenRe(
|
||||||
|
'сгенерируй|сгенерировать|генерируй|generate|нарисуй|перегенерируй|перерисуй|'
|
||||||
|
+ `сделай\\s+(картинк${stem}|изображен${stem}|фото${stem})|`
|
||||||
|
+ `хочу\\s+(картинк${stem}|изображен${stem}|фото${stem})|`
|
||||||
|
+ 'run\\s+generat|/gen',
|
||||||
|
).test(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rememberLastPatch(patch) {
|
function rememberLastPatch(patch) {
|
||||||
@@ -1374,16 +1403,29 @@
|
|||||||
return `c_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
return `c_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripJsonFencesForHistory(content) {
|
||||||
|
return String(content || '')
|
||||||
|
.replace(/```(?:json)?\s*[\s\S]*?```/gi, '')
|
||||||
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
function slimHistoryMessages(list) {
|
function slimHistoryMessages(list) {
|
||||||
return (list || [])
|
return (list || [])
|
||||||
.filter((m) => m && (m.role === 'user' || m.role === 'assistant') && !m.systemish)
|
.filter((m) => m && (m.role === 'user' || m.role === 'assistant') && !m.systemish)
|
||||||
.slice(-MAX_CHAT_MSGS)
|
.slice(-MAX_CHAT_MSGS)
|
||||||
.map((m) => ({
|
.map((m) => {
|
||||||
role: m.role,
|
let content = String(m.content || '');
|
||||||
content: String(m.content || '').slice(0, 4000),
|
if (m.role === 'assistant') {
|
||||||
persona: m.persona || undefined,
|
content = stripJsonFencesForHistory(content);
|
||||||
pack: m.pack || undefined,
|
}
|
||||||
}));
|
return {
|
||||||
|
role: m.role,
|
||||||
|
content: content.slice(0, 4000),
|
||||||
|
persona: m.persona || undefined,
|
||||||
|
pack: m.pack || undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function titleFromMessages(messages) {
|
function titleFromMessages(messages) {
|
||||||
@@ -2799,17 +2841,24 @@
|
|||||||
if (!obj || typeof obj !== 'object') {
|
if (!obj || typeof obj !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (isCardObject(obj)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return FALLBACK_PATCH_KEYS.some((k) => obj[k] !== undefined && obj[k] !== null);
|
return FALLBACK_PATCH_KEYS.some((k) => obj[k] !== undefined && obj[k] !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCardObject(obj) {
|
function isCardObject(obj) {
|
||||||
|
if (window.SA && typeof SA.isCardObject === 'function') {
|
||||||
|
return SA.isCardObject(obj);
|
||||||
|
}
|
||||||
if (!obj || typeof obj !== 'object') {
|
if (!obj || typeof obj !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Prefer card shape over gen patch when both could match.
|
// Prefer card shape over gen patch when both could match.
|
||||||
const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint);
|
const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint);
|
||||||
const genish = !!(obj.prompt != null || obj.loras || obj.actions || obj.width || obj.height
|
const genish = !!(obj.prompt != null || obj.loras || obj.actions || obj.width || obj.height
|
||||||
|| obj.steps || obj.cfg || obj.aspect || obj.seed != null);
|
|| obj.steps || obj.cfg || obj.aspect || obj.seed != null
|
||||||
|
|| obj.search_query || obj.civitai_query || obj.look_at || obj.controls);
|
||||||
if (cardish && !genish && (obj.name || obj.triggers || obj.when)) {
|
if (cardish && !genish && (obj.name || obj.triggers || obj.when)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2992,23 +3041,31 @@
|
|||||||
if (!t.trim()) {
|
if (!t.trim()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (/\b(поправь|исправь|перепиши|улучши|fix\s+it|make\s+it\s+better|rewrite)\b/i.test(t)) {
|
if (cyrTokenRe('поправь|исправь|перепиши|улучши').test(t)
|
||||||
|
|| /\b(fix\s+it|make\s+it\s+better|rewrite)\b/i.test(t)) {
|
||||||
return 'write_prompt';
|
return 'write_prompt';
|
||||||
}
|
}
|
||||||
if (/\b(опиши\s+реф|опиши\s+изображ|prompt\s+from\s+image|describe\s+(this|the|ref|image)|reverse\s*prompt)\b/i.test(t)
|
if (cyrTokenRe('опиши\\s+(реф|изображ[а-яё]*|этот|эту|картинк[а-яё]*|референс)').test(t)
|
||||||
|
|| /\b(prompt\s+from\s+image|describe\s+(this|the|ref|image)|reverse\s*prompt)\b/i.test(t)
|
||||||
|| /опиши\s+(этот|эту|картинк|референс)/i.test(t)) {
|
|| /опиши\s+(этот|эту|картинк|референс)/i.test(t)) {
|
||||||
return 'describe_ref';
|
return 'describe_ref';
|
||||||
}
|
}
|
||||||
if (/\b(critique|что\s+не\s+так|посмотри|смотри|разбери|критик)\b/i.test(t)) {
|
// Bare «посмотри/смотри» is casual chat — only critique when aimed at a result/frame.
|
||||||
|
if (/\b(critique|criticize)\b/i.test(t)
|
||||||
|
|| cyrTokenRe('критик[а-яё]*|что\\s+не\\s+так|разбери').test(t)
|
||||||
|
|| /(?:^|[^а-яёa-z0-9_])(посмотри|смотри)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген)/i.test(t)) {
|
||||||
return 'critique_image';
|
return 'critique_image';
|
||||||
}
|
}
|
||||||
if (/\b(inpaint|замажь|закрась|руки|лицо|маск|mask|img2img|init\s*image)\b/i.test(t)) {
|
if (/\b(inpaint|mask|img2img)\b/i.test(t)
|
||||||
|
|| cyrTokenRe('замажь|закрась|руки|лицо|маск[а-яё]*').test(t)
|
||||||
|
|| /init\s*image/i.test(t)) {
|
||||||
return 'inpaint_edit';
|
return 'inpaint_edit';
|
||||||
}
|
}
|
||||||
if (userTextMentionsParams(t)) {
|
if (userTextMentionsParams(t)) {
|
||||||
return 'fix_params';
|
return 'form_params';
|
||||||
}
|
}
|
||||||
if (/\b(сцен|moodboard|атмосфер|compose|scene|мизансцен)\b/i.test(t)) {
|
if (/\b(moodboard|compose|scene)\b/i.test(t)
|
||||||
|
|| cyrTokenRe('сцен[а-яё]*|атмосфер[а-яё]*|мизансцен[а-яё]*').test(t)) {
|
||||||
return 'compose_scene';
|
return 'compose_scene';
|
||||||
}
|
}
|
||||||
return 'write_prompt';
|
return 'write_prompt';
|
||||||
@@ -3402,11 +3459,15 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldParkLlmBeforeGen() {
|
||||||
|
return !!$('sa_park_llm')?.checked;
|
||||||
|
}
|
||||||
|
|
||||||
/** Unloads the chat model from VRAM so Krea 2 gets the whole GPU. Never touches the embed model. */
|
/** Unloads the chat model from VRAM so Krea 2 gets the whole GPU. Never touches the embed model. */
|
||||||
function parkLlm() {
|
function parkLlm() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const model = $('sa_model')?.value;
|
const model = $('sa_model')?.value;
|
||||||
if (!model || state.llmParked || typeof genericRequest !== 'function') {
|
if (!shouldParkLlmBeforeGen() || !model || state.llmParked || typeof genericRequest !== 'function') {
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3419,6 +3480,7 @@
|
|||||||
settled = true;
|
settled = true;
|
||||||
if (ok) {
|
if (ok) {
|
||||||
state.llmParked = true;
|
state.llmParked = true;
|
||||||
|
state.expectColdLoad = true;
|
||||||
}
|
}
|
||||||
resolve(!!ok);
|
resolve(!!ok);
|
||||||
};
|
};
|
||||||
@@ -3427,15 +3489,31 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fire-and-forget re-load of the chat model once the user is back in the chat. */
|
/** Re-load chat model after park. Returns a Promise (await after Generate so the next Send is warm). */
|
||||||
function warmLlm() {
|
function warmLlm() {
|
||||||
const model = $('sa_model')?.value;
|
return new Promise((resolve) => {
|
||||||
if (!model || !state.llmParked || typeof genericRequest !== 'function') {
|
const model = $('sa_model')?.value;
|
||||||
return;
|
if (!model || !state.llmParked || typeof genericRequest !== 'function') {
|
||||||
}
|
resolve(false);
|
||||||
state.llmParked = false;
|
return;
|
||||||
const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434';
|
}
|
||||||
genericRequest('AssistentWarmLlm', { baseUrl, model }, () => {}, 0, () => {});
|
const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434';
|
||||||
|
let settled = false;
|
||||||
|
const finish = (ok) => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
state.llmParked = false;
|
||||||
|
if (ok) {
|
||||||
|
state.expectColdLoad = false;
|
||||||
|
}
|
||||||
|
resolve(!!ok);
|
||||||
|
};
|
||||||
|
// VL 7B 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));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelWaitForNewImage() {
|
function cancelWaitForNewImage() {
|
||||||
@@ -3528,9 +3606,11 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const prev = findCurrentGenerateSrc();
|
const prev = findCurrentGenerateSrc();
|
||||||
startBusyUi('parking');
|
if (shouldParkLlmBeforeGen()) {
|
||||||
setStatus('Освобождаю VRAM…');
|
startBusyUi('parking');
|
||||||
await parkLlm();
|
setStatus('Освобождаю VRAM…');
|
||||||
|
await parkLlm();
|
||||||
|
}
|
||||||
setStatus('Генерация…');
|
setStatus('Генерация…');
|
||||||
startBusyUi('generating');
|
startBusyUi('generating');
|
||||||
state.generating = true;
|
state.generating = true;
|
||||||
@@ -3545,14 +3625,16 @@
|
|||||||
const src = await waitForNewImage(prev);
|
const src = await waitForNewImage(prev);
|
||||||
state.generating = false;
|
state.generating = false;
|
||||||
setInterruptVisible(state.busy);
|
setInterruptVisible(state.busy);
|
||||||
|
// Auto-critique / next Send load the model themselves — skip warm if critique will run.
|
||||||
|
const paneVisible = !!document.getElementById('swarm_assistent_root')?.offsetParent;
|
||||||
|
if (state.llmParked && state.view === 'chat' && paneVisible && !$('sa_auto_critique')?.checked) {
|
||||||
|
startBusyUi('warming');
|
||||||
|
setStatus('Возвращаю LLM в GPU…');
|
||||||
|
await warmLlm();
|
||||||
|
}
|
||||||
if (!state.busy) {
|
if (!state.busy) {
|
||||||
stopBusyUi(src ? 'Generate готов' : 'Generate завершён (новое изображение не найдено)');
|
stopBusyUi(src ? 'Generate готов' : 'Generate завершён (новое изображение не найдено)');
|
||||||
}
|
}
|
||||||
// Auto-critique loads the model itself on the next request — don't pay for it twice.
|
|
||||||
const paneVisible = !!document.getElementById('swarm_assistent_root')?.offsetParent;
|
|
||||||
if (state.view === 'chat' && paneVisible && !$('sa_auto_critique')?.checked) {
|
|
||||||
warmLlm();
|
|
||||||
}
|
|
||||||
if (src) {
|
if (src) {
|
||||||
const gen = generateSlot();
|
const gen = generateSlot();
|
||||||
if (gen) {
|
if (gen) {
|
||||||
@@ -3777,6 +3859,7 @@
|
|||||||
setAssistantBody(state.streamEl, '');
|
setAssistantBody(state.streamEl, '');
|
||||||
}
|
}
|
||||||
state.gotDelta = true;
|
state.gotDelta = true;
|
||||||
|
state.expectColdLoad = false;
|
||||||
if (state.busyPhase !== 'refining') {
|
if (state.busyPhase !== 'refining') {
|
||||||
setBusyPhase('streaming');
|
setBusyPhase('streaming');
|
||||||
}
|
}
|
||||||
@@ -4161,6 +4244,7 @@
|
|||||||
const autoGen = localStorage.getItem(LS_AUTO_GENERATE);
|
const autoGen = localStorage.getItem(LS_AUTO_GENERATE);
|
||||||
const autoCrit = localStorage.getItem(LS_AUTO_CRITIQUE);
|
const autoCrit = localStorage.getItem(LS_AUTO_CRITIQUE);
|
||||||
const autoDl = localStorage.getItem(LS_AUTO_DOWNLOAD);
|
const autoDl = localStorage.getItem(LS_AUTO_DOWNLOAD);
|
||||||
|
const parkLlm = localStorage.getItem(LS_PARK_LLM);
|
||||||
const paneW = localStorage.getItem(LS_PANE_WIDTH);
|
const paneW = localStorage.getItem(LS_PANE_WIDTH);
|
||||||
if (base && $('sa_base_url')) {
|
if (base && $('sa_base_url')) {
|
||||||
$('sa_base_url').value = base;
|
$('sa_base_url').value = base;
|
||||||
@@ -4186,6 +4270,10 @@
|
|||||||
if ($('sa_auto_download') && autoDl != null) {
|
if ($('sa_auto_download') && autoDl != null) {
|
||||||
$('sa_auto_download').checked = autoDl === '1';
|
$('sa_auto_download').checked = autoDl === '1';
|
||||||
}
|
}
|
||||||
|
// Default OFF — parking a VL 7B before every Generate caused 1–2 min reloads.
|
||||||
|
if ($('sa_park_llm')) {
|
||||||
|
$('sa_park_llm').checked = parkLlm === '1';
|
||||||
|
}
|
||||||
if (model) {
|
if (model) {
|
||||||
state.preferredModel = model;
|
state.preferredModel = model;
|
||||||
}
|
}
|
||||||
@@ -4214,6 +4302,7 @@
|
|||||||
auto_generate: !!$('sa_auto_generate')?.checked,
|
auto_generate: !!$('sa_auto_generate')?.checked,
|
||||||
auto_critique: !!$('sa_auto_critique')?.checked,
|
auto_critique: !!$('sa_auto_critique')?.checked,
|
||||||
auto_download: !!$('sa_auto_download')?.checked,
|
auto_download: !!$('sa_auto_download')?.checked,
|
||||||
|
park_llm: !!$('sa_park_llm')?.checked,
|
||||||
pane_width: localStorage.getItem(LS_PANE_WIDTH) || '',
|
pane_width: localStorage.getItem(LS_PANE_WIDTH) || '',
|
||||||
embed_model: $('sa_embed_model')?.value || state.preferredEmbed || '',
|
embed_model: $('sa_embed_model')?.value || state.preferredEmbed || '',
|
||||||
base_url: $('sa_base_url')?.value || '',
|
base_url: $('sa_base_url')?.value || '',
|
||||||
@@ -4267,6 +4356,7 @@
|
|||||||
['auto_generate', LS_AUTO_GENERATE, 'sa_auto_generate'],
|
['auto_generate', LS_AUTO_GENERATE, 'sa_auto_generate'],
|
||||||
['auto_critique', LS_AUTO_CRITIQUE, 'sa_auto_critique'],
|
['auto_critique', LS_AUTO_CRITIQUE, 'sa_auto_critique'],
|
||||||
['auto_download', LS_AUTO_DOWNLOAD, 'sa_auto_download'],
|
['auto_download', LS_AUTO_DOWNLOAD, 'sa_auto_download'],
|
||||||
|
['park_llm', LS_PARK_LLM, 'sa_park_llm'],
|
||||||
]) {
|
]) {
|
||||||
if (ui[key] == null || localStorage.getItem(lsKey) != null) {
|
if (ui[key] == null || localStorage.getItem(lsKey) != null) {
|
||||||
continue;
|
continue;
|
||||||
@@ -4299,6 +4389,7 @@
|
|||||||
localStorage.setItem(LS_AUTO_GENERATE, $('sa_auto_generate')?.checked ? '1' : '0');
|
localStorage.setItem(LS_AUTO_GENERATE, $('sa_auto_generate')?.checked ? '1' : '0');
|
||||||
localStorage.setItem(LS_AUTO_CRITIQUE, $('sa_auto_critique')?.checked ? '1' : '0');
|
localStorage.setItem(LS_AUTO_CRITIQUE, $('sa_auto_critique')?.checked ? '1' : '0');
|
||||||
localStorage.setItem(LS_AUTO_DOWNLOAD, $('sa_auto_download')?.checked ? '1' : '0');
|
localStorage.setItem(LS_AUTO_DOWNLOAD, $('sa_auto_download')?.checked ? '1' : '0');
|
||||||
|
localStorage.setItem(LS_PARK_LLM, $('sa_park_llm')?.checked ? '1' : '0');
|
||||||
persistServerSettings();
|
persistServerSettings();
|
||||||
saveUiStateToDisk();
|
saveUiStateToDisk();
|
||||||
}
|
}
|
||||||
@@ -4418,11 +4509,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let controlSaveTimer = null;
|
let controlSaveTimer = null;
|
||||||
|
let controlsPointerDown = false;
|
||||||
|
let pendingControlsRender = null;
|
||||||
|
|
||||||
function renderPersonaControls(schema, values) {
|
function renderPersonaControls(schema, values) {
|
||||||
const box = $('sa_persona_controls');
|
const box = $('sa_persona_controls');
|
||||||
if (!box) {
|
if (!box) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Don't rebuild DOM while the user is dragging — that snaps the thumb back.
|
||||||
|
if (controlsPointerDown) {
|
||||||
|
pendingControlsRender = { schema, values };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingControlsRender = null;
|
||||||
box.innerHTML = '';
|
box.innerHTML = '';
|
||||||
const keys = schema && typeof schema === 'object' ? Object.keys(schema) : [];
|
const keys = schema && typeof schema === 'object' ? Object.keys(schema) : [];
|
||||||
if (!keys.length) {
|
if (!keys.length) {
|
||||||
@@ -4471,16 +4571,40 @@
|
|||||||
const valEl = document.createElement('span');
|
const valEl = document.createElement('span');
|
||||||
valEl.className = 'sa-control-val';
|
valEl.className = 'sa-control-val';
|
||||||
valEl.textContent = fmt(cur);
|
valEl.textContent = fmt(cur);
|
||||||
const onInput = () => {
|
const applyLocal = (v) => {
|
||||||
const v = Number(input.value);
|
|
||||||
valEl.textContent = fmt(v);
|
valEl.textContent = fmt(v);
|
||||||
|
if (state.config) {
|
||||||
|
state.config.control_values = { ...(state.config.control_values || {}), [id]: v };
|
||||||
|
}
|
||||||
|
if (state.exact) {
|
||||||
|
state.exact.controls = { ...(state.exact.controls || {}), [id]: v };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.addEventListener('pointerdown', () => {
|
||||||
|
controlsPointerDown = true;
|
||||||
|
});
|
||||||
|
const endPointer = () => {
|
||||||
|
controlsPointerDown = false;
|
||||||
|
if (pendingControlsRender) {
|
||||||
|
const pending = pendingControlsRender;
|
||||||
|
pendingControlsRender = null;
|
||||||
|
renderPersonaControls(pending.schema, pending.values);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.addEventListener('pointerup', endPointer);
|
||||||
|
input.addEventListener('pointercancel', endPointer);
|
||||||
|
// Live label while dragging; persist only on release (change) so mid-drag saves cannot snap back.
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
applyLocal(Number(input.value));
|
||||||
|
});
|
||||||
|
input.addEventListener('change', () => {
|
||||||
|
const v = Number(input.value);
|
||||||
|
applyLocal(v);
|
||||||
if (controlSaveTimer) {
|
if (controlSaveTimer) {
|
||||||
clearTimeout(controlSaveTimer);
|
clearTimeout(controlSaveTimer);
|
||||||
}
|
}
|
||||||
controlSaveTimer = setTimeout(() => savePersonaControls({ [id]: v }), 350);
|
controlSaveTimer = setTimeout(() => savePersonaControls({ [id]: v }), 50);
|
||||||
};
|
});
|
||||||
input.addEventListener('input', onInput);
|
|
||||||
input.addEventListener('change', onInput);
|
|
||||||
row.appendChild(lab);
|
row.appendChild(lab);
|
||||||
row.appendChild(input);
|
row.appendChild(input);
|
||||||
row.appendChild(valEl);
|
row.appendChild(valEl);
|
||||||
@@ -4509,11 +4633,18 @@
|
|||||||
const max = Number(schema.horny.max ?? 100);
|
const max = Number(schema.horny.max ?? 100);
|
||||||
const cur = getControlValue('horny', Number(schema.horny.default ?? 35));
|
const cur = getControlValue('horny', Number(schema.horny.default ?? 35));
|
||||||
const next = Math.max(min, Math.min(max, cur - 30));
|
const next = Math.max(min, Math.min(max, cur - 30));
|
||||||
savePersonaControls({ horny: next });
|
const values = {
|
||||||
renderPersonaControls(schema, {
|
|
||||||
...(state.config?.control_values || state.exact?.controls || {}),
|
...(state.config?.control_values || state.exact?.controls || {}),
|
||||||
horny: next,
|
horny: next,
|
||||||
});
|
};
|
||||||
|
if (state.config) {
|
||||||
|
state.config.control_values = values;
|
||||||
|
}
|
||||||
|
if (state.exact) {
|
||||||
|
state.exact.controls = values;
|
||||||
|
}
|
||||||
|
renderPersonaControls(schema, values);
|
||||||
|
savePersonaControls({ horny: next });
|
||||||
appendSystemNote(`Хорни: ${Math.round(cur)}% → ${Math.round(next)}% (−30)`);
|
appendSystemNote(`Хорни: ${Math.round(cur)}% → ${Math.round(next)}% (−30)`);
|
||||||
setStatus(`/остынь → ${Math.round(next)}%`);
|
setStatus(`/остынь → ${Math.round(next)}%`);
|
||||||
}
|
}
|
||||||
@@ -4544,6 +4675,15 @@
|
|||||||
if (typeof genericRequest !== 'function') {
|
if (typeof genericRequest !== 'function') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Optimistic local merge so UI / next chat see the new values immediately.
|
||||||
|
if (partial && typeof partial === 'object') {
|
||||||
|
if (state.config) {
|
||||||
|
state.config.control_values = { ...(state.config.control_values || {}), ...partial };
|
||||||
|
}
|
||||||
|
if (state.exact) {
|
||||||
|
state.exact.controls = { ...(state.exact.controls || {}), ...partial };
|
||||||
|
}
|
||||||
|
}
|
||||||
genericRequest(
|
genericRequest(
|
||||||
'AssistentSaveControls',
|
'AssistentSaveControls',
|
||||||
{ persona, controls: partial || {} },
|
{ persona, controls: partial || {} },
|
||||||
@@ -4558,8 +4698,10 @@
|
|||||||
state.exact.controls = data.control_values;
|
state.exact.controls = data.control_values;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data?.controls) {
|
// Do not rebuild slider DOM here — that interrupts an in-progress drag and snaps values back.
|
||||||
renderPersonaControls(data.controls, data.control_values || {});
|
// Update live inputs in place if present and not being dragged.
|
||||||
|
if (!controlsPointerDown && data?.control_values) {
|
||||||
|
syncPersonaControlInputs(data.control_values);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
@@ -4567,6 +4709,30 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncPersonaControlInputs(values) {
|
||||||
|
const box = $('sa_persona_controls');
|
||||||
|
if (!box || !values || typeof values !== 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
box.querySelectorAll('input[data-control-id]').forEach((input) => {
|
||||||
|
const id = input.dataset.controlId;
|
||||||
|
if (values[id] == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const v = Number(values[id]);
|
||||||
|
if (!Number.isFinite(v) || input.value === String(v)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.value = String(v);
|
||||||
|
const valEl = input.parentElement?.querySelector('.sa-control-val');
|
||||||
|
if (valEl) {
|
||||||
|
const schema = state.config?.controls?.[id];
|
||||||
|
const asPercent = String(schema?.display || '').toLowerCase() === 'percent';
|
||||||
|
valEl.textContent = asPercent ? `${Math.round(v)}%` : Number(v).toFixed(2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteCurrentOverlayPersona() {
|
async function deleteCurrentOverlayPersona() {
|
||||||
const id = $('sa_persona')?.value;
|
const id = $('sa_persona')?.value;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@@ -6990,10 +7156,14 @@
|
|||||||
|
|
||||||
const chatEpoch = bumpChatEpoch();
|
const chatEpoch = bumpChatEpoch();
|
||||||
state.busy = true;
|
state.busy = true;
|
||||||
// Ollama reloads the model for this request (keep_alive 15m), so it is no longer parked.
|
// 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;
|
||||||
|
}
|
||||||
state.llmParked = false;
|
state.llmParked = false;
|
||||||
setInterruptVisible(true);
|
setInterruptVisible(true);
|
||||||
startBusyUi('thinking');
|
startBusyUi(state.expectColdLoad ? 'loading' : 'thinking');
|
||||||
saveSettings();
|
saveSettings();
|
||||||
|
|
||||||
// Always pull latest LoRA/checkpoint list before the LLM sees context
|
// Always pull latest LoRA/checkpoint list before the LLM sees context
|
||||||
@@ -7079,7 +7249,13 @@
|
|||||||
// Refresh cards into context after prefetch
|
// Refresh cards into context after prefetch
|
||||||
const refreshed = collectLiveContext();
|
const refreshed = collectLiveContext();
|
||||||
context.model_cards = refreshed.model_cards;
|
context.model_cards = refreshed.model_cards;
|
||||||
const messages = state.history.slice(-historyMessageLimit()).map((m) => ({ role: m.role, content: m.content }));
|
const messages = state.history.slice(-historyMessageLimit()).map((m) => {
|
||||||
|
let content = String(m.content || '');
|
||||||
|
if (m.role === 'assistant') {
|
||||||
|
content = stripJsonFencesForHistory(content);
|
||||||
|
}
|
||||||
|
return { role: m.role, content: content.slice(0, 4000) };
|
||||||
|
});
|
||||||
if (images && messages.length) {
|
if (images && messages.length) {
|
||||||
messages[messages.length - 1].images = images;
|
messages[messages.length - 1].images = images;
|
||||||
}
|
}
|
||||||
@@ -7127,10 +7303,14 @@
|
|||||||
if (chatEpoch !== state.chatEpoch) {
|
if (chatEpoch !== state.chatEpoch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.busy = false;
|
// Keep busy while silent Apply+Generate is still running (generating flag).
|
||||||
setInterruptVisible(state.generating);
|
|
||||||
if (!state.generating) {
|
if (!state.generating) {
|
||||||
|
state.busy = false;
|
||||||
|
setInterruptVisible(false);
|
||||||
stopBusyUi('Готово');
|
stopBusyUi('Готово');
|
||||||
|
} else {
|
||||||
|
state.busy = false;
|
||||||
|
setInterruptVisible(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -7163,10 +7343,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data.phase === 'waiting_ollama') {
|
if (data.phase === 'waiting_ollama') {
|
||||||
setBusyPhase('loading');
|
// Server always emits this before /api/chat — not proof of a cold load.
|
||||||
|
setBusyPhase(state.expectColdLoad ? 'loading' : 'waiting');
|
||||||
const label = state.streamEl?.querySelector('.sa-typing-label');
|
const label = state.streamEl?.querySelector('.sa-typing-label');
|
||||||
if (label) {
|
if (label) {
|
||||||
label.textContent = `Загружаю ${modelShort(model)} в GPU…`;
|
label.textContent = state.expectColdLoad
|
||||||
|
? `Загружаю ${modelShort(model)} в GPU…`
|
||||||
|
: 'Думаю…';
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -7748,6 +7931,7 @@
|
|||||||
$('sa_auto_generate')?.addEventListener('change', saveSettings);
|
$('sa_auto_generate')?.addEventListener('change', saveSettings);
|
||||||
$('sa_auto_critique')?.addEventListener('change', saveSettings);
|
$('sa_auto_critique')?.addEventListener('change', saveSettings);
|
||||||
$('sa_auto_download')?.addEventListener('change', saveSettings);
|
$('sa_auto_download')?.addEventListener('change', saveSettings);
|
||||||
|
$('sa_park_llm')?.addEventListener('change', saveSettings);
|
||||||
|
|
||||||
syncChipHighlight();
|
syncChipHighlight();
|
||||||
setInterval(syncChipHighlight, 2500);
|
setInterval(syncChipHighlight, 2500);
|
||||||
|
|||||||
@@ -24,11 +24,29 @@ window.SA = window.SA || {};
|
|||||||
return obj[key] !== undefined && obj[key] !== null;
|
return obj[key] !== undefined && obj[key] !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** True when the object looks like a generation patch rather than arbitrary JSON. */
|
/** Model-card JSON (catalog_card) — must not be treated as a Generate patch. */
|
||||||
|
function isCardObject(obj) {
|
||||||
|
if (!obj || typeof obj !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint);
|
||||||
|
const genish = !!(obj.prompt != null || obj.loras || obj.actions || obj.width || obj.height
|
||||||
|
|| obj.steps || obj.cfg || obj.aspect || obj.seed != null
|
||||||
|
|| obj.search_query || obj.civitai_query || obj.look_at || obj.controls);
|
||||||
|
if (cardish && !genish && (obj.name || obj.triggers || obj.when)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !!(obj.kind && obj.name && (obj.triggers || obj.when || obj.prompt_hint || obj.notes != null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when the object looks like a generation patch rather than a catalog card / arbitrary JSON. */
|
||||||
function isPatchObject(obj) {
|
function isPatchObject(obj) {
|
||||||
if (!obj || typeof obj !== 'object') {
|
if (!obj || typeof obj !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (isCardObject(obj)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return PATCH_KEYS.some((k) => has(obj, k));
|
return PATCH_KEYS.some((k) => has(obj, k));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +93,9 @@ window.SA = window.SA || {};
|
|||||||
}
|
}
|
||||||
|
|
||||||
SA.PATCH_KEYS = PATCH_KEYS;
|
SA.PATCH_KEYS = PATCH_KEYS;
|
||||||
|
SA.isCardObject = isCardObject;
|
||||||
SA.isPatchObject = isPatchObject;
|
SA.isPatchObject = isPatchObject;
|
||||||
SA.normalizePatch = normalizePatch;
|
SA.normalizePatch = normalizePatch;
|
||||||
SA.extractPatch = extractPatch;
|
SA.extractPatch = extractPatch;
|
||||||
})();
|
})();
|
||||||
|
␍
|
||||||
@@ -483,7 +483,19 @@ public partial class SwarmAssistentExtension
|
|||||||
if (tool == "civitai")
|
if (tool == "civitai")
|
||||||
{
|
{
|
||||||
string query = ExtractSearchQuery(patch);
|
string query = ExtractSearchQuery(patch);
|
||||||
if (string.IsNullOrWhiteSpace(query) || !hopDone.Add("civitai:" + query))
|
if (string.IsNullOrWhiteSpace(query))
|
||||||
|
{
|
||||||
|
if (!hopDone.Add("civitai:missing_query"))
|
||||||
|
{
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
"search_civitai skipped: provide a short search_query (LoRA keywords only). "
|
||||||
|
+ "Never search with the whole user message. Then retry with actions:[\"search_civitai\"] + search_query, "
|
||||||
|
+ "or continue using available_loras only.",
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
if (!hopDone.Add("civitai:" + query))
|
||||||
{
|
{
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1154,6 +1154,9 @@ public sealed class AssistentConfig
|
|||||||
{
|
{
|
||||||
sb.AppendLine($"*{tagline}*");
|
sb.AppendLine($"*{tagline}*");
|
||||||
}
|
}
|
||||||
|
sb.AppendLine($"**You are {title}** (the assistant in this chat). The human you talk to is the **user** — a different person.");
|
||||||
|
sb.AppendLine($"Never address or name the user «{title}» unless `## About the user` explicitly says that is their name.");
|
||||||
|
sb.AppendLine($"If asked your name («как тебя зовут?» / «who are you?»), answer with **{title}** — do not greet the user by that name instead.");
|
||||||
|
|
||||||
HashSet<string> allow = null;
|
HashSet<string> allow = null;
|
||||||
if (onlyShelves is not null)
|
if (onlyShelves is not null)
|
||||||
|
|||||||
+1
-1
@@ -284,7 +284,7 @@ public partial class SwarmAssistentExtension
|
|||||||
await ws.SendJson(new JObject
|
await ws.SendJson(new JObject
|
||||||
{
|
{
|
||||||
["phase"] = "waiting_ollama",
|
["phase"] = "waiting_ollama",
|
||||||
["notice"] = "Loading model into GPU…",
|
["notice"] = "Waiting for Ollama…",
|
||||||
}, API.WebsocketTimeout);
|
}, API.WebsocketTimeout);
|
||||||
}
|
}
|
||||||
async Task OnDelta(string delta)
|
async Task OnDelta(string delta)
|
||||||
|
|||||||
+24
-1
@@ -58,6 +58,25 @@ public partial class SwarmAssistentExtension
|
|||||||
return patch;
|
return patch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool LooksLikeCardObject(JObject obj)
|
||||||
|
{
|
||||||
|
if (obj is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool cardish = HasValue(obj, "kind") || HasValue(obj, "triggers") || HasValue(obj, "when") || HasValue(obj, "prompt_hint");
|
||||||
|
bool genish = HasValue(obj, "prompt") || HasValue(obj, "loras") || HasValue(obj, "actions")
|
||||||
|
|| HasValue(obj, "width") || HasValue(obj, "height") || HasValue(obj, "steps") || HasValue(obj, "cfg")
|
||||||
|
|| HasValue(obj, "aspect") || HasValue(obj, "seed") || HasValue(obj, "search_query")
|
||||||
|
|| HasValue(obj, "civitai_query") || HasValue(obj, "look_at") || HasValue(obj, "controls");
|
||||||
|
if (cardish && !genish && (HasValue(obj, "name") || HasValue(obj, "triggers") || HasValue(obj, "when")))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return HasValue(obj, "kind") && HasValue(obj, "name")
|
||||||
|
&& (HasValue(obj, "triggers") || HasValue(obj, "when") || HasValue(obj, "prompt_hint") || HasValue(obj, "notes"));
|
||||||
|
}
|
||||||
|
|
||||||
static JObject TryParsePatch(string reply)
|
static JObject TryParsePatch(string reply)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(reply))
|
if (string.IsNullOrWhiteSpace(reply))
|
||||||
@@ -70,7 +89,11 @@ public partial class SwarmAssistentExtension
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
JObject obj = JObject.Parse(raw);
|
JObject obj = JObject.Parse(raw);
|
||||||
if (obj is not null && Array.Exists(PatchKeys, k => obj[k] is not null))
|
if (obj is null || LooksLikeCardObject(obj))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Array.Exists(PatchKeys, k => obj[k] is not null))
|
||||||
{
|
{
|
||||||
return NormalizePatch(obj);
|
return NormalizePatch(obj);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Swarm Assistent — core contract
|
# Swarm Assistent — core contract
|
||||||
|
|
||||||
You are **Swarm Assistent**, a collaborative art director for image generation inside SwarmUI.
|
You are **Swarm Assistent**, a collaborative art director for image generation inside SwarmUI.
|
||||||
|
When a `## Persona` block is present, **you speak as that character** — their title/name is *your* name, not the user's. Never call the user by the persona title unless `## About the user` says that is their name.
|
||||||
|
|
||||||
## Priority (mandatory)
|
## Priority (mandatory)
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ Never write a “JSON Patch” section in prose without an actual fenced ```json
|
|||||||
### Actions / hops
|
### Actions / hops
|
||||||
|
|
||||||
- `"generate"` — Apply + start generation when the user wants a new image.
|
- `"generate"` — Apply + start generation when the user wants a new image.
|
||||||
- `"search_civitai"` + `search_query` — Civitai hop (user Confirms downloads).
|
- `"search_civitai"` + **required** short `search_query` — Civitai hop (user Confirms downloads). Without `search_query` the hop is skipped (never search the whole user message).
|
||||||
- `"interrupt"` — stop generation.
|
- `"interrupt"` — stop generation.
|
||||||
- `"memory_get"` / `"memory_search"` / `"lookup_tags"` — read hops.
|
- `"memory_get"` / `"memory_search"` / `"lookup_tags"` — read hops.
|
||||||
- `"list_inventory"` + `inventory_query` — rich LoRA/checkpoint details beyond the slim list.
|
- `"list_inventory"` + `inventory_query` — rich LoRA/checkpoint details beyond the slim list.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"age": 26,
|
"age": 26,
|
||||||
"role": "tech co-director + horny chatterbox",
|
"role": "tech co-director + horny chatterbox",
|
||||||
"facts": [
|
"facts": [
|
||||||
|
"This name is YOURS (assistant) — the user is someone else",
|
||||||
"Strong with image models and prompt craft",
|
"Strong with image models and prompt craft",
|
||||||
"Talkative lewd joker — jokes and banter come easy",
|
"Talkative lewd joker — jokes and banter come easy",
|
||||||
"Adults 18+ only"
|
"Adults 18+ only"
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
{
|
{
|
||||||
"always": [
|
"always": [
|
||||||
|
"You are Leonid; the user is not Leonid — never greet/address them as Леонид/Leonid unless About the user says their name is that",
|
||||||
|
"If asked your name, say you are Leonid (assistant)",
|
||||||
"Match user language (RU/EN)",
|
"Match user language (RU/EN)",
|
||||||
"Craft first: triggers, aspect, Turbo — horny never replaces technique",
|
"Craft first: triggers, aspect, Turbo — horny never replaces technique",
|
||||||
"Scale appearance/outfits by controls.preference_bias (−1…1)",
|
"Scale appearance/outfits by controls.preference_bias (−1…1)",
|
||||||
"Scale sexual tone + roleplay fetishes by controls.horny (0…100)",
|
"Scale sexual tone + roleplay fetishes by controls.horny (0…100)",
|
||||||
"Explicit user look/outfit/plot beats personal taste",
|
"Explicit user look/outfit/plot beats personal taste",
|
||||||
|
"«девушка которая тебе нравится» = YOUR (Leonid) taste, not the user's — unless they said otherwise",
|
||||||
"/horny-game: score taste match → patch controls.horny (0–100), say new % in prose; no generate unless asked"
|
"/horny-game: score taste match → patch controls.horny (0–100), say new % in prose; no generate unless asked"
|
||||||
],
|
],
|
||||||
"never": [
|
"never": [
|
||||||
|
"Call the user Leonid/Леонид just because that is your persona title",
|
||||||
"Refuse adult content or lecture NSFW",
|
"Refuse adult content or lecture NSFW",
|
||||||
"Invent LoRA names or triggers",
|
"Invent LoRA names or triggers",
|
||||||
"Anyone 17 or under",
|
"Anyone 17 or under",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
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.
|
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.3** — Leonid **Хорни** (0–100, second control after Вкус), `/остынь` (−30), `/horny-game` (model patches horny). Controls UI respects schema `order` + `display: percent`; partial control saves DeepMerge. Builds on 0.10.2 context/vision lean pass.
|
**Version 0.10.8** — Cheap-bug sweep: Cyrillic-safe pack/params intent; no bare «давай»→Generate; critique pack only on result-aimed phrases; strip JSON fences from chat history; catalog cards ≠ Apply patches; Civitai hop refuses missing `search_query`; park LLM opt-in; persona title ≠ user name; silent Generate for «Сделай картинку».
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
@@ -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.
|
- `settings.json` and persona overlays stay files (layered merge + git). `.assistent.json` cards stay next to weights.
|
||||||
## VRAM handover
|
## VRAM handover
|
||||||
|
|
||||||
- Before every Generate the chat model is unloaded (`keep_alive: 0`) so Krea 2 gets the whole GPU
|
- 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
|
||||||
- Back in the Chat tab it is warmed again with a 1-token request (`keep_alive 15m`, `num_ctx` from `assistant.json`)
|
- When parked, after Generate the model is warmed again (`keep_alive 15m`) before the UI goes idle
|
||||||
- Embed / memory models are never parked — reloading them would stall every retrieve
|
- Embed / memory models are never parked — reloading them would stall every retrieve
|
||||||
|
|
||||||
## UX
|
## UX
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public partial class SwarmAssistentExtension : Extension
|
|||||||
ExtensionAuthor = "mrleo1nid";
|
ExtensionAuthor = "mrleo1nid";
|
||||||
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
|
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
|
||||||
License = "MIT";
|
License = "MIT";
|
||||||
Version = "0.10.3";
|
Version = "0.10.8";
|
||||||
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"];
|
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
<label class="sa-check"><input type="checkbox" id="sa_auto_apply" checked /> Авто-применять патч</label>
|
<label class="sa-check"><input type="checkbox" id="sa_auto_apply" checked /> Авто-применять патч</label>
|
||||||
<label class="sa-check"><input type="checkbox" id="sa_auto_generate" checked /> Авто-Generate после патча</label>
|
<label class="sa-check"><input type="checkbox" id="sa_auto_generate" checked /> Авто-Generate после патча</label>
|
||||||
<label class="sa-check"><input type="checkbox" id="sa_auto_critique" /> Авто-критика после Generate</label>
|
<label class="sa-check"><input type="checkbox" id="sa_auto_critique" /> Авто-критика после Generate</label>
|
||||||
|
<label class="sa-check" title="Выгружает чат-модель перед Generate (keep_alive:0). Для VL 7B обратная загрузка часто 1–2 мин — включай только если Generate падает по VRAM."><input type="checkbox" id="sa_park_llm" /> Park LLM перед Generate (VRAM)</label>
|
||||||
<label class="sa-check sa-danger" title="Опасно — скачивает без Confirm"><input type="checkbox" id="sa_auto_download" /> Авто-скачивание Civitai (выкл)</label>
|
<label class="sa-check sa-danger" title="Опасно — скачивает без Confirm"><input type="checkbox" id="sa_auto_download" /> Авто-скачивание Civitai (выкл)</label>
|
||||||
<div class="sa-skills-label">Скилы (процедуры)</div>
|
<div class="sa-skills-label">Скилы (процедуры)</div>
|
||||||
<div class="sa-skills-box" id="sa_skills_box"></div>
|
<div class="sa-skills-box" id="sa_skills_box"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user