(function () { // Global connection rule: only allow downstream -> upstream. // LiteGraph checks compatibility via `LiteGraph.isValidConnection(typeA, typeB)`. if (window.LiteGraph) { const prev = window.LiteGraph.isValidConnection; window.LiteGraph.isValidConnection = function (typeA, typeB) { if (typeA === "downstream" && typeB === "upstream") return true; if (typeA === "upstream" && typeB === "downstream") return false; if (typeA === "downstream" && typeB === "downstream") return false; if (typeA === "upstream" && typeB === "upstream") return false; return prev ? prev.call(this, typeA, typeB) : true; }; } const sidebar = document.getElementById("sidebar"); const hamburger = document.getElementById("hamburger"); const output = document.getElementById("outputText"); const btnCompile = document.getElementById("btnCompile"); const btnSave = document.getElementById("btnSave"); const canvasEl = document.getElementById("graphCanvas"); let lastSuccessfulYaml = null; function setSidebarOpen(open) { if (open) { sidebar.classList.remove("hidden"); sidebar.setAttribute("aria-hidden", "false"); } else { sidebar.classList.add("hidden"); sidebar.setAttribute("aria-hidden", "true"); } } function isSidebarOpen() { return !sidebar.classList.contains("hidden"); } hamburger.addEventListener("click", (e) => { e.stopPropagation(); setSidebarOpen(!isSidebarOpen()); }); // Close sidebar when clicking outside of it (and not on hamburger) document.addEventListener("click", (e) => { if (!isSidebarOpen()) return; const target = e.target; if (target === hamburger) return; if (sidebar.contains(target)) return; setSidebarOpen(false); }); // Prevent clicks inside sidebar from bubbling to document and closing it. sidebar.addEventListener("click", (e) => { e.stopPropagation(); }); function setOutput(text) { output.value = text; output.scrollTop = output.scrollHeight; } function downloadText(filename, text) { const blob = new Blob([text], { type: "text/yaml;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } // --- LiteGraph setup --- if (!window.LiteGraph || !window.LGraph || !window.LGraphCanvas) { setOutput("LiteGraph failed to load."); return; } const graph = new LGraph(); const graphCanvas = new LGraphCanvas(canvasEl, graph); graphCanvas.background_image = ""; function resizeCanvas() { const rect = canvasEl.getBoundingClientRect(); canvasEl.width = Math.floor(rect.width * window.devicePixelRatio); canvasEl.height = Math.floor(rect.height * window.devicePixelRatio); graphCanvas.resize(); } window.addEventListener("resize", resizeCanvas); resizeCanvas(); graph.start(); // Node definitions function defineEntityNode(typeName, title, fields, io) { function EntityNode() { this.title = title; this.size = [260, 140]; this.properties = {}; // Name first this.properties.name = ""; this.addWidget("text", "Name", this.properties.name, (v) => { this.properties.name = v; }); fields.forEach((f) => { this.properties[f.key] = ""; this.addWidget("text", f.label, this.properties[f.key], (v) => { this.properties[f.key] = v; }); }); if (io === "downstream") { this.addOutput("out", "downstream"); } else { this.addInput("in", "upstream"); } } EntityNode.title = title; // Enforce downstream -> upstream only. // Some LiteGraph builds pass 0/"*" for generic types. EntityNode.prototype.onConnectOutput = function (slot, type) { return type === "upstream" || type === 0 || type === "*"; }; LiteGraph.registerNodeType(typeName, EntityNode); } defineEntityNode( "downstream.sharepoint", "Sharepoint", [ { key: "tenant_id", label: "Tenant ID" }, { key: "graph_url", label: "Graph URL" }, { key: "api_key", label: "API key" }, { key: "files_to_exclude", label: "Files to exclude" }, ], "downstream" ); defineEntityNode( "downstream.confluence", "Confluence", [ { key: "space_url", label: "Space url" }, { key: "api_key", label: "API key" }, { key: "pages_to_exclude", label: "Pages to exclude" }, ], "downstream" ); defineEntityNode( "upstream.azure_ai_search", "Azure AI Search", [ { key: "tenant_url", label: "Tenant URL" }, { key: "api_key", label: "API key" }, { key: "index_name", label: "Index name" }, ], "upstream" ); defineEntityNode( "upstream.azure_vector_store", "Azure Vector Store", [ { key: "tenant_url", label: "Tenant URL" }, { key: "api_key", label: "API key" }, { key: "index_name", label: "Index name" }, ], "upstream" ); function spawnNode(typeName) { const node = LiteGraph.createNode(typeName); if (!node) { setOutput(`Failed to create node type: ${typeName}`); return; } node.pos = [graphCanvas.ds.offset[0] * -1 + 80, graphCanvas.ds.offset[1] * -1 + 80]; graph.add(node); graphCanvas.selectNode(node); } // Entity buttons: spawn node and close sidebar document.querySelectorAll(".entityBtn").forEach((btn) => { btn.addEventListener("click", () => { const entity = btn.getAttribute("data-entity"); spawnNode(entity); setSidebarOpen(false); }); }); async function doCompile() { setOutput("Compiling..."); lastSuccessfulYaml = null; const graphJson = graph.serialize(); const res = await fetch("/api/compile", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ graph: graphJson }), }); const data = await res.json().catch(() => null); if (!data) { setOutput("Compile failed: invalid server response"); return { ok: false }; } if (!data.ok) { setOutput(data.error || "Compile failed"); return { ok: false }; } lastSuccessfulYaml = data.yaml; setOutput(data.yaml); return { ok: true, yaml: data.yaml }; } btnCompile.addEventListener("click", () => { doCompile().catch((e) => setOutput(`Compile failed: ${e}`)); }); btnSave.addEventListener("click", () => { (async () => { if (!lastSuccessfulYaml) { const r = await doCompile(); if (!r.ok) return; } downloadText("graph.yaml", lastSuccessfulYaml); })().catch((e) => setOutput(`Save failed: ${e}`)); }); setOutput("Ready. Use hamburger to add entities from the sidebar."); })();