Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_ai_step_body.js
T
DemonGatanjieuandClaude Opus 5.5 98c90dd8e4 Move training out of AMB: drop the run record and the training network
AMB only uses: training goes to its own plugin. This takes back the
record of what works (api/ai_evidence.py), the training network and
rounds (api/ai_training.py, ui_ai_network*.js, the round form), the
Gates' record switch, the ways' order by record and the external_skill
field of log_outside that only fed the record.

Kept, as part of using AMB:
- the user's word under a finished run, now only told to the AI
  (ai_run_control.set_verdict, ui_ai_verdict.js);
- the Gates' "Custom nodes: only the ways'" rule;
- a picks block left to the AI takes its recommended picture.

Reverts 73518dc, 67bf5b2, 6647b05 and 49d05cf, then puts those three
back with tests in test_ai_run_watch, test_ai_gates and test_ai_panels.
Full suite and structure check pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 21:11:05 +08:00

131 lines
6.8 KiB
JavaScript

/**
* What a step of the AI workbench's timeline says under its title, by what it is (ui_ai_workbench.js
* draws the row around it): the user's own steps (marks on a picture, materials, a change made in
* Anomalous, pictures handed over, a stop, the canvas mode, privacy mode, their word on a run, a message
* with the pictures it carries) each with whether the AI has it (ui_ai_delivery.js), a decision (ui_ai_decisions.js),
* what happened outside Anomalous (pictures changed, the AI's own word: api/ai_outside.py), a
* skill the AI went by (api/ai_skills.py) and, under a run, the skills fetched before it, or
* the AI's note laid out (a long one folded until asked).
*/
import { resolveLocale, translate as t } from './locales.js';
import { skillsBefore } from './ai_task_view.js';
import { entrySummary } from './activity_log.js';
import { changeNodeId } from './activity_diff.js';
import { noteNode } from './ai_text_format.js';
import { locateNode } from './ui_ai_results.js';
import { marksStep } from './ui_ai_marker.js';
import { toolboxStep } from './ui_ai_toolbox.js';
import { canvasChanges } from './ui_canvas_changes.js';
import { deliveryState } from './ui_ai_delivery.js';
import { decisionCard } from './ui_ai_decisions.js';
import { imageResults } from './ui_ai_images.js';
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
const said = text => el('div', 'anomalous-ai-user-text', text);
const words = value => (resolveLocale() === 'zh' ? value?.zh : value?.en) || value?.en || value?.zh || '';
/** A skill the AI fetched: its name and, for advice that goes by the model, the form it came in. */
function skillUsed(step) {
const model = step.for_model;
const box = el('div', 'anomalous-ai-skill-used');
box.append(el('strong', '', words(step.title)));
if (model) box.append(el('span', '', t(`aiSkillForm_${model.form}`, { model: model.model, billions: model.billions })));
return box;
}
const FOLD_CHARS = 420;
const FOLD_LINES = 8;
/** The AI's note; a long one folded to its first lines, with "show all" / "show less". */
function note(text) {
const node = noteNode(text);
if (text.length <= FOLD_CHARS && text.split('\n').length <= FOLD_LINES) return node;
const box = el('div', 'anomalous-ai-fold is-folded');
const toggle = el('button', 'anomalous-ai-fold-toggle', t('aiFoldMore'));
toggle.type = 'button';
toggle.onclick = () => {
const folded = box.classList.toggle('is-folded');
toggle.textContent = t(folded ? 'aiFoldMore' : 'aiFoldLess');
};
box.append(node, toggle);
return box;
}
/** The pictures a message carries ("subfolder/name.png"), as the AI's own are shown: to open, compare, keep or mark up. */
function namedPictures(step, owner) {
const pictures = (step.refs || []).filter(ref => ref.kind === 'pictures').map((ref) => {
const parts = ref.id.split('/');
return { type: 'image', folder: 'output', filename: parts.pop(), subfolder: parts.join('/') };
});
if (!pictures.length) return [];
const box = imageResults(pictures, owner);
box.classList.add('is-said'); // the size of what one hands over, not of results
return [box];
}
/** Pictures changed outside Anomalous since the AI's previous call: each named, those in the output folder shown. */
function outsideFiles(step, owner) {
const list = el('ul', 'anomalous-ai-outside-list');
for (const item of step.files || []) list.append(el('li', '', t(`aiOutside_${item.change}`, { name: `${item.folder}/${item.name}` })));
if (step.more) list.append(el('li', '', t('aiOutsideMore', { count: step.more })));
const pictures = (step.files || []).filter(item => item.folder === 'output')
.map(item => ({ type: 'image', folder: 'output', filename: item.name, subfolder: '' }));
const shown = pictures.length ? imageResults(pictures, owner) : null;
shown?.classList.add('is-said');
return [list, ...(shown ? [shown] : []), el('div', 'anomalous-ai-outside-who', t('aiOutsideWho'))];
}
/** The AI's own word on what it did with other tools. */
function outsideLog(step) {
const box = el('dl', 'anomalous-ai-outside-log');
for (const [key, value] of [['aiOutsideWhat', step.what], ['aiOutsideWhy', step.why], ['aiOutsideFiles', (step.files || []).join('\n')]]) {
if (value) box.append(el('dt', '', t(key)), el('dd', '', value));
}
return [box];
}
/** A change the user made in Anomalous while the AI worked (an activity entry), and whether the AI knows. */
function userChange(step, entries) {
const box = el('div', 'anomalous-ai-user-change');
const entry = entries.get(step.activity);
box.append(said(entry ? entrySummary(entry) : step.text));
if (entry?.source === 'canvas') {
const note = el('span', 'anomalous-ai-link-note');
box.append(canvasChanges(entry, { locate: change => locateNode(changeNodeId(change), note) }), note);
}
box.append(deliveryState(step, 'aiChangeSeen', 'aiChangeWaiting'));
return box;
}
/** The nodes under step `step`'s title. `ctx`: {taskId, owner, entries (activity entries by id), numberOf(stepId)}. */
export function stepBody(step, ctx) {
switch (step.tool) {
case 'user_marks': return [marksStep(step, ctx.taskId, ctx.owner)];
case 'user_toolbox': return [toolboxStep(step)];
case 'user_activity': return [userChange(step, ctx.entries)];
case 'user_pictures': return [said(step.text || t('aiPicturesStep', { count: (step.results || []).length })), deliveryState(step, 'aiPicturesSeen')];
case 'user_stop': return [said(t('aiRunStoppedByYou')), deliveryState(step)];
case 'user_canvas': return [said(t(step.canvas === 'new' ? 'aiCanvasChoseNew' : 'aiCanvasChoseCurrent')), deliveryState(step)];
case 'user_privacy': return [said(t(step.privacy ? 'aiPrivacyChoseOn' : 'aiPrivacyChoseOff')), deliveryState(step)];
case 'outside_files': return outsideFiles(step, ctx.owner);
case 'log_outside': return outsideLog(step);
case 'user_verdict': return [said(t(`aiVerdictSaid_${step.verdict}`)), deliveryState(step)];
case 'use_skill': return [skillUsed(step)];
case 'run_workflow': {
const used = skillsBefore(ctx.steps, step).map(item => words(item.title));
return [...(used.length ? [el('div', 'anomalous-ai-skill-used', t('aiRunSkills', { names: [...new Set(used)].join(' · ') }))] : []),
...(step.text ? [note(step.text)] : [])];
}
default:
if (step.kind === 'decision') return [decisionCard(step.decision || {}, ctx.numberOf)];
if (step.kind === 'user') return [...(step.text ? [said(step.text)] : []), ...namedPictures(step, ctx.owner), deliveryState(step)];
return step.text ? [note(step.text)] : [];
}
}