Files
ComfyUI-Ethanfel-Prompt-Bui…/web/accumulator_preview.js
T

228 lines
7.8 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
const EXTENSION = "ethanfel.prompt_builder.accumulator_preview";
const NODE_NAME = "SxCPAccumulatorPreview";
const entryCache = new Map();
function widget(node, name) {
return node.widgets?.find((w) => w.name === name);
}
function hideWidget(w) {
if (!w) return;
if (w.origType === undefined) w.origType = w.type;
w.type = "hidden";
w.hidden = true;
w.computeSize = () => [0, -4];
}
function resizeNode(node) {
const size = node.computeSize?.();
if (size) node.setSize?.(size);
app.graph?.setDirtyCanvas(true, true);
}
function nodeKey(nodeOrId) {
return String(typeof nodeOrId === "object" ? nodeOrId?.id : nodeOrId);
}
function isAccumulatorPreviewNode(node) {
return node?.comfyClass === NODE_NAME || node?.type === NODE_NAME;
}
function getNodeById(id) {
return app.graph?.getNodeById?.(Number(id)) || app.graph?._nodes_by_id?.[id] || app.graph?._nodes_by_id?.[Number(id)];
}
function asArray(value) {
if (!value) return [];
return Array.isArray(value) ? value : [value];
}
function outputStatus(output) {
const status = output?.status;
if (Array.isArray(status)) return status[0] || "";
return status || "";
}
function outputStoreKey(output) {
const key = output?.store_key;
if (Array.isArray(key)) return key[0] || "";
return key || "";
}
function outputEntries(output) {
const entries = output?.entries;
if (!entries) return [];
if (Array.isArray(entries) && entries.length === 1 && Array.isArray(entries[0])) return entries[0];
return asArray(entries);
}
function entryLabel(entry) {
const index = entry?.index ?? "?";
const id = entry?.id ? ` ${entry.id}` : "";
const image = entry?.has_image ? " image" : " value";
const shape = Array.isArray(entry?.shape) && entry.shape.length >= 2 ? ` ${entry.shape[1]}x${entry.shape[0]}` : "";
return `#${index}${id}${image}${shape}`.trim();
}
function setStatus(node, status) {
if (!node._sxcpAccumulatorStatusWidget) return;
node._sxcpAccumulatorStatusWidget.value = status || "no accumulator data";
node.setDirtyCanvas?.(true, true);
}
function setEntries(node, entries, status = "") {
entries = asArray(entries);
entryCache.set(nodeKey(node), entries);
node._sxcpAccumulatorEntries = entries;
if (node._sxcpEntrySelectWidget) {
const labels = entries.map(entryLabel);
node._sxcpEntrySelectWidget.options.values = labels.length ? labels : ["no entries"];
if (!labels.includes(node._sxcpEntrySelectWidget.value)) {
node._sxcpEntrySelectWidget.value = labels[0] || "no entries";
}
}
setStatus(node, status || `${entries.length} entries`);
resizeNode(node);
}
function selectedEntry(node) {
const entries = entryCache.get(nodeKey(node)) || node._sxcpAccumulatorEntries || [];
const selected = widget(node, "selected_entry")?.value || node._sxcpEntrySelectWidget?.value || "";
const labels = entries.map(entryLabel);
const index = labels.indexOf(selected);
return index >= 0 ? entries[index] : entries[0];
}
function storeKey(node) {
return String(widget(node, "store_key")?.value || node._sxcpResolvedStoreKey || "").trim();
}
async function postJson(path, payload) {
const response = await api.fetchApi(path, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(payload),
});
const data = await response.json();
if (!response.ok) throw new Error(data?.error || response.statusText);
return data;
}
async function refreshEntries(node) {
const key = storeKey(node);
if (!key) {
alert("Set the same explicit store_key on the Accumulator and Accumulator Preview first.");
return;
}
try {
const data = await postJson("/sxcp/accumulator/list", {store_key: key});
setEntries(node, data.entries || [], data.status || "");
} catch (err) {
console.error(`[${EXTENSION}] refresh failed`, err);
alert(`Refresh failed: ${err}`);
}
}
async function deleteSelected(node) {
const key = storeKey(node);
if (!key) {
alert("Set the same explicit store_key on the Accumulator and Accumulator Preview first.");
return;
}
const entry = selectedEntry(node);
if (!entry) {
alert("No accumulator entry selected.");
return;
}
const label = entryLabel(entry);
if (!confirm(`Delete accumulator entry ${label}?`)) return;
try {
const data = await postJson("/sxcp/accumulator/delete", {
store_key: key,
entry_id: entry.id || "",
index: entry.id ? 0 : entry.index,
clear: false,
});
setEntries(node, data.entries || [], `${data.status || ""}; deleted=${data.removed || 0}; rerun preview to refresh images`);
} catch (err) {
console.error(`[${EXTENSION}] delete failed`, err);
alert(`Delete failed: ${err}`);
}
}
async function clearStore(node) {
const key = storeKey(node);
if (!key) {
alert("Set the same explicit store_key on the Accumulator and Accumulator Preview first.");
return;
}
if (!confirm(`Clear all entries from accumulator "${key}"?`)) return;
try {
const data = await postJson("/sxcp/accumulator/delete", {store_key: key, clear: true});
setEntries(node, data.entries || [], `${data.status || ""}; cleared=${data.removed || 0}; rerun preview to refresh images`);
} catch (err) {
console.error(`[${EXTENSION}] clear failed`, err);
alert(`Clear failed: ${err}`);
}
}
function setupNode(node) {
hideWidget(widget(node, "delete_action"));
hideWidget(widget(node, "delete_entry_id"));
hideWidget(widget(node, "delete_index"));
if (!node._sxcpEntrySelectWidget) {
node._sxcpEntrySelectWidget = node.addWidget("combo", "selected_entry", "no entries", () => {}, {values: ["no entries"]});
node._sxcpEntrySelectWidget.serialize = false;
}
if (!node._sxcpAccumulatorStatusWidget) {
node._sxcpAccumulatorStatusWidget = node.addWidget("text", "accumulator_status", "no accumulator data", () => {});
node._sxcpAccumulatorStatusWidget.serialize = false;
}
if (!node._sxcpDeleteSelectedButton) {
node._sxcpDeleteSelectedButton = node.addWidget("button", "Delete Selected Entry", null, () => deleteSelected(node));
}
if (!node._sxcpClearButton) {
node._sxcpClearButton = node.addWidget("button", "Clear Accumulator", null, () => clearStore(node));
}
if (!node._sxcpRefreshButton) {
node._sxcpRefreshButton = node.addWidget("button", "Refresh Entry List", null, () => refreshEntries(node));
}
resizeNode(node);
}
app.registerExtension({
name: EXTENSION,
async setup() {
api.addEventListener("executed", ({detail}) => {
const node = getNodeById(detail?.node);
if (!isAccumulatorPreviewNode(node)) return;
const output = detail?.output || {};
node._sxcpResolvedStoreKey = outputStoreKey(output);
setEntries(node, outputEntries(output), outputStatus(output));
});
},
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== NODE_NAME) return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
setupNode(this);
return result;
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
const result = onConfigure?.apply(this, arguments);
queueMicrotask(() => setupNode(this));
return result;
};
},
});