Files

420 lines
15 KiB
JavaScript

import { app } from "/scripts/app.js";
import { applyPrimereButtonStyle, showToast } from "./frontend_helper.js";
const TARGET_NODE_NAME = "PrimereApiProcessor";
const SCHEMA_ENDPOINT = "/primere_uniapi_schema_read";
let schemaCache = null;
let schemaPromise = null;
let apiconfigChecked = false;
async function loadSchemas() {
if (schemaCache) {
return schemaCache;
}
if (!schemaPromise) {
schemaPromise = fetch(SCHEMA_ENDPOINT)
.then(async (response) => {
const payload = response.ok ? await response.json() : null;
if (!response.ok || !payload?.success) {
throw new Error(payload?.error || `Cannot load schema file (${response.status})`);
}
if (payload?.warning) {
console.warn("[Primere UniApi] Schema read warning:", payload.warning);
}
schemaCache = payload.registry || {};
return schemaCache;
})
.catch((error) => {
console.error("[Primere UniApi] Failed to load schema registry from PromptServer", error);
return {};
});
}
return schemaPromise;
}
function getWidget(node, widgetName) {
return node.widgets?.find((widget) => widget?.name === widgetName);
}
function markDynamicWidget(node, widgetName) {
const widget = getWidget(node, widgetName);
if (widget) {
widget.__primereUniApiDynamic = true;
}
return widget;
}
function ensureComboWidget(node, widgetName, values) {
const normalizedValues = Array.isArray(values) && values.length > 0
? values.map((value) => String(value))
: [`default_${widgetName}`];
let widget = getWidget(node, widgetName);
if (!widget) {
widget = node.addWidget("combo", widgetName, normalizedValues[0], () => {}, { values: normalizedValues });
}
widget.options = widget.options || {};
widget.options.values = normalizedValues;
if (!normalizedValues.includes(String(widget.value))) {
widget.value = normalizedValues[0];
}
return markDynamicWidget(node, widgetName);
}
function ensureBooleanWidget(node, widgetName, values) {
const normalizedValues = Array.isArray(values) && values.length > 0
? values.map((value) => Boolean(value))
: [false, true];
const defaultValue = normalizedValues[0];
let widget = getWidget(node, widgetName);
if (!widget || widget.type !== "toggle") {
removeWidgetByName(node, widgetName);
widget = node.addWidget("toggle", widgetName, defaultValue, () => {}, {
on: "ON",
off: "OFF",
label_on: "ON",
label_off: "OFF",
});
}
widget.options = widget.options || {};
widget.options.on = "ON";
widget.options.off = "OFF";
widget.options.label_on = "ON";
widget.options.label_off = "OFF";
if (!normalizedValues.includes(Boolean(widget.value))) {
widget.value = defaultValue;
}
return markDynamicWidget(node, widgetName);
}
function ensureNumberWidget(node, widgetName, valueType, defaultValue = null) {
const isInteger = valueType === "INT";
const effectiveDefault = defaultValue !== null ? defaultValue : (isInteger ? 1 : 1.0);
const expectedStep = isInteger ? 1 : 0.01;
const expectedPrecision = isInteger ? 0 : 4;
let widget = getWidget(node, widgetName);
if (!widget || widget.type !== "number") {
removeWidgetByName(node, widgetName);
widget = node.addWidget("number", widgetName, effectiveDefault, () => {}, {
step: expectedStep,
precision: expectedPrecision,
});
}
widget.options = widget.options || {};
widget.options.step = expectedStep;
widget.options.precision = expectedPrecision;
if (typeof widget.value !== "number" || Number.isNaN(widget.value)) {
widget.value = effectiveDefault;
} else if (isInteger) {
widget.value = Math.round(widget.value);
}
return markDynamicWidget(node, widgetName);
}
function ensureStringWidget(node, widgetName) {
let widget = getWidget(node, widgetName);
if (!widget || widget.type !== "text") {
removeWidgetByName(node, widgetName);
widget = node.addWidget("text", widgetName, "", () => {}, {});
}
if (widget.value == null) {
widget.value = "";
}
return markDynamicWidget(node, widgetName);
}
function detectParameterType(values) {
if (values === "INT" || values === "FLOAT" || values === "STRING") {
return values;
}
if (!Array.isArray(values) || values.length === 0) {
return "combo";
}
if (values.every((value) => typeof value === "boolean")) {
return "boolean";
}
return "combo";
}
function removeWidgetByName(node, widgetName) {
if (!node.widgets) {
return;
}
const widgetIndex = node.widgets.findIndex((widget) => widget?.name === widgetName);
if (widgetIndex >= 0) {
node.widgets.splice(widgetIndex, 1);
}
}
function readServiceSchema(schemaRegistry, provider, service) {
if (!schemaRegistry || typeof schemaRegistry !== "object") {
return null;
}
const providerEntry = schemaRegistry?.[provider];
if (!providerEntry || typeof providerEntry !== "object") {
return null;
}
return providerEntry?.[service] || null;
}
function listProviders(schemaRegistry) {
if (!schemaRegistry || typeof schemaRegistry !== "object") {
return [];
}
return Object.keys(schemaRegistry);
}
function listServices(schemaRegistry, provider) {
if (!schemaRegistry || typeof schemaRegistry !== "object") {
return [];
}
const providerEntry = schemaRegistry?.[provider];
if (!providerEntry || typeof providerEntry !== "object") {
return [];
}
return Object.keys(providerEntry);
}
function listServiceParameterNames(schemaRegistry, provider, service) {
const schema = readServiceSchema(schemaRegistry, provider, service);
const possible = schema?.possible_parameters;
if (!possible || typeof possible !== "object") {
return [];
}
return Object.keys(possible).filter((name) => name !== "prompt");
}
function updateServiceWidget(node, schemaRegistry) {
const providerWidget = getWidget(node, "api_provider");
const serviceWidget = getWidget(node, "api_service");
if (!providerWidget || !serviceWidget) {
return;
}
const providers = listProviders(schemaRegistry);
providerWidget.options = providerWidget.options || {};
providerWidget.options.values = providers;
if (!providers.includes(String(providerWidget.value))) {
providerWidget.value = providers[0] || providerWidget.value;
}
const services = listServices(schemaRegistry, providerWidget.value);
serviceWidget.options = serviceWidget.options || {};
serviceWidget.options.values = services;
if (!services.includes(String(serviceWidget.value))) {
serviceWidget.value = services[0] || serviceWidget.value;
}
}
function canonicalParameterKey(name) {
return String(name ?? "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
}
function getBlockedParameterNames(node) {
const names = node.__primereOptionalInputNames;
if (names instanceof Set) {
return new Set([...names].map((name) => canonicalParameterKey(name)));
}
return new Set();
}
function updateParameterWidgets(node, schemaRegistry) {
const providerWidget = getWidget(node, "api_provider");
const serviceWidget = getWidget(node, "api_service");
if (!providerWidget || !serviceWidget) {
return;
}
const provider = String(providerWidget.value);
const service = String(serviceWidget.value);
const schema = readServiceSchema(schemaRegistry, provider, service);
const possible = schema?.possible_parameters;
const constraints = schema?.parameter_constraints || {};
const activeParams = new Set(listServiceParameterNames(schemaRegistry, provider, service));
const blockedParams = getBlockedParameterNames(node);
for (const widget of [...(node.widgets || [])]) {
if (!widget || !widget.name || widget.__primereUniApiDynamic !== true) {
continue;
}
if (!activeParams.has(widget.name) || blockedParams.has(canonicalParameterKey(widget.name))) {
removeWidgetByName(node, widget.name);
}
}
if (possible && typeof possible === "object") {
for (const [paramName, values] of Object.entries(possible)) {
if (paramName === "prompt") {
continue;
}
if (blockedParams.has(canonicalParameterKey(paramName))) {
removeWidgetByName(node, paramName);
continue;
}
const paramType = detectParameterType(values);
if (paramType === "boolean") {
ensureBooleanWidget(node, paramName, values);
continue;
}
if (paramType === "INT" || paramType === "FLOAT") {
const constraint = constraints[paramName];
let defaultValue = null;
if (constraint) {
if (Array.isArray(constraint)) {
for (const rule of constraint) {
if (rule.def !== undefined) {
defaultValue = rule.def;
break;
}
}
} else if (constraint.def !== undefined) {
defaultValue = constraint.def;
}
}
ensureNumberWidget(node, paramName, paramType, defaultValue);
continue;
}
if (paramType === "STRING") {
ensureStringWidget(node, paramName);
continue;
}
ensureComboWidget(node, paramName, values);
}
}
node.setDirtyCanvas?.(true, true);
node.computeSize?.();
}
async function initializeUniApiNode(node) {
if (!(node.__primereOptionalInputNames instanceof Set)) {
node.__primereOptionalInputNames = new Set();
}
if (!node.__primereUniApiWidgetHooked) {
node.__primereUniApiWidgetHooked = true;
const refreshBtn = node.addWidget("button", "↺ Reload API Schema", null, async () => {
schemaCache = null;
schemaPromise = null;
let apiconfigMissing = false;
try {
const r = await fetch("/primere_apiconfig_check");
const data = r.ok ? await r.json() : null;
if (data && !data.exists) {
apiconfigMissing = true;
showToast("error", "API config file (json/apiconfig.json) not found. Create it with your API keys before using this node. See the manual for details.");
}
} catch (_) {}
try {
const freshRegistry = await loadSchemas();
if (!freshRegistry || Object.keys(freshRegistry).length === 0) {
showToast("error", "API Schema reload failed. PromptServer returned empty registry.");
return;
}
updateServiceWidget(node, freshRegistry);
updateParameterWidgets(node, freshRegistry);
if (!apiconfigMissing) {
const providerCount = Object.keys(freshRegistry).length;
const serviceCount = Object.values(freshRegistry).reduce((sum, p) => sum + Object.keys(p).length, 0);
showToast("success", `API Schema reloaded successfully. ${providerCount} provider(s), ${serviceCount} service(s) loaded.`);
}
} catch (error) {
showToast("error", `API Schema reload failed. ${error.message}`);
}
});
node.widgets.splice(node.widgets.indexOf(refreshBtn), 1);
node.widgets.unshift(refreshBtn);
applyPrimereButtonStyle(refreshBtn);
const onSaveResult = (event) => {
const detail = event.detail;
if (String(detail?.node_id) !== String(node.id)) return;
if (detail.status === "success") {
showToast("success", `Saved: ${detail.path}`);
} else {
showToast("error", `Save failed: ${detail.error}`);
}
};
app.api.addEventListener("primere.save_result", onSaveResult);
const originalOnRemoved = node.onRemoved;
node.onRemoved = function () {
originalOnRemoved?.call(this);
app.api.removeEventListener("primere.save_result", onSaveResult);
};
const originalOnWidgetChanged = node.onWidgetChanged;
node.onWidgetChanged = function (name, value, oldValue, widget) {
originalOnWidgetChanged?.call(this, name, value, oldValue, widget);
if (name !== "api_provider" && name !== "api_service") {
return;
}
if (name === "api_provider") {
updateServiceWidget(this, schemaCache || {});
}
updateParameterWidgets(this, schemaCache || {});
};
}
if (!apiconfigChecked) {
apiconfigChecked = true;
fetch("/primere_apiconfig_check")
.then((r) => r.ok ? r.json() : null)
.then((data) => {
if (data && !data.exists) {
showToast("error", "API config file (json/apiconfig.json) not found. Create it with your API keys before using this node. See the manual for details.");
}
})
.catch(() => {});
}
const schemaRegistry = await loadSchemas();
updateServiceWidget(node, schemaRegistry);
updateParameterWidgets(node, schemaRegistry);
}
app.registerExtension({
name: "Primere.UniApiDynamicWidgets",
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
if (nodeData.name !== TARGET_NODE_NAME) {
return;
}
const optionalInputNames = new Set(Object.keys(nodeData?.input?.optional || {}));
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated?.apply(this, []);
this.__primereOptionalInputNames = new Set(optionalInputNames);
initializeUniApiNode(this);
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (config) {
onConfigure?.apply(this, [config]);
this.__primereOptionalInputNames = new Set(optionalInputNames);
initializeUniApiNode(this);
};
},
});