lollms-webui/endpoints/libraries/lollms_flow.js

306 lines
10 KiB
JavaScript
Raw Normal View History

2024-08-19 21:12:58 +00:00
// Lollms Flow
// A library for building workflows of execution
// By ParisNeo
class WorkflowNode {
constructor(id, name, inputs, outputs, operation, x = 0, y = 0) {
this.id = id;
this.name = name;
this.inputs = inputs;
this.outputs = outputs;
this.operation = operation;
this.inputConnections = {};
this.outputConnections = {};
this.x = x;
this.y = y;
}
connect(outputIndex, targetNode, inputIndex) {
if (!this.outputConnections[outputIndex]) {
this.outputConnections[outputIndex] = [];
}
this.outputConnections[outputIndex].push({ node: targetNode, input: inputIndex });
targetNode.inputConnections[inputIndex] = { node: this, output: outputIndex };
}
execute(inputs) {
return this.operation(inputs);
}
toJSON() {
return {
id: this.id,
name: this.name,
inputs: this.inputs,
outputs: this.outputs,
x: this.x,
y: this.y
};
}
static fromJSON(json, operation) {
return new WorkflowNode(json.id, json.name, json.inputs, json.outputs, operation, json.x, json.y);
}
}
class Workflow {
constructor() {
this.nodes = {};
this.nodeList = [];
}
addNode(node) {
this.nodes[node.id] = node;
this.nodeList.push(node);
}
connectNodes(sourceId, sourceOutput, targetId, targetInput) {
const sourceNode = this.nodes[sourceId];
const targetNode = this.nodes[targetId];
if (this.canConnect(sourceNode, sourceOutput, targetNode, targetInput)) {
sourceNode.connect(sourceOutput, targetNode, targetInput);
return true;
}
return false;
}
canConnect(sourceNode, sourceOutput, targetNode, targetInput) {
return sourceNode.outputs[sourceOutput].type === targetNode.inputs[targetInput].type;
}
execute() {
const executed = new Set();
const results = {};
const executeNode = (node) => {
if (executed.has(node.id)) return results[node.id];
const inputs = {};
for (let i = 0; i < node.inputs.length; i++) {
if (node.inputConnections[i]) {
const { node: sourceNode, output } = node.inputConnections[i];
inputs[node.inputs[i].name] = executeNode(sourceNode)[sourceNode.outputs[output].name];
}
}
results[node.id] = node.execute(inputs);
executed.add(node.id);
return results[node.id];
};
this.nodeList.forEach(node => {
if (Object.keys(node.inputConnections).length === 0) {
executeNode(node);
}
});
return results;
}
toJSON() {
return {
nodes: this.nodeList.map(node => node.toJSON()),
connections: this.nodeList.flatMap(node =>
Object.entries(node.outputConnections).flatMap(([outputIndex, connections]) =>
connections.map(conn => ({
sourceId: node.id,
sourceOutput: parseInt(outputIndex),
targetId: conn.node.id,
targetInput: conn.input
}))
)
)
};
}
static fromJSON(json, nodeOperations) {
const workflow = new Workflow();
json.nodes.forEach(nodeJson => {
const node = WorkflowNode.fromJSON(nodeJson, nodeOperations[nodeJson.name]);
workflow.addNode(node);
});
json.connections.forEach(conn => {
workflow.connectNodes(conn.sourceId, conn.sourceOutput, conn.targetId, conn.targetInput);
});
return workflow;
}
}
class WorkflowVisualizer {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.workflow = new Workflow();
this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
this.container.appendChild(this.svg);
this.nodeElements = {};
this.connectionElements = [];
this.draggedNode = null;
this.offsetX = 0;
this.offsetY = 0;
this.svg.addEventListener('mousedown', this.onMouseDown.bind(this));
this.svg.addEventListener('mousemove', this.onMouseMove.bind(this));
this.svg.addEventListener('mouseup', this.onMouseUp.bind(this));
}
addNode(node) {
this.workflow.addNode(node);
this.drawNode(node);
}
connectNodes(sourceId, sourceOutput, targetId, targetInput) {
if (this.workflow.connectNodes(sourceId, sourceOutput, targetId, targetInput)) {
this.drawConnection(sourceId, sourceOutput, targetId, targetInput);
} else {
console.error("Cannot connect incompatible types");
}
}
drawNode(node) {
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
g.setAttribute("transform", `translate(${node.x}, ${node.y})`);
g.setAttribute("data-id", node.id);
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
rect.setAttribute("width", "120");
rect.setAttribute("height", "60");
rect.setAttribute("fill", "lightblue");
rect.setAttribute("stroke", "black");
g.appendChild(rect);
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("x", "60");
text.setAttribute("y", "35");
text.setAttribute("text-anchor", "middle");
text.textContent = node.name;
g.appendChild(text);
// Draw input sockets
node.inputs.forEach((input, index) => {
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.setAttribute("cx", "0");
circle.setAttribute("cy", (index + 1) * 15);
circle.setAttribute("r", "5");
circle.setAttribute("fill", this.getColorForType(input.type));
g.appendChild(circle);
});
// Draw output sockets
node.outputs.forEach((output, index) => {
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.setAttribute("cx", "120");
circle.setAttribute("cy", (index + 1) * 15);
circle.setAttribute("r", "5");
circle.setAttribute("fill", this.getColorForType(output.type));
g.appendChild(circle);
});
this.svg.appendChild(g);
this.nodeElements[node.id] = g;
}
drawConnection(sourceId, sourceOutput, targetId, targetInput) {
const sourceNode = this.workflow.nodes[sourceId];
const targetNode = this.workflow.nodes[targetId];
const line = document.createElementNS("http://www.w3.org/2000/svg", "path");
const sourcePosX = sourceNode.x + 120;
const sourcePosY = sourceNode.y + (sourceOutput + 1) * 15;
const targetPosX = targetNode.x;
const targetPosY = targetNode.y + (targetInput + 1) * 15;
const midX = (sourcePosX + targetPosX) / 2;
const d = `M ${sourcePosX} ${sourcePosY} C ${midX} ${sourcePosY}, ${midX} ${targetPosY}, ${targetPosX} ${targetPosY}`;
line.setAttribute("d", d);
line.setAttribute("fill", "none");
line.setAttribute("stroke", "black");
this.svg.appendChild(line);
this.connectionElements.push({ line, sourceId, sourceOutput, targetId, targetInput });
}
updateConnections() {
this.connectionElements.forEach(conn => {
const sourceNode = this.workflow.nodes[conn.sourceId];
const targetNode = this.workflow.nodes[conn.targetId];
const sourcePosX = sourceNode.x + 120;
const sourcePosY = sourceNode.y + (conn.sourceOutput + 1) * 15;
const targetPosX = targetNode.x;
const targetPosY = targetNode.y + (conn.targetInput + 1) * 15;
const midX = (sourcePosX + targetPosX) / 2;
const d = `M ${sourcePosX} ${sourcePosY} C ${midX} ${sourcePosY}, ${midX} ${targetPosY}, ${targetPosX} ${targetPosY}`;
conn.line.setAttribute("d", d);
});
}
getColorForType(type) {
const colors = {
number: "blue",
string: "green",
boolean: "red",
object: "purple"
};
return colors[type] || "gray";
}
onMouseDown(event) {
const target = event.target.closest("g");
if (target) {
this.draggedNode = this.workflow.nodes[target.getAttribute("data-id")];
const rect = target.getBoundingClientRect();
this.offsetX = event.clientX - rect.left;
this.offsetY = event.clientY - rect.top;
}
}
onMouseMove(event) {
if (this.draggedNode) {
const rect = this.svg.getBoundingClientRect();
this.draggedNode.x = event.clientX - rect.left - this.offsetX;
this.draggedNode.y = event.clientY - rect.top - this.offsetY;
this.nodeElements[this.draggedNode.id].setAttribute("transform", `translate(${this.draggedNode.x}, ${this.draggedNode.y})`);
this.updateConnections();
}
}
onMouseUp() {
this.draggedNode = null;
}
execute() {
return this.workflow.execute();
}
saveToJSON() {
return JSON.stringify(this.workflow.toJSON());
}
loadFromJSON(json, nodeOperations) {
this.workflow = Workflow.fromJSON(JSON.parse(json), nodeOperations);
this.redraw();
}
saveToLocalStorage(key) {
localStorage.setItem(key, this.saveToJSON());
}
loadFromLocalStorage(key, nodeOperations) {
const json = localStorage.getItem(key);
if (json) {
this.loadFromJSON(json, nodeOperations);
}
}
redraw() {
this.svg.innerHTML = '';
this.nodeElements = {};
this.connectionElements = [];
this.workflow.nodeList.forEach(node => this.drawNode(node));
this.workflow.nodeList.forEach(node => {
Object.entries(node.outputConnections).forEach(([outputIndex, connections]) => {
connections.forEach(conn => {
this.drawConnection(node.id, parseInt(outputIndex), conn.node.id, conn.input);
});
});
});
}
}