197 lines
6.8 KiB
JavaScript
197 lines
6.8 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { api } from "../../scripts/api.js";
|
|
|
|
const cssUrl = new URL("./modelverse_secrets.css", import.meta.url);
|
|
if (!document.querySelector(`link[href="${cssUrl}"]`)) {
|
|
document.head.appendChild(Object.assign(document.createElement("link"), { rel: "stylesheet", href: cssUrl }));
|
|
}
|
|
|
|
const NODE_NAME = "UCloud ModelVerse Secret Client";
|
|
const WIDGET_NAME = "secret";
|
|
const ENDPOINT = "/modelverse-secrets";
|
|
|
|
let cachedSecretNames = [];
|
|
|
|
function el(tag, props = {}, ...children) {
|
|
const node = Object.assign(document.createElement(tag), props);
|
|
node.append(...children);
|
|
return node;
|
|
}
|
|
|
|
async function fetchSecrets() {
|
|
const response = await api.fetchApi(ENDPOINT, { method: "GET" });
|
|
return await response.json();
|
|
}
|
|
|
|
async function refreshSecretNamesCache() {
|
|
const secrets = await fetchSecrets();
|
|
cachedSecretNames = Object.keys(secrets);
|
|
return cachedSecretNames;
|
|
}
|
|
|
|
async function refreshSecretDropdowns(preferredValue) {
|
|
const names = await refreshSecretNamesCache();
|
|
for (const node of app.graph._nodes ?? []) {
|
|
if (node.comfyClass !== NODE_NAME && node.type !== NODE_NAME) {
|
|
continue;
|
|
}
|
|
const widget = node.widgets?.find((item) => item.name === WIDGET_NAME);
|
|
if (!widget) {
|
|
continue;
|
|
}
|
|
widget.options.values = names.slice();
|
|
if (preferredValue && names.includes(preferredValue)) {
|
|
widget.value = preferredValue;
|
|
} else if (names.length && !names.includes(widget.value)) {
|
|
widget.value = names[0];
|
|
} else if (!names.length) {
|
|
widget.value = "";
|
|
}
|
|
if (typeof widget.callback === "function") {
|
|
try { widget.callback(widget.value); } catch (e) { /* noop */ }
|
|
}
|
|
node.setDirtyCanvas?.(true, true);
|
|
}
|
|
app.graph?.setDirtyCanvas?.(true, true);
|
|
}
|
|
|
|
function buildSecretRow(name = "", value = "", isNew = false, onSaved) {
|
|
let originalName = name;
|
|
const nameInput = el("input", { className: "mv-secret-input", placeholder: "Secret name", value: name });
|
|
const valueInput = el("input", { className: "mv-secret-input", placeholder: "API key", value, type: "password", autocomplete: "off" });
|
|
|
|
const saveButton = el("button", { className: "mv-secret-button mv-secret-save", textContent: isNew ? "Add" : "Save" });
|
|
const deleteButton = el("button", { className: "mv-secret-button mv-secret-delete", textContent: "Delete" });
|
|
const row = el("div", { className: "mv-secret-row" }, nameInput, valueInput, saveButton, deleteButton);
|
|
|
|
saveButton.onclick = async () => {
|
|
const key = nameInput.value.trim();
|
|
if (!key) {
|
|
nameInput.focus();
|
|
return;
|
|
}
|
|
|
|
const response = await api.fetchApi(ENDPOINT, {
|
|
method: "POST",
|
|
body: JSON.stringify({ key, value: valueInput.value }),
|
|
});
|
|
if (!response.ok) {
|
|
return;
|
|
}
|
|
|
|
if (!isNew && originalName && originalName !== key) {
|
|
await api.fetchApi(`${ENDPOINT}/${encodeURIComponent(originalName)}`, { method: "DELETE" });
|
|
}
|
|
|
|
originalName = key;
|
|
await refreshSecretDropdowns(key);
|
|
|
|
if (typeof onSaved === "function") {
|
|
onSaved();
|
|
}
|
|
|
|
if (isNew) {
|
|
row.replaceWith(buildSecretRow(key, valueInput.value, false, onSaved));
|
|
return;
|
|
}
|
|
|
|
saveButton.textContent = "Saved";
|
|
setTimeout(() => {
|
|
saveButton.textContent = "Save";
|
|
}, 1200);
|
|
};
|
|
|
|
deleteButton.onclick = async () => {
|
|
if (!originalName) {
|
|
row.remove();
|
|
return;
|
|
}
|
|
const response = await api.fetchApi(`${ENDPOINT}/${encodeURIComponent(originalName)}`, { method: "DELETE" });
|
|
if (response.ok) {
|
|
row.remove();
|
|
await refreshSecretDropdowns();
|
|
}
|
|
};
|
|
|
|
return row;
|
|
}
|
|
|
|
function createSecretsModal(secrets) {
|
|
const saveNotice = el("div", { className: "mv-secret-save-notice" });
|
|
const showSaveNotice = () => {
|
|
saveNotice.textContent = "Once added, there is a 10-second delay before the item shows up in the dropdown.";
|
|
saveNotice.classList.remove("mv-secret-save-notice-fade");
|
|
void saveNotice.offsetWidth;
|
|
saveNotice.classList.add("mv-secret-save-notice-visible");
|
|
clearTimeout(saveNotice._hideTimer);
|
|
saveNotice._hideTimer = setTimeout(() => {
|
|
saveNotice.classList.add("mv-secret-save-notice-fade");
|
|
}, 10000);
|
|
};
|
|
const rows = el(
|
|
"div",
|
|
{ className: "mv-secret-rows" },
|
|
...Object.entries(secrets).map(([name, value]) => buildSecretRow(name, value, false, showSaveNotice)),
|
|
);
|
|
const addButton = el("button", {
|
|
className: "mv-secret-add",
|
|
textContent: "+ Add Secret",
|
|
onclick: () => {
|
|
const row = buildSecretRow("", "", true, showSaveNotice);
|
|
rows.appendChild(row);
|
|
row.querySelector("input")?.focus();
|
|
},
|
|
});
|
|
const closeButton = el("button", { className: "mv-secret-close", textContent: "x" });
|
|
|
|
const overlay = el("div", { className: "mv-secret-overlay" },
|
|
el("div", { className: "mv-secret-dialog" },
|
|
el("div", { className: "mv-secret-header" },
|
|
el("h3", { className: "mv-secret-title", textContent: "Modelverse Secrets" }),
|
|
closeButton,
|
|
),
|
|
rows,
|
|
addButton,
|
|
saveNotice,
|
|
),
|
|
);
|
|
|
|
closeButton.onclick = () => overlay.remove();
|
|
overlay.addEventListener("click", (event) => {
|
|
if (event.target === overlay) {
|
|
overlay.remove();
|
|
}
|
|
});
|
|
|
|
return overlay;
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "compshare.modelverse_secrets",
|
|
async nodeCreated(node) {
|
|
if (node.comfyClass !== NODE_NAME && node.type !== NODE_NAME) {
|
|
return;
|
|
}
|
|
|
|
const names = await refreshSecretNamesCache();
|
|
const index = node.widgets?.findIndex((item) => item.name === WIDGET_NAME) ?? -1;
|
|
const savedValue = index >= 0 ? node.widgets[index].value : undefined;
|
|
if (index >= 0) {
|
|
node.widgets.splice(index, 1);
|
|
}
|
|
|
|
const defaultValue = savedValue && names.includes(savedValue) ? savedValue : (names[0] ?? "");
|
|
const comboOptions = {
|
|
get values() {
|
|
return cachedSecretNames.slice();
|
|
},
|
|
set values(v) { /* ignore external writes; cache is the source of truth */ },
|
|
};
|
|
node.addWidget("combo", WIDGET_NAME, defaultValue, () => {}, comboOptions);
|
|
node.addWidget("button", "Edit Secrets", null, async () => {
|
|
await refreshSecretNamesCache();
|
|
document.body.appendChild(createSecretsModal(await fetchSecrets()));
|
|
});
|
|
},
|
|
});
|