Files
olivv-cs-ComfyUI-FunPack/app/ui/shell/transport.js
T

189 lines
7.5 KiB
JavaScript

// Generate, and everything that happens after it.
//
// One object owns the whole lifecycle, because the alternative is a button
// here and a progress bar there and a status somewhere else, each with its own
// idea of what is happening. This reads ONE state object and draws it -- so
// "queued" cannot appear next to a finished result, whatever order the messages
// arrive in.
//
// It owns the CONTROLS, not a row: they are handed out and the shell puts them
// in the head of the zone the run fills. A bar across the bottom of the window
// acts on "whatever is in front" and belongs to no region -- which is what made
// this read as a dashboard rather than an editor. v4 puts Generate in the head
// of the zone it produces, and so does this.
//
// Warnings live here too, beside Generate, rather than in the panel that would
// have caused them: a setting that will do nothing has to be said where the run
// is started, not where it was switched on.
import { composer } from "../composer/composer.js";
import { IDLE, QUEUED, RUNNING, DONE, FAILED, CANCELLED } from "./run.js";
const WORKING = new Set([QUEUED, RUNNING]);
export function createTransport({ onGenerate, onCancel } = {}) {
const generate = composer.button.md({
label: "Generate", tone: "primary",
onClick: () => { if (onGenerate) onGenerate(); },
});
const cancel = composer.button.sm({
label: "Cancel", tone: "ghost",
onClick: () => { if (onCancel) onCancel(); },
});
const progress = composer.progress.bar({ value: 0, max: 100, label: "Generating" });
const status = composer.text.sm({ text: "Ready" });
// Something true about the NEXT run, said before it starts and left up while
// it does: a setting that will not be applied is not a status, and putting it
// in the status line meant it was gone the moment the run said "Queued". It
// is here rather than in the panel that would have caused it, because that is
// the panel a person has already stopped looking at.
const warning = composer.banner.warn({ text: "" });
warning.node.setAttribute("hidden", "");
// Something to say that is not about a run in progress: why the last attempt
// did not become one. It has to survive the next draw(), because a refused
// attempt leaves the run exactly where it was -- idle -- and drawing an idle
// run says "Ready", which wiped the reason a moment after it appeared.
let note = null;
// Held for a reason that is not a run: the page is still working out whether
// it already has one. Kept as a flag rather than left to whoever disabled the
// button, because draw() runs on every state change and each of those would
// otherwise hand the button back -- which is exactly what a subscription
// delivering the current state did, one line after the button was disabled.
let held = false;
// Hidden by attribute rather than removed: the row's shape then does not jump
// as a run starts and finishes, and there is one node to find in a test.
const hide = (handle, yes) => {
if (yes) handle.node.setAttribute("hidden", "");
else handle.node.removeAttribute("hidden");
};
hide(progress, true);
hide(cancel, true);
function draw(state) {
// Anything other than idle means a run answered for itself, and what it
// says outranks a note about an attempt that never started.
if (state.phase !== IDLE) note = null;
const working = WORKING.has(state.phase);
generate.setDisabled(working || held);
generate.setBusy(working);
hide(cancel, !working);
const measured = state.progress && state.progress.max;
hide(progress, !(working && measured));
if (measured) {
progress.node.setAttribute("aria-valuemax", String(state.progress.max));
progress.setValue(state.progress.value);
}
status.setText(note || describe(state));
}
/** Say why nothing was started, or -- with null -- stop saying it. Cleared by
* the next run that says otherwise. */
function say(text, state) {
note = text || null;
status.setText(note || describe(state || { phase: IDLE, images: [] }));
}
/** Stop offering Generate for a reason of the page's own, and say why. */
function hold(reason) {
held = Boolean(reason);
if (reason) say(reason);
generate.setDisabled(true);
}
/** Offer it again. The run's own state decides from here. */
function release(state) {
held = false;
say(null, state);
draw(state);
}
/** Say something about the pipeline itself, or -- with null -- stop saying it.
* Unlike say(), this survives a run: it is still true while one is going. */
function warn(text) {
warning.setText(text || "");
warning.node.toggleAttribute("hidden", !text);
}
return {
// What the shell puts in a zone head: the controls that start and stop a
// run, and -- at the far end of the same head -- what it is doing.
actions: [cancel, generate],
// The progress bar goes with the words rather than with the buttons: it is
// the same statement in another form.
status: [progress, status],
warning,
draw, say, warn, hold, release, generate, cancel, progress, statusText: status,
};
}
/**
* Why a run failed, in one line, from whichever of the two shapes it arrived in.
*
* A crash during execution names one node -- `node`. A graph the queue refused
* names as many as were wrong -- `nodes`, an object keyed by node id, each with
* its own reasons. Only the first was ever read, so the whole of the second was
* captured and thrown away: the bar showed ComfyUI's top-level string, which is
* "Prompt outputs failed validation" and says nothing about which of a dozen
* loaders to go and fix.
*/
export function failure(error) {
if (!error) return "the run failed";
const first = firstNodeError(error.nodes);
if (first) return first;
const where = error.node ? `${error.node}: ` : "";
return `${where}${error.message || "the run failed"}`;
}
function firstNodeError(nodes) {
if (!nodes || typeof nodes !== "object") return null;
for (const [id, entry] of Object.entries(nodes)) {
const reasons = entry && Array.isArray(entry.errors) ? entry.errors : [];
for (const reason of reasons) {
const what = reason && (reason.message || reason.type);
if (!what) continue;
// The class type as well as the id: a node id is a number in ComfyUI's
// own graphs and a slot name in ours, and neither on its own is much to
// go on when the message is "Value not in list".
const which = entry.class_type ? `${entry.class_type} (${id})` : id;
const detail = reason.details ? ` — ${reason.details}` : "";
return `${which}: ${what}${detail}`;
}
}
return null;
}
/** One line saying where the run is. The only place these words are decided. */
export function describe(state) {
switch (state.phase) {
case QUEUED:
return "Queued";
case RUNNING: {
const where = state.node ? `node ${state.node}` : "working";
if (state.progress && state.progress.max) {
return `${where} — ${state.progress.value} of ${state.progress.max}`;
}
return where.charAt(0).toUpperCase() + where.slice(1);
}
case DONE:
// The count, because a graph that saved nothing finished successfully and
// produced no picture, and those must not read the same.
return state.images.length
? `Done — ${state.images.length} result${state.images.length === 1 ? "" : "s"}`
: "Done, but nothing was saved";
case FAILED:
return failure(state.error);
case CANCELLED:
return "Cancelled";
default:
return "Ready";
}
}