Ship Assistent 0.14.0: chat sessions, ask-only hops, and context compression.
Per-chat Generate session with sparse deltas; drop Cards/Civitai/wanted hops; rolling history summary via the same Ollama model with a budget chip and /compress. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+263
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* Turn activity timeline — shows model commands and pipeline steps in chat.
|
||||
* Cursor-like, but compact and chat-native.
|
||||
*/
|
||||
|
||||
const STEP_ICONS = {
|
||||
think: '◇',
|
||||
stream: '✎',
|
||||
delta: '⇢',
|
||||
ask: '?',
|
||||
look: '◎',
|
||||
prep: '↻',
|
||||
generate: '▷',
|
||||
merge: '⊕',
|
||||
warm: '▲',
|
||||
park: '▼',
|
||||
inventory: '▤',
|
||||
compress: '▤',
|
||||
done: '✓',
|
||||
skip: '–',
|
||||
error: '!',
|
||||
};
|
||||
|
||||
export function createActivityController(opts = {}) {
|
||||
const {
|
||||
getMessagesEl,
|
||||
scrollToBottom,
|
||||
hideEmpty,
|
||||
} = opts;
|
||||
|
||||
let card = null;
|
||||
let listEl = null;
|
||||
let titleEl = null;
|
||||
let steps = [];
|
||||
let open = true;
|
||||
|
||||
function ensureCard() {
|
||||
const box = typeof getMessagesEl === 'function' ? getMessagesEl() : null;
|
||||
if (!box) {
|
||||
return null;
|
||||
}
|
||||
if (card && card.isConnected) {
|
||||
return card;
|
||||
}
|
||||
if (typeof hideEmpty === 'function') {
|
||||
hideEmpty();
|
||||
}
|
||||
card = document.createElement('div');
|
||||
card.className = 'sa-activity sa-activity-live';
|
||||
card.setAttribute('role', 'status');
|
||||
card.setAttribute('aria-live', 'polite');
|
||||
|
||||
const head = document.createElement('button');
|
||||
head.type = 'button';
|
||||
head.className = 'sa-activity-head';
|
||||
head.setAttribute('aria-expanded', 'true');
|
||||
|
||||
const spin = document.createElement('span');
|
||||
spin.className = 'sa-activity-spin';
|
||||
spin.setAttribute('aria-hidden', 'true');
|
||||
|
||||
titleEl = document.createElement('span');
|
||||
titleEl.className = 'sa-activity-title';
|
||||
titleEl.textContent = 'Assistent';
|
||||
|
||||
const chev = document.createElement('span');
|
||||
chev.className = 'sa-activity-chev';
|
||||
chev.setAttribute('aria-hidden', 'true');
|
||||
chev.textContent = '▾';
|
||||
|
||||
head.appendChild(spin);
|
||||
head.appendChild(titleEl);
|
||||
head.appendChild(chev);
|
||||
head.addEventListener('click', () => {
|
||||
open = !open;
|
||||
card.classList.toggle('sa-activity-collapsed', !open);
|
||||
head.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
});
|
||||
|
||||
listEl = document.createElement('div');
|
||||
listEl.className = 'sa-activity-steps';
|
||||
|
||||
card.appendChild(head);
|
||||
card.appendChild(listEl);
|
||||
box.appendChild(card);
|
||||
if (typeof scrollToBottom === 'function') {
|
||||
scrollToBottom();
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderStep(step) {
|
||||
const row = document.createElement('div');
|
||||
row.className = `sa-activity-step sa-activity-${step.status || 'running'}`;
|
||||
row.dataset.id = step.id;
|
||||
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'sa-activity-icon';
|
||||
icon.setAttribute('aria-hidden', 'true');
|
||||
icon.textContent = STEP_ICONS[step.kind] || STEP_ICONS.think;
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'sa-activity-body';
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'sa-activity-label';
|
||||
label.textContent = step.label || step.id;
|
||||
|
||||
body.appendChild(label);
|
||||
if (step.detail) {
|
||||
const detail = document.createElement('div');
|
||||
detail.className = 'sa-activity-detail';
|
||||
detail.textContent = step.detail;
|
||||
body.appendChild(detail);
|
||||
}
|
||||
|
||||
row.appendChild(icon);
|
||||
row.appendChild(body);
|
||||
return row;
|
||||
}
|
||||
|
||||
function paint() {
|
||||
if (!ensureCard() || !listEl) {
|
||||
return;
|
||||
}
|
||||
listEl.replaceChildren(...steps.map(renderStep));
|
||||
const running = steps.find((s) => s.status === 'running');
|
||||
const last = steps[steps.length - 1];
|
||||
if (titleEl) {
|
||||
titleEl.textContent = running
|
||||
? running.label
|
||||
: (last?.label || 'Assistent');
|
||||
}
|
||||
card.classList.toggle('sa-activity-live', steps.some((s) => s.status === 'running'));
|
||||
card.classList.toggle('sa-activity-done', steps.length > 0 && steps.every((s) => s.status === 'done' || s.status === 'skip'));
|
||||
if (typeof scrollToBottom === 'function') {
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
function begin(title) {
|
||||
steps = [];
|
||||
card = null;
|
||||
listEl = null;
|
||||
titleEl = null;
|
||||
open = true;
|
||||
ensureCard();
|
||||
if (titleEl && title) {
|
||||
titleEl.textContent = title;
|
||||
}
|
||||
paint();
|
||||
}
|
||||
|
||||
function upsert(id, patch) {
|
||||
ensureCard();
|
||||
let step = steps.find((s) => s.id === id);
|
||||
if (!step) {
|
||||
step = { id, kind: 'think', label: id, status: 'running', detail: '' };
|
||||
steps.push(step);
|
||||
}
|
||||
Object.assign(step, patch);
|
||||
if (!step.status) {
|
||||
step.status = 'running';
|
||||
}
|
||||
paint();
|
||||
return step;
|
||||
}
|
||||
|
||||
function done(id, patch = {}) {
|
||||
return upsert(id, { ...patch, status: 'done' });
|
||||
}
|
||||
|
||||
function skip(id, patch = {}) {
|
||||
return upsert(id, { ...patch, status: 'skip' });
|
||||
}
|
||||
|
||||
function fail(id, patch = {}) {
|
||||
return upsert(id, { ...patch, status: 'error' });
|
||||
}
|
||||
|
||||
function finish(summary) {
|
||||
steps.forEach((s) => {
|
||||
if (s.status === 'running') {
|
||||
s.status = 'done';
|
||||
}
|
||||
});
|
||||
if (summary && titleEl) {
|
||||
titleEl.textContent = summary;
|
||||
}
|
||||
paint();
|
||||
if (card) {
|
||||
card.classList.remove('sa-activity-live');
|
||||
card.classList.add('sa-activity-done');
|
||||
}
|
||||
}
|
||||
|
||||
/** Describe a sparse model patch as human-readable activity steps. */
|
||||
function noteModelCommands(patch) {
|
||||
if (!patch || typeof patch !== 'object') {
|
||||
return;
|
||||
}
|
||||
const keys = Object.keys(patch).filter((k) => patch[k] != null
|
||||
&& !['actions', 'generate', 'ask', 'look_at', 'vision_from', 'vision_slots', 'notes', 'variants'].includes(k));
|
||||
if (keys.length) {
|
||||
done('delta', {
|
||||
kind: 'delta',
|
||||
label: 'Обновил сессию',
|
||||
detail: keys.slice(0, 10).join(', '),
|
||||
});
|
||||
}
|
||||
const ask = Array.isArray(patch.ask) ? patch.ask.map(String) : (patch.ask ? [String(patch.ask)] : []);
|
||||
if (ask.length) {
|
||||
upsert('ask', {
|
||||
kind: 'ask',
|
||||
label: `Запросил ${ask.join(', ')}`,
|
||||
detail: 'подгружаю детали…',
|
||||
status: 'running',
|
||||
});
|
||||
}
|
||||
if (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null) {
|
||||
const slots = [].concat(patch.look_at || patch.vision_from || patch.vision_slots || []);
|
||||
upsert('look', {
|
||||
kind: 'look',
|
||||
label: 'Смотрит на кадр',
|
||||
detail: slots.map(String).slice(0, 4).join(', '),
|
||||
status: 'running',
|
||||
});
|
||||
}
|
||||
if (patch.generate === true
|
||||
|| (Array.isArray(patch.actions) && patch.actions.map(String).includes('generate'))) {
|
||||
upsert('generate', {
|
||||
kind: 'generate',
|
||||
label: 'Generate',
|
||||
detail: 'ждёт пайплайн…',
|
||||
status: 'running',
|
||||
});
|
||||
}
|
||||
if (Array.isArray(patch.variants) && patch.variants.length) {
|
||||
upsert('variants', {
|
||||
kind: 'generate',
|
||||
label: `Варианты ×${patch.variants.length}`,
|
||||
status: 'running',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
begin,
|
||||
upsert,
|
||||
done,
|
||||
skip,
|
||||
fail,
|
||||
finish,
|
||||
noteModelCommands,
|
||||
get steps() {
|
||||
return steps.slice();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function attachActivity(SA) {
|
||||
SA.createActivityController = createActivityController;
|
||||
}
|
||||
+1051
-1540
File diff suppressed because it is too large
Load Diff
+194
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Context budget + conversation memory helpers for chat compression.
|
||||
*/
|
||||
|
||||
export function emptyContextMemory() {
|
||||
return {
|
||||
summary: '',
|
||||
untilCount: 0,
|
||||
foldedTurns: 0,
|
||||
at: 0,
|
||||
uiCollapsed: false,
|
||||
promptEvalCount: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeContextMemory(raw) {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return emptyContextMemory();
|
||||
}
|
||||
const summary = String(raw.summary || '').trim();
|
||||
return {
|
||||
summary,
|
||||
untilCount: Math.max(0, Number(raw.untilCount) || 0),
|
||||
foldedTurns: Math.max(0, Number(raw.foldedTurns) || 0),
|
||||
at: Number(raw.at) || 0,
|
||||
uiCollapsed: !!raw.uiCollapsed && !!summary,
|
||||
promptEvalCount: raw.promptEvalCount != null ? Number(raw.promptEvalCount) || null : null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Estimate tokens from character counts. */
|
||||
export function charsToTokens(chars, charsPerToken = 3.2) {
|
||||
const cpt = Math.max(1.5, Number(charsPerToken) || 3.2);
|
||||
const n = Math.max(0, Number(chars) || 0);
|
||||
return Math.ceil(n / cpt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* systemChars?: number,
|
||||
* historyChars?: number,
|
||||
* memoryChars?: number,
|
||||
* numCtx?: number,
|
||||
* numPredict?: number,
|
||||
* charsPerToken?: number,
|
||||
* compressAt?: number,
|
||||
* promptEvalCount?: number|null,
|
||||
* }} opts
|
||||
*/
|
||||
export function estimateBudget(opts = {}) {
|
||||
const numCtx = Math.max(1024, Number(opts.numCtx) || 16384);
|
||||
const numPredict = Math.max(256, Number(opts.numPredict) || 3072);
|
||||
const charsPerToken = Math.max(1.5, Number(opts.charsPerToken) || 3.2);
|
||||
const compressAt = Math.min(0.95, Math.max(0.4, Number(opts.compressAt) || 0.7));
|
||||
const systemChars = Math.max(0, Number(opts.systemChars) || 0);
|
||||
const historyChars = Math.max(0, Number(opts.historyChars) || 0);
|
||||
const memoryChars = Math.max(0, Number(opts.memoryChars) || 0);
|
||||
const inputChars = systemChars + historyChars + memoryChars;
|
||||
const estimated = charsToTokens(inputChars, charsPerToken);
|
||||
const used = opts.promptEvalCount != null && Number(opts.promptEvalCount) > 0
|
||||
? Number(opts.promptEvalCount)
|
||||
: estimated;
|
||||
const headroom = Math.max(1024, numCtx - numPredict);
|
||||
const threshold = Math.floor(headroom * compressAt);
|
||||
const ratio = numCtx > 0 ? used / numCtx : 0;
|
||||
let level = 'ok';
|
||||
if (ratio >= 0.85 || used >= threshold) {
|
||||
level = 'hot';
|
||||
} else if (ratio >= 0.65 || used >= threshold * 0.85) {
|
||||
level = 'warn';
|
||||
}
|
||||
return {
|
||||
numCtx,
|
||||
numPredict,
|
||||
headroom,
|
||||
threshold,
|
||||
systemChars,
|
||||
historyChars,
|
||||
memoryChars,
|
||||
inputChars,
|
||||
estimated,
|
||||
used,
|
||||
fromEval: opts.promptEvalCount != null && Number(opts.promptEvalCount) > 0,
|
||||
ratio,
|
||||
level,
|
||||
charsPerToken,
|
||||
compressAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether auto-compress should run before the next chat turn.
|
||||
* @param {ReturnType<typeof estimateBudget>} budget
|
||||
* @param {{ untilCount?: number, summary?: string }} memory
|
||||
* @param {number} historyLen — full transcript length
|
||||
* @param {{ keepMessages?: number }} opts — messages kept raw (turns*2)
|
||||
*/
|
||||
export function shouldCompress(budget, memory, historyLen, opts = {}) {
|
||||
const keep = Math.max(2, Number(opts.keepMessages) || 8);
|
||||
const len = Math.max(0, Number(historyLen) || 0);
|
||||
const until = Math.max(0, Number(memory?.untilCount) || 0);
|
||||
const uncovered = Math.max(0, len - until);
|
||||
if (uncovered <= keep) {
|
||||
return false;
|
||||
}
|
||||
const used = budget?.used ?? 0;
|
||||
const threshold = budget?.threshold ?? Infinity;
|
||||
return used >= threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Messages the model should see: optional covered-by-summary skip + last keep raw.
|
||||
* @param {Array<{role:string,content?:string,systemish?:boolean}>} history
|
||||
* @param {{ untilCount?: number }} memory
|
||||
* @param {number} keepTurns
|
||||
*/
|
||||
export function assembleModelMessages(history, memory, keepTurns) {
|
||||
const keep = Math.max(1, Number(keepTurns) || 4) * 2;
|
||||
const until = Math.max(0, Number(memory?.untilCount) || 0);
|
||||
const list = (history || []).filter((m) => m && (m.role === 'user' || m.role === 'assistant') && !m.systemish);
|
||||
const afterSummary = until > 0 ? list.slice(until) : list;
|
||||
const window = afterSummary.length > keep ? afterSummary.slice(-keep) : afterSummary;
|
||||
return window.map((m) => ({
|
||||
role: m.role,
|
||||
content: String(m.content || '').slice(0, 4000),
|
||||
}));
|
||||
}
|
||||
|
||||
/** How many leading messages can be folded into a new summary (leave keep raw). */
|
||||
export function messagesToFold(history, memory, keepTurns) {
|
||||
const keep = Math.max(1, Number(keepTurns) || 4) * 2;
|
||||
const list = (history || []).filter((m) => m && (m.role === 'user' || m.role === 'assistant') && !m.systemish);
|
||||
const until = Math.max(0, Number(memory?.untilCount) || 0);
|
||||
const foldEnd = Math.max(until, list.length - keep);
|
||||
if (foldEnd <= until) {
|
||||
return [];
|
||||
}
|
||||
return list.slice(until, foldEnd);
|
||||
}
|
||||
|
||||
export function mergeSummary(oldSummary, incoming) {
|
||||
const next = String(incoming || '').trim();
|
||||
if (!next) {
|
||||
return String(oldSummary || '').trim();
|
||||
}
|
||||
const prev = String(oldSummary || '').trim();
|
||||
if (!prev) {
|
||||
return next;
|
||||
}
|
||||
// Prefer the model output when it already incorporates prior memory.
|
||||
return next;
|
||||
}
|
||||
|
||||
export function formatTokenShort(n) {
|
||||
const v = Math.max(0, Number(n) || 0);
|
||||
if (v >= 10000) {
|
||||
return `${(v / 1000).toFixed(1)}k`;
|
||||
}
|
||||
if (v >= 1000) {
|
||||
return `${(v / 1000).toFixed(1)}k`;
|
||||
}
|
||||
return String(Math.round(v));
|
||||
}
|
||||
|
||||
export function conversationMemoryBlock(memory, maxChars = 2400) {
|
||||
const m = normalizeContextMemory(memory);
|
||||
if (!m.summary) {
|
||||
return null;
|
||||
}
|
||||
let text = m.summary;
|
||||
if (text.length > maxChars) {
|
||||
text = `${text.slice(0, maxChars)}…`;
|
||||
}
|
||||
return {
|
||||
summary: text,
|
||||
until_count: m.untilCount,
|
||||
folded_turns: m.foldedTurns,
|
||||
};
|
||||
}
|
||||
|
||||
export function attachContext(SA) {
|
||||
SA.context = {
|
||||
emptyContextMemory,
|
||||
normalizeContextMemory,
|
||||
charsToTokens,
|
||||
estimateBudget,
|
||||
shouldCompress,
|
||||
assembleModelMessages,
|
||||
messagesToFold,
|
||||
mergeSummary,
|
||||
formatTokenShort,
|
||||
conversationMemoryBlock,
|
||||
};
|
||||
}
|
||||
+21
-140
@@ -1,4 +1,4 @@
|
||||
/** Turn intent heuristics — pure functions testable with node --test. */
|
||||
/** Turn intent — veto only; generate comes from model `generate: true` (or legacy actions). */
|
||||
|
||||
export function cyrTokenRe(alts) {
|
||||
const boundary = '(^|[^0-9A-Za-z_А-Яа-яЁё])';
|
||||
@@ -6,55 +6,9 @@ export function cyrTokenRe(alts) {
|
||||
return new RegExp(`${boundary}(?:${alts})${end}`, 'i');
|
||||
}
|
||||
|
||||
export function userAsksGenerate(text) {
|
||||
const t = String(text || '').trim();
|
||||
if (!t) {
|
||||
return false;
|
||||
}
|
||||
if (/^(gen|generate|go|рисуй|нарисуй)([!.…\s]|$)/i.test(t)) {
|
||||
return true;
|
||||
}
|
||||
if (/^(ещё|еще)(\s+раз)?([!.…\s]|$)/i.test(t)) {
|
||||
return true;
|
||||
}
|
||||
const letter = '[0-9A-Za-z_А-Яа-яЁё]';
|
||||
const stem = `${letter}*`;
|
||||
return cyrTokenRe(
|
||||
'сгенерируй|сгенерировать|генерируй|generate|нарисуй|перегенерируй|перерисуй|'
|
||||
+ `сделай\\s+(картинк${stem}|изображен${stem}|фото${stem})|`
|
||||
+ `хочу\\s+(картинк${stem}|изображен${stem}|фото${stem})|`
|
||||
+ 'run\\s+generat|/gen',
|
||||
).test(t);
|
||||
}
|
||||
|
||||
export function userAsksContinue(text) {
|
||||
const t = String(text || '').trim();
|
||||
if (!t) {
|
||||
return false;
|
||||
}
|
||||
if (/^(давай\s+дальше|продолжай|продолжим|go\s+on|continue|keep\s+going|next(\s+one)?|next\s+frame)([!.…\s]|$)/i.test(t)) {
|
||||
return true;
|
||||
}
|
||||
return cyrTokenRe(
|
||||
'давай\\s+дальше|следующ(ий|ая|ее|ую)\\s+кадр|ещё\\s+кадр|еще\\s+кадр|'
|
||||
+ 'кадр\\s*№?\\s*\\d+|сделай\\s+следующ',
|
||||
).test(t);
|
||||
}
|
||||
|
||||
export function isSameButAspectRequest(text) {
|
||||
const t = String(text || '').trim();
|
||||
if (!t) {
|
||||
return false;
|
||||
}
|
||||
return cyrTokenRe(
|
||||
'тот\\s+же\\s+(кадр|сцена|промпт|prompt)|так\\s+же\\s+но\\s+(друг|иной)\\s+(формат|размер|aspect|соотношен)|'
|
||||
+ 'same\\s+but\\s+(wider|taller|16:9|4:3|portrait|landscape)',
|
||||
).test(t);
|
||||
}
|
||||
|
||||
export function userAsksNoGenerate(text) {
|
||||
const t = String(text || '').trim();
|
||||
if (!t || userAsksGenerate(t)) {
|
||||
if (!t) {
|
||||
return false;
|
||||
}
|
||||
if (/\b(remember|save\s+(this\s+)?(as\s+)?(the\s+)?(base\s+)?(prompt|template)|don'?t\s+generat|do\s+not\s+generat|no\s+generat|without\s+generat)\b/i.test(t)) {
|
||||
@@ -63,19 +17,12 @@ export function userAsksNoGenerate(text) {
|
||||
return cyrTokenRe(
|
||||
'запомн|запомни|запомним|сохрани|сохраним|шаблон|'
|
||||
+ 'базов(ый|ого|ому|ым|ая|ую|ое)?\\s+промпт|'
|
||||
+ 'не\\s+генерир|без\\s+генерац|не\\s+надо\\s+генер|только\\s+запомн|пока\\s+запомн|'
|
||||
+ 'не\\s+рисуй|не\\s+запускай\\s+генер',
|
||||
+ 'не\\s+генерир[а-яё]*|без\\s+генерац[а-яё]*|не\\s+надо\\s+генер[а-яё]*|только\\s+запомн[а-яё]*|пока\\s+запомн[а-яё]*|'
|
||||
+ 'не\\s+рисуй|не\\s+запускай\\s+генер[а-яё]*|'
|
||||
+ 'только\\s+(ответь|скажи|объясни)',
|
||||
).test(t);
|
||||
}
|
||||
|
||||
export function userCommandsGenerate(text) {
|
||||
const t = String(text || '').trim();
|
||||
if (!t || userAsksNoGenerate(t)) {
|
||||
return false;
|
||||
}
|
||||
return userAsksGenerate(t) || userAsksContinue(t) || isSameButAspectRequest(t);
|
||||
}
|
||||
|
||||
export function userAsksLook(text) {
|
||||
const t = String(text || '').trim();
|
||||
if (!t) {
|
||||
@@ -93,68 +40,15 @@ export function userAsksLook(text) {
|
||||
return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t);
|
||||
}
|
||||
|
||||
export function userIsChatNotFrame(text) {
|
||||
export function isSameButAspectRequest(text) {
|
||||
const t = String(text || '').trim();
|
||||
if (!t) {
|
||||
return false;
|
||||
}
|
||||
if (/^(ок|окей|ok|okay|ладно|хорошо|понял|ясно|спасибо|thanks)([!.…\s]*)$/i.test(t)) {
|
||||
return true;
|
||||
}
|
||||
if (cyrTokenRe(
|
||||
'что\\s+такое|как\\s+работает|зачем\\s+|какие\\s+(лор|модел|чекпоинт)|'
|
||||
+ 'список\\s+лор|где\\s+настрой|что\\s+значит|'
|
||||
+ 'нравит|спасибо|благодар|почему\\s+так|что\\s+ты\\s+(сделал|изменил)|'
|
||||
+ 'только\\s+(ответь|скажи|объясни)|без\\s+(кадр|генерац)|не\\s+надо\\s+кадр',
|
||||
).test(t) && !userAsksGenerate(t) && !userAsksContinue(t)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function userImpliesGenerate(text) {
|
||||
const t = String(text || '').trim();
|
||||
if (!t || userAsksNoGenerate(t) || userIsChatNotFrame(t)) {
|
||||
return false;
|
||||
}
|
||||
if (userCommandsGenerate(t)) {
|
||||
return true;
|
||||
}
|
||||
if (t.length < 8) {
|
||||
return false;
|
||||
}
|
||||
const wantsLook = userAsksLook(t);
|
||||
const wantsRedraw = cyrTokenRe('поправь|исправь|перегенерир|перерисуй|улучши|переделай').test(t)
|
||||
|| /\b(fix|redo|redraw|improve)\b/i.test(t);
|
||||
if (wantsLook && !wantsRedraw) {
|
||||
return false;
|
||||
}
|
||||
if (cyrTokenRe(
|
||||
'нарису|сгенер|перерису|'
|
||||
+ 'сделай\\s+(картинк|изображен|фото|кадр)|'
|
||||
+ 'хочу\\s+(картинк|изображен|фото|увидеть|видеть)|'
|
||||
+ 'покажи\\s+как\\s+(она|он|это)|'
|
||||
+ 'сделай\\s+(её|ее|его|мне)\\s|'
|
||||
+ 'пусть\\s+будет|'
|
||||
+ 'другой\\s+(ракурс|свет|наряд|поза)|'
|
||||
+ 'поменяй\\s+(позу|свет|одежд|фон)|добавь\\s+(свет|детал)|'
|
||||
+ 'ещё\\s+одн|еще\\s+одн',
|
||||
).test(t)) {
|
||||
return true;
|
||||
}
|
||||
if (/\b(draw|paint|render|make her|make him|another one|new frame)\b/i.test(t)) {
|
||||
return true;
|
||||
}
|
||||
const isQuestion = /[??]\s*$/.test(t);
|
||||
if (isQuestion) {
|
||||
return cyrTokenRe('нарису|сгенер|можешь\\s+(сделать|нарисовать)|можно\\s+(картинк|сгенер)').test(t);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function packBlocksAutoGenerate(pack) {
|
||||
const p = String(pack || '');
|
||||
return p === 'describe_ref' || p === 'catalog_card' || p === 'author_persona' || p === 'debug_explain';
|
||||
return cyrTokenRe(
|
||||
'тот\\s+же\\s+(кадр|сцена|промпт|prompt)|так\\s+же\\s+но\\s+(друг|иной)\\s+(формат|размер|aspect|соотношен)|'
|
||||
+ 'same\\s+but\\s+(wider|taller|16:9|4:3|portrait|landscape)',
|
||||
).test(t);
|
||||
}
|
||||
|
||||
export function packWantsVision(pack) {
|
||||
@@ -162,30 +56,17 @@ export function packWantsVision(pack) {
|
||||
return p === 'critique_image' || p === 'describe_ref' || p === 'compose_scene' || p === 'inpaint_edit';
|
||||
}
|
||||
|
||||
export function resolveTurnIntent(patch, userText, opts = {}, packId = '') {
|
||||
const machine = !!opts.machineTurn;
|
||||
const vetoed = !machine && userAsksNoGenerate(userText);
|
||||
const commanded = !!opts.userWantsGenerate || (!machine && userCommandsGenerate(userText));
|
||||
const implied = !machine && userImpliesGenerate(userText);
|
||||
const modelAsked = Array.isArray(patch?.actions) && patch.actions.map(String).includes('generate');
|
||||
|
||||
let generate;
|
||||
if (vetoed || opts.fromAutoCritique) {
|
||||
generate = false;
|
||||
} else if (commanded) {
|
||||
generate = true;
|
||||
} else if (packBlocksAutoGenerate(packId)) {
|
||||
generate = false;
|
||||
} else {
|
||||
generate = modelAsked || implied;
|
||||
}
|
||||
|
||||
/** Model generate + explicit veto. No RU imply/command heuristics. */
|
||||
export function resolveTurnIntent(patch, userText, opts = {}) {
|
||||
const vetoed = !opts.machineTurn && userAsksNoGenerate(userText);
|
||||
const modelAsked = patch?.generate === true
|
||||
|| (Array.isArray(patch?.actions) && patch.actions.map(String).includes('generate'));
|
||||
const generate = !vetoed && !opts.fromAutoCritique && !!modelAsked;
|
||||
const hasLook = !!patch
|
||||
&& (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
|
||||
const honorLook = opts.fromAutoCritique || opts.fromVisionHop
|
||||
|| (!machine && userAsksLook(userText))
|
||||
|| packWantsVision(packId);
|
||||
const look = !!(hasLook && !vetoed && !generate && honorLook);
|
||||
|
||||
return { generate, look, vetoed };
|
||||
const look = !!(hasLook && !vetoed && !generate);
|
||||
const ask = Array.isArray(patch?.ask)
|
||||
? patch.ask.map(String)
|
||||
: (typeof patch?.ask === 'string' && patch.ask ? [patch.ask] : []);
|
||||
return { generate, look, vetoed, ask };
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { attachApi } from './api.js';
|
||||
import { attachPatch, setPatchKeys } from './patch.js';
|
||||
import { attachPersist } from './persist.js';
|
||||
import { attachSession } from './session.js';
|
||||
import { attachContext } from './context.js';
|
||||
import { attachActivity } from './activity.js';
|
||||
|
||||
window.SA = window.SA || {};
|
||||
attachApi(window.SA);
|
||||
attachPatch(window.SA);
|
||||
attachPersist(window.SA);
|
||||
attachSession(window.SA);
|
||||
attachContext(window.SA);
|
||||
attachActivity(window.SA);
|
||||
|
||||
/** Called from app after AssistentGetConfig — single source: Config/_base/patch-keys.json */
|
||||
window.SA.applyConfigPatchKeys = function (config) {
|
||||
|
||||
+19
-39
@@ -1,17 +1,15 @@
|
||||
/** Patch detection / extraction — mirrors AssistentPatch.cs. */
|
||||
/** Patch detection / extraction — mirrors AssistentPatch.cs (0.14 sparse session deltas). */
|
||||
|
||||
const DEFAULT_PATCH_KEYS = [
|
||||
'prompt', 'negative', 'loras', 'width', 'height', 'steps', 'cfg', 'seed', 'sigma_shift', 'sampler', 'scheduler',
|
||||
'actions', 'search_query', 'civitai_query',
|
||||
'actions', 'generate', 'ask',
|
||||
'use_init_image', 'clear_init_image', 'init_creativity', 'denoise',
|
||||
'use_mask_image', 'clear_mask_image', 'mask_blur', 'mask_grow',
|
||||
'look_at', 'vision_from', 'vision_slots', 'slot_to_init', 'slot_to_mask',
|
||||
'snapshot_generate', 'select_slot', 'aspect', 'images', 'batch', 'vary', 'lock_seed',
|
||||
'creativity', 'intensity', 'complexity', 'movement',
|
||||
'clear_prompt_images', 'slot_to_prompt_image', 'pack', 'memories', 'memory',
|
||||
'memory_query', 'memory_kind', 'tag_query', 'user_prefs',
|
||||
'inventory_query', 'skills', 'persona_shelves', 'persona_clone', 'persona', 'controls',
|
||||
'variants',
|
||||
'clear_prompt_images', 'slot_to_prompt_image', 'pack', 'persona', 'controls',
|
||||
'inventory_query', 'variants',
|
||||
];
|
||||
|
||||
let PATCH_KEYS = DEFAULT_PATCH_KEYS.slice();
|
||||
@@ -32,27 +30,10 @@ function has(obj, key) {
|
||||
return obj[key] !== undefined && obj[key] !== null;
|
||||
}
|
||||
|
||||
export function isCardObject(obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return false;
|
||||
}
|
||||
const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint);
|
||||
const genish = !!(obj.prompt != null || obj.negative != null || obj.loras || obj.actions
|
||||
|| obj.width || obj.height || obj.steps != null || obj.cfg != null || obj.aspect || obj.seed != null
|
||||
|| obj.search_query || obj.civitai_query || obj.look_at || obj.controls);
|
||||
if (cardish && !genish && (obj.name || obj.triggers || obj.when)) {
|
||||
return true;
|
||||
}
|
||||
return !!(obj.kind && obj.name && (obj.triggers || obj.when || obj.prompt_hint || obj.notes != null));
|
||||
}
|
||||
|
||||
export function isPatchObject(obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (isCardObject(obj)) {
|
||||
return false;
|
||||
}
|
||||
return PATCH_KEYS.some((k) => has(obj, k));
|
||||
}
|
||||
|
||||
@@ -60,8 +41,12 @@ export function normalizePatch(patch) {
|
||||
if (!patch || typeof patch !== 'object') {
|
||||
return patch;
|
||||
}
|
||||
if (!has(patch, 'search_query') && has(patch, 'civitai_query')) {
|
||||
patch.search_query = patch.civitai_query;
|
||||
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
|
||||
if (patch.generate === true || acts.includes('generate')) {
|
||||
patch.generate = true;
|
||||
}
|
||||
if (typeof patch.ask === 'string') {
|
||||
patch.ask = [patch.ask];
|
||||
}
|
||||
if (!has(patch, 'init_creativity') && has(patch, 'denoise')) {
|
||||
patch.init_creativity = patch.denoise;
|
||||
@@ -100,7 +85,13 @@ export function isTerminalStreamPatch(obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (isCardObject(obj)) {
|
||||
if (obj.generate === true) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(obj.ask) && obj.ask.length) {
|
||||
return true;
|
||||
}
|
||||
if (typeof obj.ask === 'string' && obj.ask) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(obj.variants) && obj.variants.length) {
|
||||
@@ -109,17 +100,8 @@ export function isTerminalStreamPatch(obj) {
|
||||
if (obj.look_at != null || obj.vision_from != null || obj.vision_slots != null) {
|
||||
return true;
|
||||
}
|
||||
if (obj.search_query != null || obj.civitai_query != null
|
||||
|| obj.memory_query != null || obj.tag_query != null || obj.inventory_query != null) {
|
||||
return true;
|
||||
}
|
||||
const acts = Array.isArray(obj.actions) ? obj.actions.map(String) : [];
|
||||
const hopOrGen = [
|
||||
'skill_load', 'persona_read', 'memory_get', 'memory_search', 'lookup_tags',
|
||||
'list_inventory', 'search_civitai', 'interrupt', 'generate',
|
||||
'memory_upsert', 'user_pref_upsert',
|
||||
];
|
||||
if (acts.some((a) => hopOrGen.includes(a))) {
|
||||
if (acts.includes('generate')) {
|
||||
return true;
|
||||
}
|
||||
if (String(obj.prompt || '').trim().length >= 48) {
|
||||
@@ -127,8 +109,7 @@ export function isTerminalStreamPatch(obj) {
|
||||
}
|
||||
if (obj.loras != null || obj.aspect != null || obj.steps != null
|
||||
|| obj.width != null || obj.height != null || obj.cfg != null
|
||||
|| obj.seed != null || obj.controls != null
|
||||
|| obj.memories != null || obj.user_prefs != null) {
|
||||
|| obj.seed != null || obj.controls != null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -137,7 +118,6 @@ export function isTerminalStreamPatch(obj) {
|
||||
export function attachPatch(SA) {
|
||||
SA.PATCH_KEYS = PATCH_KEYS;
|
||||
SA.setPatchKeys = setPatchKeys;
|
||||
SA.isCardObject = isCardObject;
|
||||
SA.isPatchObject = isPatchObject;
|
||||
SA.isTerminalStreamPatch = isTerminalStreamPatch;
|
||||
SA.normalizePatch = normalizePatch;
|
||||
|
||||
+368
@@ -0,0 +1,368 @@
|
||||
/**
|
||||
* Per-chat generation session — source of truth for Generate params, board, LoRAs.
|
||||
* Sparse model deltas merge into the active session; buttons always generate from it.
|
||||
*/
|
||||
|
||||
const MAX_DATA_URL_CHARS = 350_000;
|
||||
|
||||
const GEN_KEYS = [
|
||||
'prompt', 'negative', 'width', 'height', 'aspect', 'steps', 'cfg', 'sigma_shift',
|
||||
'seed', 'sampler', 'scheduler', 'batch', 'checkpoint', 'loras', 'controls',
|
||||
'use_init_image', 'clear_init_image', 'init_creativity', 'denoise',
|
||||
'use_mask_image', 'clear_mask_image', 'mask_blur', 'mask_grow',
|
||||
];
|
||||
|
||||
export function emptySession() {
|
||||
return {
|
||||
gen: {
|
||||
prompt: '',
|
||||
negative: '',
|
||||
width: null,
|
||||
height: null,
|
||||
aspect: null,
|
||||
steps: null,
|
||||
cfg: null,
|
||||
sigma_shift: null,
|
||||
seed: null,
|
||||
sampler: null,
|
||||
scheduler: null,
|
||||
batch: null,
|
||||
checkpoint: null,
|
||||
loras: [],
|
||||
controls: {},
|
||||
use_init_image: false,
|
||||
clear_init_image: false,
|
||||
init_creativity: null,
|
||||
denoise: null,
|
||||
use_mask_image: false,
|
||||
clear_mask_image: false,
|
||||
mask_blur: null,
|
||||
mask_grow: null,
|
||||
},
|
||||
board: {
|
||||
slots: [],
|
||||
selectedSlotId: 'ref1',
|
||||
genResults: [],
|
||||
selectedGenResultId: null,
|
||||
refSeq: 1,
|
||||
},
|
||||
persona: 'neutral',
|
||||
pack: 'ordinary',
|
||||
context_memory: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Normalize boolean generate + legacy actions:["generate"]. */
|
||||
export function normalizeDelta(raw) {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return null;
|
||||
}
|
||||
const delta = { ...raw };
|
||||
const acts = Array.isArray(delta.actions) ? delta.actions.map(String) : [];
|
||||
if (delta.generate === true || acts.includes('generate')) {
|
||||
delta.generate = true;
|
||||
}
|
||||
if (typeof delta.ask === 'string') {
|
||||
delta.ask = [delta.ask];
|
||||
}
|
||||
if (!Array.isArray(delta.ask)) {
|
||||
delete delta.ask;
|
||||
} else {
|
||||
delta.ask = delta.ask.map(String).filter(Boolean);
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
export function patchWantsGenerate(patch) {
|
||||
if (!patch || typeof patch !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (patch.generate === true) {
|
||||
return true;
|
||||
}
|
||||
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
|
||||
return acts.includes('generate');
|
||||
}
|
||||
|
||||
export function patchAskList(patch) {
|
||||
const n = normalizeDelta(patch);
|
||||
return Array.isArray(n?.ask) ? n.ask : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge sparse model delta into session.gen (and pack/persona if present).
|
||||
* Does not run Generate — caller decides via patchWantsGenerate + veto.
|
||||
*/
|
||||
export function mergeDelta(session, rawDelta) {
|
||||
const base = session && typeof session === 'object' ? structuredCloneSession(session) : emptySession();
|
||||
const delta = normalizeDelta(rawDelta);
|
||||
if (!delta) {
|
||||
return base;
|
||||
}
|
||||
if (!base.gen) {
|
||||
base.gen = emptySession().gen;
|
||||
}
|
||||
for (const key of GEN_KEYS) {
|
||||
if (delta[key] === undefined || delta[key] === null) {
|
||||
continue;
|
||||
}
|
||||
if (key === 'loras' && Array.isArray(delta.loras)) {
|
||||
base.gen.loras = delta.loras.map((l) => ({
|
||||
name: l?.name || l,
|
||||
weight: l?.weight != null ? Number(l.weight) : 1,
|
||||
triggers: Array.isArray(l?.triggers) ? l.triggers : undefined,
|
||||
trigger_phrase: l?.trigger_phrase || undefined,
|
||||
})).filter((l) => l.name);
|
||||
continue;
|
||||
}
|
||||
if (key === 'controls' && typeof delta.controls === 'object') {
|
||||
base.gen.controls = { ...(base.gen.controls || {}), ...delta.controls };
|
||||
continue;
|
||||
}
|
||||
if (key === 'checkpoint') {
|
||||
base.gen.checkpoint = typeof delta.checkpoint === 'object'
|
||||
? { ...delta.checkpoint }
|
||||
: { name: String(delta.checkpoint) };
|
||||
continue;
|
||||
}
|
||||
base.gen[key] = delta[key];
|
||||
}
|
||||
if (delta.images != null && delta.batch == null) {
|
||||
base.gen.batch = delta.images;
|
||||
}
|
||||
if (delta.pack) {
|
||||
base.pack = String(delta.pack);
|
||||
}
|
||||
if (delta.persona) {
|
||||
base.persona = String(delta.persona);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function structuredCloneSession(session) {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(session));
|
||||
} catch {
|
||||
return emptySession();
|
||||
}
|
||||
}
|
||||
|
||||
function slimSrc(src) {
|
||||
if (!src || typeof src !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const s = src.trim();
|
||||
if (!s || s.startsWith('#')) {
|
||||
return null;
|
||||
}
|
||||
if (s.startsWith('data:') && s.length > MAX_DATA_URL_CHARS) {
|
||||
return null;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/** Snapshot live UI + board into a persistable session object. */
|
||||
export function snapshotFromLive({
|
||||
genFields,
|
||||
board,
|
||||
persona,
|
||||
pack,
|
||||
context_memory,
|
||||
}) {
|
||||
const session = emptySession();
|
||||
if (genFields && typeof genFields === 'object') {
|
||||
for (const key of GEN_KEYS) {
|
||||
if (genFields[key] !== undefined) {
|
||||
session.gen[key] = genFields[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
session.persona = persona || 'neutral';
|
||||
session.pack = pack || 'ordinary';
|
||||
if (context_memory && typeof context_memory === 'object') {
|
||||
session.context_memory = context_memory;
|
||||
}
|
||||
if (board && typeof board === 'object') {
|
||||
session.board = {
|
||||
slots: (board.slots || []).map((s) => ({
|
||||
id: s.id,
|
||||
type: s.type,
|
||||
label: s.label,
|
||||
src: slimSrc(s.src),
|
||||
attach: !!s.attach,
|
||||
note: s.note || null,
|
||||
})),
|
||||
selectedSlotId: board.selectedSlotId || 'ref1',
|
||||
genResults: (board.genResults || []).map((r) => ({
|
||||
id: r.id,
|
||||
label: r.label,
|
||||
src: slimSrc(r.src),
|
||||
patch: r.patch || null,
|
||||
})),
|
||||
selectedGenResultId: board.selectedGenResultId || null,
|
||||
refSeq: board.refSeq || 1,
|
||||
};
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
/** Convert legacy flat chat.params into session shape. */
|
||||
export function sessionFromLegacyParams(params) {
|
||||
if (!params || typeof params !== 'object') {
|
||||
return emptySession();
|
||||
}
|
||||
if (params.gen && typeof params.gen === 'object') {
|
||||
const s = emptySession();
|
||||
s.gen = { ...s.gen, ...params.gen };
|
||||
if (params.board && typeof params.board === 'object') {
|
||||
s.board = { ...s.board, ...params.board };
|
||||
}
|
||||
s.persona = params.persona || s.persona;
|
||||
s.pack = params.pack || s.pack;
|
||||
if (params.context_memory && typeof params.context_memory === 'object') {
|
||||
s.context_memory = params.context_memory;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
const s = emptySession();
|
||||
for (const key of GEN_KEYS) {
|
||||
if (params[key] !== undefined && params[key] !== null) {
|
||||
s.gen[key] = params[key];
|
||||
}
|
||||
}
|
||||
if (Array.isArray(params.loras)) {
|
||||
s.gen.loras = params.loras;
|
||||
}
|
||||
if (params.checkpoint) {
|
||||
s.gen.checkpoint = typeof params.checkpoint === 'object'
|
||||
? params.checkpoint
|
||||
: { name: String(params.checkpoint) };
|
||||
}
|
||||
s.persona = params.persona || 'neutral';
|
||||
s.pack = params.pack || 'ordinary';
|
||||
if (params.context_memory && typeof params.context_memory === 'object') {
|
||||
s.context_memory = params.context_memory;
|
||||
}
|
||||
s.board.genResults = Array.isArray(params.genResults) ? params.genResults : [];
|
||||
s.board.selectedGenResultId = params.selectedGenResultId || null;
|
||||
if (Array.isArray(params.slots)) {
|
||||
s.board.slots = params.slots;
|
||||
}
|
||||
if (params.selectedSlotId) {
|
||||
s.board.selectedSlotId = params.selectedSlotId;
|
||||
}
|
||||
if (params.refSeq) {
|
||||
s.board.refSeq = params.refSeq;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/** Persist blob for AssistentSaveChat.params */
|
||||
export function toPersistParams(session) {
|
||||
const s = session && typeof session === 'object' ? session : emptySession();
|
||||
const out = {
|
||||
gen: s.gen || emptySession().gen,
|
||||
board: s.board || emptySession().board,
|
||||
persona: s.persona || 'neutral',
|
||||
pack: s.pack || 'ordinary',
|
||||
};
|
||||
if (s.context_memory && typeof s.context_memory === 'object') {
|
||||
out.context_memory = s.context_memory;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function slimText(t, max) {
|
||||
const s = String(t || '');
|
||||
if (s.length <= max) {
|
||||
return s;
|
||||
}
|
||||
return `${s.slice(0, max)}…`;
|
||||
}
|
||||
|
||||
/** Compact context for every LLM turn. */
|
||||
export function compactContext(session, extras = {}) {
|
||||
const s = session && typeof session === 'object' ? session : emptySession();
|
||||
const g = s.gen || {};
|
||||
const board = s.board || {};
|
||||
const slots = board.slots || [];
|
||||
const genSlot = slots.find((x) => x.type === 'generate' || x.id === 'generate');
|
||||
const refs = slots.filter((x) => x.type === 'ref' || String(x.id || '').startsWith('ref'));
|
||||
return {
|
||||
session: true,
|
||||
prompt: slimText(g.prompt, extras.promptMax || 2000),
|
||||
negative: slimText(g.negative, 500),
|
||||
aspect: g.aspect || null,
|
||||
width: g.width ?? null,
|
||||
height: g.height ?? null,
|
||||
steps: g.steps ?? null,
|
||||
cfg: g.cfg ?? null,
|
||||
seed: g.seed ?? null,
|
||||
sigma_shift: g.sigma_shift ?? null,
|
||||
sampler: g.sampler || null,
|
||||
scheduler: g.scheduler || null,
|
||||
batch: g.batch ?? null,
|
||||
checkpoint: g.checkpoint?.name || g.checkpoint || null,
|
||||
selected_loras: (g.loras || []).map((l) => ({
|
||||
name: l.name || l,
|
||||
weight: l.weight != null ? l.weight : 1,
|
||||
})),
|
||||
persona: s.persona || 'neutral',
|
||||
pack: s.pack || 'ordinary',
|
||||
board: {
|
||||
has_generate: !!(genSlot?.src || (board.genResults || []).some((r) => r.src)),
|
||||
refs: refs.map((r) => ({ id: r.id, has_image: !!r.src, attach: !!r.attach })),
|
||||
gen_results: (board.genResults || []).map((r) => ({
|
||||
id: r.id,
|
||||
label: r.label,
|
||||
has_image: !!r.src,
|
||||
selected: r.id === board.selectedGenResultId,
|
||||
})),
|
||||
selected_slot: board.selectedSlotId || null,
|
||||
},
|
||||
architecture_ok: extras.architecture_ok !== false,
|
||||
...extras.extra,
|
||||
};
|
||||
}
|
||||
|
||||
/** Full dump for ask:settings hop. */
|
||||
export function fullSettingsDump(session, extras = {}) {
|
||||
const compact = compactContext(session, extras);
|
||||
const s = session && typeof session === 'object' ? session : emptySession();
|
||||
return {
|
||||
...compact,
|
||||
detail: 'settings',
|
||||
gen: { ...(s.gen || {}) },
|
||||
controls: s.gen?.controls || {},
|
||||
exact: extras.exact || null,
|
||||
krea_profiles: extras.kreaProfiles || null,
|
||||
session_exact: extras.sessionExact || null,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveTurnIntent(patch, userText, { vetoFn } = {}) {
|
||||
const delta = normalizeDelta(patch) || {};
|
||||
const vetoed = typeof vetoFn === 'function' ? !!vetoFn(userText) : false;
|
||||
const generate = !vetoed && patchWantsGenerate(delta);
|
||||
const hasLook = delta.look_at != null || delta.vision_from != null || delta.vision_slots != null;
|
||||
const look = !!(hasLook && !generate && !vetoed);
|
||||
const ask = patchAskList(delta);
|
||||
return { generate, look, vetoed, ask };
|
||||
}
|
||||
|
||||
export function attachSession(SA) {
|
||||
SA.session = {
|
||||
emptySession,
|
||||
normalizeDelta,
|
||||
mergeDelta,
|
||||
patchWantsGenerate,
|
||||
patchAskList,
|
||||
snapshotFromLive,
|
||||
sessionFromLegacyParams,
|
||||
toPersistParams,
|
||||
compactContext,
|
||||
fullSettingsDump,
|
||||
resolveTurnIntent,
|
||||
GEN_KEYS,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user