Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_ai_run_live.js
T
DemonGatanjieuandClaude Opus 5.5 049b265126 Run control: stop a run, see its console, time left, VRAM and size warnings; materials as a side panel
- A stop button on a running run (timeline, and beside the run-status button), and stop_run for
  the AI: only this run's prompts leave the queue or are interrupted; the step ends "cancelled"
  and a user's stop reaches the AI as a "stop" message (ai_run_control.py).
- The run's progress carries ComfyUI's console since it began (the bar's note, models loading),
  time left and per step, and VRAM; once queued, each sampler's working size is estimated from
  the prompt, with warnings for an upscaled picture sampled again, a size past the card and a
  huge decode, for the user and in the AI's run news (ai_run_watch.py, ui_ai_run_live.js).
- The materials picker opens as a side panel that leaves the page usable (the full-screen
  browser makes room), expands to the large window and back, and remembers the choice.
- Esc closes the run-status card only (data-anomalous-escape).

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

142 lines
7.1 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 run the AI started, while it goes (api/ai_task_runs.py, api/ai_run_watch.py): runProgress
* draws where it stands (queued, the node and its steps, the percent), the time left and per
* step, the graphics card's memory, the console's own note and last lines (what the console
* window shows), and a stop button (api/ai_run_control.py); estimateBox the size each sampler
* works at and the warnings for a run too big for the card. Used on the run's step in the
* timeline (ui_ai_results.js) and in the run-status card (ui_ai_status.js).
*/
import { translate as t } from './locales.js';
import { anomalousAlert } from './ui_dialog.js';
import { icon } from './ui_ai_icons.js';
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
/** "1:05" / "12 s" for `seconds`. */
export function clock(seconds) {
const whole = Math.max(0, Math.round(seconds));
if (whole < 60) return t('aiRunSeconds', { seconds: whole });
const hours = Math.floor(whole / 3600);
const minutes = Math.floor((whole % 3600) / 60);
const rest = String(whole % 60).padStart(2, '0');
return hours ? `${hours}:${String(minutes).padStart(2, '0')}:${rest}` : `${minutes}:${rest}`;
}
/** "3.9 / 12 GB" free of all. */
export function memory(vram) {
const gb = mb => (mb / 1024).toFixed(mb >= 10240 ? 0 : 1);
return t('aiRunVram', { free: gb(vram.free_mb), total: gb(vram.total_mb) });
}
/** The stop button of run step `stepId` in task `taskId`; `stopped()` after the server took it.
* `stopping`: the run was stopped already and ends when its node does (ComfyUI stops between nodes). */
export function stopButton(taskId, stepId, stopped = () => {}, compact = false, stopping = false) {
const node = el('button', `anomalous-ai-run-stop${compact ? ' is-compact' : ''}${stopping ? ' is-busy' : ''}`);
node.type = 'button';
node.title = t(stopping ? 'aiRunStoppingHint' : 'aiRunStopHint');
node.disabled = stopping;
node.append(icon('stop', compact ? 13 : 14));
if (!compact) node.append(el('span', '', t(stopping ? 'aiRunStopping' : 'aiRunStop')));
node.onclick = async (event) => {
event.stopPropagation();
node.disabled = true;
node.classList.add('is-busy');
try {
const response = await fetch(`/anomalous/ai_tasks/${encodeURIComponent(taskId)}/runs/${encodeURIComponent(stepId)}/stop`, { method: 'POST' });
const answer = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(answer.error || response.statusText);
node.replaceChildren(icon('stop', compact ? 13 : 14), ...(compact ? [] : [el('span', '', t('aiRunStopping'))]));
stopped();
} catch (error) {
node.disabled = false;
node.classList.remove('is-busy');
await anomalousAlert(error.message);
}
};
return node;
}
/** Where a run stands; with `stop` ({task, step, stopped}) a stop button beside the bar. */
export function runProgress(progress, stop = null) {
const box = el('div', 'anomalous-ai-run-progress');
let words;
let share = null;
if (progress.state === 'submitting') {
words = t('aiExecution_submitting');
} else if (progress.state === 'queued') {
words = t('aiRunQueued', { ahead: progress.ahead });
} else {
const parts = [];
if (progress.prompts > 1) parts.push(t('aiRunPrompt', { at: progress.prompt, count: progress.prompts }));
if (progress.node) parts.push(progress.max > 1 ? `${progress.node} ${progress.value}/${progress.max}` : progress.node);
if (progress.nodes) parts.push(t('aiRunNodes', { done: progress.nodes_done, count: progress.nodes }));
words = parts.join(' · ') || t('aiRunRunning');
if (Number.isFinite(progress.percent)) share = progress.percent / 100;
else if (progress.max > 1) share = progress.value / progress.max;
else if (progress.nodes) share = progress.nodes_done / progress.nodes;
}
const line = el('div', 'anomalous-ai-run-line');
line.append(el('span', 'anomalous-ai-run-words', words));
if (share !== null) line.append(el('b', 'anomalous-ai-run-percent', `${Math.round(Math.min(1, Math.max(0, share)) * 100)}%`));
const bar = el('div', `anomalous-ai-run-bar${share === null ? ' is-waiting' : ''}`);
const fill = el('span', 'anomalous-ai-run-fill');
if (share !== null) fill.style.width = `${Math.round(Math.min(1, Math.max(0, share)) * 100)}%`;
bar.append(fill);
const row = el('div', 'anomalous-ai-run-row');
row.append(bar);
if (stop?.task && stop?.step) row.append(stopButton(stop.task, stop.step, stop.stopped, false, Boolean(progress.stopping)));
box.append(line, row);
const facts = el('div', 'anomalous-ai-run-facts');
const fact = (name, text, title) => {
const item = el('span', 'anomalous-ai-run-fact');
item.append(icon(name, 13), el('span', '', text));
if (title) item.title = title;
facts.append(item);
};
if (Number.isFinite(progress.left)) fact('clock', t('aiRunLeft', { time: clock(progress.left) }), t('aiRunLeftHint'));
if (Number.isFinite(progress.rate)) fact('pulse', t('aiRunRate', { seconds: progress.rate }));
if (progress.vram?.total_mb) fact('chip', memory(progress.vram), t('aiRunVramHint'));
if (facts.childNodes.length) box.append(facts);
const lines = progress.console || [];
if (progress.note || lines.length) {
const say = el('details', 'anomalous-ai-run-console');
const head = el('summary', 'anomalous-ai-run-console-head');
head.append(icon('terminal', 13), el('span', 'anomalous-ai-run-console-last', progress.note || lines.at(-1)));
head.title = t('aiRunConsoleHint');
say.append(head);
if (lines.length) say.append(el('pre', 'anomalous-ai-run-console-lines', lines.join('\n')));
box.append(say);
}
return box;
}
/** The size each sampler of a run works at, and what is too big for the card; null when nothing to say. */
export function estimateBox(estimate) {
const warnings = estimate?.warnings || [];
const samplers = estimate?.samplers || [];
if (!warnings.length && !samplers.length) return null;
const box = el('div', `anomalous-ai-estimate${warnings.length ? ' is-warn' : ''}`);
for (const warning of warnings) {
const item = el('div', 'anomalous-ai-estimate-warning');
item.append(icon('alert', 15), el('span', '', t(`aiRunWarn_${warning.kind}`, {
title: warning.title, width: warning.width, height: warning.height, steps: warning.steps ?? '?',
megapixels: warning.megapixels ?? '', comfort: estimate.comfort_mp ?? '',
})));
box.append(item);
}
if (samplers.length) {
box.append(el('div', 'anomalous-ai-estimate-sizes', t('aiRunSizes', {
sizes: samplers.map(item => `${item.title} ${item.width}×${item.height}${item.batch > 1 ? ` ×${item.batch}` : ''}${item.steps ? ` · ${t('aiRunSteps', { steps: item.steps })}` : ''}`).join('; '),
})));
}
return box;
}