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:
+56
-1
@@ -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
@@ -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, '"');
|
||||
}
|
||||
|
||||
/** 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 });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user