Files
DemonGatanjieuandClaude Opus 5.5 d778aec946 Fix what a walk through every page found before 1.63
Every page in both languages, all themes and at 480:
- Give the window its own text colour: text with none took ComfyUI's, white on the light theme (Tidy headings and file names, the site chip).
- Keep a task's link chip in its row, its x in reach, and colour the next-task chip.
- Let choices in Settings wrap, the Models search shrink, and a workflow's preset row go to two lines.
- Add --amb-on-link for text on the accent: pale in the dark themes, so white was hard to read.
- Lists and colons follow the language (listSeparator, wordColon); tests/words_in_language.mjs keeps it so.
- Right plurals in English, "AI" on the rail, one line left in Chinese translated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 03:18:07 +08:00

112 lines
6.4 KiB
JavaScript

/**
* A task's key decisions (api/ai_decisions.py) in the AI workbench. On the timeline, the card of
* one the AI recorded: what was decided, why, what was left out, who decided, and the steps it is
* about. In the side column (ui_ai_side_views.js), the "Decisions" view: every decision of the
* task in order, the user's own choices in Anomalous too (a panel answer, allowing a step,
* materials, the canvas mode, a stop), each leading back to its step, with a quiet word when the
* AI ran or changed the canvas since it last recorded one. Text only (textContent).
*/
import { translate as t } from './locales.js';
import { timeLabel } from './activity_log.js';
import { toolKey } from './ai_task_view.js';
import { icon } from './ui_ai_icons.js';
import { sideHead } from './ui_ai_side.js';
const BY = { user: 'aiDecisionByUser', ai: 'aiDecisionByAi', together: 'aiDecisionByTogether' };
const MARKS = { logged: 'pin', answer: 'question', gate: 'shield', toolbox: 'sparkle', canvas: 'copy', stop: 'stop' };
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = String(text);
return node;
}
/** Brings the timeline's row of step `id` into view and marks it a moment (a narrow window shows the timeline first). */
export function showStepRow(id) {
document.querySelector('.anomalous-ai.shows-stage .anomalous-ai-stage .anomalous-ai-tab')?.click();
requestAnimationFrame(() => {
const row = document.querySelector(`.anomalous-ai-timeline [data-step-ids~="${CSS.escape(id)}"]`);
if (!row) return;
row.scrollIntoView({ block: 'center', behavior: 'smooth' });
row.classList.add('is-flash');
setTimeout(() => row.classList.remove('is-flash'), 1600);
});
}
function stepLink(id, numberOf, label) {
const node = el('button', 'anomalous-ai-decision-link', label || t('aiDecisionStep', { number: numberOf?.(id) || '?' }));
node.type = 'button';
node.onclick = () => showStepRow(id);
return node;
}
/** The card of a decision the AI recorded (`record`: ai_decisions.log's). */
export function decisionCard(record, numberOf = null) {
const card = el('div', 'anomalous-ai-decision');
const head = el('div', 'anomalous-ai-decision-head');
head.append(icon('pin', 14), el('span', 'anomalous-ai-decision-title', record.title || ''));
if (BY[record.by]) head.append(el('span', `anomalous-ai-decision-by is-${record.by}`, t(BY[record.by])));
card.append(head, el('div', 'anomalous-ai-decision-text', record.decision || ''));
if (record.reason) card.append(el('div', 'anomalous-ai-decision-why', t('aiDecisionWhy', { reason: record.reason })));
if (record.rejected?.length) card.append(el('div', 'anomalous-ai-decision-not', t('aiDecisionRejected', { options: record.rejected.join(t('listSeparator')) })));
if (record.related?.length) {
const links = el('div', 'anomalous-ai-decision-links');
links.append(el('span', '', t('aiDecisionAbout')), ...record.related.map(id => stepLink(id, numberOf)));
card.append(links);
}
return card;
}
/** One decision of the list, by its type: [title, words]. */
function described(item) {
switch (item.type) {
case 'logged': return [item.title, item.decision];
case 'answer': return [item.question || t('aiDecisionAnswer'), item.skipped ? t('aiPanelSkipped') : item.timed_out ? t('aiPanelTookRecommended', { answer: item.answer }) : item.answer];
case 'gate': return [t(toolKey(item.tool)), [t(item.allowed ? 'aiDecisionAllowed' : 'aiDecisionRefused'), item.text].filter(Boolean).join(t('wordColon'))];
case 'toolbox': return [t('aiDecisionToolbox'), item.count ? t('aiDecisionToolboxText', { count: item.count, mode: t(`aiTbMode_${item.mode}`) }) : t('aiToolboxCleared')];
case 'canvas': return [t('aiDecisionCanvas'), t(item.mode === 'new' ? 'aiCanvasChoseNew' : 'aiCanvasChoseCurrent')];
case 'privacy': return [t('aiPrivacy'), t(item.on ? 'aiPrivacyChoseOn' : 'aiPrivacyChoseOff')];
default: return [t('aiDecisionStop'), t('aiRunStoppedByYou')];
}
}
/**
* Every decision of `task` (the server's `decisions` and `decisions_gap`) into `pane`, oldest
* first. `ctx`: {numberOf(stepId), close()}. Drawn again only when they changed.
*/
export function renderDecisionsView(pane, task, ctx) {
const items = task.decisions || [];
const key = JSON.stringify([task.id, items, task.decisions_gap]);
if (pane.dataset.key === key) return;
pane.dataset.key = key;
const body = el('div', 'anomalous-ai-stage-body anomalous-ai-decisions');
const gap = task.decisions_gap;
if (gap && items.length) {
const parts = [gap.runs ? t('aiDecisionGapRuns', { count: gap.runs }) : '', gap.canvas ? t('aiDecisionGapCanvas', { count: gap.canvas }) : ''].filter(Boolean);
body.append(el('div', 'anomalous-ai-decisions-gap', t('aiDecisionGap', { what: parts.join(t('listSeparator')) })));
}
if (!items.length) body.append(el('div', 'anomalous-ai-stage-empty anomalous-ai-decisions-none', t('aiDecisionsNone')));
for (const item of items) {
const row = el('div', `anomalous-ai-decisions-row is-${item.type}`);
const mark = el('span', 'anomalous-ai-decisions-mark');
mark.append(icon(MARKS[item.type] || 'pin', 14));
const [title, words] = described(item);
const text = el('div', 'anomalous-ai-decisions-text');
const top = el('div', 'anomalous-ai-decisions-top');
top.append(el('span', 'anomalous-ai-decisions-title', title || ''));
const by = item.type === 'logged' ? item.by : 'user';
if (BY[by]) top.append(el('span', `anomalous-ai-decision-by is-${by}`, t(BY[by])));
text.append(top, el('div', 'anomalous-ai-decisions-words', words || ''));
if (item.type === 'logged' && item.reason) text.append(el('div', 'anomalous-ai-decision-why', t('aiDecisionWhy', { reason: item.reason })));
if (item.type === 'logged' && item.rejected?.length) text.append(el('div', 'anomalous-ai-decision-not', t('aiDecisionRejected', { options: item.rejected.join(t('listSeparator')) })));
const foot = el('div', 'anomalous-ai-decision-links');
foot.append(el('span', '', timeLabel(item.at)), stepLink(item.step, ctx.numberOf));
text.append(foot);
row.append(mark, text);
body.append(row);
}
pane.replaceChildren(sideHead(t('aiSideDecisions'), ctx.close), body);
}