Ship Assistent 0.10.13: ordinary pack, senior chat default, aspect/critique fixes, prose UI.

Default pack is Обычный; prefer default_chat/senior Ollama tag; client-apply same-but-aspect; leave critique pack after hops; render Critique as Критика instead of raw ### markdown.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-22 03:50:58 +03:00
co-authored by Cursor
parent 6e7272ffc9
commit 43984208fc
18 changed files with 687 additions and 198 deletions
+56 -1
View File
@@ -1154,10 +1154,65 @@
} }
.sa-msg-body { .sa-msg-body {
white-space: pre-wrap;
word-break: break-word; word-break: break-word;
} }
.sa-msg-body.sa-prose {
white-space: normal;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.sa-prose-h {
font-weight: 650;
letter-spacing: 0.01em;
line-height: 1.25;
margin: 0.15rem 0 0.05rem;
color: color-mix(in srgb, currentColor 92%, #fff);
}
.sa-prose-h1 {
font-size: 1.15rem;
}
.sa-prose-h2 {
font-size: 1.05rem;
}
.sa-prose-h3 {
font-size: 0.98rem;
opacity: 0.92;
}
.sa-prose-p {
margin: 0;
line-height: 1.45;
white-space: pre-wrap;
}
.sa-prose-list {
margin: 0.1rem 0 0.2rem 1.1rem;
padding: 0;
line-height: 1.4;
}
.sa-prose-list li {
margin: 0.15rem 0;
}
.sa-prose-code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.88em;
padding: 0.05em 0.3em;
border-radius: 0.25rem;
background: color-mix(in srgb, currentColor 12%, transparent);
}
.sa-prose-gap {
height: 0.35rem;
}
.sa-msg.error { .sa-msg.error {
align-self: stretch; align-self: stretch;
border: 1px solid color-mix(in srgb, #c44 55%, transparent); border: 1px solid color-mix(in srgb, #c44 55%, transparent);
+398 -60
View File
@@ -7,6 +7,9 @@
const LS_MODEL = 'swarm_assistent_model'; const LS_MODEL = 'swarm_assistent_model';
const LS_EMBED = 'swarm_assistent_embed_model'; const LS_EMBED = 'swarm_assistent_embed_model';
const LS_PACK = 'swarm_assistent_pack'; const LS_PACK = 'swarm_assistent_pack';
const LS_PACK_ORDINARY_MIG = 'swarm_assistent_pack_ordinary_v1';
/** One-shot: drop junior chat tags stuck in LS so UI/warm pick preferred/senior. */
const LS_MODEL_SENIOR_MIG = 'swarm_assistent_model_senior_v1';
const LS_PERSONA = 'swarm_assistent_persona'; const LS_PERSONA = 'swarm_assistent_persona';
const LS_VIEW = 'swarm_assistent_view'; const LS_VIEW = 'swarm_assistent_view';
const LS_AUTO_VISION = 'swarm_assistent_auto_vision'; const LS_AUTO_VISION = 'swarm_assistent_auto_vision';
@@ -44,6 +47,11 @@
}; };
let PACK_ALIASES = { let PACK_ALIASES = {
ordinary: 'ordinary',
combine: 'ordinary',
normal: 'ordinary',
general: 'ordinary',
default: 'ordinary',
write: 'write_prompt', write: 'write_prompt',
write_prompt: 'write_prompt', write_prompt: 'write_prompt',
critique: 'critique_image', critique: 'critique_image',
@@ -138,9 +146,11 @@
tasteSaveTimer: null, tasteSaveTimer: null,
streamEl: null, streamEl: null,
streamMeta: null, streamMeta: null,
streamText: '',
critiqueHopUsed: false, critiqueHopUsed: false,
visionHopUsed: false, visionHopUsed: false,
lastSystemChars: 0, lastSystemChars: 0,
lastSystemLayers: null,
lastContextChars: 0, lastContextChars: 0,
busyPhase: 'idle', busyPhase: 'idle',
busyStarted: 0, busyStarted: 0,
@@ -459,6 +469,131 @@
.replace(/"/g, '&quot;'); .replace(/"/g, '&quot;');
} }
/** Known ### headings → RU display labels. JSON Patch is hidden (real UI is the patch strip). */
const PROSE_SECTION_TITLES = {
critique: 'Критика',
критика: 'Критика',
analysis: 'Разбор',
разбор: 'Разбор',
notes: 'Заметки',
заметки: 'Заметки',
summary: 'Кратко',
кратко: 'Кратко',
prompt: 'Промпт',
промпт: 'Промпт',
'improved prompt': 'Промпт',
'next prompt': 'Промпт',
deliverable: 'Итог',
итог: 'Итог',
verdict: 'Вердикт',
вердикт: 'Вердикт',
issues: 'Проблемы',
проблемы: 'Проблемы',
fixes: 'Правки',
правки: 'Правки',
suggestion: 'Предложение',
suggestions: 'Предложения',
предложения: 'Предложения',
};
function localizeProseHeading(raw) {
const cleaned = String(raw || '').replace(/[*_`#]/g, '').trim();
if (!cleaned) {
return null;
}
const key = cleaned.toLowerCase().replace(/\s+/g, ' ');
if (/^json\s*patch$/.test(key) || /^патч$/.test(key) || /^json\s*патч$/.test(key)) {
return null;
}
if (PROSE_SECTION_TITLES[key]) {
return PROSE_SECTION_TITLES[key];
}
// "Critique — blur" → take first token bucket
const head = key.split(/[—:\-|]/)[0].trim();
if (PROSE_SECTION_TITLES[head]) {
return PROSE_SECTION_TITLES[head];
}
return cleaned;
}
function formatProseInline(escapedLine) {
let t = escapedLine;
t = t.replace(/`([^`]+)`/g, '<code class="sa-prose-code">$1</code>');
t = t.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
t = t.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1<em>$2</em>');
return t;
}
/** Lightweight chat prose: ### Critique → «Критика», lists, bold — not a full markdown engine. */
function formatAssistantProseHtml(raw) {
let text = String(raw || '').replace(/\r\n/g, '\n');
text = text.replace(/(?:^|\n)#{1,6}\s*JSON\s*Patch\s*(?=\n|$)/gi, '\n');
text = text.replace(/\n{3,}/g, '\n\n').trim();
if (!text) {
return '';
}
const lines = text.split('\n');
const parts = [];
let listItems = [];
const flushList = () => {
if (!listItems.length) {
return;
}
parts.push(
`<ul class="sa-prose-list">${listItems.map((li) => `<li>${formatProseInline(escapeHtml(li))}</li>`).join('')}</ul>`,
);
listItems = [];
};
for (const line of lines) {
const heading = line.match(/^#{1,3}\s+(.+?)\s*$/);
if (heading) {
flushList();
const title = localizeProseHeading(heading[1]);
if (!title) {
continue;
}
const level = Math.min((line.match(/^#+/) || ['###'])[0].length, 3);
parts.push(
`<div class="sa-prose-h sa-prose-h${level}" role="heading" aria-level="${level}">${escapeHtml(title)}</div>`,
);
continue;
}
const bullet = line.match(/^\s*[-*•]\s+(.+)$/);
if (bullet) {
listItems.push(bullet[1]);
continue;
}
flushList();
if (!line.trim()) {
parts.push('<div class="sa-prose-gap" aria-hidden="true"></div>');
continue;
}
parts.push(`<p class="sa-prose-p">${formatProseInline(escapeHtml(line))}</p>`);
}
flushList();
return parts.join('');
}
function setAssistantBody(div, text) {
if (!div) {
return;
}
let body = div.querySelector('.sa-msg-body');
if (!body) {
body = document.createElement('div');
body.className = 'sa-msg-body';
div.appendChild(body);
}
const raw = text || '';
body.classList.add('sa-prose');
const html = formatAssistantProseHtml(raw);
if (html) {
body.innerHTML = html;
} else {
body.textContent = '';
}
}
function val(id) { function val(id) {
const el = document.getElementById(id); const el = document.getElementById(id);
return el ? el.value : ''; return el ? el.value : '';
@@ -495,12 +630,70 @@
return new RegExp(`${boundary}(?:${alts})${end}`, 'i'); return new RegExp(`${boundary}(?:${alts})${end}`, 'i');
} }
/** Parse aspect from chat: 9:16, 9x16, 9×16, «9 на 16», «9 к 16». */
function parseAspectFromUserText(text) {
const t = String(text || '');
if (!t.trim()) {
return null;
}
const ratio = t.match(/(?:^|[^0-9])(\d+(?:\.\d+)?)\s*[:x×хX]\s*(\d+(?:\.\d+)?)(?=$|[^0-9])/);
if (ratio) {
const key = normalizeAspect(`${ratio[1]}:${ratio[2]}`);
if (key) {
return key;
}
}
const na = t.match(/(?:^|[^0-9])(\d+(?:\.\d+)?)\s*(?:на|к|to)\s*(\d+(?:\.\d+)?)(?=$|[^0-9])/i);
if (na) {
const key = normalizeAspect(`${na[1]}:${na[2]}`);
if (key) {
return key;
}
}
const named = t.match(/\b(16:9|9:16|1:1|4:5|2:3|3:2|4:3|2\.35:1)\b/i);
if (named) {
return normalizeAspect(named[1]);
}
if (cyrTokenRe('портрет|вертикал[а-яё]*').test(t) || /\b(portrait|vertical)\b/i.test(t)) {
return normalizeAspect('9:16') || normalizeAspect('2:3');
}
if (cyrTokenRe('альбом|горизонтал[а-яё]*').test(t) || /\b(landscape|horizontal|widescreen)\b/i.test(t)) {
return normalizeAspect('16:9');
}
return null;
}
/** «такую же, только 9 на 16» — keep prompt, change aspect, generate (no LLM needed). */
function isSameButAspectRequest(text) {
const t = String(text || '');
if (!parseAspectFromUserText(t)) {
return false;
}
return /такую\s+же|тот\s+же\s+промпт|same\s+(one|prompt|thing|again)|только\s+(поменя|смени|поставь)|поменяй\s+на|смени\s+на|only\s+change|just\s+change/i.test(t)
|| /поменяй\s+(размер|aspect|соотношен)/i.test(t)
|| /смени\s+(размер|aspect|соотношен)/i.test(t);
}
function userTextMentionsParams(text) { function userTextMentionsParams(text) {
const t = String(text || ''); const t = String(text || '');
if (parseAspectFromUserText(t)) {
return true;
}
if (/\b(steps?|cfg|seed|sigma|aspect|resolution|batch|turbo|raw)\b/i.test(t)) { if (/\b(steps?|cfg|seed|sigma|aspect|resolution|batch|turbo|raw)\b/i.test(t)) {
return true; return true;
} }
return cyrTokenRe('размер|ширин[а-яё]*|высот[а-яё]*|турбо').test(t); return cyrTokenRe('размер|ширин[а-яё]*|высот[а-яё]*|соотношен[а-яё]*|турбо|портрет|вертикал[а-яё]*').test(t);
}
function replyMissingJsonPatch(reply) {
const t = String(reply || '');
if (!t.trim()) {
return false;
}
if (/```(?:json)?\s*\{[\s\S]*?\}```/i.test(t)) {
return false;
}
return /###\s*JSON\s*Patch\b/i.test(t) || /JSON\s*Patch\s*:?\s*$/im.test(t);
} }
function userAsksGenerate(text) { function userAsksGenerate(text) {
@@ -548,13 +741,20 @@
} }
} }
function defaultPackId() {
return state.config?.assistant?.default_pack
|| $('sa_pack')?.querySelector('option')?.value
|| 'ordinary';
}
function syncModeBadge() { function syncModeBadge() {
const badge = $('sa_mode_badge'); const badge = $('sa_mode_badge');
const pack = $('sa_pack')?.value || 'write_prompt'; const pack = $('sa_pack')?.value || defaultPackId();
if (!badge) { if (!badge) {
return; return;
} }
const shortMap = { const shortMap = {
ordinary: 'обычный',
write_prompt: 'write', write_prompt: 'write',
critique_image: 'critique', critique_image: 'critique',
compose_scene: 'compose', compose_scene: 'compose',
@@ -562,6 +762,7 @@
inpaint_edit: 'inpaint', inpaint_edit: 'inpaint',
describe_ref: 'describe', describe_ref: 'describe',
catalog_card: 'card', catalog_card: 'card',
author_persona: 'persona',
}; };
const short = shortMap[pack] || pack.replace(/_/g, ' ').slice(0, 12); const short = shortMap[pack] || pack.replace(/_/g, ' ').slice(0, 12);
badge.textContent = short; badge.textContent = short;
@@ -1406,6 +1607,9 @@
function stripJsonFencesForHistory(content) { function stripJsonFencesForHistory(content) {
return String(content || '') return String(content || '')
.replace(/```(?:json)?\s*[\s\S]*?```/gi, '') .replace(/```(?:json)?\s*[\s\S]*?```/gi, '')
// Drop echoed critique templates / empty patch headers so the next turn doesn't copy them.
.replace(/###\s*Critique\b[\s\S]*?(?=###|$)/gi, '')
.replace(/###\s*JSON\s*Patch\b[\s\S]*$/gi, '')
.replace(/\n{3,}/g, '\n\n') .replace(/\n{3,}/g, '\n\n')
.trim(); .trim();
} }
@@ -1472,7 +1676,7 @@
batch: parseInt(val('input_images') || val('input_batchsize') || '0', 10) || null, batch: parseInt(val('input_images') || val('input_batchsize') || '0', 10) || null,
loras, loras,
persona: $('sa_persona')?.value || 'neutral', persona: $('sa_persona')?.value || 'neutral',
pack: $('sa_pack')?.value || 'write_prompt', pack: $('sa_pack')?.value || defaultPackId(),
sessionExact, sessionExact,
lastPatch, lastPatch,
}; };
@@ -2487,11 +2691,11 @@
} }
return out; return out;
}); });
ctx.enabled_loras = ctx.selected_loras; // selected_loras = enabled; do not also emit enabled_loras (duplicate).
} }
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
// Recommendation cards: checkpoint + selected LoRAs only (no has_card sweep). // Recommendation cards: checkpoint + selected LoRAs only when they add beyond inventory.
const cardKeys = []; const cardKeys = [];
const seenCard = new Set(); const seenCard = new Set();
const addKey = (kind, name) => { const addKey = (kind, name) => {
@@ -2515,9 +2719,24 @@
} }
for (const k of cardKeys) { for (const k of cardKeys) {
const cached = state.modelCards[`${k.kind}:${k.name}`]; const cached = state.modelCards[`${k.kind}:${k.name}`];
if (cached) { if (!cached) {
ctx.model_cards.push(slimCardForContext(cached)); continue;
} }
const slim = slimCardForContext(cached);
if (!slim) {
continue;
}
if (k.kind === 'lora') {
const sel = (ctx.selected_loras || []).find(
(l) => String(l.name || '').toLowerCase() === String(k.name).toLowerCase(),
);
const inventoryRich = !!(sel && (sel.triggers?.length || sel.trigger_phrase || sel.blurb));
const cardExtra = !!(slim.when || slim.avoid || slim.prompt_hint || slim.notes);
if (inventoryRich && !cardExtra) {
continue;
}
}
ctx.model_cards.push(slim);
} }
// Fallback if inventory empty // Fallback if inventory empty
@@ -3037,10 +3256,19 @@
if (state.packUserTouched) { if (state.packUserTouched) {
return null; return null;
} }
// Комбайн «Обычный» сам выбирает поведение — не переключаем pack.
const cur = $('sa_pack')?.value || defaultPackId();
if (cur === 'ordinary') {
return null;
}
const t = String(text || '').toLowerCase(); const t = String(text || '').toLowerCase();
if (!t.trim()) { if (!t.trim()) {
return null; return null;
} }
// Param / aspect asks must leave a stuck critique_image pack from auto-critique.
if (userTextMentionsParams(t) || parseAspectFromUserText(t)) {
return cur === 'critique_image' || cur === 'describe_ref' ? 'ordinary' : 'form_params';
}
if (cyrTokenRe('поправь|исправь|перепиши|улучши').test(t) if (cyrTokenRe('поправь|исправь|перепиши|улучши').test(t)
|| /\b(fix\s+it|make\s+it\s+better|rewrite)\b/i.test(t)) { || /\b(fix\s+it|make\s+it\s+better|rewrite)\b/i.test(t)) {
return 'write_prompt'; return 'write_prompt';
@@ -3061,8 +3289,9 @@
|| /init\s*image/i.test(t)) { || /init\s*image/i.test(t)) {
return 'inpaint_edit'; return 'inpaint_edit';
} }
if (userTextMentionsParams(t)) { // Any non-critique follow-up after auto-critique should leave critique mode.
return 'form_params'; if (cur === 'critique_image') {
return 'ordinary';
} }
if (/\b(moodboard|compose|scene)\b/i.test(t) if (/\b(moodboard|compose|scene)\b/i.test(t)
|| cyrTokenRe('сцен[а-яё]*|атмосфер[а-яё]*|мизансцен[а-яё]*').test(t)) { || cyrTokenRe('сцен[а-яё]*|атмосфер[а-яё]*|мизансцен[а-яё]*').test(t)) {
@@ -3071,6 +3300,16 @@
return 'write_prompt'; return 'write_prompt';
} }
function restoreDefaultPackAfterHop() {
if (state.packUserTouched) {
return;
}
const cur = $('sa_pack')?.value || '';
if (cur === 'critique_image' || cur === 'describe_ref') {
setPackValue(defaultPackId(), { flash: true });
}
}
function patchHasGenTrigger(patch) { function patchHasGenTrigger(patch) {
if (!patch) { if (!patch) {
return false; return false;
@@ -3689,6 +3928,7 @@
} }
setStatus('Auto-critique…'); setStatus('Auto-critique…');
await sendChat({ fromAutoCritique: true, forceSlotIds: [GEN_ID] }); await sendChat({ fromAutoCritique: true, forceSlotIds: [GEN_ID] });
restoreDefaultPackAfterHop();
} }
/** After Generate: send look_at with JPEG when sa_auto_vision is on (skipped if auto-critique already attaches vision). */ /** After Generate: send look_at with JPEG when sa_auto_vision is on (skipped if auto-critique already attaches vision). */
@@ -3713,6 +3953,7 @@
} }
setStatus('Auto look_at…'); setStatus('Auto look_at…');
await sendChat({ fromVisionHop: true, forceSlotIds: [GEN_ID], skipAutoPack: true }); await sendChat({ fromVisionHop: true, forceSlotIds: [GEN_ID], skipAutoPack: true });
restoreDefaultPackAfterHop();
} }
/** Board action: attach the finished Generate frame and ask for a verdict. */ /** Board action: attach the finished Generate frame and ask for a verdict. */
@@ -3775,7 +4016,7 @@
chip.textContent = persona.title || persona.id; chip.textContent = persona.title || persona.id;
chip.title = `Характер: ${persona.title || persona.id}${pack ? ` · режим ${pack}` : ''}`; chip.title = `Характер: ${persona.title || persona.id}${pack ? ` · режим ${pack}` : ''}`;
row.appendChild(chip); row.appendChild(chip);
if (pack && pack !== 'write_prompt') { if (pack && pack !== 'ordinary' && pack !== 'write_prompt') {
const packEl = document.createElement('span'); const packEl = document.createElement('span');
packEl.className = 'sa-pack-mark'; packEl.className = 'sa-pack-mark';
packEl.textContent = pack.replace(/_/g, ' '); packEl.textContent = pack.replace(/_/g, ' ');
@@ -3785,19 +4026,6 @@
div.insertBefore(row, div.firstChild); div.insertBefore(row, div.firstChild);
} }
function setAssistantBody(div, text) {
if (!div) {
return;
}
let body = div.querySelector('.sa-msg-body');
if (!body) {
body = document.createElement('div');
body.className = 'sa-msg-body';
div.appendChild(body);
}
body.textContent = text || '';
}
function appendMessage(role, text, patch, civitaiResults, meta) { function appendMessage(role, text, patch, civitaiResults, meta) {
const box = $('sa_messages'); const box = $('sa_messages');
if (!box) { if (!box) {
@@ -3856,6 +4084,7 @@
if (state.streamEl) { if (state.streamEl) {
if (state.streamEl.classList.contains('sa-typing')) { if (state.streamEl.classList.contains('sa-typing')) {
state.streamEl.classList.remove('sa-typing'); state.streamEl.classList.remove('sa-typing');
state.streamText = '';
setAssistantBody(state.streamEl, ''); setAssistantBody(state.streamEl, '');
} }
state.gotDelta = true; state.gotDelta = true;
@@ -3863,12 +4092,8 @@
if (state.busyPhase !== 'refining') { if (state.busyPhase !== 'refining') {
setBusyPhase('streaming'); setBusyPhase('streaming');
} }
let body = state.streamEl.querySelector('.sa-msg-body'); state.streamText = (state.streamText || '') + (delta || '');
if (!body) { setAssistantBody(state.streamEl, state.streamText);
setAssistantBody(state.streamEl, '');
body = state.streamEl.querySelector('.sa-msg-body');
}
body.textContent += delta;
const box = $('sa_messages'); const box = $('sa_messages');
if (box) { if (box) {
box.scrollTop = box.scrollHeight; box.scrollTop = box.scrollHeight;
@@ -3881,6 +4106,7 @@
const meta = state.streamMeta; const meta = state.streamMeta;
state.streamEl = null; state.streamEl = null;
state.streamMeta = null; state.streamMeta = null;
state.streamText = '';
if (!el) { if (!el) {
appendMessage('assistant', fullReply, null, civitaiResults, meta || undefined); appendMessage('assistant', fullReply, null, civitaiResults, meta || undefined);
return; return;
@@ -4234,6 +4460,13 @@
} }
function loadSettings() { function loadSettings() {
// One-shot: old default was write_prompt → migrate to ordinary комбайн.
if (!localStorage.getItem(LS_PACK_ORDINARY_MIG)) {
if (localStorage.getItem(LS_PACK) === 'write_prompt') {
localStorage.setItem(LS_PACK, 'ordinary');
}
localStorage.setItem(LS_PACK_ORDINARY_MIG, '1');
}
const base = localStorage.getItem(LS_BASE); const base = localStorage.getItem(LS_BASE);
const model = localStorage.getItem(LS_MODEL); const model = localStorage.getItem(LS_MODEL);
const pack = localStorage.getItem(LS_PACK); const pack = localStorage.getItem(LS_PACK);
@@ -4295,7 +4528,7 @@
function collectUiState() { function collectUiState() {
return { return {
pack: $('sa_pack')?.value || 'write_prompt', pack: $('sa_pack')?.value || defaultPackId(),
persona: $('sa_persona')?.value || 'neutral', persona: $('sa_persona')?.value || 'neutral',
auto_vision: !!$('sa_auto_vision')?.checked, auto_vision: !!$('sa_auto_vision')?.checked,
auto_apply: !!$('sa_auto_apply')?.checked, auto_apply: !!$('sa_auto_apply')?.checked,
@@ -4381,7 +4614,7 @@
localStorage.setItem(LS_BASE, $('sa_base_url')?.value || ''); localStorage.setItem(LS_BASE, $('sa_base_url')?.value || '');
localStorage.setItem(LS_MODEL, $('sa_model')?.value || ''); localStorage.setItem(LS_MODEL, $('sa_model')?.value || '');
localStorage.setItem(LS_EMBED, $('sa_embed_model')?.value || state.preferredEmbed || ''); localStorage.setItem(LS_EMBED, $('sa_embed_model')?.value || state.preferredEmbed || '');
localStorage.setItem(LS_PACK, $('sa_pack')?.value || 'write_prompt'); localStorage.setItem(LS_PACK, $('sa_pack')?.value || defaultPackId());
localStorage.setItem(LS_PERSONA, $('sa_persona')?.value || 'neutral'); localStorage.setItem(LS_PERSONA, $('sa_persona')?.value || 'neutral');
localStorage.setItem(LS_VIEW, state.view || 'chat'); localStorage.setItem(LS_VIEW, state.view || 'chat');
localStorage.setItem(LS_AUTO_VISION, $('sa_auto_vision')?.checked ? '1' : '0'); localStorage.setItem(LS_AUTO_VISION, $('sa_auto_vision')?.checked ? '1' : '0');
@@ -4804,7 +5037,7 @@
if (!sel) { if (!sel) {
return; return;
} }
const cur = preferred || sel.value || localStorage.getItem(LS_PACK) || 'write_prompt'; const cur = preferred || sel.value || localStorage.getItem(LS_PACK) || defaultPackId();
sel.innerHTML = ''; sel.innerHTML = '';
const list = (packs || []).slice().sort((a, b) => (a.order || 100) - (b.order || 100)); const list = (packs || []).slice().sort((a, b) => (a.order || 100) - (b.order || 100));
for (const p of list) { for (const p of list) {
@@ -4896,14 +5129,69 @@
); );
} }
function setModelOptions(models, { error } = {}) { /** Larger param tags win (32b > 8b > 7b); instruct / qwen3 preferred over thinking/:latest. */
function chatModelSeniority(name) {
const n = String(name || '').toLowerCase();
let score = 0;
const m = n.match(/(?:^|[:\-/])(\d+)\s*b\b/);
if (m) {
score += Number(m[1]) * 1e6;
}
if (n.includes('instruct')) {
score += 5e4;
}
if (n.includes('qwen3')) {
score += 2e4;
}
if (n.includes('thinking') || n.endsWith(':latest')) {
score -= 1e4;
}
return score;
}
function pickSeniorChatModel(names) {
const list = (names || []).map((n) => String(n || '').trim()).filter(Boolean);
if (!list.length) {
return '';
}
return [...list].sort((a, b) => chatModelSeniority(b) - chatModelSeniority(a) || a.localeCompare(b))[0];
}
/**
* Resolve which chat tag to select / warm.
* Priority: api preferred (ollama-roles default_chat) senior heuristic LS (after one-shot junior upgrade).
*/
function resolveChatModel(names, apiPreferred) {
const list = (names || []).map((n) => String(n || '').trim()).filter(Boolean);
if (!list.length) {
return '';
}
const preferred = apiPreferred && list.includes(apiPreferred)
? apiPreferred
: pickSeniorChatModel(list);
const ls = state.preferredModel || localStorage.getItem(LS_MODEL) || '';
if (!localStorage.getItem(LS_MODEL_SENIOR_MIG)) {
localStorage.setItem(LS_MODEL_SENIOR_MIG, '1');
if (preferred && (!ls || !list.includes(ls) || chatModelSeniority(ls) < chatModelSeniority(preferred))) {
state.preferredModel = preferred;
return preferred;
}
}
if (ls && list.includes(ls)) {
return ls;
}
return preferred || list[0];
}
function setModelOptions(models, { error, preferred } = {}) {
const sel = $('sa_model'); const sel = $('sa_model');
const sel2 = $('sa_settings_chat_model'); const sel2 = $('sa_settings_chat_model');
const apply = (target) => { const apply = (target) => {
if (!target) { if (!target) {
return; return;
} }
const names = (models || []).map((n) => String(n || '').trim()).filter(Boolean); let names = (models || []).map((n) => String(n || '').trim()).filter(Boolean);
names = [...names].sort((a, b) => chatModelSeniority(b) - chatModelSeniority(a) || a.localeCompare(b));
target.innerHTML = ''; target.innerHTML = '';
if (error) { if (error) {
const opt = document.createElement('option'); const opt = document.createElement('option');
@@ -4927,9 +5215,9 @@
opt.textContent = name; opt.textContent = name;
target.appendChild(opt); target.appendChild(opt);
} }
const prefer = state.preferredModel || localStorage.getItem(LS_MODEL); const pick = resolveChatModel(names, preferred);
if (prefer && names.includes(prefer)) { if (pick) {
target.value = prefer; target.value = pick;
} }
}; };
apply(sel); apply(sel);
@@ -4982,11 +5270,17 @@
(data) => { (data) => {
const models = data.models || []; const models = data.models || [];
const memoryModels = data.memory_models || []; const memoryModels = data.memory_models || [];
setModelOptions(models); const preferred = (data.preferred || '').trim();
setModelOptions(models, { preferred });
setEmbedModelOptions(memoryModels); setEmbedModelOptions(memoryModels);
const prefer = state.preferredModel || localStorage.getItem(LS_MODEL); const pick = resolveChatModel(models, preferred);
if (prefer && models.includes(prefer) && $('sa_model')) { if (pick && $('sa_model')) {
$('sa_model').value = prefer; $('sa_model').value = pick;
if ($('sa_settings_chat_model')) {
$('sa_settings_chat_model').value = pick;
}
state.preferredModel = pick;
localStorage.setItem(LS_MODEL, pick);
} }
setStatus(models.length ? `${models.length} chat · ${memoryModels.length} memory` : 'No Ollama models (gpu-rent: ollama pull)'); setStatus(models.length ? `${models.length} chat · ${memoryModels.length} memory` : 'No Ollama models (gpu-rent: ollama pull)');
if (models.length) { if (models.length) {
@@ -6648,10 +6942,21 @@
return; return;
} }
const { patch } = extractPatch(reply); const { patch } = extractPatch(reply);
if (patch) { let effective = patch;
rememberLastPatch(patch); if (!effective && !fromVisionHop && !fromAutoCritique && !fromDebug) {
const aspect = parseAspectFromUserText(opts.userText || '');
if (aspect && (replyMissingJsonPatch(reply) || isSameButAspectRequest(opts.userText || ''))) {
effective = { aspect, actions: ['generate'] };
if (state.lastPatch?.prompt) {
effective.prompt = state.lastPatch.prompt;
} }
if (Array.isArray(patch?.actions) && patch.actions.map(String).includes('interrupt')) { appendSystemNote(`Патч пустой — применил aspect ${aspect} сам.`);
}
}
if (effective) {
rememberLastPatch(effective);
}
if (Array.isArray(effective?.actions) && effective.actions.map(String).includes('interrupt')) {
doInterruptNow(); doInterruptNow();
} }
if (civitaiResults && civitaiResults.length && $('sa_auto_download')?.checked) { if (civitaiResults && civitaiResults.length && $('sa_auto_download')?.checked) {
@@ -6661,8 +6966,8 @@
downloadCivitaiLoRA(pick, null); downloadCivitaiLoRA(pick, null);
} }
} }
if (patch && !fromVisionHop && !fromAutoCritique) { if (effective && !fromVisionHop && !fromAutoCritique) {
const hopped = await maybeVisionHop(patch, opts.attachedSlotIds || []); const hopped = await maybeVisionHop(effective, opts.attachedSlotIds || []);
if (hopped) { if (hopped) {
return; return;
} }
@@ -6673,20 +6978,20 @@
return; return;
} }
const wantsGen = !!(opts.userWantsGenerate || state.pendingSilentGen const wantsGen = !!(opts.userWantsGenerate || state.pendingSilentGen
|| (Array.isArray(patch?.actions) && patch.actions.map(String).includes('generate'))); || (Array.isArray(effective?.actions) && effective.actions.map(String).includes('generate')));
const doApply = !!(patch && (wantsGen || $('sa_auto_apply')?.checked)); const doApply = !!(effective && (wantsGen || $('sa_auto_apply')?.checked));
if (doApply) { if (doApply) {
if (wantsGen) { if (wantsGen) {
startBusyUi('silent_gen'); startBusyUi('silent_gen');
} else { } else {
setBusyPhase('applying'); setBusyPhase('applying');
} }
await applyPatch(patch, 'all'); await applyPatch(effective, 'all');
syncLiveParamsBar(); syncLiveParamsBar();
updateTasteFromPatch(patch, opts.userText || ''); updateTasteFromPatch(effective, opts.userText || '');
if (!fromAutoCritique && (wantsGen || $('sa_auto_generate')?.checked)) { if (!fromAutoCritique && (wantsGen || $('sa_auto_generate')?.checked)) {
const src = await runGenerateFromPatch( const src = await runGenerateFromPatch(
{ ...patch, actions: Array.isArray(patch.actions) ? patch.actions : ['generate'] }, { ...effective, actions: Array.isArray(effective.actions) ? effective.actions : ['generate'] },
{ force: wantsGen }, { force: wantsGen },
); );
if (src) { if (src) {
@@ -6696,6 +7001,8 @@
} else if (!state.generating) { } else if (!state.generating) {
stopBusyUi(wantsGen ? 'Применено' : ''); stopBusyUi(wantsGen ? 'Применено' : '');
} }
} else if (replyMissingJsonPatch(reply) && !fromAutoCritique && !fromVisionHop) {
setStatus('Ответ без JSON-патча — ничего не применено');
} }
state.pendingSilentGen = false; state.pendingSilentGen = false;
} }
@@ -6767,7 +7074,7 @@
function buildDebugSummary() { function buildDebugSummary() {
const persona = $('sa_persona')?.value || 'neutral'; const persona = $('sa_persona')?.value || 'neutral';
const pack = $('sa_pack')?.value || 'write_prompt'; const pack = $('sa_pack')?.value || defaultPackId();
const chatModel = $('sa_model')?.value || '—'; const chatModel = $('sa_model')?.value || '—';
const embed = $('sa_embed_model')?.value || state.preferredEmbed || '—'; const embed = $('sa_embed_model')?.value || state.preferredEmbed || '—';
const profile = detectKreaProfileName(); const profile = detectKreaProfileName();
@@ -6817,6 +7124,9 @@
` init=${!!ctx.has_init_image} mask=${!!ctx.has_mask_image} prompt_images=${ctx.prompt_image_count || 0}`, ` init=${!!ctx.has_init_image} mask=${!!ctx.has_mask_image} prompt_images=${ctx.prompt_image_count || 0}`,
` has_vision_image=${!!ctx.has_vision_image} · images_in_request=${!!ctx.images_in_request} · vision_ready=${visionReadySlots().length}`, ` has_vision_image=${!!ctx.has_vision_image} · images_in_request=${!!ctx.images_in_request} · vision_ready=${visionReadySlots().length}`,
` context_json_chars≈${JSON.stringify(ctx).length} · last_system_chars=${state.lastSystemChars || '—'} · last_context_chars=${state.lastContextChars || '—'}`, ` context_json_chars≈${JSON.stringify(ctx).length} · last_system_chars=${state.lastSystemChars || '—'} · last_context_chars=${state.lastContextChars || '—'}`,
state.lastSystemLayers
? ` system_layers: ${Object.entries(state.lastSystemLayers).map(([k, v]) => `${k}=${v}`).join(' · ')}`
: ' system_layers: — (отправь сообщение, чтобы заполнить)',
'', '',
'Exact defaults (merged):', 'Exact defaults (merged):',
` generation=${JSON.stringify(exactGen)}`, ` generation=${JSON.stringify(exactGen)}`,
@@ -7027,7 +7337,7 @@
if ($('sa_input')) { if ($('sa_input')) {
$('sa_input').value = `Find a Krea 2 LoRA for: ${arg}`; $('sa_input').value = `Find a Krea 2 LoRA for: ${arg}`;
} }
setPackValue('write_prompt', { flash: true }); setPackValue(defaultPackId(), { flash: true });
await sendChat({ await sendChat({
skipAutoPack: true, skipAutoPack: true,
forcedUserText: `Search Civitai for Krea-compatible LoRA: ${arg}. Prefer actions search_civitai.`, forcedUserText: `Search Civitai for Krea-compatible LoRA: ${arg}. Prefer actions search_civitai.`,
@@ -7113,7 +7423,7 @@
} }
if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.fromDebug) { if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.fromDebug) {
state.lastUserParamIntent = userTextMentionsParams(text); state.lastUserParamIntent = userTextMentionsParams(text);
state.pendingSilentGen = userAsksGenerate(text); state.pendingSilentGen = userAsksGenerate(text) || isSameButAspectRequest(text);
} }
if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.skipSlash) { if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.skipSlash) {
@@ -7128,6 +7438,31 @@
} }
} }
// «такую же, только 9 на 16» — apply aspect + Generate without waiting for an empty LLM critique.
if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.fromDebug
&& !opts.fromCards && isSameButAspectRequest(text)) {
const aspect = parseAspectFromUserText(text);
if (aspect) {
if ($('sa_input')) {
$('sa_input').value = '';
}
appendMessage('user', text);
state.history.push({ role: 'user', content: text });
persistHistory();
restoreDefaultPackAfterHop();
const patch = { aspect, actions: ['generate'] };
if (state.lastPatch?.prompt) {
patch.prompt = state.lastPatch.prompt;
}
if (Array.isArray(state.lastPatch?.loras) && state.lastPatch.loras.length) {
patch.loras = state.lastPatch.loras;
}
appendSystemNote(`Ставлю ${aspect} и Generate (тот же промпт) — без повторной критики.`);
await applyQuickPatch(patch, `Aspect ${aspect}`);
return;
}
}
if (!updateGate()) { if (!updateGate()) {
setStatus('Выбери модель Krea 2'); setStatus('Выбери модель Krea 2');
return; return;
@@ -7145,7 +7480,7 @@
setPackValue('catalog_card', { flash: false }); setPackValue('catalog_card', { flash: false });
} }
const pack = $('sa_pack')?.value || 'write_prompt'; const pack = $('sa_pack')?.value || defaultPackId();
const persona = $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral'; const persona = $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral';
const model = $('sa_model')?.value; const model = $('sa_model')?.value;
if (!model) { if (!model) {
@@ -7283,6 +7618,9 @@
if (meta.system_chars != null) { if (meta.system_chars != null) {
state.lastSystemChars = Number(meta.system_chars) || 0; state.lastSystemChars = Number(meta.system_chars) || 0;
} }
if (meta.system_layers && typeof meta.system_layers === 'object') {
state.lastSystemLayers = meta.system_layers;
}
try { try {
state.lastContextChars = (context && JSON.stringify(context).length) || 0; state.lastContextChars = (context && JSON.stringify(context).length) || 0;
} catch (e) { } catch (e) {
@@ -7374,7 +7712,7 @@
const reply = data.reply || (state.streamEl?.querySelector('.sa-msg-body')?.textContent) || ''; const reply = data.reply || (state.streamEl?.querySelector('.sa-msg-body')?.textContent) || '';
const civitai = data.civitai_results || []; const civitai = data.civitai_results || [];
finalizeStreamMessage(reply, civitai); finalizeStreamMessage(reply, civitai);
finishOk(reply, civitai, { system_chars: data.system_chars }); finishOk(reply, civitai, { system_chars: data.system_chars, system_layers: data.system_layers });
} }
}, },
0, 0,
@@ -7402,7 +7740,7 @@
} }
const reply = data.reply || ''; const reply = data.reply || '';
appendMessage('assistant', reply, null, data.civitai_results || [], msgMeta); appendMessage('assistant', reply, null, data.civitai_results || [], msgMeta);
finishOk(reply, data.civitai_results || [], { system_chars: data.system_chars }); finishOk(reply, data.civitai_results || [], { system_chars: data.system_chars, system_layers: data.system_layers });
}, },
0, 0,
(err2) => finishErr(String(err2 || err || 'Chat failed')), (err2) => finishErr(String(err2 || err || 'Chat failed')),
@@ -7425,7 +7763,7 @@
} }
const reply = data.reply || ''; const reply = data.reply || '';
appendMessage('assistant', reply, null, data.civitai_results || [], msgMeta); appendMessage('assistant', reply, null, data.civitai_results || [], msgMeta);
finishOk(reply, data.civitai_results || [], { system_chars: data.system_chars }); finishOk(reply, data.civitai_results || [], { system_chars: data.system_chars, system_layers: data.system_layers });
}, },
0, 0,
(err) => finishErr(String(err || 'Chat failed')), (err) => finishErr(String(err || 'Chat failed')),
@@ -7554,7 +7892,7 @@
preferSelected: false, preferSelected: false,
}); });
const pack = $('sa_pack'); const pack = $('sa_pack');
if (pack && pack.value === 'write_prompt') { if (pack && (pack.value === 'ordinary' || pack.value === 'write_prompt')) {
pack.value = 'critique_image'; pack.value = 'critique_image';
saveSettings(); saveSettings();
} }
@@ -7813,7 +8151,7 @@
} }
await setInitFromSrc(src); await setInitFromSrc(src);
const pack = $('sa_pack'); const pack = $('sa_pack');
if (pack && pack.value === 'write_prompt') { if (pack && (pack.value === 'ordinary' || pack.value === 'write_prompt')) {
setPackValue('inpaint_edit', { flash: true }); setPackValue('inpaint_edit', { flash: true });
} }
}); });
+112 -53
View File
@@ -18,19 +18,31 @@ public partial class SwarmAssistentExtension
const int MaxCivitaiHopsFallback = 2; const int MaxCivitaiHopsFallback = 2;
const int MaxToolHopsFallback = 4; const int MaxToolHopsFallback = 4;
List<JObject> BuildOllamaMessages(string packName, bool includeBase, string contextJson, JArray userMessages, string extraSystem = null, string personaId = null, IEnumerable<string> skillIds = null) (List<JObject> messages, JObject systemLayers) BuildOllamaMessages(string packName, bool includeBase, string contextJson, JArray userMessages, string extraSystem = null, string personaId = null, IEnumerable<string> skillIds = null)
{ {
List<JObject> ollamaMessages = []; List<JObject> ollamaMessages = [];
StringBuilder system = new(); StringBuilder system = new();
JObject layers = new();
string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId(); string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId();
void AddLayer(string name, string block)
{
if (string.IsNullOrWhiteSpace(block))
{
return;
}
if (system.Length > 0)
{
system.AppendLine();
}
int before = system.Length;
system.AppendLine(block.TrimEnd());
layers[name] = system.Length - before;
}
if (includeBase) if (includeBase)
{ {
string core = Config.LoadCorePrompt(pid); AddLayer("core", Config.LoadCorePrompt(pid));
if (!string.IsNullOrWhiteSpace(core))
{
system.AppendLine(core);
}
} }
if (Memory is not null) if (Memory is not null)
@@ -40,12 +52,7 @@ public partial class SwarmAssistentExtension
JObject asst = Config.LoadAssistant(pid); JObject asst = Config.LoadAssistant(pid);
double weight = asst["user_prefs_weight"]?.Value<double?>() ?? 1.0; double weight = asst["user_prefs_weight"]?.Value<double?>() ?? 1.0;
int maxPrefs = asst["user_prefs_max"]?.Value<int?>() ?? 16; int maxPrefs = asst["user_prefs_max"]?.Value<int?>() ?? 16;
string about = Memory.FormatUserPrefsBlock(pid, weight, maxPrefs); AddLayer("prefs", Memory.FormatUserPrefsBlock(pid, weight, maxPrefs));
if (!string.IsNullOrWhiteSpace(about))
{
system.AppendLine();
system.AppendLine(about);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -54,56 +61,55 @@ public partial class SwarmAssistentExtension
} }
JObject exact = Config.LoadExactForPrompt(pid); JObject exact = Config.LoadExactForPrompt(pid);
if (exact is not null && exact.Count > 0 && LiveContextHasSize(contextJson))
{
exact.Remove("aspect_table");
}
if (exact is not null && exact.Count > 0) if (exact is not null && exact.Count > 0)
{ {
system.AppendLine(); AddLayer("exact",
system.AppendLine("## Exact memory (canonical KV defaults — prefer over RAG for numbers)"); "## Exact memory (canonical KV defaults — prefer over RAG for numbers)\n```json\n"
system.AppendLine("```json"); + exact.ToString(Newtonsoft.Json.Formatting.None) + "\n```");
system.AppendLine(exact.ToString(Newtonsoft.Json.Formatting.None));
system.AppendLine("```");
} }
StringBuilder skillsBlock = new();
foreach (string skillId in skillIds ?? Config.ResolveEnabledSkills(pid, null)) foreach (string skillId in skillIds ?? Config.ResolveEnabledSkills(pid, null))
{ {
string skillText = Config.LoadSkillPrompt(pid, skillId); string skillText = Config.LoadSkillPrompt(pid, skillId);
if (!string.IsNullOrWhiteSpace(skillText)) if (!string.IsNullOrWhiteSpace(skillText))
{ {
system.AppendLine(); if (skillsBlock.Length > 0)
system.AppendLine($"## Skill: {skillId}");
system.AppendLine(skillText);
}
}
string identity = Config.RenderIdentityBlock(pid);
if (!string.IsNullOrWhiteSpace(identity))
{ {
system.AppendLine(); skillsBlock.AppendLine();
system.AppendLine(identity);
} }
skillsBlock.AppendLine($"## Skill: {skillId}");
skillsBlock.AppendLine(skillText.TrimEnd());
}
}
AddLayer("skills", skillsBlock.ToString());
AddLayer("identity", Config.RenderIdentityBlock(pid));
if (!string.IsNullOrWhiteSpace(packName)) if (!string.IsNullOrWhiteSpace(packName))
{ {
string situational = Config.LoadPackPrompt(pid, packName); string situational = Config.LoadPackPrompt(pid, packName);
if (!string.IsNullOrWhiteSpace(situational)) if (!string.IsNullOrWhiteSpace(situational))
{ {
system.AppendLine(); AddLayer("pack", $"## Active mode: {packName}\n{situational.TrimEnd()}");
system.AppendLine($"## Active mode: {packName}");
system.AppendLine(situational);
} }
} }
if (!string.IsNullOrWhiteSpace(contextJson)) if (!string.IsNullOrWhiteSpace(contextJson))
{ {
system.AppendLine(); AddLayer("live",
system.AppendLine("## Live SwarmUI context (JSON — trust this over guesses)"); "## Live SwarmUI context (JSON — trust this over guesses)\n```json\n"
system.AppendLine("```json"); + contextJson + "\n```");
system.AppendLine(contextJson);
system.AppendLine("```");
} }
if (!string.IsNullOrWhiteSpace(extraSystem)) if (!string.IsNullOrWhiteSpace(extraSystem))
{ {
system.AppendLine(); AddLayer("extra", extraSystem);
system.AppendLine(extraSystem);
} }
layers["total"] = system.Length;
if (system.Length > 0) if (system.Length > 0)
{ {
ollamaMessages.Add(new JObject ollamaMessages.Add(new JObject
@@ -129,10 +135,10 @@ public partial class SwarmAssistentExtension
} }
ollamaMessages.Add(copy); ollamaMessages.Add(copy);
} }
return ollamaMessages; return (ollamaMessages, layers);
} }
async Task<(string reply, JObject raw, JArray civitaiResults, int systemChars)> RunChatWithHops( async Task<(string reply, JObject raw, JArray civitaiResults, int systemChars, JObject systemLayers)> RunChatWithHops(
Session session, Session session,
string root, string root,
string modelName, string modelName,
@@ -177,16 +183,10 @@ public partial class SwarmAssistentExtension
string enrichedContext = InjectMemoryHits(contextJson, hits); string enrichedContext = InjectMemoryHits(contextJson, hits);
enrichedContext = EnrichPersonaContext(enrichedContext, pid, packName); enrichedContext = EnrichPersonaContext(enrichedContext, pid, packName);
List<JObject> messages = BuildOllamaMessages(packName, includeBase, enrichedContext, userMessages, personaId: pid, skillIds: skills); (List<JObject> messages, JObject systemLayers) = BuildOllamaMessages(packName, includeBase, enrichedContext, userMessages, personaId: pid, skillIds: skills);
int systemChars = 0; int systemChars = systemLayers["total"]?.Value<int?>()
foreach (JObject m in messages) ?? messages.FirstOrDefault(m => string.Equals(m["role"]?.ToString(), "system", StringComparison.OrdinalIgnoreCase))?["content"]?.ToString()?.Length
{ ?? 0;
if (string.Equals(m["role"]?.ToString(), "system", StringComparison.OrdinalIgnoreCase))
{
systemChars = m["content"]?.ToString()?.Length ?? 0;
break;
}
}
JArray civitaiResults = []; JArray civitaiResults = [];
string reply = ""; string reply = "";
JObject lastRaw = null; JObject lastRaw = null;
@@ -229,7 +229,7 @@ public partial class SwarmAssistentExtension
messages.Add(new JObject { ["role"] = "assistant", ["content"] = assistantContent }); messages.Add(new JObject { ["role"] = "assistant", ["content"] = assistantContent });
messages.Add(new JObject { ["role"] = "user", ["content"] = follow }); messages.Add(new JObject { ["role"] = "user", ["content"] = follow });
} }
return (reply, lastRaw, civitaiResults, systemChars); return (reply, lastRaw, civitaiResults, systemChars, systemLayers);
} }
static string BuildRetrieveQuery(JArray userMessages, string contextJson, string packName = null) static string BuildRetrieveQuery(JArray userMessages, string contextJson, string packName = null)
@@ -249,7 +249,18 @@ public partial class SwarmAssistentExtension
{ {
sb.Append(ckpt).Append(' '); sb.Append(ckpt).Append(' ');
} }
if (ctx["enabled_loras"] is JArray en) if (ctx["selected_loras"] is JArray selLoras)
{
foreach (JToken t in selLoras.Take(12))
{
string n = t?["name"]?.ToString() ?? t?.ToString();
if (!string.IsNullOrWhiteSpace(n))
{
sb.Append(n).Append(' ');
}
}
}
else if (ctx["enabled_loras"] is JArray en)
{ {
foreach (JToken t in en.Take(12)) foreach (JToken t in en.Take(12))
{ {
@@ -587,6 +598,10 @@ public partial class SwarmAssistentExtension
{ {
continue; continue;
} }
if (IsExactPointerHit(ho))
{
continue;
}
JObject copy = (JObject)ho.DeepClone(); JObject copy = (JObject)ho.DeepClone();
string text = copy["text"]?.ToString() ?? ""; string text = copy["text"]?.ToString() ?? "";
if (text.Length > hitChars) if (text.Length > hitChars)
@@ -598,6 +613,7 @@ public partial class SwarmAssistentExtension
} }
ctx["memory_hits"] = clippedHits; ctx["memory_hits"] = clippedHits;
ctx.Remove("taste_profile"); ctx.Remove("taste_profile");
ctx.Remove("enabled_loras"); // alias of selected_loras — do not double-feed
try try
{ {
string pid = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? Config?.DefaultPersonaId() ?? "neutral"; string pid = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? Config?.DefaultPersonaId() ?? "neutral";
@@ -623,6 +639,49 @@ public partial class SwarmAssistentExtension
return ctx.ToString(Newtonsoft.Json.Formatting.None); return ctx.ToString(Newtonsoft.Json.Formatting.None);
} }
static bool LiveContextHasSize(string contextJson)
{
if (string.IsNullOrWhiteSpace(contextJson))
{
return false;
}
try
{
JObject ctx = JObject.Parse(contextJson);
int? w = ctx["width"]?.Value<int?>();
int? h = ctx["height"]?.Value<int?>();
return w is > 0 && h is > 0;
}
catch
{
return false;
}
}
/// <summary>Drop RAG rows that only point at Exact (legacy krea_facts seed / "see Exact memory…").</summary>
static bool IsExactPointerHit(JObject ho)
{
if (ho is null)
{
return false;
}
string key = (ho["key"]?.ToString() ?? "").Trim().ToLowerInvariant();
if (key.StartsWith("krea2_", StringComparison.Ordinal))
{
return true;
}
string text = (ho["text"]?.ToString() ?? "").ToLowerInvariant();
if (string.IsNullOrWhiteSpace(text))
{
return false;
}
return text.Contains("live in exact memory", StringComparison.Ordinal)
|| text.Contains("see exact memory", StringComparison.Ordinal)
|| text.Contains("prefer exact kv", StringComparison.Ordinal)
|| text.Contains("exact.facts.", StringComparison.Ordinal)
|| text.Contains("exact memory profiles.", StringComparison.Ordinal);
}
void SlimAvailableLorasInContext(JObject ctx, JArray hits) void SlimAvailableLorasInContext(JObject ctx, JArray hits)
{ {
if (ctx["available_loras"] is not JArray allLoras || allLoras.Count == 0) if (ctx["available_loras"] is not JArray allLoras || allLoras.Count == 0)
@@ -851,6 +910,8 @@ public partial class SwarmAssistentExtension
} }
} }
JArray catalog = []; JArray catalog = [];
if (authorPack)
{
foreach (var p in Config.ListPersonaCatalog()) foreach (var p in Config.ListPersonaCatalog())
{ {
catalog.Add(new JObject catalog.Add(new JObject
@@ -860,8 +921,6 @@ public partial class SwarmAssistentExtension
}); });
} }
ctx["personas"] = catalog; ctx["personas"] = catalog;
if (authorPack)
{
JObject shelves = Config.LoadIdentityParts(pid); JObject shelves = Config.LoadIdentityParts(pid);
shelves.Remove("extra"); shelves.Remove("extra");
ctx["persona_shelves"] = shelves; ctx["persona_shelves"] = shelves;
+46 -4
View File
@@ -96,12 +96,18 @@ public partial class SwarmAssistentExtension
|| n.StartsWith(fallback.Split(':')[0], StringComparison.OrdinalIgnoreCase))); || n.StartsWith(fallback.Split(':')[0], StringComparison.OrdinalIgnoreCase)));
} }
} }
string preferred = roles["default_chat"]?.ToString()?.Trim() ?? "";
if (string.IsNullOrWhiteSpace(preferred) || !models.Any(t => string.Equals(t.ToString(), preferred, StringComparison.OrdinalIgnoreCase)))
{
preferred = PickSeniorChatModel(models.Select(t => t.ToString()).ToList());
}
return new JObject return new JObject
{ {
["success"] = true, ["success"] = true,
["base_url"] = root, ["base_url"] = root,
["models"] = models, ["models"] = models,
["memory_models"] = memoryModels, ["memory_models"] = memoryModels,
["preferred"] = preferred ?? "",
}; };
} }
catch (Exception ex) catch (Exception ex)
@@ -116,6 +122,40 @@ public partial class SwarmAssistentExtension
return n.Contains("embed") || n.Contains("nomic") || n.Contains("bge-") || n.Contains("minilm") || n.Contains("e5-"); return n.Contains("embed") || n.Contains("nomic") || n.Contains("bge-") || n.Contains("minilm") || n.Contains("e5-");
} }
/// <summary>Prefer larger param tags (32b &gt; 8b &gt; 7b), then instruct / qwen3.</summary>
static string PickSeniorChatModel(IList<string> names)
{
if (names is null || names.Count == 0)
{
return "";
}
return names.OrderByDescending(ChatModelSeniority).ThenBy(n => n, StringComparer.OrdinalIgnoreCase).First();
}
static long ChatModelSeniority(string name)
{
string n = (name ?? "").ToLowerInvariant();
long score = 0;
System.Text.RegularExpressions.Match m = System.Text.RegularExpressions.Regex.Match(n, @"(?:^|[:\-/])(\d+)\s*b\b");
if (m.Success && long.TryParse(m.Groups[1].Value, out long bil))
{
score += bil * 1_000_000;
}
if (n.Contains("instruct"))
{
score += 50_000;
}
if (n.Contains("qwen3"))
{
score += 20_000;
}
if (n.Contains("thinking") || n.EndsWith(":latest"))
{
score -= 10_000;
}
return score;
}
async Task<(string reply, JObject raw)> CallOllamaChat( async Task<(string reply, JObject raw)> CallOllamaChat(
string root, string root,
string modelName, string modelName,
@@ -239,11 +279,11 @@ public partial class SwarmAssistentExtension
{ {
return new JObject { ["error"] = invalid }; return new JObject { ["error"] = invalid };
} }
string packName = (pack ?? "write_prompt").Trim(); string packName = (pack ?? "ordinary").Trim();
string embedModel = raw?["embed_model"]?.ToString() ?? Config.LoadSettings()["embed_model"]?.ToString(); string embedModel = raw?["embed_model"]?.ToString() ?? Config.LoadSettings()["embed_model"]?.ToString();
try try
{ {
(string reply, JObject parsed, JArray civitai, int systemChars) = await RunChatWithHops( (string reply, JObject parsed, JArray civitai, int systemChars, JObject systemLayers) = await RunChatWithHops(
session, root, modelName, packName, includeBase, contextJson, userMessages, personaId: persona, skillIds: skills, embedModel: embedModel); session, root, modelName, packName, includeBase, contextJson, userMessages, personaId: persona, skillIds: skills, embedModel: embedModel);
return new JObject return new JObject
{ {
@@ -255,6 +295,7 @@ public partial class SwarmAssistentExtension
["raw"] = parsed, ["raw"] = parsed,
["civitai_results"] = civitai, ["civitai_results"] = civitai,
["system_chars"] = systemChars, ["system_chars"] = systemChars,
["system_layers"] = systemLayers,
}; };
} }
catch (Exception ex) catch (Exception ex)
@@ -275,7 +316,7 @@ public partial class SwarmAssistentExtension
await ws.SendJson(new JObject { ["error"] = invalid }, API.WebsocketTimeout); await ws.SendJson(new JObject { ["error"] = invalid }, API.WebsocketTimeout);
return null; return null;
} }
string packName = (pack ?? "write_prompt").Trim(); string packName = (pack ?? "ordinary").Trim();
string embedModel = raw?["embed_model"]?.ToString() ?? Config.LoadSettings()["embed_model"]?.ToString(); string embedModel = raw?["embed_model"]?.ToString() ?? Config.LoadSettings()["embed_model"]?.ToString();
try try
{ {
@@ -306,7 +347,7 @@ public partial class SwarmAssistentExtension
}, API.WebsocketTimeout); }, API.WebsocketTimeout);
} }
} }
(string reply, JObject parsed, JArray civitai, int systemChars) = await RunChatWithHops( (string reply, JObject parsed, JArray civitai, int systemChars, JObject systemLayers) = await RunChatWithHops(
session, root, modelName, packName, includeBase, contextJson, userMessages, OnDelta, OnHopStart, persona, skills, embedModel); session, root, modelName, packName, includeBase, contextJson, userMessages, OnDelta, OnHopStart, persona, skills, embedModel);
await ws.SendJson(new JObject await ws.SendJson(new JObject
{ {
@@ -319,6 +360,7 @@ public partial class SwarmAssistentExtension
["raw"] = parsed, ["raw"] = parsed,
["civitai_results"] = civitai, ["civitai_results"] = civitai,
["system_chars"] = systemChars, ["system_chars"] = systemChars,
["system_layers"] = systemLayers,
}, API.WebsocketTimeout); }, API.WebsocketTimeout);
} }
catch (Exception ex) catch (Exception ex)
+2 -2
View File
@@ -9,7 +9,7 @@
"inventory_prompt_names": 24, "inventory_prompt_names": 24,
"inventory_hop_limit": 20, "inventory_hop_limit": 20,
"max_ref_slots": 4, "max_ref_slots": 4,
"default_pack": "write_prompt", "default_pack": "ordinary",
"default_persona": "neutral", "default_persona": "neutral",
"embed_model": "nomic-embed-text", "embed_model": "nomic-embed-text",
"memory_top_k": 8, "memory_top_k": 8,
@@ -28,7 +28,7 @@
"note": 4, "note": 4,
"model": 2 "model": 2
}, },
"seed_version": 2, "seed_version": 3,
"gate": { "gate": {
"architecture": "krea2", "architecture": "krea2",
"keywords": ["krea"] "keywords": ["krea"]
+6 -6
View File
@@ -18,18 +18,18 @@ 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. 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. Keep prose short (a few lines). 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.
## Live context ## Live context
"Live SwarmUI context" JSON is ground truth for this turn: "Live SwarmUI context" JSON is ground truth for this turn:
- Use only LoRA/checkpoint **names** from `available_loras` / `enabled_loras` (or Civitai hop results). Prefer listed `triggers` / `trigger_phrase` / `blurb` — **never invent**. - Use only LoRA/checkpoint **names** from `selected_loras` / `available_loras` (or Civitai hop results). `selected_loras` = currently enabled. Prefer listed `triggers` / `trigger_phrase` / `blurb` — **never invent**.
- Rich entries (blurbs/triggers) are enabled + top krea-likely. Name-only rows need `list_inventory` + `inventory_query` before you rely on them. - Rich entries (blurbs/triggers) are selected + top krea-likely. Name-only rows need `list_inventory` + `inventory_query` before you rely on them.
- `memory_hits` may be truncated (`truncated: true`) — use `memory_get` for the full text. - `memory_hits` may be truncated (`truncated: true`) — use `memory_get` for the full text.
- `has_vision_image` true means a real board frame exists. `images_in_request` true means JPEG bytes are in **this** request. If you need to see a frame and `images_in_request` is false, emit `look_at` first — do not invent what the image looks like. - `has_vision_image` true means a real board frame exists. `images_in_request` true means JPEG bytes are in **this** request. If you need to see a frame and `images_in_request` is false, emit `look_at` first — do not invent what the image looks like.
- Prefer `krea_likely` / Krea architecture; ignore FLUX/SDXL. Respect current params unless asked or pack is `form_params`. - Prefer `krea_likely` / Krea architecture; ignore FLUX/SDXL. Respect current params unless asked or pack is `form_params`.
- Init/inpaint flags and `image_slots` are in the JSON. Extra pack fields (aspect, inpaint, persona authoring) are documented in the active pack. - Init/inpaint flags and `image_slots` are in the JSON. Extra pack fields are documented in the active pack.
## Memory (short) ## Memory (short)
@@ -57,7 +57,7 @@ Never write a “JSON Patch” section in prose without an actual fenced ```json
- Omit unchanged keys. Prefer omitting Exact-matching `steps`/`cfg`/`sigma_shift`/`aspect`. - Omit unchanged keys. Prefer omitting Exact-matching `steps`/`cfg`/`sigma_shift`/`aspect`.
- `loras` replaces the full intended set for Apply. Prefer `aspect` over raw width/height. - `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. - 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). UI draws every slider automatically (ordered by `order`). Clamp to min/max. Example: `"controls": { "horny": 55 }`. Do not invent control ids. - **`controls`** — only keys declared in this persona's `controls.json` (Exact). Clamp to min/max. Example: `"controls": { "horny": 55 }`. Do not invent control ids.
- Do not invent model or LoRA filenames. - Do not invent model or LoRA filenames.
### Actions / hops ### Actions / hops
@@ -68,7 +68,7 @@ Never write a “JSON Patch” section in prose without an actual fenced ```json
- `"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.
- `"skill_load"` + `skills: ["memory"]` — load fat skill text. - `"skill_load"` + `skills: ["memory"]` — load fat skill text.
- `"persona_read"` — load lore shelves (appearance/outfits/…) not in always-on identity. - `"persona_read"` — load lore shelves not in always-on identity (e.g. `roleplay` / `craft` / `humor` when NSFW tone or craft detail needs them).
- `"memory_upsert"` / `"memory_forget"` / `"user_pref_upsert"` / `"user_pref_forget"` — writes. - `"memory_upsert"` / `"memory_forget"` / `"user_pref_upsert"` / `"user_pref_forget"` — writes.
- `"persona_clone"` / `"persona_write"` / `"persona_switch"` — `author_persona` only. Never `"persona_delete"`. - `"persona_clone"` / `"persona_write"` / `"persona_switch"` — `author_persona` only. Never `"persona_delete"`.
- `look_at: ["generate"|"ref1"|…]` — vision hop (JPEG arrives on the follow-up). - `look_at: ["generate"|"ref1"|…]` — vision hop (JPEG arrives on the follow-up).
+1 -32
View File
@@ -1,32 +1 @@
[ []
{
"kind": "model",
"key": "krea2_architecture",
"tags": ["krea", "architecture"],
"text": "Krea 2 architecture facts live in Exact memory (exact.facts.architecture). Prefer Exact KV over this note. Never suggest FLUX/SDXL LoRAs."
},
{
"kind": "model",
"key": "krea2_turbo",
"tags": ["krea", "turbo", "params"],
"text": "Turbo numeric defaults (steps/CFG/sigma) live in Exact memory profiles.turbo / generation — do not invent numbers; never use CFG 0."
},
{
"kind": "model",
"key": "krea2_raw",
"tags": ["krea", "raw", "params"],
"text": "RAW numeric defaults live in Exact memory profiles.raw. If checkpoint looks like RAW (not Turbo), use that profile. Turbo LoRA weight ~0.6 for photoreal when needed."
},
{
"kind": "model",
"key": "krea2_negatives",
"tags": ["krea", "prompting"],
"text": "See Exact memory facts.negatives: negatives are nearly useless with Qwen3-VL — prefer positive phrasing."
},
{
"kind": "model",
"key": "krea2_prompt_images",
"tags": ["krea", "board"],
"text": "See Exact memory facts.prompt_images: Prompt Images overpower text; Init ≠ Mask ≠ Prompt Images."
}
]
+2
View File
@@ -25,3 +25,5 @@ Bullet the real issues you see (anatomy, eyes, lighting, aspect, LoRA triggers).
3. `actions: ["generate"]` when proposing a revised generation. 3. `actions: ["generate"]` when proposing a revised generation.
Never describe a patch in prose without the fenced JSON object. Never describe a patch in prose without the fenced JSON object.
Never repeat a previous critique template. Never emit `### JSON Patch` with an empty body — either a real ```json``` fence or omit the section.
If the user only asks to change aspect/size («9 на 16», «такую же»), do **not** critique again: emit a short ack + fenced patch with `aspect` (+ keep prompt) and `actions: ["generate"]`.
+7
View File
@@ -0,0 +1,7 @@
{
"id": "ordinary",
"title": "Обычный",
"order": 1,
"aliases": ["ordinary", "combine", "normal", "general", "default"],
"prompt_file": "ordinary.md"
}
+29
View File
@@ -0,0 +1,29 @@
# Mode: ordinary (комбайн)
Default all-rounder. Handle this turn from the user message + live context — do **not** wait for a specialized pack.
## What you cover here
- **Write / improve prompt** → patch with `prompt` (+ `loras` when useful) and `actions: ["generate"]` when they want a new image.
- **Light critique / improve last frame** → short notes + better `prompt`; use `look_at: ["generate"]` if `has_vision_image` and you have not seen the JPEG this turn.
- **Scene / mood** → compose direction into the prompt (same patch rules).
- **Params** → only when they ask (steps/CFG/aspect/seed); omit Exact-matching numbers otherwise.
- **Inpaint / img2img** → set init/mask fields when they ask and flags allow; else say what is missing.
- **Describe a ref** → only with a real attached / look_at frame.
Prompt prose recipe = skill `prompting`. Creativity sliders = skill `creativity_sliders`.
## When to leave this mode
Emit `"pack": "<id>"` in the JSON patch only if the user clearly needs a dedicated workflow:
- `critique_image` — deep frame critique loop
- `inpaint_edit` — regional edit / mask workflow
- `catalog_card` / `author_persona` — Cards or persona authoring
- `describe_ref` — reverse-prompt a reference at length
Otherwise **stay in ordinary** and just do the work.
## Deliverable
Same as write_prompt: short reply + one fenced JSON patch; `actions: ["generate"]` when they want an image.
+7 -23
View File
@@ -1,33 +1,17 @@
# Mode: write_prompt # Mode: write_prompt
Goal: craft or improve a **Krea 2** prompt that will generate well on Turbo (local Swarm). Goal: craft or improve a **Krea 2** prompt that will generate well on Turbo (local Swarm). Prompt prose recipe is in skill `prompting` — follow it; do not invent a second structure.
## How to write the prompt ## Deliverable
Structure as flowing prose (not tag soup): - Brief note of what you changed.
- JSON patch with at least `prompt`, and `loras` when relevant.
1. **Subject + pose/action** (gaze, expression, body language) - `actions: ["generate"]` when the user wants a new image — UI applies + Generate without Apply buttons.
2. **Clothes / hair / materials** (fabric, fit, how light hits surfaces) - Prefer Exact Turbo defaults / `recommended_params`. Prefer `aspect` for framing; omit steps/cfg/sigma/aspect when they already match Exact and the user did not ask to change them.
3. **Props & environment** - Missing style LoRA → `actions: ["search_civitai"]` + short `search_query` (Krea-compatible).
4. **Composition** (shot type, angle, DoF, framing)
5. **Lighting, palette, mood**
6. **Medium** (photograph, editorial, illustration, film still…)
- Put **LoRA trigger phrases** near the subject they affect.
- Short user ideas → expand. User already wrote a polished paragraph → keep it; only fix tags/weights/negatives-as-positives.
- Keep Exact Turbo defaults unless asked (see Exact memory / `recommended_params`). Prefer `aspect` for framing. Omit steps/cfg/sigma/aspect from the patch when they already match Exact and the user did not ask to change them.
- Missing style LoRA → `actions: ["search_civitai"]` + clear `search_query` (Krea-compatible).
- Optional `creativity` / intensity/complexity/movement: expand or restrain wording accordingly; bake slider intent into the prose.
### Bad → good ### Bad → good
Bad: `cute fox, snow, masterpiece, best quality, 8k, detailed, no blur` Bad: `cute fox, snow, masterpiece, best quality, 8k, detailed, no blur`
Good: `A fluffy red fox sitting alert in fresh powder snow, ears forward, breath faintly visible in the cold air, soft morning light from the left catching orange fur and casting long blue shadows, shot on an 85mm lens at f/2.8 with creamy bokeh, calm winter atmosphere, sharp eyes and whiskers.` Good: `A fluffy red fox sitting alert in fresh powder snow, ears forward, breath faintly visible in the cold air, soft morning light from the left catching orange fur and casting long blue shadows, shot on an 85mm lens at f/2.8 with creamy bokeh, calm winter atmosphere, sharp eyes and whiskers.`
## Deliverable
- Explain briefly what you changed.
- JSON patch with at least `prompt`, and `loras` when relevant.
- `actions: ["generate"]` when the user wants a new image — the UI will apply the patch and Generate **without** showing Apply buttons.
- `aspect` (or width/height) only if framing should change.
+2 -2
View File
@@ -1,4 +1,4 @@
# Skill: creativity & sliders (LLM-only) # Skill: creativity & sliders (LLM-only)
- `creativity`: `raw` | `low` | `medium` | `high` — how much **you** expand the user's wording into the prompt. Not a SwarmUI field. - `creativity`: `raw` | `low` | `medium` | `high` — how much **you** expand the user's wording. Not a SwarmUI field.
- Optional `intensity` / `complexity` / `movement` (100..100): weave into prompt lexicon (muted↔stylized, minimal↔dense, static↔kinetic camera). Do not invent UI sliders. - Optional `intensity` / `complexity` / `movement` (100..100): weave into prose (muted↔stylized, minimal↔dense, static↔kinetic). Do not invent UI sliders.
+6 -1
View File
@@ -1,6 +1,6 @@
{ {
"welcome_html": "<div class=\"sa-welcome-title\">Assistent · Krea 2</div><ul><li><strong>Generate</strong> слева — живой просмотр. В чат сам не уходит.</li><li><strong>Refs</strong> — референсы на отдельной вкладке: drop / paste / Снимок gen.</li><li>Галочка vision на окне — отправить кадр модели.</li><li>Чипсы aspect / seed / Vary / Turbo·RAW. В чате: <code>/help</code>.</li><li>Кнопки патча только у последнего предложения.</li></ul>Напиши, что сгенерировать — или кинь референс и попроси правку.", "welcome_html": "<div class=\"sa-welcome-title\">Assistent · Krea 2</div><ul><li><strong>Generate</strong> слева — живой просмотр. В чат сам не уходит.</li><li><strong>Refs</strong> — референсы на отдельной вкладке: drop / paste / Снимок gen.</li><li>Галочка vision на окне — отправить кадр модели.</li><li>Чипсы aspect / seed / Vary / Turbo·RAW. В чате: <code>/help</code>.</li><li>Кнопки патча только у последнего предложения.</li></ul>Напиши, что сгенерировать — или кинь референс и попроси правку.",
"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|persona\n/persona new — интервью: клон текущей личности (overlay)\n/persona clone <id> — клон с указанной\n/persona save — записать согласованный черновик\n/civitai <query> — поиск LoRA (Confirm в чате)\n/inventory — rescan моделей + обновить список LoRA\n\nЧипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.\nПри старте всегда новый чат; смена чата восстанавливает параметры.\nOverlay-личности удаляет только кнопка ✕ рядом с селектом (не модель).", "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|ordinary|critique|compose|params|inpaint|describe|card|persona\n/persona new — интервью: клон текущей личности (overlay)\n/persona clone <id> — клон с указанной\n/persona save — записать согласованный черновик\n/civitai <query> — поиск LoRA (Confirm в чате)\n/inventory — rescan моделей + обновить список LoRA\n\nЧипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.\nПри старте всегда новый чат; смена чата восстанавливает параметры.\nOverlay-личности удаляет только кнопка ✕ рядом с селектом (не модель).",
"chips": [ "chips": [
{ "label": "1:1", "action": "aspect", "value": "1:1", "title": "1024×1024" }, { "label": "1:1", "action": "aspect", "value": "1:1", "title": "1024×1024" },
{ "label": "4:5", "action": "aspect", "value": "4:5", "title": "928×1152" }, { "label": "4:5", "action": "aspect", "value": "4:5", "title": "928×1152" },
@@ -38,6 +38,11 @@
{ "cmd": "/persona save", "hint": "записать черновик", "action": "persona_save" } { "cmd": "/persona save", "hint": "записать черновик", "action": "persona_save" }
], ],
"pack_aliases": { "pack_aliases": {
"ordinary": "ordinary",
"combine": "ordinary",
"normal": "ordinary",
"general": "ordinary",
"default": "ordinary",
"write": "write_prompt", "write": "write_prompt",
"write_prompt": "write_prompt", "write_prompt": "write_prompt",
"critique": "critique_image", "critique": "critique_image",
+1 -4
View File
@@ -3,13 +3,10 @@
"persona", "persona",
"bio", "bio",
"voice", "voice",
"humor",
"rules", "rules",
"likes", "likes",
"dislikes", "dislikes",
"appearance", "appearance",
"outfits", "outfits"
"roleplay",
"craft"
] ]
} }
+1
View File
@@ -8,6 +8,7 @@
"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", "«девушка которая тебе нравится» = 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",
"/horny-game: score taste match → patch controls.horny (0100), say new % in prose; no generate unless asked" "/horny-game: score taste match → patch controls.horny (0100), say new % in prose; no generate unless asked"
], ],
"never": [ "never": [
+2 -2
View File
@@ -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.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 «Сделай картинку». **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.
## Layout ## Layout
@@ -125,7 +125,7 @@ Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart.
## Packs & skills ## Packs & skills
**Packs** (one active): `write_prompt`, `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`, `catalog_card`, `author_persona`. **Packs** (one active): `ordinary` (default комбайн), `write_prompt`, `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`, `catalog_card`, `author_persona`.
**Skills** (checkboxes): `prompting`, `creativity_sliders`, `memory` — procedures; encyclopedia numbers live in Exact, soft notes in memory-seed / RAG, human taste in UserPrefs. **Skills** (checkboxes): `prompting`, `creativity_sliders`, `memory` — procedures; encyclopedia numbers live in Exact, soft notes in memory-seed / RAG, human taste in UserPrefs.
+1 -1
View File
@@ -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.8"; Version = "0.10.13";
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"]; Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"];
} }
+2 -1
View File
@@ -63,9 +63,10 @@
</div> </div>
<div class="sa-persona-controls" id="sa_persona_controls" hidden></div> <div class="sa-persona-controls" id="sa_persona_controls" hidden></div>
<select id="sa_pack" class="sa-select" title="Пакет промпта"> <select id="sa_pack" class="sa-select" title="Пакет промпта">
<option value="ordinary">Обычный</option>
<option value="write_prompt">Написать промпт</option> <option value="write_prompt">Написать промпт</option>
</select> </select>
<span class="sa-mode-badge" id="sa_mode_badge" title="Активный pack">write</span> <span class="sa-mode-badge" id="sa_mode_badge" title="Активный pack">обычный</span>
<select id="sa_model" class="sa-select sa-model-select" title="Модель Ollama (чат)"> <select id="sa_model" class="sa-select sa-model-select" title="Модель Ollama (чат)">
<option value="">Загрузка моделей…</option> <option value="">Загрузка моделей…</option>
</select> </select>