Files
2026-08-26 16:47:07 +02:00

131 lines
4.9 KiB
JavaScript

// APNext H3 - make a connected socket visibly win over the widgets it replaces
//
// Some optional inputs on the H3 nodes override widgets on the same node:
//
// llm (APNext H3 LLM Backend) -> `model`, `draft_model`
// The backend's model is the one that writes; the dropdown kept
// showing "sonnet", which read as "still using Claude Code".
// cut_plan (APNext H3 Cut Plan) -> `segment_mode`, `max_segment_seconds`,
// `min_segment_seconds`. The plan's scenes are used verbatim.
//
// While such a socket is linked this greys the overridden widgets out and
// relabels them with what is actually in charge (the backend's model name,
// or "cut plan"), and restores them when the link goes away.
import { app } from "../../../scripts/app.js";
const BACKEND_TYPE = "H3LLMBackend";
// input name -> { type: socket type to match, widgets, tag(sourceNode) -> label }
const RULES = {
llm: {
type: "APNEXT_LLM",
widgets: ["model", "draft_model"],
tag: (source, widgetName) => (widgetName === "draft_model" ? "llm backend" : backendModel(source) || "llm backend"),
},
cut_plan: {
type: "STRING",
widgets: ["segment_mode", "max_segment_seconds", "min_segment_seconds"],
tag: () => "cut plan",
},
};
function backendModel(node) {
// Mirrors H3LLMBackend.build(): `model` unless it is the custom entry, then
// `model_name`; a bare Ollama tag is understood as ollama:<tag>.
const w = (name) => node?.widgets?.find((x) => x.name === name)?.value;
let chosen = String(w("model") ?? "").trim();
if (!chosen || chosen.startsWith("custom")) chosen = String(w("model_name") ?? "").trim();
if (chosen && !chosen.includes(":")) chosen = `ollama:${chosen}`;
return chosen;
}
function linkedSource(node, inputName) {
const input = node.inputs?.find((i) => i.name === inputName);
if (input?.link == null) return null;
const graph = node.graph ?? app.graph;
const link = graph.links?.[input.link];
return link ? graph.getNodeById(link.origin_id) : null;
}
function syncOverride(node) {
if (!node?.widgets) return;
for (const [inputName, rule] of Object.entries(RULES)) {
if (!node.inputs?.some((i) => i.name === inputName)) continue;
const source = linkedSource(node, inputName);
for (const name of rule.widgets) {
const widget = node.widgets.find((w) => w.name === name);
if (!widget) continue;
if (widget.__h3Label === undefined) widget.__h3Label = widget.label;
if (source) {
widget.label = `${name} → ${rule.tag(source, name)}`;
widget.disabled = true;
} else {
widget.label = widget.__h3Label;
widget.disabled = false;
}
}
}
node.setDirtyCanvas?.(true, true);
}
function syncAllConsumers(sourceNode) {
const graph = sourceNode.graph ?? app.graph;
for (const out of sourceNode.outputs ?? []) {
for (const linkId of out.links ?? []) {
const link = graph.links?.[linkId];
const target = link && graph.getNodeById(link.target_id);
if (target) syncOverride(target);
}
}
}
app.registerExtension({
name: "apnext.h3.llm_socket",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name === BACKEND_TYPE) {
// A backend's model pick changed: refresh the label on every node it feeds.
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated?.apply(this, arguments);
for (const widget of this.widgets ?? []) {
if (widget.name !== "model" && widget.name !== "model_name") continue;
const cb = widget.callback;
widget.callback = (...args) => {
const out = cb?.apply(widget, args);
setTimeout(() => syncAllConsumers(this), 0);
return out;
};
}
return r;
};
return;
}
const optional = nodeData?.input?.optional ?? {};
const watched = Object.entries(RULES).filter(([name, rule]) => optional[name]?.[0] === rule.type).map(([name]) => name);
if (!watched.length) return;
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
nodeType.prototype.onConnectionsChange = function (type, index, connected, linkInfo, ioSlot) {
const result = onConnectionsChange?.apply(this, arguments);
if (watched.includes(ioSlot?.name || "")) setTimeout(() => syncOverride(this), 0);
return result;
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
const result = onConfigure?.apply(this, arguments);
setTimeout(() => syncOverride(this), 0);
return result;
};
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
setTimeout(() => syncOverride(this), 0);
return result;
};
},
});