mirror of
https://github.com/ParisNeo/lollms-webui.git
synced 2024-12-20 21:03:07 +00:00
306 lines
10 KiB
JavaScript
306 lines
10 KiB
JavaScript
|
// 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);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
}
|