214 lines
7.5 KiB
JavaScript
214 lines
7.5 KiB
JavaScript
import { app } from "../../../scripts/app.js";
|
|
|
|
function hideInternalWidgets(node) {
|
|
["existing_project", "project_id"].forEach((name) => {
|
|
const w = node.widgets?.find((w) => w.name === name);
|
|
if (w) {
|
|
w.hidden = true;
|
|
w.computeSize = () => [0, -4];
|
|
}
|
|
});
|
|
}
|
|
|
|
function setNameDisabled(widget, disabled) {
|
|
if (!widget) return;
|
|
const el = widget.inputEl || widget.textareaEl || widget.element;
|
|
widget.readOnly = !!disabled;
|
|
if (el) {
|
|
el.readOnly = !!disabled;
|
|
el.disabled = !!disabled;
|
|
el.tabIndex = disabled ? -1 : 0;
|
|
const st = el.style;
|
|
if (st) {
|
|
st.pointerEvents = disabled ? "none" : "auto";
|
|
st.opacity = disabled ? "0.7" : "1";
|
|
st.cursor = disabled ? "not-allowed" : "";
|
|
}
|
|
}
|
|
widget.disabled = !!disabled;
|
|
}
|
|
|
|
function sanitizeFileName(name) {
|
|
return (name || "").replace(/[^a-zA-Z0-9._-]/g, "");
|
|
}
|
|
|
|
async function fetchProjectFiles() {
|
|
const res = await fetch("/vantage/projects", { credentials: "same-origin" });
|
|
console.log("[VantageJS] GET /vantage/projects ->", res.status);
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const data = await res.json();
|
|
console.log("[VantageJS] list files:", data);
|
|
const files = Array.isArray(data.files) ? data.files.filter(Boolean) : [];
|
|
return ["none", ...files];
|
|
}
|
|
|
|
// Unified hook that wires node.onExecuted and socket "executed"
|
|
function hookAfterExec(node, handler) {
|
|
const orig = node.onExecuted?.bind(node);
|
|
node.onExecuted = function (output) {
|
|
console.log("[VantageJS] onExecuted payload:", output);
|
|
try {
|
|
handler(output);
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
return orig ? orig(output) : undefined;
|
|
};
|
|
|
|
const s = app?.socket;
|
|
if (s && !node.__vp_exec_hooked) {
|
|
node.__vp_exec_hooked = true;
|
|
s.on("executed", (msg) => {
|
|
// Docs show executed contains node id and output = ui payload when ui is returned by node. [web:3]
|
|
const nid = msg?.node_id ?? msg?.node;
|
|
if (nid !== node.id) return;
|
|
console.log("[VantageJS] socket executed:", msg);
|
|
try {
|
|
handler(msg?.output ?? msg);
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "VantageProjectNode",
|
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (nodeData.name !== "VantageProject") return;
|
|
|
|
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
const r = origOnNodeCreated ? origOnNodeCreated.apply(this, arguments) : undefined;
|
|
|
|
const getW = (n) => this.widgets?.find((w) => w.name === n);
|
|
const projectListW = getW("project_list");
|
|
const projectNameW = getW("project_name");
|
|
const promptW = getW("positive_text");
|
|
const existingW = getW("existing_project");
|
|
const idW = getW("project_id");
|
|
const startPromptW = getW("start_prompt");
|
|
|
|
hideInternalWidgets(this);
|
|
|
|
// Refresh dropdown and optionally select a file
|
|
this.refreshProjectList = async (preferredFile) => {
|
|
try {
|
|
const items = await fetchProjectFiles();
|
|
const ctrl = projectListW;
|
|
if (!ctrl) return;
|
|
|
|
// Support legacy and modern combo widgets
|
|
if (ctrl.options) ctrl.options.values = items;
|
|
ctrl.values = items;
|
|
|
|
// Selection logic
|
|
let nextVal = ctrl.value;
|
|
if (preferredFile && items.includes(preferredFile)) nextVal = preferredFile;
|
|
else if (!items.includes(nextVal)) nextVal = items[0] || "none";
|
|
|
|
const changed = nextVal !== ctrl.value;
|
|
ctrl.value = nextVal;
|
|
|
|
if (typeof ctrl.callback === "function") ctrl.callback(nextVal, this, "project_list");
|
|
this.onWidgetChanged?.(ctrl, nextVal, "project_list");
|
|
|
|
this.setDirtyCanvas(true, true);
|
|
requestAnimationFrame(() => this.setDirtyCanvas(true, true));
|
|
} catch (e) {
|
|
console.warn("[VantageJS] refresh failed:", e);
|
|
}
|
|
};
|
|
|
|
// Optional partial load helper
|
|
const doPartialLoad = async () => {
|
|
const fileName = projectListW?.value;
|
|
if (!fileName || fileName === "none") return;
|
|
const safe = sanitizeFileName(fileName);
|
|
try {
|
|
const res = await fetch(`/vantage/preview?file=${encodeURIComponent(safe)}`, {
|
|
credentials: "same-origin",
|
|
});
|
|
console.log("[VantageJS] preview", safe, "->", res.status);
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const data = await res.json();
|
|
|
|
if (idW) idW.value = data.id || "";
|
|
if (projectNameW) projectNameW.value = data.name || safe.replace(/\.json$/i, "");
|
|
const uiPrompt = Array.isArray(data.prompt_lines)
|
|
? data.prompt_lines.map((x) => (x == null ? "" : String(x))).join("\n")
|
|
: data.prompt || "";
|
|
if (promptW) promptW.value = uiPrompt;
|
|
if (existingW) existingW.value = !!data.existing;
|
|
if (startPromptW) startPromptW.value = Number.isFinite(data.start_prompt) ? data.start_prompt : 0;
|
|
|
|
setNameDisabled(projectNameW, !!data.existing);
|
|
this.graph?.setDirtyCanvas?.(true, true);
|
|
} catch (e) {
|
|
console.warn("[VantageJS] preview failed:", e);
|
|
}
|
|
};
|
|
|
|
// Buttons
|
|
const addBtn = (label, fn) => {
|
|
const b = this.addWidget("button", label, null, () => fn());
|
|
b.serialize = false;
|
|
return b;
|
|
};
|
|
addBtn("Load Project", () => doPartialLoad());
|
|
addBtn("New Project", () => {
|
|
if (existingW) existingW.value = false;
|
|
if (idW) idW.value = "";
|
|
if (projectNameW) projectNameW.value = "";
|
|
if (promptW) promptW.value = "";
|
|
if (startPromptW) startPromptW.value = 0;
|
|
setNameDisabled(projectNameW, false);
|
|
this.graph?.setDirtyCanvas?.(true, true);
|
|
});
|
|
addBtn("Refresh List", () => this.refreshProjectList(projectListW?.value));
|
|
|
|
// Executed -> bind state (refresh list, set id, set existing true)
|
|
const postBind = (file, pid) => {
|
|
console.log("[VantageJS] postBind file=", file, "pid=", pid);
|
|
if (!file) return;
|
|
|
|
if (existingW) existingW.value = true;
|
|
if (idW && pid) idW.value = pid;
|
|
|
|
this.refreshProjectList?.(file);
|
|
setNameDisabled(projectNameW, true);
|
|
|
|
this.graph?.setDirtyCanvas?.(true, true);
|
|
requestAnimationFrame(() => this.graph?.setDirtyCanvas?.(true, true));
|
|
};
|
|
|
|
// Robust payload extraction:
|
|
// - Prefer UI payload from executed (msg.output object with id/file) per docs. [web:3]
|
|
// - Fallback: plain object typed output with id/file
|
|
// - Fallback: string or tuple[string] JSON
|
|
hookAfterExec(this, (output) => {
|
|
let obj = null;
|
|
|
|
// Prefer grouped UI: { project: [ {...} ] }
|
|
if (output && output.project && Array.isArray(output.project) && output.project.length) {
|
|
obj = output.project[0];
|
|
} else if (output && typeof output === "object" && (output.id || output.file)) {
|
|
obj = output;
|
|
} else if (output && output.ui && typeof output.ui === "object") {
|
|
const u = output.ui;
|
|
if (u.project && Array.isArray(u.project) && u.project.length) obj = u.project[0];
|
|
} else if (Array.isArray(output) && output.length && typeof output[0] === "string") {
|
|
try { obj = JSON.parse(output[0]); } catch {}
|
|
} else if (typeof output === "string") {
|
|
try { obj = JSON.parse(output); } catch {}
|
|
}
|
|
|
|
if (!obj) { console.warn("[VantageJS] no usable executed payload"); return; }
|
|
postBind(obj.file || null, obj.id || null);
|
|
});
|
|
|
|
return r;
|
|
};
|
|
},
|
|
});
|