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]}` : ""; const metadata = entry?.has_metadata ? " metadata" : ""; return `#${index}${id}${image}${shape}${metadata}`.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; }; }, });