Files
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

172 lines
7.6 KiB
JavaScript

/** Compact toolbar icon-button + fixed-position popover for confirmed task execution status; while
* a run goes, a stop button beside it, and the card shows the run's time left, memory, console and
* size warnings (ui_ai_run_live.js). */
import { translate as t } from './locales.js';
import { taskStatus } from './ai_task_status.js';
import { icon } from './ui_ai_icons.js';
import { estimateBox, runProgress, stopButton } from './ui_ai_run_live.js';
let openTaskId = null;
let boundGlobal = false;
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function closeOpenPopover() {
openTaskId = null;
const wrap = document.querySelector('.anomalous-ai-execution.is-open');
if (wrap) {
wrap.classList.remove('is-open');
wrap.querySelector('.anomalous-ai-status-popover')?.remove();
}
}
function positionPopover(card, anchor) {
if (!anchor?.isConnected) return;
const rect = anchor.getBoundingClientRect();
const host = anchor.closest('.anomalous-ai-task') || anchor.closest('.anomalous-ai') || document.body;
const bounds = host.getBoundingClientRect();
const width = Math.min(300, Math.max(220, window.innerWidth - 24));
card.style.width = `${width}px`;
const minLeft = Math.max(12, bounds.left + 10);
const maxLeft = Math.max(minLeft, Math.min(window.innerWidth - width - 12, bounds.right - width - 10));
let left = rect.right - width;
if (left < minLeft) left = Math.min(Math.max(rect.left, minLeft), maxLeft);
left = Math.max(12, Math.min(left, window.innerWidth - width - 12));
const top = Math.min(rect.bottom + 6, window.innerHeight - 220);
card.style.left = `${Math.round(left)}px`;
card.style.top = `${Math.round(Math.max(12, top))}px`;
}
function bindGlobalListeners() {
if (boundGlobal || typeof document === 'undefined') return;
boundGlobal = true;
document.addEventListener('pointerdown', (event) => {
if (!openTaskId) return;
const wrap = document.querySelector('.anomalous-ai-execution.is-open');
if (wrap && !wrap.contains(event.target)) closeOpenPopover();
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && openTaskId) closeOpenPopover();
});
window.addEventListener('resize', () => {
const wrap = document.querySelector('.anomalous-ai-execution.is-open');
const btn = wrap?.querySelector('.anomalous-ai-status-btn');
const card = wrap?.querySelector('.anomalous-ai-status-popover');
if (btn && card) positionPopover(card, btn);
});
document.addEventListener('scroll', (event) => {
if (!openTaskId) return;
const wrap = document.querySelector('.anomalous-ai-execution.is-open');
if (wrap && event.target instanceof Node && event.target.contains(wrap)) {
const btn = wrap.querySelector('.anomalous-ai-status-btn');
const card = wrap.querySelector('.anomalous-ai-status-popover');
if (btn && card) positionPopover(card, btn);
}
}, true);
}
function activeLabel(status) {
if (status.state === 'running' && Number.isFinite(status.run?.progress?.percent)) {
return `${status.run.progress.percent}%`;
}
if (status.state === 'queued' || status.state === 'submitting') {
return t(`aiExecution_${status.state}`);
}
return '';
}
function buildPopover(task, status, refresh) {
const card = el('section', `anomalous-ai-status-popover is-${status.state}`);
card.setAttribute('aria-label', t('aiExecution'));
card.dataset.anomalousEscape = ''; // Esc closes this card, not the browser (ui_browser_navigation.js)
const top = el('div', 'anomalous-ai-execution-top');
top.append(
el('span', 'anomalous-ai-execution-label', t('aiExecution')),
el('strong', `anomalous-ai-execution-badge is-${status.state}`, t(`aiExecution_${status.state}`)),
);
const check = el('button', 'anomalous-ai-link', t('aiExecutionRefresh'));
check.type = 'button';
check.onclick = async (event) => {
event.stopPropagation();
check.disabled = true;
try { await refresh(); } finally { check.disabled = false; }
};
top.append(check);
card.append(top);
if (status.run) {
const run = status.run;
if (run.state === 'running') card.append(runProgress({ state: 'submitting', ...run.progress }, { task: task.id, step: run.id, stopped: refresh }));
const sizes = estimateBox(run.estimate);
if (sizes) card.append(sizes);
const count = (run.prompt_ids || []).length;
const pictures = (run.results || []).filter(item => item.type === 'image').length;
const rows = el('div', 'anomalous-ai-execution-rows');
const addRow = (dotClass, label) => {
const row = el('div', 'anomalous-ai-execution-row');
row.append(el('span', `anomalous-ai-status-dot ${dotClass}`), el('span', '', label));
rows.append(row);
};
addRow('is-time', t('aiExecutionTime', { seconds: status.seconds }));
if (count) addRow('is-batch', t('aiExecutionBatch', { count }));
if (pictures) addRow('is-done', t('aiCountImages', { count: pictures }));
card.append(rows);
if (run.error) card.append(el('div', 'anomalous-ai-execution-error', run.error));
if (run.state === 'done' && !task.ended) card.append(el('div', 'anomalous-ai-execution-hint', t('aiExecutionReview')));
} else {
card.append(el('p', 'anomalous-ai-execution-hint', status.active
? t('aiExecutionTool', { tool: status.active.tool })
: t(status.state === 'untracked' ? 'aiExecutionUntracked' : status.state === 'ended' ? 'aiExecutionEnded' : 'aiExecutionWaiting')));
}
if (!task.ended) card.append(el('div', 'anomalous-ai-execution-contact', t('aiExecutionContact', { seconds: status.age })));
return card;
}
export function executionStatus(task, refresh) {
bindGlobalListeners();
const status = taskStatus(task);
const isOpen = openTaskId === task.id;
const label = activeLabel(status);
const wrap = el('div', `anomalous-ai-execution is-${status.state}${isOpen ? ' is-open' : ''}`);
const btn = el('button', `anomalous-ai-tool anomalous-ai-status-btn is-${status.state}${label ? '' : ' is-icon'}`);
btn.type = 'button';
const fullTitle = `${t('aiExecution')}: ${t(`aiExecution_${status.state}`)}`;
btn.title = fullTitle;
btn.setAttribute('aria-label', fullTitle);
btn.append(
icon('pulse', 15),
el('span', `anomalous-ai-status-dot is-${status.state}`),
);
if (label) btn.append(el('span', 'anomalous-ai-status-text', label));
const openPopover = () => {
wrap.querySelector('.anomalous-ai-status-popover')?.remove();
const card = buildPopover(task, status, refresh);
wrap.append(card);
requestAnimationFrame(() => positionPopover(card, btn));
};
btn.onclick = (event) => {
event.stopPropagation();
if (openTaskId === task.id) {
closeOpenPopover();
} else {
openTaskId = task.id;
wrap.classList.add('is-open');
openPopover();
}
};
wrap.append(btn);
if (status.run?.state === 'running' && status.run.prompt_ids?.length) {
wrap.append(stopButton(task.id, status.run.id, refresh, true, Boolean(status.run.stop || status.run.progress?.stopping)));
}
if (status.run?.state === 'running' && status.run.estimate?.warnings?.length) btn.classList.add('has-warning');
if (isOpen) openPopover();
return wrap;
}