149 lines
6.8 KiB
JavaScript
149 lines
6.8 KiB
JavaScript
// Step Preview 🆎: the picture while it is being made, one step at a time,
|
|
// with a Stop button. It needs no wires: it shows whichever sampler in this
|
|
// workflow is making a picture, then the finished picture once the run has
|
|
// saved it, and keeps that after the run.
|
|
//
|
|
// The pictures and the run come from shared/step_preview.mjs; the server
|
|
// side asks for them (nodes/_step_preview_helpers.py).
|
|
|
|
import { app } from "/scripts/app.js";
|
|
import { BRAND, chainCallback, keepDomWidgetWidthAuto } from "../shared/index.mjs";
|
|
import { WIDGET_FRAME, fillNodeHeight } from "../shared/panel_layout.mjs";
|
|
import { ensureStepCss, mountStepOverlay, queuedRuns, stopAllRuns, stopStepRun, watchSteps } from "../shared/step_preview.mjs";
|
|
|
|
const NODE_CLASS = "AUSBOSS_NODES_StepPreview";
|
|
const CSS_ID = "ausboss-step-preview-node-css";
|
|
const WIDGET_NAME = "ausboss_step_preview_panel";
|
|
const DEFAULT_SIZE = [320, 340];
|
|
const PANEL_MIN_WIDTH = 240;
|
|
const BAR_HEIGHT = 20;
|
|
const STAGE_MIN_HEIGHT = 140;
|
|
const PANEL_MIN_HEIGHT = BAR_HEIGHT + 4 + STAGE_MIN_HEIGHT + 6;
|
|
const IDLE_HINT = "Run the workflow to watch the picture form here.";
|
|
|
|
function ensureCss() {
|
|
ensureStepCss();
|
|
if (document.getElementById(CSS_ID)) return;
|
|
const style = document.createElement("style");
|
|
style.id = CSS_ID;
|
|
style.textContent = `
|
|
.ausboss-steppv{box-sizing:border-box;display:flex;flex-direction:column;gap:4px;width:100%;height:100%;padding:0 6px 6px;pointer-events:none;overflow:hidden;}
|
|
.ausboss-steppv-bar{box-sizing:border-box;flex:none;display:flex;align-items:center;gap:6px;height:${BAR_HEIGHT}px;padding:0 2px;}
|
|
.ausboss-steppv-bar button{box-sizing:border-box;height:${BAR_HEIGHT}px;min-width:24px;padding:0 6px;border:1px solid rgba(0,180,170,.52);border-radius:4px;background:rgba(0,0,0,.7);color:#c8dddd;font:700 9px/18px "Segoe UI",sans-serif;white-space:nowrap;cursor:pointer;pointer-events:auto;}
|
|
.ausboss-steppv-bar button:hover{border-color:${BRAND};color:#fff;background:rgba(0,79,75,.78);}
|
|
.ausboss-steppv-bar button:disabled{opacity:.38;cursor:default;pointer-events:none;}
|
|
.ausboss-steppv-bar button.ausboss-steps-stop:disabled{border-color:rgba(0,180,170,.3)!important;background:rgba(0,0,0,.5)!important;color:#7d9593!important;}
|
|
.ausboss-steppv-bar button.ausboss-steps-stop:disabled::before{background:#5f7674;}
|
|
.ausboss-steppv-bar .all[hidden]{display:none;}
|
|
.ausboss-steppv-state{margin-left:auto;color:#5f7674;font:10px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;letter-spacing:.08em;text-transform:uppercase;white-space:nowrap;}
|
|
.ausboss-steppv-state.live{color:${BRAND};}
|
|
.ausboss-steppv-stage{position:relative;flex:1 1 auto;min-height:0;width:100%;overflow:hidden;border:1px solid rgba(0,180,170,.27);border-radius:6px;background:rgba(0,0,0,.28);}
|
|
[data-node-id] .ausboss-steppv-stage{box-sizing:border-box;min-height:${STAGE_MIN_HEIGHT}px;}
|
|
.ausboss-steppv-stage.ausboss-steps-showing .ausboss-steps-hint{display:none;}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
function button(label, title, onClick, signal) {
|
|
const element = document.createElement("button");
|
|
element.type = "button";
|
|
element.textContent = label;
|
|
element.title = title;
|
|
// A press here is a click on the button, not the start of a node drag.
|
|
element.addEventListener("pointerdown", (event) => event.stopPropagation(), { signal });
|
|
element.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
Promise.resolve(onClick()).catch(() => {});
|
|
}, { signal });
|
|
return element;
|
|
}
|
|
|
|
const STATE_TEXT = { running: "running", done: "done", stopped: "stopped", failed: "failed" };
|
|
|
|
function sync(state, run) {
|
|
const running = run.status === "running";
|
|
const waiting = Math.max(0, queuedRuns() - (running ? 1 : 0));
|
|
state.stop.disabled = !running;
|
|
state.all.hidden = waiting < 1;
|
|
state.all.textContent = `STOP ALL · ${waiting}`;
|
|
state.all.title = `Stop this run and empty the queue (${waiting} waiting).`;
|
|
state.readout.textContent = STATE_TEXT[run.status] ?? "";
|
|
state.readout.classList.toggle("live", running);
|
|
// The finished picture replaces the blurry last step as soon as the run
|
|
// saves it, and stays after the run, until the next one starts.
|
|
if (run.url || run.result) state.overlay.show(run, { result: true });
|
|
else if (running || run.status === "idle") state.overlay.hide();
|
|
}
|
|
|
|
function buildPanel(node) {
|
|
if (node.__ausbossStepPreview) return node.__ausbossStepPreview;
|
|
ensureCss();
|
|
const abort = new AbortController();
|
|
const root = document.createElement("div");
|
|
root.className = "ausboss-steppv";
|
|
const bar = document.createElement("div");
|
|
bar.className = "ausboss-steppv-bar";
|
|
const stage = document.createElement("div");
|
|
stage.className = "ausboss-steppv-stage";
|
|
const hint = document.createElement("div");
|
|
hint.className = "ausboss-steps-hint";
|
|
hint.textContent = IDLE_HINT;
|
|
stage.append(hint);
|
|
root.append(bar, stage);
|
|
|
|
const stop = button("STOP", "Stop this run now. The next run in the queue still starts.", stopStepRun, abort.signal);
|
|
stop.classList.add("ausboss-steps-stop");
|
|
const all = button("STOP ALL", "Stop this run and empty the queue.", stopAllRuns, abort.signal);
|
|
all.classList.add("all");
|
|
all.hidden = true;
|
|
const readout = document.createElement("span");
|
|
readout.className = "ausboss-steppv-state";
|
|
bar.append(stop, all, readout);
|
|
|
|
const widget = node.addDOMWidget(WIDGET_NAME, "ausboss_step_preview", root, {
|
|
serialize: false,
|
|
hideOnZoom: false,
|
|
getMinHeight: () => PANEL_MIN_HEIGHT + WIDGET_FRAME,
|
|
});
|
|
keepDomWidgetWidthAuto(widget);
|
|
// Not saved with the workflow either: options.serialize only keeps it out
|
|
// of the prompt, and saved values come back by position.
|
|
widget.serialize = false;
|
|
fillNodeHeight(widget, {
|
|
minWidth: PANEL_MIN_WIDTH,
|
|
minHeight: () => PANEL_MIN_HEIGHT + WIDGET_FRAME,
|
|
minNodeSize: [PANEL_MIN_WIDTH, PANEL_MIN_HEIGHT + 40],
|
|
exactMinWidth: true,
|
|
});
|
|
|
|
const state = { node, root, stage, stop, all, readout, widget, abort };
|
|
state.overlay = mountStepOverlay(stage);
|
|
state.forget = watchSteps((run) => sync(state, run));
|
|
node.__ausbossStepPreview = state;
|
|
return state;
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "ausboss.step_preview",
|
|
beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (nodeData?.name !== NODE_CLASS) return;
|
|
chainCallback(nodeType.prototype, "onNodeCreated", function () {
|
|
buildPanel(this);
|
|
// A new node opens at a picture's size; onConfigure puts a saved
|
|
// workflow's own size back after this.
|
|
this.setSize?.([
|
|
Math.max(this.size?.[0] ?? 0, DEFAULT_SIZE[0]),
|
|
Math.max(this.size?.[1] ?? 0, DEFAULT_SIZE[1]),
|
|
]);
|
|
});
|
|
chainCallback(nodeType.prototype, "onRemoved", function () {
|
|
const state = this.__ausbossStepPreview;
|
|
if (!state) return;
|
|
state.forget?.();
|
|
state.abort.abort();
|
|
this.__ausbossStepPreview = null;
|
|
});
|
|
},
|
|
});
|