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 {
white-space: pre-wrap;
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 {
align-self: stretch;
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_EMBED = 'swarm_assistent_embed_model';
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_VIEW = 'swarm_assistent_view';
const LS_AUTO_VISION = 'swarm_assistent_auto_vision';
@@ -44,6 +47,11 @@
};
let PACK_ALIASES = {
ordinary: 'ordinary',
combine: 'ordinary',
normal: 'ordinary',
general: 'ordinary',
default: 'ordinary',
write: 'write_prompt',
write_prompt: 'write_prompt',
critique: 'critique_image',
@@ -138,9 +146,11 @@
tasteSaveTimer: null,
streamEl: null,
streamMeta: null,
streamText: '',
critiqueHopUsed: false,
visionHopUsed: false,
lastSystemChars: 0,
lastSystemLayers: null,
lastContextChars: 0,
busyPhase: 'idle',
busyStarted: 0,
@@ -459,6 +469,131 @@
.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) {
const el = document.getElementById(id);
return el ? el.value : '';
@@ -495,12 +630,70 @@
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) {
const t = String(text || '');
if (parseAspectFromUserText(t)) {
return true;
}
if (/\b(steps?|cfg|seed|sigma|aspect|resolution|batch|turbo|raw)\b/i.test(t)) {
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) {
@@ -548,13 +741,20 @@
}
}
function defaultPackId() {
return state.config?.assistant?.default_pack
|| $('sa_pack')?.querySelector('option')?.value
|| 'ordinary';
}
function syncModeBadge() {
const badge = $('sa_mode_badge');
const pack = $('sa_pack')?.value || 'write_prompt';
const pack = $('sa_pack')?.value || defaultPackId();
if (!badge) {
return;
}
const shortMap = {
ordinary: 'обычный',
write_prompt: 'write',
critique_image: 'critique',
compose_scene: 'compose',
@@ -562,6 +762,7 @@
inpaint_edit: 'inpaint',
describe_ref: 'describe',
catalog_card: 'card',
author_persona: 'persona',
};
const short = shortMap[pack] || pack.replace(/_/g, ' ').slice(0, 12);
badge.textContent = short;
@@ -1406,6 +1607,9 @@
function stripJsonFencesForHistory(content) {
return String(content || '')
.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')
.trim();
}
@@ -1472,7 +1676,7 @@
batch: parseInt(val('input_images') || val('input_batchsize') || '0', 10) || null,
loras,
persona: $('sa_persona')?.value || 'neutral',
pack: $('sa_pack')?.value || 'write_prompt',
pack: $('sa_pack')?.value || defaultPackId(),
sessionExact,
lastPatch,
};
@@ -2487,11 +2691,11 @@
}
return out;
});
ctx.enabled_loras = ctx.selected_loras;
// selected_loras = enabled; do not also emit enabled_loras (duplicate).
}
} 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 seenCard = new Set();
const addKey = (kind, name) => {
@@ -2515,9 +2719,24 @@
}
for (const k of cardKeys) {
const cached = state.modelCards[`${k.kind}:${k.name}`];
if (cached) {
ctx.model_cards.push(slimCardForContext(cached));
if (!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
@@ -3037,10 +3256,19 @@
if (state.packUserTouched) {
return null;
}
// Комбайн «Обычный» сам выбирает поведение — не переключаем pack.
const cur = $('sa_pack')?.value || defaultPackId();
if (cur === 'ordinary') {
return null;
}
const t = String(text || '').toLowerCase();
if (!t.trim()) {
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)
|| /\b(fix\s+it|make\s+it\s+better|rewrite)\b/i.test(t)) {
return 'write_prompt';
@@ -3061,8 +3289,9 @@
|| /init\s*image/i.test(t)) {
return 'inpaint_edit';
}
if (userTextMentionsParams(t)) {
return 'form_params';
// Any non-critique follow-up after auto-critique should leave critique mode.
if (cur === 'critique_image') {
return 'ordinary';
}
if (/\b(moodboard|compose|scene)\b/i.test(t)
|| cyrTokenRe('сцен[а-яё]*|атмосфер[а-яё]*|мизансцен[а-яё]*').test(t)) {
@@ -3071,6 +3300,16 @@
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) {
if (!patch) {
return false;
@@ -3689,6 +3928,7 @@
}
setStatus('Auto-critique…');
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). */
@@ -3713,6 +3953,7 @@
}
setStatus('Auto look_at…');
await sendChat({ fromVisionHop: true, forceSlotIds: [GEN_ID], skipAutoPack: true });
restoreDefaultPackAfterHop();
}
/** Board action: attach the finished Generate frame and ask for a verdict. */
@@ -3775,7 +4016,7 @@
chip.textContent = persona.title || persona.id;
chip.title = `Характер: ${persona.title || persona.id}${pack ? ` · режим ${pack}` : ''}`;
row.appendChild(chip);
if (pack && pack !== 'write_prompt') {
if (pack && pack !== 'ordinary' && pack !== 'write_prompt') {
const packEl = document.createElement('span');
packEl.className = 'sa-pack-mark';
packEl.textContent = pack.replace(/_/g, ' ');
@@ -3785,19 +4026,6 @@
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) {
const box = $('sa_messages');
if (!box) {
@@ -3856,6 +4084,7 @@
if (state.streamEl) {
if (state.streamEl.classList.contains('sa-typing')) {
state.streamEl.classList.remove('sa-typing');
state.streamText = '';
setAssistantBody(state.streamEl, '');
}
state.gotDelta = true;
@@ -3863,12 +4092,8 @@
if (state.busyPhase !== 'refining') {
setBusyPhase('streaming');
}
let body = state.streamEl.querySelector('.sa-msg-body');
if (!body) {
setAssistantBody(state.streamEl, '');
body = state.streamEl.querySelector('.sa-msg-body');
}
body.textContent += delta;
state.streamText = (state.streamText || '') + (delta || '');
setAssistantBody(state.streamEl, state.streamText);
const box = $('sa_messages');
if (box) {
box.scrollTop = box.scrollHeight;
@@ -3881,6 +4106,7 @@
const meta = state.streamMeta;
state.streamEl = null;
state.streamMeta = null;
state.streamText = '';
if (!el) {
appendMessage('assistant', fullReply, null, civitaiResults, meta || undefined);
return;
@@ -4234,6 +4460,13 @@
}
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 model = localStorage.getItem(LS_MODEL);
const pack = localStorage.getItem(LS_PACK);
@@ -4295,7 +4528,7 @@
function collectUiState() {
return {
pack: $('sa_pack')?.value || 'write_prompt',
pack: $('sa_pack')?.value || defaultPackId(),
persona: $('sa_persona')?.value || 'neutral',
auto_vision: !!$('sa_auto_vision')?.checked,
auto_apply: !!$('sa_auto_apply')?.checked,
@@ -4381,7 +4614,7 @@
localStorage.setItem(LS_BASE, $('sa_base_url')?.value || '');
localStorage.setItem(LS_MODEL, $('sa_model')?.value || '');
localStorage.setItem(LS_EMBED, $('sa_embed_model')?.value || state.preferredEmbed || '');
localStorage.setItem(LS_PACK, $('sa_pack')?.value || 'write_prompt');
localStorage.setItem(LS_PACK, $('sa_pack')?.value || defaultPackId());
localStorage.setItem(LS_PERSONA, $('sa_persona')?.value || 'neutral');
localStorage.setItem(LS_VIEW, state.view || 'chat');
localStorage.setItem(LS_AUTO_VISION, $('sa_auto_vision')?.checked ? '1' : '0');
@@ -4804,7 +5037,7 @@
if (!sel) {
return;
}
const cur = preferred || sel.value || localStorage.getItem(LS_PACK) || 'write_prompt';
const cur = preferred || sel.value || localStorage.getItem(LS_PACK) || defaultPackId();
sel.innerHTML = '';
const list = (packs || []).slice().sort((a, b) => (a.order || 100) - (b.order || 100));
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 sel2 = $('sa_settings_chat_model');
const apply = (target) => {
if (!target) {
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 = '';
if (error) {
const opt = document.createElement('option');
@@ -4927,9 +5215,9 @@
opt.textContent = name;
target.appendChild(opt);
}
const prefer = state.preferredModel || localStorage.getItem(LS_MODEL);
if (prefer && names.includes(prefer)) {
target.value = prefer;
const pick = resolveChatModel(names, preferred);
if (pick) {
target.value = pick;
}
};
apply(sel);
@@ -4982,11 +5270,17 @@
(data) => {
const models = data.models || [];
const memoryModels = data.memory_models || [];
setModelOptions(models);
const preferred = (data.preferred || '').trim();
setModelOptions(models, { preferred });
setEmbedModelOptions(memoryModels);
const prefer = state.preferredModel || localStorage.getItem(LS_MODEL);
if (prefer && models.includes(prefer) && $('sa_model')) {
$('sa_model').value = prefer;
const pick = resolveChatModel(models, preferred);
if (pick && $('sa_model')) {
$('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)');
if (models.length) {
@@ -6648,10 +6942,21 @@
return;
}
const { patch } = extractPatch(reply);
if (patch) {
rememberLastPatch(patch);
let effective = 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;
}
appendSystemNote(`Патч пустой — применил aspect ${aspect} сам.`);
}
}
if (Array.isArray(patch?.actions) && patch.actions.map(String).includes('interrupt')) {
if (effective) {
rememberLastPatch(effective);
}
if (Array.isArray(effective?.actions) && effective.actions.map(String).includes('interrupt')) {
doInterruptNow();
}
if (civitaiResults && civitaiResults.length && $('sa_auto_download')?.checked) {
@@ -6661,8 +6966,8 @@
downloadCivitaiLoRA(pick, null);
}
}
if (patch && !fromVisionHop && !fromAutoCritique) {
const hopped = await maybeVisionHop(patch, opts.attachedSlotIds || []);
if (effective && !fromVisionHop && !fromAutoCritique) {
const hopped = await maybeVisionHop(effective, opts.attachedSlotIds || []);
if (hopped) {
return;
}
@@ -6673,20 +6978,20 @@
return;
}
const wantsGen = !!(opts.userWantsGenerate || state.pendingSilentGen
|| (Array.isArray(patch?.actions) && patch.actions.map(String).includes('generate')));
const doApply = !!(patch && (wantsGen || $('sa_auto_apply')?.checked));
|| (Array.isArray(effective?.actions) && effective.actions.map(String).includes('generate')));
const doApply = !!(effective && (wantsGen || $('sa_auto_apply')?.checked));
if (doApply) {
if (wantsGen) {
startBusyUi('silent_gen');
} else {
setBusyPhase('applying');
}
await applyPatch(patch, 'all');
await applyPatch(effective, 'all');
syncLiveParamsBar();
updateTasteFromPatch(patch, opts.userText || '');
updateTasteFromPatch(effective, opts.userText || '');
if (!fromAutoCritique && (wantsGen || $('sa_auto_generate')?.checked)) {
const src = await runGenerateFromPatch(
{ ...patch, actions: Array.isArray(patch.actions) ? patch.actions : ['generate'] },
{ ...effective, actions: Array.isArray(effective.actions) ? effective.actions : ['generate'] },
{ force: wantsGen },
);
if (src) {
@@ -6696,6 +7001,8 @@
} else if (!state.generating) {
stopBusyUi(wantsGen ? 'Применено' : '');
}
} else if (replyMissingJsonPatch(reply) && !fromAutoCritique && !fromVisionHop) {
setStatus('Ответ без JSON-патча — ничего не применено');
}
state.pendingSilentGen = false;
}
@@ -6767,7 +7074,7 @@
function buildDebugSummary() {
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 embed = $('sa_embed_model')?.value || state.preferredEmbed || '—';
const profile = detectKreaProfileName();
@@ -6817,6 +7124,9 @@
` 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}`,
` 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):',
` generation=${JSON.stringify(exactGen)}`,
@@ -7027,7 +7337,7 @@
if ($('sa_input')) {
$('sa_input').value = `Find a Krea 2 LoRA for: ${arg}`;
}
setPackValue('write_prompt', { flash: true });
setPackValue(defaultPackId(), { flash: true });
await sendChat({
skipAutoPack: true,
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) {
state.lastUserParamIntent = userTextMentionsParams(text);
state.pendingSilentGen = userAsksGenerate(text);
state.pendingSilentGen = userAsksGenerate(text) || isSameButAspectRequest(text);
}
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()) {
setStatus('Выбери модель Krea 2');
return;
@@ -7145,7 +7480,7 @@
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 model = $('sa_model')?.value;
if (!model) {
@@ -7283,6 +7618,9 @@
if (meta.system_chars != null) {
state.lastSystemChars = Number(meta.system_chars) || 0;
}
if (meta.system_layers && typeof meta.system_layers === 'object') {
state.lastSystemLayers = meta.system_layers;
}
try {
state.lastContextChars = (context && JSON.stringify(context).length) || 0;
} catch (e) {
@@ -7374,7 +7712,7 @@
const reply = data.reply || (state.streamEl?.querySelector('.sa-msg-body')?.textContent) || '';
const civitai = data.civitai_results || [];
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,
@@ -7402,7 +7740,7 @@
}
const reply = data.reply || '';
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,
(err2) => finishErr(String(err2 || err || 'Chat failed')),
@@ -7425,7 +7763,7 @@
}
const reply = data.reply || '';
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,
(err) => finishErr(String(err || 'Chat failed')),
@@ -7554,7 +7892,7 @@
preferSelected: false,
});
const pack = $('sa_pack');
if (pack && pack.value === 'write_prompt') {
if (pack && (pack.value === 'ordinary' || pack.value === 'write_prompt')) {
pack.value = 'critique_image';
saveSettings();
}
@@ -7813,7 +8151,7 @@
}
await setInitFromSrc(src);
const pack = $('sa_pack');
if (pack && pack.value === 'write_prompt') {
if (pack && (pack.value === 'ordinary' || pack.value === 'write_prompt')) {
setPackValue('inpaint_edit', { flash: true });
}
});
+118 -59
View File
@@ -18,19 +18,31 @@ public partial class SwarmAssistentExtension
const int MaxCivitaiHopsFallback = 2;
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 = [];
StringBuilder system = new();
JObject layers = new();
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)
{
string core = Config.LoadCorePrompt(pid);
if (!string.IsNullOrWhiteSpace(core))
{
system.AppendLine(core);
}
AddLayer("core", Config.LoadCorePrompt(pid));
}
if (Memory is not null)
@@ -40,12 +52,7 @@ public partial class SwarmAssistentExtension
JObject asst = Config.LoadAssistant(pid);
double weight = asst["user_prefs_weight"]?.Value<double?>() ?? 1.0;
int maxPrefs = asst["user_prefs_max"]?.Value<int?>() ?? 16;
string about = Memory.FormatUserPrefsBlock(pid, weight, maxPrefs);
if (!string.IsNullOrWhiteSpace(about))
{
system.AppendLine();
system.AppendLine(about);
}
AddLayer("prefs", Memory.FormatUserPrefsBlock(pid, weight, maxPrefs));
}
catch (Exception ex)
{
@@ -54,56 +61,55 @@ public partial class SwarmAssistentExtension
}
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)
{
system.AppendLine();
system.AppendLine("## Exact memory (canonical KV defaults — prefer over RAG for numbers)");
system.AppendLine("```json");
system.AppendLine(exact.ToString(Newtonsoft.Json.Formatting.None));
system.AppendLine("```");
AddLayer("exact",
"## Exact memory (canonical KV defaults — prefer over RAG for numbers)\n```json\n"
+ exact.ToString(Newtonsoft.Json.Formatting.None) + "\n```");
}
StringBuilder skillsBlock = new();
foreach (string skillId in skillIds ?? Config.ResolveEnabledSkills(pid, null))
{
string skillText = Config.LoadSkillPrompt(pid, skillId);
if (!string.IsNullOrWhiteSpace(skillText))
{
system.AppendLine();
system.AppendLine($"## Skill: {skillId}");
system.AppendLine(skillText);
if (skillsBlock.Length > 0)
{
skillsBlock.AppendLine();
}
skillsBlock.AppendLine($"## Skill: {skillId}");
skillsBlock.AppendLine(skillText.TrimEnd());
}
}
AddLayer("skills", skillsBlock.ToString());
string identity = Config.RenderIdentityBlock(pid);
if (!string.IsNullOrWhiteSpace(identity))
{
system.AppendLine();
system.AppendLine(identity);
}
AddLayer("identity", Config.RenderIdentityBlock(pid));
if (!string.IsNullOrWhiteSpace(packName))
{
string situational = Config.LoadPackPrompt(pid, packName);
if (!string.IsNullOrWhiteSpace(situational))
{
system.AppendLine();
system.AppendLine($"## Active mode: {packName}");
system.AppendLine(situational);
AddLayer("pack", $"## Active mode: {packName}\n{situational.TrimEnd()}");
}
}
if (!string.IsNullOrWhiteSpace(contextJson))
{
system.AppendLine();
system.AppendLine("## Live SwarmUI context (JSON — trust this over guesses)");
system.AppendLine("```json");
system.AppendLine(contextJson);
system.AppendLine("```");
AddLayer("live",
"## Live SwarmUI context (JSON — trust this over guesses)\n```json\n"
+ contextJson + "\n```");
}
if (!string.IsNullOrWhiteSpace(extraSystem))
{
system.AppendLine();
system.AppendLine(extraSystem);
AddLayer("extra", extraSystem);
}
layers["total"] = system.Length;
if (system.Length > 0)
{
ollamaMessages.Add(new JObject
@@ -129,10 +135,10 @@ public partial class SwarmAssistentExtension
}
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,
string root,
string modelName,
@@ -177,16 +183,10 @@ public partial class SwarmAssistentExtension
string enrichedContext = InjectMemoryHits(contextJson, hits);
enrichedContext = EnrichPersonaContext(enrichedContext, pid, packName);
List<JObject> messages = BuildOllamaMessages(packName, includeBase, enrichedContext, userMessages, personaId: pid, skillIds: skills);
int systemChars = 0;
foreach (JObject m in messages)
{
if (string.Equals(m["role"]?.ToString(), "system", StringComparison.OrdinalIgnoreCase))
{
systemChars = m["content"]?.ToString()?.Length ?? 0;
break;
}
}
(List<JObject> messages, JObject systemLayers) = BuildOllamaMessages(packName, includeBase, enrichedContext, userMessages, personaId: pid, skillIds: skills);
int systemChars = systemLayers["total"]?.Value<int?>()
?? messages.FirstOrDefault(m => string.Equals(m["role"]?.ToString(), "system", StringComparison.OrdinalIgnoreCase))?["content"]?.ToString()?.Length
?? 0;
JArray civitaiResults = [];
string reply = "";
JObject lastRaw = null;
@@ -229,7 +229,7 @@ public partial class SwarmAssistentExtension
messages.Add(new JObject { ["role"] = "assistant", ["content"] = assistantContent });
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)
@@ -249,7 +249,18 @@ public partial class SwarmAssistentExtension
{
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))
{
@@ -587,6 +598,10 @@ public partial class SwarmAssistentExtension
{
continue;
}
if (IsExactPointerHit(ho))
{
continue;
}
JObject copy = (JObject)ho.DeepClone();
string text = copy["text"]?.ToString() ?? "";
if (text.Length > hitChars)
@@ -598,6 +613,7 @@ public partial class SwarmAssistentExtension
}
ctx["memory_hits"] = clippedHits;
ctx.Remove("taste_profile");
ctx.Remove("enabled_loras"); // alias of selected_loras — do not double-feed
try
{
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);
}
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)
{
if (ctx["available_loras"] is not JArray allLoras || allLoras.Count == 0)
@@ -851,17 +910,17 @@ public partial class SwarmAssistentExtension
}
}
JArray catalog = [];
foreach (var p in Config.ListPersonaCatalog())
{
catalog.Add(new JObject
{
["id"] = p.id,
["title"] = p.title,
});
}
ctx["personas"] = catalog;
if (authorPack)
{
foreach (var p in Config.ListPersonaCatalog())
{
catalog.Add(new JObject
{
["id"] = p.id,
["title"] = p.title,
});
}
ctx["personas"] = catalog;
JObject shelves = Config.LoadIdentityParts(pid);
shelves.Remove("extra");
ctx["persona_shelves"] = shelves;
+46 -4
View File
@@ -96,12 +96,18 @@ public partial class SwarmAssistentExtension
|| 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
{
["success"] = true,
["base_url"] = root,
["models"] = models,
["memory_models"] = memoryModels,
["preferred"] = preferred ?? "",
};
}
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-");
}
/// <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(
string root,
string modelName,
@@ -239,11 +279,11 @@ public partial class SwarmAssistentExtension
{
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();
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);
return new JObject
{
@@ -255,6 +295,7 @@ public partial class SwarmAssistentExtension
["raw"] = parsed,
["civitai_results"] = civitai,
["system_chars"] = systemChars,
["system_layers"] = systemLayers,
};
}
catch (Exception ex)
@@ -275,7 +316,7 @@ public partial class SwarmAssistentExtension
await ws.SendJson(new JObject { ["error"] = invalid }, API.WebsocketTimeout);
return null;
}
string packName = (pack ?? "write_prompt").Trim();
string packName = (pack ?? "ordinary").Trim();
string embedModel = raw?["embed_model"]?.ToString() ?? Config.LoadSettings()["embed_model"]?.ToString();
try
{
@@ -306,7 +347,7 @@ public partial class SwarmAssistentExtension
}, 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);
await ws.SendJson(new JObject
{
@@ -319,6 +360,7 @@ public partial class SwarmAssistentExtension
["raw"] = parsed,
["civitai_results"] = civitai,
["system_chars"] = systemChars,
["system_layers"] = systemLayers,
}, API.WebsocketTimeout);
}
catch (Exception ex)
+2 -2
View File
@@ -9,7 +9,7 @@
"inventory_prompt_names": 24,
"inventory_hop_limit": 20,
"max_ref_slots": 4,
"default_pack": "write_prompt",
"default_pack": "ordinary",
"default_persona": "neutral",
"embed_model": "nomic-embed-text",
"memory_top_k": 8,
@@ -28,7 +28,7 @@
"note": 4,
"model": 2
},
"seed_version": 2,
"seed_version": 3,
"gate": {
"architecture": "krea2",
"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.
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 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**.
- Rich entries (blurbs/triggers) are enabled + top krea-likely. Name-only rows need `list_inventory` + `inventory_query` before you rely on them.
- 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 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.
- `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`.
- 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)
@@ -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`.
- `loras` replaces the full intended set for Apply. Prefer `aspect` over raw width/height.
- Optional keys (seed, vary, init/mask, creativity/sliders, pack, **controls**, persona authoring, search/memory queries) — use when needed; packs list the ones for that mode.
- **`controls`** — only keys declared in this persona's `controls.json` (Exact). 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.
### 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.
- `"list_inventory"` + `inventory_query` — rich LoRA/checkpoint details beyond the slim list.
- `"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.
- `"persona_clone"` / `"persona_write"` / `"persona_switch"` — `author_persona` only. Never `"persona_delete"`.
- `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.
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
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):
1. **Subject + pose/action** (gaze, expression, body language)
2. **Clothes / hair / materials** (fabric, fit, how light hits surfaces)
3. **Props & environment**
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.
- Brief note of what you changed.
- JSON patch with at least `prompt`, and `loras` when relevant.
- `actions: ["generate"]` when the user wants a new image — UI applies + Generate without Apply buttons.
- 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.
- Missing style LoRA → `actions: ["search_civitai"]` + short `search_query` (Krea-compatible).
### Bad → good
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.`
## 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)
- `creativity`: `raw` | `low` | `medium` | `high` — how much **you** expand the user's wording into the prompt. 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.
- `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 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>Напиши, что сгенерировать — или кинь референс и попроси правку.",
"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": [
{ "label": "1:1", "action": "aspect", "value": "1:1", "title": "1024×1024" },
{ "label": "4:5", "action": "aspect", "value": "4:5", "title": "928×1152" },
@@ -38,6 +38,11 @@
{ "cmd": "/persona save", "hint": "записать черновик", "action": "persona_save" }
],
"pack_aliases": {
"ordinary": "ordinary",
"combine": "ordinary",
"normal": "ordinary",
"general": "ordinary",
"default": "ordinary",
"write": "write_prompt",
"write_prompt": "write_prompt",
"critique": "critique_image",
+1 -4
View File
@@ -3,13 +3,10 @@
"persona",
"bio",
"voice",
"humor",
"rules",
"likes",
"dislikes",
"appearance",
"outfits",
"roleplay",
"craft"
"outfits"
]
}
+1
View File
@@ -8,6 +8,7 @@
"Scale sexual tone + roleplay fetishes by controls.horny (0…100)",
"Explicit user look/outfit/plot beats personal taste",
"«девушка которая тебе нравится» = YOUR (Leonid) taste, not the user's — unless they said otherwise",
"High horny / NSFW plot / craft detail: persona_read shelves [\"roleplay\"] (and humor/craft if needed) — they are not always-on",
"/horny-game: score taste match → patch controls.horny (0100), say new % in prose; no generate unless asked"
],
"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.
**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
@@ -125,7 +125,7 @@ Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart.
## 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.
+1 -1
View File
@@ -36,7 +36,7 @@ public partial class SwarmAssistentExtension : Extension
ExtensionAuthor = "mrleo1nid";
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
License = "MIT";
Version = "0.10.8";
Version = "0.10.13";
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"];
}
+2 -1
View File
@@ -63,9 +63,10 @@
</div>
<div class="sa-persona-controls" id="sa_persona_controls" hidden></div>
<select id="sa_pack" class="sa-select" title="Пакет промпта">
<option value="ordinary">Обычный</option>
<option value="write_prompt">Написать промпт</option>
</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 (чат)">
<option value="">Загрузка моделей…</option>
</select>