221 lines
7.5 KiB
JavaScript
221 lines
7.5 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 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 || "").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 || {};
|
|
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;
|
|
};
|
|
},
|
|
});
|