Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_ai_decisions.js
T
DemonGatanjieuandClaude Opus 5.5 cb523930da Guide the AI with fewer detours and guesses
- An AI app calling by an older tool list (tool_help or use_tool not offered,
  use_tool for a tool offered directly, a tool the list lacks) is told so, and
  the task header shows how to reconnect until the app fetches tools/list again.
- add_lora / set_model compare the base models on the model line
  (model_advice.py; the page reports the line) and add base_model_note.
- get_model gives author_examples (sampler, steps, CFG, size, a LoRA's weight
  from the Civitai record's example pictures) and the version name.
- run_workflow says ComfyUI's refusal as which node, what is wrong and the tool
  that fixes it (run_refusal.py; mcp_run.js passes the refusal as data).
- Gates "When I don't answer": wait for me, or go with the recommendation once
  the waiting time runs out (choices with a recommended option only), handed to
  the AI in the same call and shown on the stage and under Decisions.
- A tool budget test (count, size, description length, required arguments) and
  the exam tasks with their scorecard (docs/architecture/tool-exam.md,
  tools/tool_exam.mjs). The tool set's text recommends Full for Antigravity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 14:26:41 +08:00

111 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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('、') })));
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(':')];
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')];
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('、') })));
}
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('、') })));
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);
}