227 lines
8.0 KiB
JavaScript
227 lines
8.0 KiB
JavaScript
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;
|
|
};
|
|
},
|
|
});
|