mirror of
https://github.com/ParisNeo/lollms-webui.git
synced 2024-12-24 06:36:37 +00:00
upgraded lollms flow
This commit is contained in:
parent
630ab5591d
commit
f961f1fc8a
@ -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.
|
1. `WorkflowNode`: Represents a single node in the workflow.
|
||||||
2. **Workflow**: Manages the entire workflow, including nodes and their connections.
|
- Constructor: `new WorkflowNode(id, name, inputs, outputs, operation, options, x, y)`
|
||||||
3. **WorkflowVisualizer**: Handles the visual representation and interaction of the workflow.
|
- 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:
|
3. `WorkflowVisualizer`: Handles visualization using SVG.
|
||||||
|
- Constructor: `new WorkflowVisualizer(containerId)`
|
||||||
1. **number**: Represents numeric values (integers or floating-point numbers).
|
- Key methods: `addNode()`, `connectNodes()`, `execute()`, `saveToJSON()`, `loadFromJSON()`
|
||||||
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.
|
|
||||||
|
|
||||||
## Basic Usage
|
## 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:
|
||||||
<script src="/lollms_assets/js/lollms_flow"></script>
|
```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
|
5. Execute workflow:
|
||||||
const visualizer = new WorkflowVisualizer("workflow-container");
|
```javascript
|
||||||
```
|
const results = visualizer.execute();
|
||||||
|
```
|
||||||
|
|
||||||
### 2. Define Node Operations
|
6. Save/Load workflow:
|
||||||
|
```javascript
|
||||||
|
const json = visualizer.saveToJSON();
|
||||||
|
visualizer.loadFromJSON(json, nodeOperations);
|
||||||
|
```
|
||||||
|
|
||||||
```javascript
|
## Key Features
|
||||||
const nodeOperations = {
|
|
||||||
"Add": (inputs) => ({ sum: inputs.a + inputs.b }),
|
|
||||||
"Multiply": (inputs) => ({ product: inputs.x * inputs.y }),
|
|
||||||
"Output": (inputs) => console.log("Result:", inputs.result)
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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
|
## Best Practices
|
||||||
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);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Connect Nodes
|
- Use unique node IDs
|
||||||
|
- Define clear input/output types
|
||||||
```javascript
|
- Implement error handling in operations
|
||||||
visualizer.connectNodes(sourceId, sourceOutput, targetId, targetInput);
|
- Use descriptive names for nodes and sockets
|
||||||
```
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
@ -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
|
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.
|
||||||
- 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
|
|
||||||
|
|
||||||
## 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
|
||||||
<script src="/lollms_assets/js/lollms_flow"></script>
|
|
||||||
|
```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
|
||||||
<script src="http://localhost:9600/lollms_assets/js/lollms_flow"></script>
|
- `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
|
## Usage
|
||||||
|
|
||||||
1. Create a container for the workflow in your HTML:
|
1. Create a WorkflowVisualizer instance:
|
||||||
|
|
||||||
```html
|
|
||||||
<div id="workflow-container"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Initialize the WorkflowVisualizer:
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const visualizer = new WorkflowVisualizer("workflow-container");
|
const visualizer = new WorkflowVisualizer('workflow-container');
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Define node operations:
|
2. Define node operations:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const nodeOperations = {
|
const nodeOperations = {
|
||||||
"Add": (inputs) => ({ sum: inputs.a + inputs.b }),
|
'Add': (inputs) => ({ result: inputs.a + inputs.b }),
|
||||||
"Multiply": (inputs) => ({ product: inputs.x * inputs.y }),
|
'Multiply': (inputs) => ({ result: inputs.a * inputs.b })
|
||||||
"Output": (inputs) => console.log("Result:", inputs.result)
|
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Create and add nodes:
|
3. Create and add nodes:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const addNode = new WorkflowNode(0, "Add", [
|
const addNode = new WorkflowNode('1', 'Add',
|
||||||
{ name: "a", type: "number" },
|
[{ name: 'a', type: 'number' }, { name: 'b', type: 'number' }],
|
||||||
{ name: "b", type: "number" }
|
[{ name: 'result', type: 'number' }],
|
||||||
], [
|
nodeOperations['Add']
|
||||||
{ name: "sum", type: "number" }
|
);
|
||||||
], nodeOperations["Add"], 50, 50);
|
|
||||||
|
|
||||||
visualizer.addNode(addNode);
|
visualizer.addNode(addNode);
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Connect nodes:
|
4. Connect nodes:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
visualizer.connectNodes(0, 0, 1, 0);
|
visualizer.connectNodes('1', 0, '2', 0);
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Execute the workflow:
|
5. Execute the workflow:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const results = visualizer.execute();
|
const results = visualizer.execute();
|
||||||
console.log(results);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## API Reference
|
6. Save and load workflows:
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
```javascript
|
```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", [
|
1. First, let's define the node operation:
|
||||||
{ name: "a", type: "number" },
|
|
||||||
{ name: "b", type: "number" }
|
|
||||||
], [
|
|
||||||
{ name: "sum", type: "number" }
|
|
||||||
], nodeOperations["Add"], 50, 50);
|
|
||||||
|
|
||||||
const multiplyNode = new WorkflowNode(1, "Multiply", [
|
```javascript
|
||||||
{ name: "x", type: "number" },
|
const nodeOperations = {
|
||||||
{ name: "y", type: "number" }
|
'TextInput': (inputs, options) => ({ text: options.inputText })
|
||||||
], [
|
};
|
||||||
{ 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);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
@ -2,12 +2,13 @@
|
|||||||
// A library for building workflows of execution
|
// A library for building workflows of execution
|
||||||
// By ParisNeo
|
// By ParisNeo
|
||||||
class WorkflowNode {
|
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.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.inputs = inputs;
|
this.inputs = inputs;
|
||||||
this.outputs = outputs;
|
this.outputs = outputs;
|
||||||
this.operation = operation;
|
this.operation = operation;
|
||||||
|
this.options = options;
|
||||||
this.inputConnections = {};
|
this.inputConnections = {};
|
||||||
this.outputConnections = {};
|
this.outputConnections = {};
|
||||||
this.x = x;
|
this.x = x;
|
||||||
@ -23,7 +24,7 @@ class WorkflowNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
execute(inputs) {
|
execute(inputs) {
|
||||||
return this.operation(inputs);
|
return this.operation(inputs, this.options);
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
@ -32,13 +33,14 @@ class WorkflowNode {
|
|||||||
name: this.name,
|
name: this.name,
|
||||||
inputs: this.inputs,
|
inputs: this.inputs,
|
||||||
outputs: this.outputs,
|
outputs: this.outputs,
|
||||||
|
options: this.options,
|
||||||
x: this.x,
|
x: this.x,
|
||||||
y: this.y
|
y: this.y
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJSON(json, operation) {
|
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.offsetX = 0;
|
||||||
this.offsetY = 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 = `
|
||||||
|
<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('mousemove', this.onMouseMove.bind(this));
|
||||||
this.svg.addEventListener('mouseup', this.onMouseUp.bind(this));
|
this.svg.addEventListener('mouseup', this.onMouseUp.bind(this));
|
||||||
}
|
}
|
||||||
@ -155,100 +186,198 @@ class WorkflowVisualizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
drawNode(node) {
|
drawNode(node) {
|
||||||
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||||
g.setAttribute("transform", `translate(${node.x}, ${node.y})`);
|
g.setAttribute("transform", `translate(${node.x}, ${node.y})`);
|
||||||
g.setAttribute("data-id", node.id);
|
g.setAttribute("data-id", node.id);
|
||||||
|
g.classList.add("node");
|
||||||
|
|
||||||
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||||
rect.setAttribute("width", "120");
|
rect.setAttribute("width", "140");
|
||||||
rect.setAttribute("height", "60");
|
rect.setAttribute("height", this.calculateNodeHeight(node));
|
||||||
rect.setAttribute("fill", "lightblue");
|
rect.setAttribute("rx", "5");
|
||||||
rect.setAttribute("stroke", "black");
|
rect.setAttribute("ry", "5");
|
||||||
|
rect.setAttribute("fill", node.color || "#f0f0f0");
|
||||||
|
rect.setAttribute("stroke", "#333");
|
||||||
|
rect.setAttribute("stroke-width", "2");
|
||||||
g.appendChild(rect);
|
g.appendChild(rect);
|
||||||
|
|
||||||
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||||
text.setAttribute("x", "60");
|
text.setAttribute("x", "70");
|
||||||
text.setAttribute("y", "35");
|
text.setAttribute("y", "20");
|
||||||
text.setAttribute("text-anchor", "middle");
|
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;
|
text.textContent = node.name;
|
||||||
g.appendChild(text);
|
g.appendChild(text);
|
||||||
|
|
||||||
// Draw input sockets
|
this.drawSockets(g, node.inputs, 'input');
|
||||||
node.inputs.forEach((input, index) => {
|
this.drawSockets(g, node.outputs, 'output');
|
||||||
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
this.drawOptions(g, node.options);
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
g.addEventListener('mousedown', this.onNodeMouseDown.bind(this));
|
||||||
this.svg.appendChild(g);
|
this.svg.appendChild(g);
|
||||||
this.nodeElements[node.id] = 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) {
|
drawConnection(sourceId, sourceOutput, targetId, targetInput) {
|
||||||
const sourceNode = this.workflow.nodes[sourceId];
|
const sourceNode = this.workflow.nodes[sourceId];
|
||||||
const targetNode = this.workflow.nodes[targetId];
|
const targetNode = this.workflow.nodes[targetId];
|
||||||
const line = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
const path = this.createConnectionPath(sourceNode, sourceOutput, targetNode, targetInput);
|
||||||
const sourcePosX = sourceNode.x + 120;
|
this.svg.appendChild(path);
|
||||||
const sourcePosY = sourceNode.y + (sourceOutput + 1) * 15;
|
this.connectionElements.push({ path, sourceId, sourceOutput, targetId, targetInput });
|
||||||
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}`;
|
createConnectionPath(sourceNode, sourceOutput, targetNode, targetInput) {
|
||||||
line.setAttribute("d", d);
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||||
line.setAttribute("fill", "none");
|
const d = this.calculatePathD(sourceNode, sourceOutput, targetNode, targetInput);
|
||||||
line.setAttribute("stroke", "black");
|
path.setAttribute("d", d);
|
||||||
this.svg.appendChild(line);
|
path.setAttribute("fill", "none");
|
||||||
this.connectionElements.push({ line, sourceId, sourceOutput, targetId, targetInput });
|
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() {
|
updateConnections() {
|
||||||
this.connectionElements.forEach(conn => {
|
this.connectionElements.forEach(conn => {
|
||||||
const sourceNode = this.workflow.nodes[conn.sourceId];
|
const sourceNode = this.workflow.nodes[conn.sourceId];
|
||||||
const targetNode = this.workflow.nodes[conn.targetId];
|
const targetNode = this.workflow.nodes[conn.targetId];
|
||||||
const sourcePosX = sourceNode.x + 120;
|
const d = this.calculatePathD(sourceNode, conn.sourceOutput, targetNode, conn.targetInput);
|
||||||
const sourcePosY = sourceNode.y + (conn.sourceOutput + 1) * 15;
|
conn.path.setAttribute("d", d);
|
||||||
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) {
|
getColorForType(type) {
|
||||||
const colors = {
|
const colors = {
|
||||||
number: "blue",
|
number: "#4285F4",
|
||||||
string: "green",
|
string: "#34A853",
|
||||||
boolean: "red",
|
boolean: "#EA4335",
|
||||||
object: "purple"
|
object: "#FBBC05"
|
||||||
};
|
};
|
||||||
return colors[type] || "gray";
|
return colors[type] || "#9E9E9E";
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseDown(event) {
|
onNodeMouseDown(event) {
|
||||||
const target = event.target.closest("g");
|
if (event.target.classList.contains('socket')) return;
|
||||||
if (target) {
|
const nodeElement = event.currentTarget;
|
||||||
this.draggedNode = this.workflow.nodes[target.getAttribute("data-id")];
|
this.draggedNode = this.workflow.nodes[nodeElement.getAttribute("data-id")];
|
||||||
const rect = target.getBoundingClientRect();
|
const rect = nodeElement.getBoundingClientRect();
|
||||||
this.offsetX = event.clientX - rect.left;
|
this.offsetX = event.clientX - rect.left;
|
||||||
this.offsetY = event.clientY - rect.top;
|
this.offsetY = event.clientY - rect.top;
|
||||||
}
|
nodeElement.setAttribute("filter", "url(#dropShadow)");
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseMove(event) {
|
onMouseMove(event) {
|
||||||
@ -259,10 +388,72 @@ class WorkflowVisualizer {
|
|||||||
this.nodeElements[this.draggedNode.id].setAttribute("transform", `translate(${this.draggedNode.x}, ${this.draggedNode.y})`);
|
this.nodeElements[this.draggedNode.id].setAttribute("transform", `translate(${this.draggedNode.x}, ${this.draggedNode.y})`);
|
||||||
this.updateConnections();
|
this.updateConnections();
|
||||||
}
|
}
|
||||||
|
if (this.tempLine) {
|
||||||
|
const rect = this.svg.getBoundingClientRect();
|
||||||
|
this.updateTempLine(event.clientX - rect.left, event.clientY - rect.top);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseUp() {
|
onMouseUp(event) {
|
||||||
this.draggedNode = null;
|
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() {
|
execute() {
|
||||||
@ -291,6 +482,7 @@ class WorkflowVisualizer {
|
|||||||
|
|
||||||
redraw() {
|
redraw() {
|
||||||
this.svg.innerHTML = '';
|
this.svg.innerHTML = '';
|
||||||
|
this.addDefs();
|
||||||
this.nodeElements = {};
|
this.nodeElements = {};
|
||||||
this.connectionElements = [];
|
this.connectionElements = [];
|
||||||
this.workflow.nodeList.forEach(node => this.drawNode(node));
|
this.workflow.nodeList.forEach(node => this.drawNode(node));
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit d6a776d592d5744b310dd6ed8a13771e76934b8f
|
Subproject commit c9e90668d73d20b11fa02036220931e0d30724cb
|
@ -1 +1 @@
|
|||||||
Subproject commit c3a351fb8db5f030cfbc4bcf3091095a21b26fed
|
Subproject commit 567eb2b4e0a62cfb58c1ab55129aa71a29975d2a
|
Loading…
Reference in New Issue
Block a user