mirror of
https://github.com/ParisNeo/lollms-webui.git
synced 2024-12-27 15:48:50 +00:00
660 lines
26 KiB
JavaScript
660 lines
26 KiB
JavaScript
// Lollms Flow
|
|
// A library for building workflows of execution
|
|
// By ParisNeo
|
|
class WorkflowNode {
|
|
constructor(id, name, inputs, outputs, operation, options = {}, x = 0, y = 0) {
|
|
this.id = id;
|
|
this.name = name;
|
|
this.inputs = inputs;
|
|
this.outputs = outputs;
|
|
this.operation = operation;
|
|
this.options = options;
|
|
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, this.options);
|
|
}
|
|
|
|
toJSON() {
|
|
return {
|
|
id: this.id,
|
|
name: this.name,
|
|
inputs: this.inputs,
|
|
outputs: this.outputs,
|
|
options: this.options,
|
|
x: this.x,
|
|
y: this.y
|
|
};
|
|
}
|
|
|
|
static fromJSON(json, operation) {
|
|
return new WorkflowNode(json.id, json.name, json.inputs, json.outputs, operation, json.options, 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.setAttribute('width', '100%');
|
|
this.svg.setAttribute('height', '600px');
|
|
|
|
this.tempLine = null;
|
|
this.startSocket = null;
|
|
|
|
this.optionsPopup = null;
|
|
this.setupOptionsPopup();
|
|
|
|
|
|
this.addDefs();
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
addDefs() {
|
|
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
|
|
|
// Node shadow filter
|
|
const filter = document.createElementNS("http://www.w3.org/2000/svg", "filter");
|
|
filter.setAttribute("id", "dropShadow");
|
|
filter.innerHTML = `
|
|
<feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blur"/>
|
|
<feOffset in="blur" dx="2" dy="2" result="offsetBlur"/>
|
|
<feMerge>
|
|
<feMergeNode in="offsetBlur"/>
|
|
<feMergeNode in="SourceGraphic"/>
|
|
</feMerge>
|
|
`;
|
|
defs.appendChild(filter);
|
|
|
|
this.svg.appendChild(defs);
|
|
}
|
|
|
|
setupEventListeners() {
|
|
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);
|
|
g.classList.add("node");
|
|
|
|
const titleHeight = 30;
|
|
const buttonHeight = 25;
|
|
const buttonMargin = 5;
|
|
const nodeWidth = 160;
|
|
const nodeHeight = titleHeight + (Object.keys(node.options).length * (buttonHeight + buttonMargin)) + buttonMargin;
|
|
|
|
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
rect.setAttribute("width", nodeWidth);
|
|
rect.setAttribute("height", nodeHeight);
|
|
rect.setAttribute("rx", "5");
|
|
rect.setAttribute("ry", "5");
|
|
rect.setAttribute("fill", node.color || "#f0f0f0");
|
|
rect.setAttribute("stroke", "#333");
|
|
rect.setAttribute("stroke-width", "2");
|
|
g.appendChild(rect);
|
|
|
|
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
text.setAttribute("x", nodeWidth / 2);
|
|
text.setAttribute("y", titleHeight / 2 + 5);
|
|
text.setAttribute("text-anchor", "middle");
|
|
text.setAttribute("font-family", "Arial, sans-serif");
|
|
text.setAttribute("font-size", "14");
|
|
text.setAttribute("fill", "#333");
|
|
text.textContent = node.name;
|
|
g.appendChild(text);
|
|
|
|
this.drawSockets(g, node.inputs, 'input', nodeHeight);
|
|
this.drawSockets(g, node.outputs, 'output', nodeHeight);
|
|
this.drawOptionButtons(g, node, titleHeight, buttonHeight, buttonMargin, nodeWidth);
|
|
|
|
g.addEventListener('mousedown', this.onNodeMouseDown.bind(this));
|
|
this.svg.appendChild(g);
|
|
this.nodeElements[node.id] = g;
|
|
}
|
|
|
|
drawOptionButtons(nodeGroup, node, titleHeight, buttonHeight, buttonMargin, nodeWidth) {
|
|
Object.entries(node.options).forEach(([key, option], index) => {
|
|
const button = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
button.setAttribute("x", buttonMargin);
|
|
button.setAttribute("y", titleHeight + index * (buttonHeight + buttonMargin));
|
|
button.setAttribute("width", nodeWidth - 2 * buttonMargin);
|
|
button.setAttribute("height", buttonHeight);
|
|
button.setAttribute("rx", "3");
|
|
button.setAttribute("ry", "3");
|
|
button.setAttribute("fill", "#ddd");
|
|
button.setAttribute("stroke", "#333");
|
|
button.setAttribute("stroke-width", "1");
|
|
button.classList.add("option-button");
|
|
|
|
const buttonText = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
buttonText.setAttribute("x", nodeWidth / 2);
|
|
buttonText.setAttribute("y", titleHeight + index * (buttonHeight + buttonMargin) + buttonHeight / 2 + 5);
|
|
buttonText.setAttribute("text-anchor", "middle");
|
|
buttonText.setAttribute("font-family", "Arial, sans-serif");
|
|
buttonText.setAttribute("font-size", "12");
|
|
buttonText.setAttribute("fill", "#333");
|
|
buttonText.textContent = option.name; // Use the option name (key) instead of index
|
|
|
|
button.addEventListener('click', (event) => {
|
|
event.stopPropagation();
|
|
this.showOptionsPopup(node, key, option.name);
|
|
});
|
|
|
|
nodeGroup.appendChild(button);
|
|
nodeGroup.appendChild(buttonText);
|
|
});
|
|
}
|
|
|
|
|
|
drawSockets(nodeGroup, sockets, type, nodeHeight) {
|
|
sockets.forEach((socket, index) => {
|
|
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
circle.setAttribute("cx", type === 'input' ? "0" : "160");
|
|
circle.setAttribute("cy", (index + 1) * (nodeHeight / (sockets.length + 1)));
|
|
circle.setAttribute("r", "6");
|
|
circle.setAttribute("fill", this.getColorForType(socket.type));
|
|
circle.setAttribute("stroke", "#333");
|
|
circle.setAttribute("stroke-width", "2");
|
|
circle.classList.add("socket", `${type}-socket`);
|
|
circle.setAttribute("data-node-id", nodeGroup.getAttribute("data-id"));
|
|
circle.setAttribute("data-socket-index", index);
|
|
circle.setAttribute("data-socket-type", socket.type);
|
|
|
|
this.addSocketListeners(circle, type);
|
|
nodeGroup.appendChild(circle);
|
|
});
|
|
}
|
|
|
|
setupOptionsPopup() {
|
|
this.optionsPopup = document.createElement('div');
|
|
this.optionsPopup.style.position = 'absolute';
|
|
this.optionsPopup.style.zIndex = '1000';
|
|
this.optionsPopup.style.backgroundColor = 'white';
|
|
this.optionsPopup.style.border = '1px solid #333';
|
|
this.optionsPopup.style.borderRadius = '5px';
|
|
this.optionsPopup.style.padding = '10px';
|
|
this.optionsPopup.style.display = 'none';
|
|
this.optionsPopup.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
|
|
document.body.appendChild(this.optionsPopup);
|
|
|
|
// Close popup when clicking outside
|
|
document.addEventListener('click', (event) => {
|
|
if (!this.optionsPopup.contains(event.target) && !event.target.closest('.option-button')) {
|
|
this.optionsPopup.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
showOptionsPopup(node, optionKey, optionName) {
|
|
this.optionsPopup.innerHTML = '';
|
|
|
|
const title = document.createElement('h3');
|
|
title.textContent = `${node.name}: ${optionName}`;
|
|
title.style.marginTop = '0';
|
|
this.optionsPopup.appendChild(title);
|
|
|
|
const option = node.options[optionKey];
|
|
const optionContainer = document.createElement('div');
|
|
optionContainer.style.marginBottom = '10px';
|
|
|
|
let input;
|
|
switch (option.type) {
|
|
case 'checkbox':
|
|
input = document.createElement('input');
|
|
input.type = 'checkbox';
|
|
input.checked = option.value;
|
|
break;
|
|
case 'radio':
|
|
option.options.forEach(optionValue => {
|
|
const radioInput = document.createElement('input');
|
|
radioInput.type = 'radio';
|
|
radioInput.name = optionKey;
|
|
radioInput.value = optionValue;
|
|
radioInput.checked = option.value === optionValue;
|
|
const radioLabel = document.createElement('label');
|
|
radioLabel.textContent = optionValue;
|
|
optionContainer.appendChild(radioInput);
|
|
optionContainer.appendChild(radioLabel);
|
|
});
|
|
break;
|
|
case 'select':
|
|
input = document.createElement('select');
|
|
option.options.forEach(optionValue => {
|
|
const optionElement = document.createElement('option');
|
|
optionElement.value = optionValue;
|
|
optionElement.textContent = optionValue;
|
|
optionElement.selected = option.value === optionValue;
|
|
input.appendChild(optionElement);
|
|
});
|
|
break;
|
|
case 'file':
|
|
input = document.createElement('input');
|
|
input.type = 'file';
|
|
break;
|
|
case 'textarea':
|
|
input = document.createElement('textarea');
|
|
input.value = option.value;
|
|
input.rows = 3;
|
|
break;
|
|
default:
|
|
input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.value = option.value;
|
|
}
|
|
|
|
if (input) {
|
|
input.addEventListener('change', (e) => {
|
|
option.value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
|
|
});
|
|
optionContainer.appendChild(input);
|
|
}
|
|
|
|
this.optionsPopup.appendChild(optionContainer);
|
|
|
|
const closeButton = document.createElement('button');
|
|
closeButton.textContent = 'Close';
|
|
closeButton.style.marginTop = '10px';
|
|
closeButton.addEventListener('click', () => {
|
|
this.optionsPopup.style.display = 'none';
|
|
});
|
|
this.optionsPopup.appendChild(closeButton);
|
|
|
|
const rect = this.svg.getBoundingClientRect();
|
|
this.optionsPopup.style.left = (rect.left + node.x + 170) + 'px';
|
|
this.optionsPopup.style.top = (rect.top + node.y) + 'px';
|
|
this.optionsPopup.style.display = 'block';
|
|
}
|
|
|
|
calculateNodeHeight(node) {
|
|
const baseHeight = 80;
|
|
const optionsHeight = Object.keys(node.options).length * 30;
|
|
return baseHeight + optionsHeight;
|
|
}
|
|
|
|
drawOptions(nodeGroup, options) {
|
|
let yOffset = 50;
|
|
Object.entries(options).forEach(([key, option]) => {
|
|
const foreignObject = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
|
|
foreignObject.setAttribute("x", "10");
|
|
foreignObject.setAttribute("y", yOffset.toString());
|
|
foreignObject.setAttribute("width", "120");
|
|
foreignObject.setAttribute("height", "25");
|
|
|
|
const div = document.createElement("div");
|
|
div.style.display = "flex";
|
|
div.style.alignItems = "center";
|
|
|
|
const label = document.createElement("label");
|
|
label.textContent = key + ": ";
|
|
label.style.marginRight = "5px";
|
|
div.appendChild(label);
|
|
|
|
let input;
|
|
switch (option.type) {
|
|
case 'checkbox':
|
|
input = document.createElement("input");
|
|
input.type = "checkbox";
|
|
input.checked = option.value;
|
|
break;
|
|
case 'radio':
|
|
option.options.forEach(optionValue => {
|
|
const radioInput = document.createElement("input");
|
|
radioInput.type = "radio";
|
|
radioInput.name = key;
|
|
radioInput.value = optionValue;
|
|
radioInput.checked = option.value === optionValue;
|
|
div.appendChild(radioInput);
|
|
const radioLabel = document.createElement("label");
|
|
radioLabel.textContent = optionValue;
|
|
div.appendChild(radioLabel);
|
|
});
|
|
break;
|
|
case 'select':
|
|
input = document.createElement("select");
|
|
option.options.forEach(optionValue => {
|
|
const optionElement = document.createElement("option");
|
|
optionElement.value = optionValue;
|
|
optionElement.textContent = optionValue;
|
|
optionElement.selected = option.value === optionValue;
|
|
input.appendChild(optionElement);
|
|
});
|
|
break;
|
|
case 'file':
|
|
input = document.createElement("input");
|
|
input.type = "file";
|
|
break;
|
|
case 'textarea':
|
|
input = document.createElement("textarea");
|
|
input.value = option.value;
|
|
input.rows = 3;
|
|
break;
|
|
default:
|
|
input = document.createElement("input");
|
|
input.type = "text";
|
|
input.value = option.value;
|
|
}
|
|
|
|
if (input) {
|
|
input.addEventListener('change', (e) => {
|
|
option.value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
|
|
});
|
|
div.appendChild(input);
|
|
}
|
|
|
|
foreignObject.appendChild(div);
|
|
nodeGroup.appendChild(foreignObject);
|
|
|
|
yOffset += 30;
|
|
});
|
|
}
|
|
|
|
drawSockets(nodeGroup, sockets, type) {
|
|
sockets.forEach((socket, index) => {
|
|
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
circle.setAttribute("cx", type === 'input' ? "0" : "140");
|
|
circle.setAttribute("cy", (index + 1) * 20);
|
|
circle.setAttribute("r", "6");
|
|
circle.setAttribute("fill", this.getColorForType(socket.type));
|
|
circle.setAttribute("stroke", "#333");
|
|
circle.setAttribute("stroke-width", "2");
|
|
circle.classList.add("socket", `${type}-socket`);
|
|
circle.setAttribute("data-node-id", nodeGroup.getAttribute("data-id"));
|
|
circle.setAttribute("data-socket-index", index);
|
|
circle.setAttribute("data-socket-type", socket.type);
|
|
|
|
this.addSocketListeners(circle, type);
|
|
nodeGroup.appendChild(circle);
|
|
});
|
|
}
|
|
|
|
drawConnection(sourceId, sourceOutput, targetId, targetInput) {
|
|
const sourceNode = this.workflow.nodes[sourceId];
|
|
const targetNode = this.workflow.nodes[targetId];
|
|
const path = this.createConnectionPath(sourceNode, sourceOutput, targetNode, targetInput);
|
|
this.svg.appendChild(path);
|
|
this.connectionElements.push({ path, sourceId, sourceOutput, targetId, targetInput });
|
|
}
|
|
|
|
createConnectionPath(sourceNode, sourceOutput, targetNode, targetInput) {
|
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
const d = this.calculatePathD(sourceNode, sourceOutput, targetNode, targetInput);
|
|
path.setAttribute("d", d);
|
|
path.setAttribute("fill", "none");
|
|
path.setAttribute("stroke", "#666");
|
|
path.setAttribute("stroke-width", "2");
|
|
return path;
|
|
}
|
|
|
|
calculatePathD(sourceNode, sourceOutput, targetNode, targetInput) {
|
|
const sourcePosX = sourceNode.x + 140;
|
|
const sourcePosY = sourceNode.y + (sourceOutput + 1) * 20;
|
|
const targetPosX = targetNode.x;
|
|
const targetPosY = targetNode.y + (targetInput + 1) * 20;
|
|
const midX = (sourcePosX + targetPosX) / 2;
|
|
return `M ${sourcePosX} ${sourcePosY} C ${midX} ${sourcePosY}, ${midX} ${targetPosY}, ${targetPosX} ${targetPosY}`;
|
|
}
|
|
|
|
updateConnections() {
|
|
this.connectionElements.forEach(conn => {
|
|
const sourceNode = this.workflow.nodes[conn.sourceId];
|
|
const targetNode = this.workflow.nodes[conn.targetId];
|
|
const d = this.calculatePathD(sourceNode, conn.sourceOutput, targetNode, conn.targetInput);
|
|
conn.path.setAttribute("d", d);
|
|
});
|
|
}
|
|
|
|
getColorForType(type) {
|
|
const colors = {
|
|
number: "#4285F4",
|
|
string: "#34A853",
|
|
boolean: "#EA4335",
|
|
object: "#FBBC05"
|
|
};
|
|
return colors[type] || "#9E9E9E";
|
|
}
|
|
|
|
onNodeMouseDown(event) {
|
|
if (event.target.classList.contains('socket')) return;
|
|
const nodeElement = event.currentTarget;
|
|
this.draggedNode = this.workflow.nodes[nodeElement.getAttribute("data-id")];
|
|
const rect = nodeElement.getBoundingClientRect();
|
|
this.offsetX = event.clientX - rect.left;
|
|
this.offsetY = event.clientY - rect.top;
|
|
nodeElement.setAttribute("filter", "url(#dropShadow)");
|
|
}
|
|
|
|
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();
|
|
}
|
|
if (this.tempLine) {
|
|
const rect = this.svg.getBoundingClientRect();
|
|
this.updateTempLine(event.clientX - rect.left, event.clientY - rect.top);
|
|
}
|
|
}
|
|
|
|
onMouseUp(event) {
|
|
if (this.draggedNode) {
|
|
this.nodeElements[this.draggedNode.id].removeAttribute("filter");
|
|
this.draggedNode = null;
|
|
}
|
|
if (this.tempLine && this.startSocket) {
|
|
const endSocket = event.target.closest('.input-socket');
|
|
if (endSocket && this.canConnect(this.startSocket, endSocket)) {
|
|
const sourceId = this.startSocket.getAttribute('data-node-id');
|
|
const sourceOutput = this.startSocket.getAttribute('data-socket-index');
|
|
const targetId = endSocket.getAttribute('data-node-id');
|
|
const targetInput = endSocket.getAttribute('data-socket-index');
|
|
this.connectNodes(sourceId, parseInt(sourceOutput), targetId, parseInt(targetInput));
|
|
}
|
|
this.svg.removeChild(this.tempLine);
|
|
this.tempLine = null;
|
|
this.startSocket = null;
|
|
}
|
|
}
|
|
|
|
addSocketListeners(socket, type) {
|
|
socket.addEventListener('mouseenter', () => socket.setAttribute('r', '8'));
|
|
socket.addEventListener('mouseleave', () => socket.setAttribute('r', '6'));
|
|
|
|
if (type === 'output') {
|
|
socket.addEventListener('mousedown', this.onSocketMouseDown.bind(this));
|
|
}
|
|
}
|
|
|
|
onSocketMouseDown(event) {
|
|
event.stopPropagation();
|
|
this.startSocket = event.target;
|
|
const rect = this.svg.getBoundingClientRect();
|
|
const startX = parseFloat(this.startSocket.getAttribute('cx')) + this.startSocket.parentElement.transform.baseVal[0].matrix.e;
|
|
const startY = parseFloat(this.startSocket.getAttribute('cy')) + this.startSocket.parentElement.transform.baseVal[0].matrix.f;
|
|
this.tempLine = this.createTempLine(startX, startY, event.clientX - rect.left, event.clientY - rect.top);
|
|
this.svg.appendChild(this.tempLine);
|
|
}
|
|
|
|
createTempLine(startX, startY, endX, endY) {
|
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
const d = `M ${startX} ${startY} C ${(startX + endX) / 2} ${startY}, ${(startX + endX) / 2} ${endY}, ${endX} ${endY}`;
|
|
path.setAttribute("d", d);
|
|
path.setAttribute("fill", "none");
|
|
path.setAttribute("stroke", "#666");
|
|
path.setAttribute("stroke-width", "2");
|
|
path.setAttribute("stroke-dasharray", "5,5");
|
|
return path;
|
|
}
|
|
|
|
updateTempLine(endX, endY) {
|
|
const startX = parseFloat(this.startSocket.getAttribute('cx')) + this.startSocket.parentElement.transform.baseVal[0].matrix.e;
|
|
const startY = parseFloat(this.startSocket.getAttribute('cy')) + this.startSocket.parentElement.transform.baseVal[0].matrix.f;
|
|
const d = `M ${startX} ${startY} C ${(startX + endX) / 2} ${startY}, ${(startX + endX) / 2} ${endY}, ${endX} ${endY}`;
|
|
this.tempLine.setAttribute("d", d);
|
|
}
|
|
|
|
canConnect(sourceSocket, targetSocket) {
|
|
return sourceSocket.getAttribute('data-socket-type') === targetSocket.getAttribute('data-socket-type') &&
|
|
sourceSocket.getAttribute('data-node-id') !== targetSocket.getAttribute('data-node-id');
|
|
}
|
|
|
|
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.addDefs();
|
|
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);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|