diff --git a/endpoints/docs/lollms_flow/DOC.md b/endpoints/docs/lollms_flow/DOC.md index 5785dcb7..382ff952 100644 --- a/endpoints/docs/lollms_flow/DOC.md +++ b/endpoints/docs/lollms_flow/DOC.md @@ -1,101 +1,67 @@ -# LollmsFlow Documentation -LollmsFlow is a JavaScript library for creating and visualizing workflows. It allows you to build, connect, and execute nodes in a workflow, as well as visualize the workflow in an interactive SVG-based interface. +# Lollms Flow Library - Quick Reference -## Key Components +## Core Classes -1. **WorkflowNode**: Represents a single node in the workflow. -2. **Workflow**: Manages the entire workflow, including nodes and their connections. -3. **WorkflowVisualizer**: Handles the visual representation and interaction of the workflow. +1. `WorkflowNode`: Represents a single node in the workflow. + - Constructor: `new WorkflowNode(id, name, inputs, outputs, operation, options, x, y)` + - Key methods: `connect()`, `execute()`, `toJSON()`, `fromJSON()` -## Supported Data Types +2. `Workflow`: Manages the entire workflow. + - Key methods: `addNode()`, `connectNodes()`, `execute()`, `toJSON()`, `fromJSON()` -LollmsFlow supports the following data types for node inputs and outputs: - -1. **number**: Represents numeric values (integers or floating-point numbers). -2. **string**: Represents text data. -3. **boolean**: Represents true/false values. -4. **object**: Represents complex data structures or custom objects. - -Each data type is associated with a specific color in the workflow visualization: - -- number: blue -- string: green -- boolean: red -- object: purple - -Any other type not explicitly defined will be represented with a gray color. +3. `WorkflowVisualizer`: Handles visualization using SVG. + - Constructor: `new WorkflowVisualizer(containerId)` + - Key methods: `addNode()`, `connectNodes()`, `execute()`, `saveToJSON()`, `loadFromJSON()` ## Basic Usage -### 0. Import the LollmsFlow Library -First, include the LollmsFlow library in your HTML file: +1. Create a visualizer: + ```javascript + const visualizer = new WorkflowVisualizer('container-id'); + ``` -```html - -``` +2. Define node operations: + ```javascript + const nodeOperations = { + 'OperationName': (inputs, options) => ({ output: result }) + }; + ``` +3. Create and add nodes: + ```javascript + const node = new WorkflowNode(id, name, inputs, outputs, operation, options); + visualizer.addNode(node); + ``` -### 1. Create a Workflow Visualizer +4. Connect nodes: + ```javascript + visualizer.connectNodes(sourceId, sourceOutput, targetId, targetInput); + ``` -```javascript -const visualizer = new WorkflowVisualizer("workflow-container"); -``` +5. Execute workflow: + ```javascript + const results = visualizer.execute(); + ``` -### 2. Define Node Operations +6. Save/Load workflow: + ```javascript + const json = visualizer.saveToJSON(); + visualizer.loadFromJSON(json, nodeOperations); + ``` -```javascript -const nodeOperations = { - "Add": (inputs) => ({ sum: inputs.a + inputs.b }), - "Multiply": (inputs) => ({ product: inputs.x * inputs.y }), - "Output": (inputs) => console.log("Result:", inputs.result) -}; -``` +## Key Features -### 3. Create and Add Nodes +- SVG-based visualization with drag-and-drop +- Interactive socket connections +- Node options (checkbox, radio, select, file, textarea) +- JSON serialization and local storage integration +- Custom node and socket colors +- Event handling for interactivity -```javascript -const addNode = new WorkflowNode(0, "Add", - [{ name: "a", type: "number" }, { name: "b", type: "number" }], - [{ name: "sum", type: "number" }], - nodeOperations["Add"], 50, 50 -); -visualizer.addNode(addNode); -``` +## Best Practices -### 4. Connect Nodes - -```javascript -visualizer.connectNodes(sourceId, sourceOutput, targetId, targetInput); -``` - -### 5. Execute the Workflow - -```javascript -const results = visualizer.execute(); -``` - -## Advanced Features - -- **Save/Load Workflow**: Use `saveToJSON()` and `loadFromJSON()` methods. -- **Local Storage**: Save/load workflows using `saveToLocalStorage()` and `loadFromLocalStorage()`. -- **Drag and Drop**: Nodes can be moved around in the visualizer. - -## Key Methods for AI Usage - -1. `addNode(node)`: Add a new node to the workflow. -2. `connectNodes(sourceId, sourceOutput, targetId, targetInput)`: Connect two nodes. -3. `execute()`: Run the workflow and get results. -4. `saveToJSON()`: Convert the workflow to a JSON string. -5. `loadFromJSON(json, nodeOperations)`: Load a workflow from a JSON string. - -## Tips for AI Implementation - -1. Define a set of node types and their operations, using the supported data types. -2. Create nodes dynamically based on user input or predefined templates. -3. Use the visualization features to display the workflow to users, leveraging the color-coding for different data types. -4. Implement save/load functionality to persist workflows. -5. Utilize the execution feature to process data through the workflow. -6. When creating custom nodes, ensure that the input and output types match one of the supported data types for proper visualization and connection validation. - -This library provides a flexible framework for creating visual, interactive workflows in web applications. It can be particularly useful for data processing, algorithm visualization, or any application requiring a flow-based interface. The support for various data types allows for diverse and complex workflows to be created and managed effectively. \ No newline at end of file +- Use unique node IDs +- Define clear input/output types +- Implement error handling in operations +- Use descriptive names for nodes and sockets diff --git a/endpoints/docs/lollms_flow/README.md b/endpoints/docs/lollms_flow/README.md index 86591f90..88c13b08 100644 --- a/endpoints/docs/lollms_flow/README.md +++ b/endpoints/docs/lollms_flow/README.md @@ -1,164 +1,267 @@ -# Lollms Flow +Certainly! I'll provide a comprehensive documentation for the Lollms Flow library. This library seems to be a powerful tool for building and visualizing workflows with nodes and connections. Here's the full documentation: -Lollms Flow is a powerful JavaScript library for building and visualizing workflows of execution. It provides an intuitive way to create, connect, and manage nodes in a workflow, as well as visualize and interact with the workflow through a drag-and-drop interface. +# Lollms Flow Library Documentation -## Features +## Overview -- Create custom workflow nodes with inputs and outputs -- Connect nodes to form complex workflows -- Visualize workflows with an interactive SVG-based interface -- Drag-and-drop functionality for easy node positioning -- Save and load workflows to/from JSON -- Execute workflows and obtain results -- Integration with local storage for persistent workflows -- Customizable node operations +Lollms Flow is a JavaScript library for building, visualizing, and executing workflows. It provides a set of classes and methods to create nodes, connect them, and visualize the resulting workflow in an SVG-based interface. -## Installation +## Classes -### For Lollms Users +### 1. WorkflowNode -If you're using Lollms with the server running, you can include Lollms Flow directly in your HTML: +Represents a single node in the workflow. -```html - +#### Constructor + +```javascript +new WorkflowNode(id, name, inputs, outputs, operation, options = {}, x = 0, y = 0) ``` -### For Non-Lollms Users +- `id`: Unique identifier for the node +- `name`: Display name of the node +- `inputs`: Array of input sockets +- `outputs`: Array of output sockets +- `operation`: Function to execute when the node is processed +- `options`: Object containing node-specific options +- `x`, `y`: Initial position of the node in the visualization -If you're not using Lollms or need to specify the full server path: +#### Methods -```html - +- `connect(outputIndex, targetNode, inputIndex)`: Connect this node's output to another node's input +- `execute(inputs)`: Execute the node's operation +- `toJSON()`: Convert the node to a JSON representation +- `static fromJSON(json, operation)`: Create a node from a JSON representation + +### 2. Workflow + +Manages the entire workflow, including nodes and their connections. + +#### Constructor + +```javascript +new Workflow() ``` -Note: Make sure to activate the server of the app in Lollms, or the CORS policy may prevent access. +#### Methods + +- `addNode(node)`: Add a node to the workflow +- `connectNodes(sourceId, sourceOutput, targetId, targetInput)`: Connect two nodes +- `canConnect(sourceNode, sourceOutput, targetNode, targetInput)`: Check if two nodes can be connected +- `execute()`: Execute the entire workflow +- `toJSON()`: Convert the workflow to a JSON representation +- `static fromJSON(json, nodeOperations)`: Create a workflow from a JSON representation + +### 3. WorkflowVisualizer + +Handles the visualization of the workflow using SVG. + +#### Constructor + +```javascript +new WorkflowVisualizer(containerId) +``` + +- `containerId`: ID of the HTML element to contain the SVG visualization + +#### Methods + +- `addNode(node)`: Add a node to the workflow and visualize it +- `connectNodes(sourceId, sourceOutput, targetId, targetInput)`: Connect two nodes and visualize the connection +- `execute()`: Execute the workflow +- `saveToJSON()`: Save the workflow to a JSON string +- `loadFromJSON(json, nodeOperations)`: Load a workflow from a JSON string +- `saveToLocalStorage(key)`: Save the workflow to local storage +- `loadFromLocalStorage(key, nodeOperations)`: Load a workflow from local storage +- `redraw()`: Redraw the entire workflow visualization ## Usage -1. Create a container for the workflow in your HTML: - -```html -
-``` - -2. Initialize the WorkflowVisualizer: +1. Create a WorkflowVisualizer instance: ```javascript -const visualizer = new WorkflowVisualizer("workflow-container"); +const visualizer = new WorkflowVisualizer('workflow-container'); ``` -3. Define node operations: +2. Define node operations: ```javascript const nodeOperations = { - "Add": (inputs) => ({ sum: inputs.a + inputs.b }), - "Multiply": (inputs) => ({ product: inputs.x * inputs.y }), - "Output": (inputs) => console.log("Result:", inputs.result) + 'Add': (inputs) => ({ result: inputs.a + inputs.b }), + 'Multiply': (inputs) => ({ result: inputs.a * inputs.b }) }; ``` -4. Create and add nodes: +3. Create and add nodes: ```javascript -const addNode = new WorkflowNode(0, "Add", [ - { name: "a", type: "number" }, - { name: "b", type: "number" } -], [ - { name: "sum", type: "number" } -], nodeOperations["Add"], 50, 50); - +const addNode = new WorkflowNode('1', 'Add', + [{ name: 'a', type: 'number' }, { name: 'b', type: 'number' }], + [{ name: 'result', type: 'number' }], + nodeOperations['Add'] +); visualizer.addNode(addNode); ``` -5. Connect nodes: +4. Connect nodes: ```javascript -visualizer.connectNodes(0, 0, 1, 0); +visualizer.connectNodes('1', 0, '2', 0); ``` -6. Execute the workflow: +5. Execute the workflow: ```javascript const results = visualizer.execute(); -console.log(results); ``` -## API Reference - -### WorkflowNode - -Constructor: `WorkflowNode(id, name, inputs, outputs, operation, x = 0, y = 0)` - -Methods: -- `connect(outputIndex, targetNode, inputIndex)` -- `execute(inputs)` -- `toJSON()` -- `static fromJSON(json, operation)` - -### Workflow - -Constructor: `Workflow()` - -Methods: -- `addNode(node)` -- `connectNodes(sourceId, sourceOutput, targetId, targetInput)` -- `canConnect(sourceNode, sourceOutput, targetNode, targetInput)` -- `execute()` -- `toJSON()` -- `static fromJSON(json, nodeOperations)` - -### WorkflowVisualizer - -Constructor: `WorkflowVisualizer(containerId)` - -Methods: -- `addNode(node)` -- `connectNodes(sourceId, sourceOutput, targetId, targetInput)` -- `execute()` -- `saveToJSON()` -- `loadFromJSON(json, nodeOperations)` -- `saveToLocalStorage(key)` -- `loadFromLocalStorage(key, nodeOperations)` -- `redraw()` - -## Example +6. Save and load workflows: ```javascript -const visualizer = new WorkflowVisualizer("workflow-container"); +const json = visualizer.saveToJSON(); +visualizer.loadFromJSON(json, nodeOperations); +``` +## Advanced version with options -const addNode = new WorkflowNode(0, "Add", [ - { name: "a", type: "number" }, - { name: "b", type: "number" } -], [ - { name: "sum", type: "number" } -], nodeOperations["Add"], 50, 50); +1. First, let's define the node operation: -const multiplyNode = new WorkflowNode(1, "Multiply", [ - { name: "x", type: "number" }, - { name: "y", type: "number" } -], [ - { name: "product", type: "number" } -], nodeOperations["Multiply"], 250, 50); - -const outputNode = new WorkflowNode(2, "Output", [ - { name: "result", type: "number" } -], [], nodeOperations["Output"], 450, 50); - -visualizer.addNode(addNode); -visualizer.addNode(multiplyNode); -visualizer.addNode(outputNode); - -visualizer.connectNodes(0, 0, 1, 0); -visualizer.connectNodes(1, 0, 2, 0); - -const results = visualizer.execute(); -console.log(results); +```javascript +const nodeOperations = { + 'TextInput': (inputs, options) => ({ text: options.inputText }) +}; ``` -## Contributing +2. Now, let's create the node with the textarea option: -Contributions are welcome! Please feel free to submit a Pull Request. +```javascript +const textInputNode = new WorkflowNode( + '1', // id + 'Text Input', // name + [], // inputs (empty array as we don't need input sockets) + [{ name: 'text', type: 'string' }], // outputs + nodeOperations['TextInput'], // operation + { // options + inputText: { + type: 'textarea', + value: '' // initial value + } + } +); +``` -## License +3. Add the node to the visualizer: + +```javascript +visualizer.addNode(textInputNode); +``` + +4. The WorkflowVisualizer class already handles the creation of the textarea in the `drawOptions` method. When the user types in the textarea, it will automatically update the `options.inputText.value`. + +5. To execute the node and get the output: + +```javascript +const results = visualizer.execute(); +console.log(results['1'].text); // This will log the text entered in the textarea +``` + +Here's a complete example of how to set this up: + +```javascript +// Assume we have already created the WorkflowVisualizer +const visualizer = new WorkflowVisualizer('workflow-container'); + +// Define the node operation +const nodeOperations = { + 'TextInput': (inputs, options) => ({ text: options.inputText.value }) +}; + +// Create the node +const textInputNode = new WorkflowNode( + '1', + 'Text Input', + [], + [{ name: 'text', type: 'string' }], + nodeOperations['TextInput'], + { + inputText: { + type: 'textarea', + value: 'Enter your text here' + } + }, + 50, // x position + 50 // y position +); + +// Add the node to the visualizer +visualizer.addNode(textInputNode); + +// To execute and get the result: +document.getElementById('executeButton').addEventListener('click', () => { + const results = visualizer.execute(); + console.log('Output text:', results['1'].text); +}); +``` + +In this setup: + +1. We define a 'TextInput' node operation that simply returns the text from the options. +2. We create a WorkflowNode with no inputs, one 'text' output, and an 'inputText' option of type 'textarea'. +3. We add the node to the visualizer, which will create the visual representation including the textarea. +4. When executed, the node will output whatever text is currently in the textarea. + +The user can interact with the node in the visualization, typing text into the textarea. When the workflow is executed, it will output the current content of the textarea. + +This approach allows for dynamic, user-input text to be part of your workflow, which can then be processed by other nodes or used as the final output of the workflow. + +## Features + +- Dynamic node creation and connection +- SVG-based visualization +- Drag-and-drop node positioning +- Interactive socket connections +- Node options with various input types (checkbox, radio, select, file, textarea) +- Workflow execution +- JSON serialization and deserialization +- Local storage integration + +## Customization + +The library allows for extensive customization: + +- Node colors can be set individually +- Socket colors are determined by data type +- Node shadows and hover effects are included +- Connection paths are drawn as curved lines + +## Event Handling + +The library handles various mouse events for interactivity: + +- Node dragging +- Socket connection creation +- Socket highlighting on hover + +## Best Practices + +1. Ensure unique IDs for each node +2. Define clear input and output types for proper connections +3. Implement error handling in node operations +4. Use meaningful names for nodes and sockets +5. Regularly save workflows to prevent data loss + +## Limitations + +- The library currently does not support undo/redo operations +- Circular dependencies in the workflow are not handled automatically + +## Future Enhancements + +Potential areas for improvement include: + +- Implementing undo/redo functionality +- Adding support for subflows or grouped nodes +- Enhancing the UI with zoom and pan capabilities +- Implementing a node search or categorization system + +This documentation provides a comprehensive overview of the Lollms Flow library, its classes, methods, and usage. It should help users understand and effectively utilize the library for building and visualizing workflows. -This project is licensed under the MIT License. diff --git a/endpoints/libraries/lollms_flow.js b/endpoints/libraries/lollms_flow.js index c32ac68d..f4bb2b32 100644 --- a/endpoints/libraries/lollms_flow.js +++ b/endpoints/libraries/lollms_flow.js @@ -2,12 +2,13 @@ // A library for building workflows of execution // By ParisNeo class WorkflowNode { - constructor(id, name, inputs, outputs, operation, x = 0, y = 0) { + 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; @@ -23,7 +24,7 @@ class WorkflowNode { } execute(inputs) { - return this.operation(inputs); + return this.operation(inputs, this.options); } toJSON() { @@ -32,13 +33,14 @@ class WorkflowNode { 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.x, json.y); + return new WorkflowNode(json.id, json.name, json.inputs, json.outputs, operation, json.options, json.x, json.y); } } @@ -137,7 +139,36 @@ class WorkflowVisualizer { this.offsetX = 0; this.offsetY = 0; - this.svg.addEventListener('mousedown', this.onMouseDown.bind(this)); + this.svg.setAttribute('width', '100%'); + this.svg.setAttribute('height', '600px'); + + this.tempLine = null; + this.startSocket = null; + + 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 = ` + + + + + + + `; + defs.appendChild(filter); + + this.svg.appendChild(defs); + } + + setupEventListeners() { this.svg.addEventListener('mousemove', this.onMouseMove.bind(this)); this.svg.addEventListener('mouseup', this.onMouseUp.bind(this)); } @@ -155,100 +186,198 @@ class WorkflowVisualizer { } } + 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 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"); + rect.setAttribute("width", "140"); + rect.setAttribute("height", this.calculateNodeHeight(node)); + 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", "60"); - text.setAttribute("y", "35"); + text.setAttribute("x", "70"); + text.setAttribute("y", "20"); 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); - // 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.drawSockets(g, node.inputs, 'input'); + this.drawSockets(g, node.outputs, 'output'); + this.drawOptions(g, node.options); + g.addEventListener('mousedown', this.onNodeMouseDown.bind(this)); this.svg.appendChild(g); this.nodeElements[node.id] = g; } + 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 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 path = this.createConnectionPath(sourceNode, sourceOutput, targetNode, targetInput); + this.svg.appendChild(path); + this.connectionElements.push({ path, sourceId, sourceOutput, targetId, targetInput }); + } - 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 }); + 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 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); + const d = this.calculatePathD(sourceNode, conn.sourceOutput, targetNode, conn.targetInput); + conn.path.setAttribute("d", d); }); } getColorForType(type) { const colors = { - number: "blue", - string: "green", - boolean: "red", - object: "purple" + number: "#4285F4", + string: "#34A853", + boolean: "#EA4335", + object: "#FBBC05" }; - return colors[type] || "gray"; + return colors[type] || "#9E9E9E"; } - 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; - } + 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) { @@ -259,10 +388,72 @@ class WorkflowVisualizer { 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() { - this.draggedNode = null; + 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() { @@ -291,6 +482,7 @@ class WorkflowVisualizer { redraw() { this.svg.innerHTML = ''; + this.addDefs(); this.nodeElements = {}; this.connectionElements = []; this.workflow.nodeList.forEach(node => this.drawNode(node)); diff --git a/zoos/bindings_zoo b/zoos/bindings_zoo index d6a776d5..c9e90668 160000 --- a/zoos/bindings_zoo +++ b/zoos/bindings_zoo @@ -1 +1 @@ -Subproject commit d6a776d592d5744b310dd6ed8a13771e76934b8f +Subproject commit c9e90668d73d20b11fa02036220931e0d30724cb diff --git a/zoos/personalities_zoo b/zoos/personalities_zoo index c3a351fb..567eb2b4 160000 --- a/zoos/personalities_zoo +++ b/zoos/personalities_zoo @@ -1 +1 @@ -Subproject commit c3a351fb8db5f030cfbc4bcf3091095a21b26fed +Subproject commit 567eb2b4e0a62cfb58c1ab55129aa71a29975d2a