Fold the AI's working steps in a row into one timeline row
Three or more reads, canvas changes and decisions in a row, two of them not reads, become one row: how many steps, chips of what they did, the last one while the AI is at it, and undo for their canvas changes. It opens to the steps as before, and a jump to one of them opens it. Words, notes, questions, panels, runs, errors and waiting or running steps stay rows of their own. The folded rows move to ui_ai_chain.js. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
24a9eb9ed9
commit
a33d838d7f
+3
-1
@@ -367,7 +367,9 @@ covered by the `styles.css` manifest.
|
||||
ready" (the gallery grid of `ui_ai_gallery_pick.js` feeding the message box, moved in, the
|
||||
materials button and the user's own steps so far). The pictures (`web/assets/ai_*.webp`)
|
||||
were made with ChatGPT. `ai_task_view.js` holds
|
||||
its rules without DOM (folding reads, tool words, counts, result addresses).
|
||||
its rules without DOM (folding reads and chains of the AI's work, tool words, counts, result addresses);
|
||||
`ui_ai_chain.js` draws the folded rows: reads in a row, and a chain of reads, canvas changes and
|
||||
decisions as one row of chips with undo for its canvas changes, opening to its steps.
|
||||
`ask_user` (`api/mcp_task_tools.py`) is the AI's question in the workbench: a one-block panel
|
||||
(choices with an optional `recommended` option and an `other` line, or an input) whose answer
|
||||
it waits for in the same call; the server instructions and `begin_task` tell the AI to ask
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
- Lines for the AI's chat stay over the message box: whenever the AI is not in a call, a run or a question, the AI workbench offers a line to copy that fits what waits (about the run that ended; "I replied in Anomalous, take a look and go on" when it has not got your reply yet; else "Go on"), and one that picks the task up in a new chat. The lines under a finished run's step moved here. The Hand to AI sentence no longer tells the AI to ask with show_panel.
|
||||
- AI 在 AMB 里问你时至少等 30 秒:就算它自己说不等(Gemini 常这样),你在 AMB 里点了选项,它在同一步里就收到,接着做,不用再回 AI 的对话框说“继续”。“加节点”工具的说明也提醒 AI:要加一组互相连着的节点(重绘、放大一段),用一次调用的“加一组节点”。
|
||||
- ask_user waits at least 30 s even when the AI asks it not to, so an answer given in Anomalous right away reaches the same call and the AI goes on without a word in its chat. add_node's description points to add_nodes for several nodes wired to each other (an inpaint or upscale branch).
|
||||
- 时间线更短:AI 连着做的事(查资料、加节点、连线、写提示词、改参数、记决策)三步以上、其中至少两步不是查资料时,叠成一行“做了 N 步”,下面一排小标签写做了什么(加了节点 ×5、连了线 ×10……),点开就是原来的每一步。行上有“撤销这一串”,一下撤掉这一串在画布上的改动。你的话、AI 留言、问你的、面板、出图、出错的、等你允许的、正在做的一直露在外面。AI 还在做时,这一行写着最后一步是什么。一个修手的任务从 43 行变成 18 行。
|
||||
- A shorter timeline: three or more of the AI's working steps in a row (reads, canvas changes, prompts, settings, decisions; two of them not reads) fold into one row, "Did N steps", with chips of what they did and "Undo these" for their canvas changes; it opens to the steps as before. Your words, the AI's notes and questions, panels, runs, errors, steps waiting for you and running ones stay in sight. While the AI is at it, the row names its last step. A hand-fix task went from 43 rows to 18.
|
||||
|
||||
- 修复任务画布标签关闭或切换失败后误用当前工作流的问题:操作会明确报错,重新打开或明确指定新画布后才能继续。下载缺失模型现在也跟随任务画布;留言里的 `@` 搭配和工作流按完整名称查找,不再因模糊搜索前几项占满而误报缺失。
|
||||
- Task canvas actions now fail when their recorded tab is closed or cannot be activated instead of using another workflow. Missing-model downloads follow the task tab, and message references resolve exact combo/workflow names before search limits.
|
||||
|
||||
+1
-1
@@ -202,7 +202,7 @@ add_nodes(nodes={
|
||||
左边栏的 **AI 工作台** 记下 AI 通过这个接口做的每一步,按**任务**分好:
|
||||
|
||||
- AI 开始干活时会调用 `begin_task` 起个标题;没起的话,同一个 AI 软件连续做的事自动算一个任务,停了 20 分钟以上就算新的。
|
||||
- 每个任务是一条时间线:查了什么(自动折叠成一行,点开看每一次)、画布上改了什么、运行、下载、扫描、配音,还有 AI 自己的留言(`report` 工具,也可以把它用别的方式做出来的图交上来)。
|
||||
- 每个任务是一条时间线:查了什么(自动折叠成一行,点开看每一次)、画布上改了什么(AI 连着查资料、改画布、记决策,三步以上、其中至少两步不是查资料时,叠成一行“做了 N 步”,下面一排小标签写做了什么,点开看每一步;“撤销这一串”一下撤掉这一串在画布上的改动)、运行、下载、扫描、配音,还有 AI 自己的留言(`report` 工具,也可以把它用别的方式做出来的图交上来)。
|
||||
- **做出来的东西都留着,点一下就到**:运行出的图会一直等到跑完再收进任务里,最上面的“这次做出来的”可以直接点开看大图;用到的模型点一下打开模型页,搭配打开搭配,配音可以直接听;画布上的改动可以“在画布上找到”,也可以**撤销**(一步,或者整个任务的画布改动一起,从最后一步往前,遇到你自己后来又改过的地方就停下)。
|
||||
- AI 还在干活时,时间线会自己往下长;你在别的页面时,左边栏的 AI 工作台会亮一个小点。首页也有“AI 最近做了什么”。
|
||||
- **给 AI 留言**:任务时间线下面有个留言框,比如“背景再亮一点”“先别跑了”。留言会成为任务里的一步,AI 下一次调用任何工具(或 `get_answers`)时都会自动收到,下面写着它有没有看到。AMB 没法叫醒已经停下的 AI:它如果已经结束回合,请回到 AI 软件里让它继续。
|
||||
|
||||
+19
-3
@@ -1,14 +1,14 @@
|
||||
// The AI workbench's rules (web/modules/ai_task_view.js): folding reads, words, counts, addresses.
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
activityIds, audioResult, duration, flowStages, groupSteps, isVideo, lookTools, outputUrl, outputPicture, pictureKey, picturePreview, uniquePictures, stepDetail, taskCounts, taskResults, taskState, toolKey,
|
||||
activityIds, audioResult, chainTools, duration, flowStages, foldLooks, groupSteps, isVideo, lookTools, outputUrl, outputPicture, pictureKey, picturePreview, uniquePictures, stepDetail, taskCounts, taskResults, taskState, toolKey,
|
||||
} from '../web/modules/ai_task_view.js';
|
||||
|
||||
const look = (tool, ok = true, state = 'done') => ({ kind: 'look', tool, ok, state, summary: tool });
|
||||
const act = (tool, kind = 'canvas') => ({ kind, tool, ok: true, state: 'done', summary: `${tool}: K` });
|
||||
|
||||
// Reads that follow each other fold into one row; a running or failed read and the work stay rows.
|
||||
const items = groupSteps([look('search_models'), look('describe_canvas'), act('place_combo'), look('list_combos', false), look('search_images')]);
|
||||
// Reads that follow each other fold into one row; a running or failed read and what the user looks for stay rows.
|
||||
const items = groupSteps([look('search_models'), look('describe_canvas'), act('report', 'note'), look('list_combos', false), look('search_images')]);
|
||||
assert.deepEqual(items.map(item => item.type), ['looks', 'step', 'step', 'looks']);
|
||||
assert.equal(items[0].steps.length, 2);
|
||||
const inFlight = groupSteps([look('search_models'), look('describe_canvas', true, 'running')]);
|
||||
@@ -16,6 +16,22 @@ assert.deepEqual(inFlight.map(item => item.type), ['looks', 'step'], 'an in-flig
|
||||
assert.deepEqual(groupSteps(undefined), []);
|
||||
assert.deepEqual(lookTools([look('a'), look('b'), look('a')]), [{ tool: 'a', count: 2 }, { tool: 'b', count: 1 }]);
|
||||
|
||||
// The AI's work in a row (reads, canvas changes, decisions) folds into a chain of three or more.
|
||||
const work = [look('find_nodes'), act('add_node'), act('add_node'), act('connect_nodes'), act('log_decision', 'decision'), act('run_workflow', 'run'), look('run_status')];
|
||||
const chained = groupSteps(work);
|
||||
assert.deepEqual(chained.map(item => item.type), ['chain', 'step', 'looks']);
|
||||
assert.equal(chained[0].steps.length, 5);
|
||||
assert.deepEqual(chainTools(chained[0].steps), [{ tool: 'look', kind: 'look', count: 1 }, { tool: 'add_node', kind: 'canvas', count: 2 },
|
||||
{ tool: 'connect_nodes', kind: 'canvas', count: 1 }, { tool: 'log_decision', kind: 'decision', count: 1 }]);
|
||||
assert.deepEqual(foldLooks(chained[0].steps).map(item => item.type), ['looks', 'step', 'step', 'step', 'step'], 'a chain opens to its steps, reads still folded');
|
||||
assert.deepEqual(groupSteps([look('a'), act('set_prompt'), look('b')]).map(item => item.type), ['looks', 'step', 'looks'], 'one change among reads stays in sight');
|
||||
assert.deepEqual(groupSteps([act('set_prompt'), act('set_values')]).map(item => item.type), ['step', 'step'], 'two steps stay as they are');
|
||||
assert.deepEqual(groupSteps([look('a'), look('b'), look('c')]).map(item => item.type), ['looks'], 'only reads stay a row of reads');
|
||||
const broken = groupSteps([act('add_node'), act('add_node'), { ...act('connect_nodes'), ok: false }, act('add_node'), act('add_node'), act('add_node')]);
|
||||
assert.deepEqual(broken.map(item => item.type), ['step', 'step', 'step', 'chain'], 'a failed step stays in sight and splits the work');
|
||||
assert.deepEqual(groupSteps([act('add_node'), act('add_node'), { ...act('set_values'), state: 'asking' }]).map(item => item.type), ['step', 'step', 'step'],
|
||||
'a step waiting at a gate is not folded away');
|
||||
|
||||
// Words.
|
||||
assert.equal(toolKey('place_combo'), 'aiTool_place_combo');
|
||||
assert.equal(toolKey('x<y>'), 'aiTool_xy');
|
||||
|
||||
@@ -1,21 +1,49 @@
|
||||
/**
|
||||
* The AI workbench's rules without DOM (ui_ai_workbench.js draws them): which steps fold
|
||||
* together, what each tool is called, what a task counts, and where a result leads. A task
|
||||
* The AI workbench's rules without DOM (ui_ai_workbench.js and ui_ai_chain.js draw them): which
|
||||
* steps fold together, what each tool is called, what a task counts, and where a result leads. A task
|
||||
* and its steps are api/ai_tasks.py's shapes.
|
||||
*/
|
||||
|
||||
/** The kinds of step, in the order a task's counts name them. */
|
||||
export const KINDS = ['panel', 'user', 'decision', 'canvas', 'run', 'speech', 'download', 'scan', 'show', 'note', 'act', 'outside', 'look'];
|
||||
|
||||
const WORK = new Set(['look', 'canvas', 'decision']); // the AI going about the work, not what the user looks for
|
||||
const CHAIN_MIN = 3;
|
||||
const settled = step => step.ok && !['running', 'asking', 'allowed'].includes(step.state);
|
||||
|
||||
/**
|
||||
* The timeline: each step, but completed reads that follow each other fold into one row (`looks`),
|
||||
* so looking around does not push the work out of sight. A running or failed read stays a row of its own.
|
||||
* The timeline. Reads, canvas changes and decisions that follow each other, finished and fine,
|
||||
* fold into one row (`chain`) when there are three or more and two are not reads, so the work does
|
||||
* not push what the user looks for (their words, the AI's notes and questions, panels, runs,
|
||||
* errors) out of sight; else the reads among them fold alone (`looks`). A running, waiting or
|
||||
* failed step stays a row of its own.
|
||||
*/
|
||||
export function groupSteps(steps) {
|
||||
const items = [];
|
||||
let work = [];
|
||||
const close = () => {
|
||||
if (work.length >= CHAIN_MIN && work.filter(step => step.kind !== 'look').length >= 2) items.push({ type: 'chain', steps: work });
|
||||
else items.push(...foldLooks(work));
|
||||
work = [];
|
||||
};
|
||||
for (const step of steps || []) {
|
||||
if (WORK.has(step.kind) && settled(step)) {
|
||||
work.push(step);
|
||||
} else {
|
||||
close();
|
||||
items.push({ type: 'step', step });
|
||||
}
|
||||
}
|
||||
close();
|
||||
return items;
|
||||
}
|
||||
|
||||
/** `steps` with the finished reads that follow each other as one `looks` item (a chain opens to these). */
|
||||
export function foldLooks(steps) {
|
||||
const items = [];
|
||||
for (const step of steps) {
|
||||
const last = items.at(-1);
|
||||
if (step.kind === 'look' && step.ok && step.state !== 'running') {
|
||||
if (step.kind === 'look' && settled(step)) {
|
||||
if (last?.type === 'looks') last.steps.push(step);
|
||||
else items.push({ type: 'looks', steps: [step] });
|
||||
} else {
|
||||
@@ -25,6 +53,18 @@ export function groupSteps(steps) {
|
||||
return items;
|
||||
}
|
||||
|
||||
/** What a chain did, in the order first done: each tool with its kind and how often, the reads counted together as `look`. */
|
||||
export function chainTools(steps) {
|
||||
const counts = new Map();
|
||||
for (const step of steps) {
|
||||
const tool = step.kind === 'look' ? 'look' : step.tool;
|
||||
const item = counts.get(tool) || { tool, kind: step.kind, count: 0 };
|
||||
item.count += 1;
|
||||
counts.set(tool, item);
|
||||
}
|
||||
return [...counts.values()];
|
||||
}
|
||||
|
||||
/**
|
||||
* The stages a task's steps went through, in the order they first came, with the last step's
|
||||
* state for each: 'asking' (waits for the user's OK), 'running', 'error' or 'done'. Notes that
|
||||
|
||||
@@ -1426,6 +1426,10 @@ export const i18n = {
|
||||
aiDecisionsNone: "这个任务还没有决策记录。你在面板上的选择、允许或不允许、交出去的素材都会自动记在这里;你和 AI 在它那边商量出的结论,它会用 log_decision 记过来。",
|
||||
aiKind_act: "操作",
|
||||
aiLooks: "看了 {count} 次资料",
|
||||
aiChain: "做了 {count} 步",
|
||||
aiChainLast: "最后:{what}",
|
||||
aiChainUndo: "撤销这一串",
|
||||
aiChainUndoConfirm: "从最后一步往前,撤销这一串在画布上的 {count} 处改动?你自己后来又改过的地方会停下,不会硬撤。",
|
||||
aiFound: "找到 {count} 个",
|
||||
aiMade: "本次成果",
|
||||
aiCompareParams: "查看参数差异",
|
||||
@@ -4781,6 +4785,10 @@ export const i18n = {
|
||||
aiDecisionsNone: "No decisions yet. Your choices on panels, what you allow or refuse and the materials you hand over are kept here by themselves; what you settle with the AI in its own chat, it records here with log_decision.",
|
||||
aiKind_act: "Action",
|
||||
aiLooks: "Looked things up {count} {count|time|times}",
|
||||
aiChain: "Did {count} {count|step|steps}",
|
||||
aiChainLast: "last: {what}",
|
||||
aiChainUndo: "Undo these",
|
||||
aiChainUndoConfirm: "Undo these {count} canvas {count|change|changes}, the last first? It stops at anything you changed since, instead of forcing it.",
|
||||
aiFound: "found {count}",
|
||||
aiMade: "Task results",
|
||||
aiCompareParams: "See parameter differences",
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* The folded rows of the AI workbench's timeline (ai_task_view.js groupSteps says which): reads
|
||||
* that follow each other as one line ("looked things up 5 times: …") opening to a line each, and a
|
||||
* chain of the AI's work (reads, canvas changes, decisions) as one row: how many steps, what they
|
||||
* did as chips, the last one while the AI is at it, and undo for its canvas changes. A chain opens
|
||||
* to its steps as the timeline draws them; which chains are open is kept while the page lives, so
|
||||
* the AI's next steps, which draw the row again, do not shut one.
|
||||
*/
|
||||
|
||||
import { app } from '../../../scripts/app.js';
|
||||
import { translate as t } from './locales.js';
|
||||
import { timeLabel } from './activity_log.js';
|
||||
import { canUndo, undoEntry } from './canvas_undo.js';
|
||||
import { anomalousConfirm } from './ui_dialog.js';
|
||||
import { iconButton } from './ui_ai_icons.js';
|
||||
import { chainTools, foldLooks, lookTools, stepDetail, toolKey } from './ai_task_view.js';
|
||||
|
||||
const opened = new Set(); // the first step of each chain the user opened
|
||||
|
||||
function el(tag, className, text) {
|
||||
const node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (text !== undefined) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
const counted = (tool, count) => (count > 1 ? `${t(toolKey(tool))} ×${count}` : t(toolKey(tool)));
|
||||
|
||||
/** A row of reads that follow each other; `number` is its place in the timeline. */
|
||||
export function looksRow(steps, number) {
|
||||
const row = el('div', 'anomalous-ai-step is-look is-folded');
|
||||
row.dataset.stepIds = steps.map(step => step.id).join(' ');
|
||||
row.append(el('span', 'anomalous-ai-step-dot', number));
|
||||
const body = el('div', 'anomalous-ai-step-body');
|
||||
const tools = lookTools(steps).map(({ tool, count }) => counted(tool, count));
|
||||
const head = el('button', 'anomalous-ai-looks', `${t('aiLooks', { count: steps.length })}:${tools.join('、')}`);
|
||||
head.type = 'button';
|
||||
head.onclick = () => {
|
||||
details.hidden = !details.hidden;
|
||||
head.setAttribute('aria-expanded', String(!details.hidden));
|
||||
};
|
||||
head.setAttribute('aria-expanded', 'false');
|
||||
const details = el('div', 'anomalous-ai-looks-details');
|
||||
details.hidden = true;
|
||||
for (const step of steps) {
|
||||
const detail = stepDetail(step);
|
||||
const found = Number.isFinite(step.count) ? t('aiFound', { count: step.count }) : '';
|
||||
details.append(el('div', 'anomalous-ai-look', [timeLabel(step.at), t(toolKey(step.tool)), detail, found].filter(Boolean).join(' · ')));
|
||||
}
|
||||
body.append(head, details);
|
||||
row.append(body, el('div', 'anomalous-ai-step-when', timeLabel(steps[0].at)));
|
||||
return row;
|
||||
}
|
||||
|
||||
/** The activity entries of the canvas changes `steps` made, newest first (`entries`: id -> entry). */
|
||||
function changesOf(steps, entries) {
|
||||
return steps.flatMap(step => (step.results || []).filter(result => result.type === 'canvas'))
|
||||
.map(result => entries.get(result.activity)).filter(Boolean).reverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Undoes the canvas changes `steps` made, newest first, once the user agrees (`ask`: the locale
|
||||
* key of the question); stops at a later change of theirs in the way. Whether it undid any.
|
||||
*/
|
||||
export async function undoChanges(steps, entries, label, ask) {
|
||||
const changes = changesOf(steps, entries);
|
||||
const undoable = changes.filter(entry => canUndo(app, entry));
|
||||
if (!undoable.length || !await anomalousConfirm(t(ask, { count: undoable.length }), label)) return false;
|
||||
for (const entry of changes) {
|
||||
if (!canUndo(app, entry)) break;
|
||||
try { undoEntry(app, entry); } catch (_) { break; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A chain of the AI's work as one row. `ctx`: {entries (activity id -> entry), stepRow(step,
|
||||
* number) for its steps when open, changed() after an undo, live (the AI may still add to it)}.
|
||||
* `row.reveal()` opens it (a jump to one of its steps).
|
||||
*/
|
||||
export function chainRow(steps, number, ctx) {
|
||||
const row = el('div', 'anomalous-ai-step is-chain is-folded');
|
||||
row.dataset.stepIds = steps.map(step => step.id).join(' ');
|
||||
row.append(el('span', 'anomalous-ai-step-dot', number));
|
||||
const body = el('div', 'anomalous-ai-step-body');
|
||||
const head = el('div', 'anomalous-ai-chain-head');
|
||||
const open = el('button', 'anomalous-ai-looks anomalous-ai-chain-open');
|
||||
open.type = 'button';
|
||||
open.append(el('span', 'anomalous-ai-chain-title', t('aiChain', { count: steps.length })));
|
||||
if (ctx.live) open.append(el('span', 'anomalous-ai-chain-last', t('aiChainLast', { what: t(toolKey(steps.at(-1).tool)) })));
|
||||
head.append(open);
|
||||
if (changesOf(steps, ctx.entries).some(entry => canUndo(app, entry))) {
|
||||
head.append(iconButton('anomalous-ai-chain-undo', 'undo', t('aiChainUndo'), async () => {
|
||||
if (await undoChanges(steps, ctx.entries, t('aiChainUndo'), 'aiChainUndoConfirm')) ctx.changed();
|
||||
}));
|
||||
}
|
||||
const chips = el('div', 'anomalous-ai-chain-chips');
|
||||
for (const { tool, kind, count } of chainTools(steps)) {
|
||||
chips.append(el('span', `anomalous-ai-chain-chip is-${kind}`, tool === 'look' ? t('aiLooks', { count }) : counted(tool, count)));
|
||||
}
|
||||
const inside = el('div', 'anomalous-ai-chain-steps');
|
||||
const key = steps[0].id;
|
||||
const show = (on) => {
|
||||
inside.hidden = !on;
|
||||
open.setAttribute('aria-expanded', String(on));
|
||||
if (on && !inside.childNodes.length) {
|
||||
inside.append(...foldLooks(steps).map((item, at) => (item.type === 'looks' ? looksRow(item.steps, at + 1) : ctx.stepRow(item.step, at + 1))));
|
||||
}
|
||||
};
|
||||
open.onclick = () => {
|
||||
const on = inside.hidden;
|
||||
if (on) opened.add(key);
|
||||
else opened.delete(key);
|
||||
show(on);
|
||||
};
|
||||
row.reveal = () => {
|
||||
opened.add(key);
|
||||
show(true);
|
||||
};
|
||||
show(opened.has(key));
|
||||
body.append(head, chips, inside);
|
||||
const [first, last] = [timeLabel(steps[0].at), timeLabel(steps.at(-1).at)];
|
||||
row.append(body, el('div', 'anomalous-ai-step-when', first === last ? first : `${first}–${last}`));
|
||||
return row;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* The AI workbench page: what AI apps did through Anomalous's MCP endpoint (api/ai_tasks.py).
|
||||
* The tasks on the left, newest first; on the right one task: its title and counts, what it
|
||||
* made (pictures, models, combos, sounds) as a strip on top, then its steps as a timeline,
|
||||
* reads folded into one row, each step with what it made and where that leads (the picture
|
||||
* the AI's work in a row folded into one (ui_ai_chain.js), each step with what it made and where that leads (the picture
|
||||
* in the viewer, the model's page, the combo, the canvas change found or undone). A task the
|
||||
* AI is still working on grows by itself ("anomalous.ai_task" events) and says so at the foot.
|
||||
* The column on the right shows one view, picked by the switch at the top right
|
||||
@@ -26,7 +26,7 @@ import { app } from '../../../scripts/app.js';
|
||||
import { api } from '../../../scripts/api.js';
|
||||
import { translate as t } from './locales.js';
|
||||
import { dayLabel, fetchActivity, timeLabel } from './activity_log.js';
|
||||
import { canUndo, undoEntry } from './canvas_undo.js';
|
||||
import { canUndo } from './canvas_undo.js';
|
||||
import { anomalousAlert, anomalousConfirm, anomalousPrompt } from './ui_dialog.js';
|
||||
import { messageBox, updateMessageBox } from './ui_ai_message.js';
|
||||
import { atFoot, newestButton } from './ui_ai_newest.js';
|
||||
@@ -46,12 +46,13 @@ import { compareBar, setMarker } from './ui_ai_images.js';
|
||||
import { openMarker } from './ui_ai_marker.js';
|
||||
import { toolboxCount } from './ui_ai_toolbox.js';
|
||||
import { stepBody } from './ui_ai_step_body.js';
|
||||
import { chainRow, looksRow, undoChanges } from './ui_ai_chain.js';
|
||||
import { taskLinks } from './ui_ai_task_links.js';
|
||||
import { TIMELINE_MIN, connectSide, drawSide, pickView, sideShown, sideSwitch, sideWidth, viewLabel } from './ui_ai_side_views.js';
|
||||
import { executionStatus } from './ui_ai_status.js';
|
||||
import { canvasControls } from './ui_ai_task_canvas.js';
|
||||
import {
|
||||
activityIds, duration, groupSteps, lookTools, stepDetail, taskCounts, taskResults, taskState, toolKey,
|
||||
activityIds, duration, groupSteps, stepDetail, taskCounts, taskResults, taskState, toolKey,
|
||||
} from './ai_task_view.js';
|
||||
|
||||
const NARROW_PX = 720;
|
||||
@@ -166,38 +167,8 @@ function stepRow(step, number) {
|
||||
return row;
|
||||
}
|
||||
|
||||
function looksRow(steps, number) {
|
||||
const row = el('div', 'anomalous-ai-step is-look is-folded');
|
||||
row.dataset.stepIds = steps.map(step => step.id).join(' ');
|
||||
row.append(el('span', 'anomalous-ai-step-dot', number));
|
||||
const body = el('div', 'anomalous-ai-step-body');
|
||||
const tools = lookTools(steps).map(({ tool, count }) => (count > 1 ? `${t(toolKey(tool))} ×${count}` : t(toolKey(tool))));
|
||||
const head = button('anomalous-ai-looks', `${t('aiLooks', { count: steps.length })}:${tools.join('、')}`, () => {
|
||||
details.hidden = !details.hidden;
|
||||
head.setAttribute('aria-expanded', String(!details.hidden));
|
||||
});
|
||||
head.setAttribute('aria-expanded', 'false');
|
||||
const details = el('div', 'anomalous-ai-looks-details');
|
||||
details.hidden = true;
|
||||
for (const step of steps) {
|
||||
const detail = stepDetail(step);
|
||||
const found = Number.isFinite(step.count) ? t('aiFound', { count: step.count }) : '';
|
||||
details.append(el('div', 'anomalous-ai-look', [timeLabel(step.at), t(toolKey(step.tool)), detail, found].filter(Boolean).join(' · ')));
|
||||
}
|
||||
body.append(head, details);
|
||||
row.append(body, el('div', 'anomalous-ai-step-when', timeLabel(steps[0].at)));
|
||||
return row;
|
||||
}
|
||||
|
||||
async function undoAll(task) {
|
||||
const entries = taskResults(task).canvas.map(result => state.entries.get(result.activity)).filter(Boolean).reverse();
|
||||
const undoable = entries.filter(entry => canUndo(app, entry));
|
||||
if (!undoable.length || !await anomalousConfirm(t('aiUndoAllConfirm', { count: undoable.length }), t('aiUndoAll'))) return;
|
||||
for (const entry of entries) {
|
||||
if (!canUndo(app, entry)) break; // a later change of the user's is in the way: stop there
|
||||
try { undoEntry(app, entry); } catch (_) { break; }
|
||||
}
|
||||
renderTask();
|
||||
if (await undoChanges(task.steps, state.entries, t('aiUndoAll'), 'aiUndoAllConfirm')) renderTask();
|
||||
}
|
||||
|
||||
function renderTask() {
|
||||
@@ -286,10 +257,14 @@ function renderTask() {
|
||||
|
||||
// Numbered in the order they happened, so the timeline reads 1, 2, 3 from the top.
|
||||
const items = groupSteps(task.steps);
|
||||
const rows = items.map((item, at) => (item.type === 'looks'
|
||||
? { key: JSON.stringify(['looks', at, item.steps]), make: () => looksRow(item.steps, at + 1) }
|
||||
: { key: JSON.stringify(['step', at, item.step, undoable(item.step)]), make: () => stepRow(item.step, at + 1) }));
|
||||
state.numbers = new Map(items.flatMap((item, at) => (item.type === 'looks' ? item.steps : [item.step]).map(step => [step.id, at + 1])));
|
||||
const rows = items.map((item, at) => {
|
||||
if (item.type === 'looks') return { key: JSON.stringify(['looks', at, item.steps]), make: () => looksRow(item.steps, at + 1) };
|
||||
if (item.type === 'step') return { key: JSON.stringify(['step', at, item.step, undoable(item.step)]), make: () => stepRow(item.step, at + 1) };
|
||||
const live = at === items.length - 1 && !task.ended; // the AI may still add to it
|
||||
return { key: JSON.stringify(['chain', at, item.steps, item.steps.map(undoable), live]),
|
||||
make: () => chainRow(item.steps, at + 1, { entries: state.entries, stepRow, changed: renderTask, live }) };
|
||||
});
|
||||
state.numbers = new Map(items.flatMap((item, at) => (item.type === 'step' ? [item.step] : item.steps).map(step => [step.id, at + 1])));
|
||||
// A task the user started waits for an AI: how to get one on it, and a page to put things
|
||||
// ready for it meanwhile (their own steps are drawn there; its gallery feeds the message box).
|
||||
const preparing = Boolean(task.waiting) && items.every(item => item.type === 'step' && item.step.kind === 'user');
|
||||
@@ -315,9 +290,12 @@ function renderTask() {
|
||||
drawStage(task, news.arrived);
|
||||
}
|
||||
|
||||
/** Brings a step's row into view (from the flow), marked for a moment. */
|
||||
/** Brings a step's row into view (from the flow), marked for a moment; a folded chain opens to it. */
|
||||
function showStep(timeline, stepId) {
|
||||
const row = [...timeline.querySelectorAll('[data-step-ids]')].find(item => item.dataset.stepIds.split(' ').includes(stepId));
|
||||
const has = item => item.dataset.stepIds.split(' ').includes(stepId);
|
||||
const chain = [...timeline.children].find(has);
|
||||
chain?.reveal?.();
|
||||
const row = [...timeline.querySelectorAll('[data-step-ids]')].findLast(has);
|
||||
if (!row) return;
|
||||
row.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
row.classList.add('is-flash');
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@
|
||||
@import url("./styles/34-voice-over-panel.css?v=20261021-sample");
|
||||
@import url("./styles/35-voice-over-drafts.css?v=20261020-drafts");
|
||||
@import url("./styles/36-reference-audio.css?v=20261020-remove");
|
||||
@import url("./styles/37-ai-workbench.css?v=20261024-outside");
|
||||
@import url("./styles/37-ai-workbench.css?v=20261024-chain");
|
||||
@import url("./styles/38-ai-panels.css?v=20261024-passed");
|
||||
@import url("./styles/39-ai-stage.css?v=20261021-stage");
|
||||
@import url("./styles/40-ai-side.css?v=20261024-outside");
|
||||
|
||||
@@ -686,6 +686,43 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* The AI's work in a row folded into one (ui_ai_chain.js): what it did as chips; open, its steps in a column of their own. */
|
||||
.anomalous-ai-step.is-chain { --kind: var(--ai-canvas); }
|
||||
.anomalous-ai-chain-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }
|
||||
.anomalous-ai-chain-title { color: var(--amb-text-main); font-weight: 650; }
|
||||
.anomalous-ai-chain-last { color: var(--amb-text-dim); font-size: 12px; }
|
||||
.anomalous-ai-chain-undo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin-left: auto;
|
||||
padding: 2px 9px;
|
||||
border: 1px solid var(--amb-border);
|
||||
border-radius: 999px;
|
||||
background: none;
|
||||
color: var(--amb-text-soft);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.anomalous-ai-chain-undo:hover { border-color: var(--amb-border-strong); color: var(--amb-text-main); }
|
||||
.anomalous-ai-chain-chips { display: flex; flex-wrap: wrap; gap: 5px; }
|
||||
.anomalous-ai-chain-chip {
|
||||
--chip: var(--ai-look);
|
||||
padding: 1px 8px;
|
||||
border: 1px solid color-mix(in srgb, var(--chip) 40%, var(--amb-border));
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--chip) 10%, transparent);
|
||||
color: var(--amb-text-soft);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
.anomalous-ai-chain-chip.is-canvas { --chip: var(--ai-canvas); }
|
||||
.anomalous-ai-chain-chip.is-decision { --chip: #e0a63c; }
|
||||
.anomalous-ai-chain-steps { display: flex; flex-direction: column; margin-top: 4px; padding-left: 12px; border-left: 2px solid var(--amb-border); }
|
||||
.anomalous-ai-chain-steps[hidden] { display: none; }
|
||||
.anomalous-ai-chain-steps .anomalous-ai-step { grid-template-columns: 20px minmax(0, 1fr) auto; gap: 10px; padding: 8px 0; }
|
||||
.anomalous-ai-chain-steps .anomalous-ai-step-dot { width: 20px; height: 20px; margin-top: -2px; font-size: 10px; box-shadow: none; }
|
||||
|
||||
.anomalous-ai-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user