feat: first functioning code
This commit is contained in:
241
static/app.js
Normal file
241
static/app.js
Normal file
@@ -0,0 +1,241 @@
|
||||
(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.");
|
||||
})();
|
||||
Reference in New Issue
Block a user