diff --git a/CHANGELOG.md b/CHANGELOG.md index 30dc0dd..4e14e79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### ✨ AI workbench: see everything the AI did, open it, undo it (AI 工作台) - **A new page on the rail, *AI workbench***: every step an AI app (Claude Code, Cursor, Cherry Studio… connected through Anomalous's MCP) takes is kept, grouped into **tasks**. The AI names a task when it starts (a new `begin_task` tool); otherwise one app's calls that follow each other make one task. -- **A task is a timeline**: what it looked up (folded into one line, a click shows each), what it changed on the canvas, the workflow runs, downloads, scans and spoken lines, and the AI's own notes (a new `report` tool, also for pictures it made another way). Each step says when, how long it took, and why it failed when it did. +- **A task is a timeline**: what it looked up (folded into one line, a click shows each), what it changed on the canvas, the workflow runs, downloads, scans and spoken lines, and the AI's own notes (a new `report` tool, also for pictures it made another way). Each step says when, how long it took, and why it failed when it did. Steps are **numbered in order** (1, 2, 3… down the line), each dot in its kind's colour; while the AI works, the next number waits at the foot. - **What it made stays, and leads there**: the pictures a run made are kept with the task (watched until the run ends) and shown on top as *What it made*; a click opens the picture, a model opens its page, a combo opens the combo, a spoken line plays, a canvas change is found on the canvas or **undone** (also all of a task's canvas changes at once, stopping at anything you changed since). - **Live**: a task the AI is still working on grows by itself and says *The AI is working…*; the page follows the newest task unless you picked another. While you are elsewhere, the rail's *AI workbench* gets a dot. Home gets *What the AI did lately*. - Tasks can be renamed or removed from the list (only the record goes: pictures and the canvas stay). The newest 300 are kept, in ComfyUI's user folder. Without any AI connected, the page shows the address to add in the AI app. diff --git a/web/modules/ui_ai_workbench.js b/web/modules/ui_ai_workbench.js index 82728a4..44e78ff 100644 --- a/web/modules/ui_ai_workbench.js +++ b/web/modules/ui_ai_workbench.js @@ -221,9 +221,10 @@ function resultLinks(step) { // ---------------------------------------------------------------- one task -function stepRow(step) { +/** One step of the timeline; `number` is its place in the task (the dot shows it). */ +function stepRow(step, number) { const row = el('div', `anomalous-ai-step is-${step.kind}${step.ok ? '' : ' is-failed'}`); - row.append(el('span', 'anomalous-ai-step-dot')); + row.append(el('span', 'anomalous-ai-step-dot', number)); const body = el('div', 'anomalous-ai-step-body'); const line = el('div', 'anomalous-ai-step-line'); line.append(el('span', 'anomalous-ai-kind', t(`aiKind_${step.kind}`)), el('span', 'anomalous-ai-step-title', t(toolKey(step.tool)))); @@ -257,9 +258,9 @@ function stepRow(step) { return row; } -function looksRow(steps) { +function looksRow(steps, number) { const row = el('div', 'anomalous-ai-step is-look is-folded'); - row.append(el('span', 'anomalous-ai-step-dot')); + 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('、')}`, () => { @@ -360,13 +361,21 @@ function renderTask() { header.append(titleLine, el('div', 'anomalous-ai-task-meta', meta.join(' · '))); const timeline = el('div', 'anomalous-ai-timeline'); - for (const item of groupSteps(task.steps)) timeline.append(item.type === 'looks' ? looksRow(item.steps) : stepRow(item.step)); + // Numbered in the order they happened, so the timeline reads 1, 2, 3 from the top. + const items = groupSteps(task.steps); + items.forEach((item, at) => timeline.append(item.type === 'looks' ? looksRow(item.steps, at + 1) : stepRow(item.step, at + 1))); if (!(task.steps || []).length) timeline.append(el('div', 'anomalous-ai-empty-task', t('aiNoSteps'))); const parts = [header]; const made = strip(task); if (made) parts.push(made); parts.push(timeline); - if (status === 'live') parts.push(el('div', 'anomalous-ai-working', (task.steps || []).some(step => step.state === 'running') ? t('aiRunRunning') : t('aiWorking'))); + if (status === 'live') { + // What comes next: the following number, waiting. + const next = el('div', 'anomalous-ai-step is-next'); + next.append(el('span', 'anomalous-ai-step-dot', items.length + 1), + el('div', 'anomalous-ai-working', (task.steps || []).some(step => step.state === 'running') ? t('aiRunRunning') : t('aiWorking'))); + timeline.append(next); + } box.replaceChildren(...parts); box.scrollTop = keepBottom && status === 'live' ? box.scrollHeight : keepTop; } diff --git a/web/styles/37-ai-workbench.css b/web/styles/37-ai-workbench.css index 0c24044..097f284 100644 --- a/web/styles/37-ai-workbench.css +++ b/web/styles/37-ai-workbench.css @@ -295,9 +295,9 @@ .anomalous-ai-timeline::before { content: ''; position: absolute; - top: 8px; - bottom: 8px; - left: 6px; + top: 14px; + bottom: 14px; + left: 11px; width: 2px; border-radius: 1px; background: var(--amb-border); @@ -307,7 +307,7 @@ --kind: var(--ai-look); position: relative; display: grid; - grid-template-columns: 14px minmax(0, 1fr) auto; + grid-template-columns: 24px minmax(0, 1fr) auto; gap: 12px; padding: 9px 0; } @@ -322,20 +322,35 @@ .anomalous-ai-step.is-act { --kind: var(--amb-text-soft); } .anomalous-ai-step.is-failed { --kind: var(--amb-danger); } +/* The step's number in a dot of its kind's colour; the line through the dots is the order. */ .anomalous-ai-step-dot { position: relative; z-index: 1; - width: 14px; - height: 14px; - margin-top: 2px; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + margin-top: -3px; box-sizing: border-box; - border: 3px solid var(--amb-bg-panel, #16161a); border-radius: 50%; background: var(--kind); + color: #fff; + font-size: 11px; + font-weight: 700; + box-shadow: 0 0 0 3px var(--amb-bg-panel, #16161a); } .anomalous-ai-step.is-folded .anomalous-ai-step-dot { - transform: scale(0.72); + border: 1.5px solid var(--kind); + background: var(--amb-bg-panel, #16161a); + color: var(--amb-text-soft); +} + +.anomalous-ai-step.is-next .anomalous-ai-step-dot { + border: 1.5px dashed var(--amb-ok); + background: var(--amb-bg-panel, #16161a); + color: var(--amb-ok-text); } .anomalous-ai-step-body { @@ -407,7 +422,6 @@ } .anomalous-ai-working { - margin: 10px 0 0 26px; animation: anomalous-ai-breathe 1.8s ease-in-out infinite; }