import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; const EXTENSION = "ethanfel.prompt_builder.profile_buttons"; const profileCache = 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 isProfileSaveNode(node) { return node?.comfyClass === "SxCPCharacterProfileSave" || node?.type === "SxCPCharacterProfileSave"; } function isProfileLoadNode(node) { return node?.comfyClass === "SxCPCharacterProfileLoad" || node?.type === "SxCPCharacterProfileLoad"; } function firstOutput(output, key) { const value = output?.[key]; return Array.isArray(value) ? value[0] : value || ""; } function cacheStatus(cache) { if (!cache?.profile_json) return "no cached profile"; const name = cache.profile_name || "unnamed"; const descriptor = cache.descriptor ? ` - ${cache.descriptor}` : ""; const text = `cached: ${name}${descriptor}`; return text.length > 120 ? `${text.slice(0, 117)}...` : text; } function updateCacheWidget(node) { const cache = profileCache.get(nodeKey(node)) || node._sxcpProfileCache; if (node._sxcpCachedProfileWidget) node._sxcpCachedProfileWidget.value = cacheStatus(cache); node.setDirtyCanvas?.(true, true); } function setProfileCache(node, cache) { if (!node || !cache?.profile_json) return; node._sxcpProfileCache = cache; profileCache.set(nodeKey(node), cache); updateCacheWidget(node); } function getNodeById(id) { return app.graph?.getNodeById?.(Number(id)) || app.graph?._nodes_by_id?.[id] || app.graph?._nodes_by_id?.[Number(id)]; } function addProfileChoice(profileName) { if (!profileName) return; for (const node of app.graph?._nodes || []) { if (!isProfileLoadNode(node)) continue; const profileWidget = widget(node, "profile_name"); const values = profileWidget?.options?.values; if (Array.isArray(values) && !values.includes(profileName)) values.push(profileName); } } async function saveCachedProfile(node) { const cache = profileCache.get(nodeKey(node)) || node._sxcpProfileCache; if (!cache?.profile_json) { alert("No cached profile yet. Run the workflow once with this save node connected, then press Save Profile Now."); return; } const profileName = (widget(node, "profile_name")?.value || cache.profile_name || "").trim(); if (!profileName) { alert("Fill profile_name before saving."); return; } try { const response = await api.fetchApi("/sxcp/profile/save_cached", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ profile_name: profileName, profile_json: cache.profile_json, }), }); const data = await response.json(); if (!response.ok) throw new Error(data?.error || response.statusText); setProfileCache(node, { profile_json: data.profile_json || cache.profile_json, descriptor: data.descriptor || cache.descriptor || "", profile_name: data.profile_name || profileName, saved_path: data.saved_path || "", status: data.status || "saved", }); addProfileChoice(data.profile_name || profileName); alert(`Saved profile "${data.profile_name || profileName}".`); } catch (err) { console.error(`[${EXTENSION}] save cached profile failed`, err); alert(`Save failed: ${err}`); } } async function queueOnce(node, triggerName) { const trigger = widget(node, triggerName); if (!trigger) { alert(`Missing trigger widget: ${triggerName}`); return; } trigger.value = true; node.setDirtyCanvas?.(true, true); try { try { await app.queuePrompt(0, 1); } catch (_err) { await app.queuePrompt(0); } } catch (err) { console.error(`[${EXTENSION}] queue failed`, err); alert(`Queue failed: ${err}`); } finally { trigger.value = false; node.setDirtyCanvas?.(true, true); } } function setupSaveNode(node) { hideWidget(widget(node, "save_now")); if (!node._sxcpCachedProfileWidget) { node._sxcpCachedProfileWidget = node.addWidget("text", "cached_profile", "no cached profile", () => {}); node._sxcpCachedProfileWidget.serialize = false; } if (!node._sxcpSaveButton) { node._sxcpSaveButton = node.addWidget("button", "Save Profile Now", null, () => saveCachedProfile(node)); } updateCacheWidget(node); resizeNode(node); } function setupLoadNode(node) { hideWidget(widget(node, "delete_now")); hideWidget(widget(node, "rename_now")); if (!node._sxcpDeleteButton) { node._sxcpDeleteButton = node.addWidget("button", "Delete Selected Profile", null, () => { const profile = widget(node, "profile_name")?.value || ""; if (!profile || profile === "manual") { alert("Select a saved profile before deleting."); return; } if (!confirm(`Delete saved profile "${profile}"?`)) return; queueOnce(node, "delete_now"); }); } if (!node._sxcpRenameButton) { node._sxcpRenameButton = node.addWidget("button", "Rename Selected Profile", null, () => { const profile = widget(node, "profile_name")?.value || ""; const target = widget(node, "rename_to")?.value || ""; if (!profile || profile === "manual") { alert("Select a saved profile before renaming."); return; } if (!target.trim()) { alert("Fill rename_to before renaming."); return; } if (!confirm(`Rename saved profile "${profile}" to "${target}"?`)) return; queueOnce(node, "rename_now"); }); } resizeNode(node); } app.registerExtension({ name: EXTENSION, async setup() { api.addEventListener("executed", ({detail}) => { const node = getNodeById(detail?.node); if (!isProfileSaveNode(node)) return; const output = detail?.output || {}; const profileJson = firstOutput(output, "profile_json"); if (!profileJson) return; setProfileCache(node, { profile_json: profileJson, descriptor: firstOutput(output, "descriptor"), profile_name: firstOutput(output, "profile_name"), saved_path: firstOutput(output, "saved_path"), status: firstOutput(output, "status"), }); }); }, async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== "SxCPCharacterProfileSave" && nodeData.name !== "SxCPCharacterProfileLoad") return; const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { const result = onNodeCreated?.apply(this, arguments); if (nodeData.name === "SxCPCharacterProfileSave") setupSaveNode(this); if (nodeData.name === "SxCPCharacterProfileLoad") setupLoadNode(this); return result; }; const onConfigure = nodeType.prototype.onConfigure; nodeType.prototype.onConfigure = function () { const result = onConfigure?.apply(this, arguments); queueMicrotask(() => { if (nodeData.name === "SxCPCharacterProfileSave") setupSaveNode(this); if (nodeData.name === "SxCPCharacterProfileLoad") setupLoadNode(this); }); return result; }; }, });