Spaces:
Sleeping
Sleeping
Commit ·
d57a1c0
1
Parent(s): 2dc2fbc
Resolve a bug related to the Running Node
Browse files- frontend/src/App.js +68 -18
- frontend/src/OutputNode.css +50 -0
- frontend/src/OutputNode.js +51 -4
- frontend/src/RunNode.js +14 -6
frontend/src/App.js
CHANGED
|
@@ -32,6 +32,7 @@ let nodeIdCounter = 0;
|
|
| 32 |
function App() {
|
| 33 |
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
| 34 |
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
|
|
| 35 |
const { addLog } = useContext(LogContext);
|
| 36 |
|
| 37 |
const nodeTypes = useMemo(() => ({
|
|
@@ -271,10 +272,33 @@ function App() {
|
|
| 271 |
}
|
| 272 |
};
|
| 273 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
const resetNodeStatuses = () => {
|
| 275 |
setNodes(nds => nds.map(n => {
|
| 276 |
-
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
| 278 |
}));
|
| 279 |
};
|
| 280 |
|
|
@@ -475,22 +499,48 @@ function App() {
|
|
| 475 |
}
|
| 476 |
};
|
| 477 |
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
});
|
| 487 |
-
|
| 488 |
-
const executionResults = new Map();
|
| 489 |
-
const initialNodes = nodes.filter(n => inDegree.get(n.id) === 0);
|
| 490 |
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
}
|
| 495 |
};
|
| 496 |
|
|
@@ -504,7 +554,7 @@ function App() {
|
|
| 504 |
</div>
|
| 505 |
</div>
|
| 506 |
<div style={{ flexGrow: 1, position: 'relative' }}>
|
| 507 |
-
<WorkflowContext.Provider value={{ handleRun }}>
|
| 508 |
<ReactFlow
|
| 509 |
nodes={nodes}
|
| 510 |
edges={edges}
|
|
|
|
| 32 |
function App() {
|
| 33 |
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
| 34 |
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
| 35 |
+
const [isRunning, setIsRunning] = useState(false);
|
| 36 |
const { addLog } = useContext(LogContext);
|
| 37 |
|
| 38 |
const nodeTypes = useMemo(() => ({
|
|
|
|
| 272 |
}
|
| 273 |
};
|
| 274 |
|
| 275 |
+
// Find all nodes reachable from a starting node (for isolated workflow execution)
|
| 276 |
+
const findReachableNodes = useCallback((startNodeId) => {
|
| 277 |
+
const reachableNodes = new Set([startNodeId]);
|
| 278 |
+
const nodesToProcess = [startNodeId];
|
| 279 |
+
|
| 280 |
+
while (nodesToProcess.length > 0) {
|
| 281 |
+
const currentNodeId = nodesToProcess.shift();
|
| 282 |
+
|
| 283 |
+
// Find all edges where the current node is the source
|
| 284 |
+
edges.forEach(edge => {
|
| 285 |
+
if (edge.source === currentNodeId && !reachableNodes.has(edge.target)) {
|
| 286 |
+
reachableNodes.add(edge.target);
|
| 287 |
+
nodesToProcess.push(edge.target);
|
| 288 |
+
}
|
| 289 |
+
});
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
return Array.from(reachableNodes);
|
| 293 |
+
}, [edges]);
|
| 294 |
+
|
| 295 |
const resetNodeStatuses = () => {
|
| 296 |
setNodes(nds => nds.map(n => {
|
| 297 |
+
if (n.data && ('status' in n.data || 'result' in n.data)) {
|
| 298 |
+
const { status, result, error, ...restData } = n.data;
|
| 299 |
+
return { ...n, data: restData };
|
| 300 |
+
}
|
| 301 |
+
return n;
|
| 302 |
}));
|
| 303 |
};
|
| 304 |
|
|
|
|
| 499 |
}
|
| 500 |
};
|
| 501 |
|
| 502 |
+
// Function to run workflow from a specific run node
|
| 503 |
+
const handleRunFromNode = async (startNodeId) => {
|
| 504 |
+
if (isRunning) {
|
| 505 |
+
addLog("A workflow is already running. Please wait for it to complete.", 'WARN');
|
| 506 |
+
return;
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
setIsRunning(true);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
|
| 511 |
+
try {
|
| 512 |
+
addLog(`================== WORKFLOW RUNNING FROM NODE ${startNodeId} ==================`, 'INFO');
|
| 513 |
+
resetNodeStatuses();
|
| 514 |
+
|
| 515 |
+
// Get only the nodes reachable from this specific Run Node
|
| 516 |
+
const reachableNodeIds = findReachableNodes(startNodeId);
|
| 517 |
+
addLog(`This workflow includes ${reachableNodeIds.length} node(s)`, 'INFO');
|
| 518 |
+
|
| 519 |
+
// Map of all nodes for easier access
|
| 520 |
+
const nodesMap = new Map(nodes.map(n => [n.id, n]));
|
| 521 |
+
// Track execution results
|
| 522 |
+
const executionResults = new Map();
|
| 523 |
+
|
| 524 |
+
// Start execution from the run node
|
| 525 |
+
await executeNode(nodesMap.get(startNodeId), executionResults, nodesMap);
|
| 526 |
+
|
| 527 |
+
addLog(`================== WORKFLOW COMPLETED ==================`, 'SUCCESS');
|
| 528 |
+
} catch (error) {
|
| 529 |
+
addLog(`Workflow execution failed: ${error.message}`, 'ERROR');
|
| 530 |
+
} finally {
|
| 531 |
+
setIsRunning(false);
|
| 532 |
+
}
|
| 533 |
+
};
|
| 534 |
+
|
| 535 |
+
// For backward compatibility or general run
|
| 536 |
+
const handleRun = async () => {
|
| 537 |
+
// Find all run nodes
|
| 538 |
+
const runNodes = nodes.filter(node => node.type === 'runNode');
|
| 539 |
+
if (runNodes.length > 0) {
|
| 540 |
+
// Run from the first run node found (for backward compatibility)
|
| 541 |
+
await handleRunFromNode(runNodes[0].id);
|
| 542 |
+
} else {
|
| 543 |
+
addLog("No Run Nodes found in the workflow. Please add a Run Node.", 'WARN');
|
| 544 |
}
|
| 545 |
};
|
| 546 |
|
|
|
|
| 554 |
</div>
|
| 555 |
</div>
|
| 556 |
<div style={{ flexGrow: 1, position: 'relative' }}>
|
| 557 |
+
<WorkflowContext.Provider value={{ handleRun, handleRunFromNode, isRunning }}>
|
| 558 |
<ReactFlow
|
| 559 |
nodes={nodes}
|
| 560 |
edges={edges}
|
frontend/src/OutputNode.css
CHANGED
|
@@ -110,4 +110,54 @@
|
|
| 110 |
display: flex;
|
| 111 |
justify-content: flex-end;
|
| 112 |
padding: 0 15px 10px 15px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
}
|
|
|
|
| 110 |
display: flex;
|
| 111 |
justify-content: flex-end;
|
| 112 |
padding: 0 15px 10px 15px;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
/* Reusable indicator styling */
|
| 117 |
+
.reusable-indicator {
|
| 118 |
+
display: inline-flex;
|
| 119 |
+
align-items: center;
|
| 120 |
+
margin-left: 8px;
|
| 121 |
+
color: #1a73e8;
|
| 122 |
+
cursor: help;
|
| 123 |
+
opacity: 0.7;
|
| 124 |
+
transition: opacity 0.2s;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.reusable-indicator:hover {
|
| 128 |
+
opacity: 1;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
/* Handle styling for better visibility */
|
| 132 |
+
.react-flow__handle {
|
| 133 |
+
width: 10px;
|
| 134 |
+
height: 10px;
|
| 135 |
+
border-radius: 50%;
|
| 136 |
+
background-color: #1a73e8;
|
| 137 |
+
border: 2px solid white;
|
| 138 |
+
transition: background-color 0.2s;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.react-flow__handle:hover {
|
| 142 |
+
background-color: #0d5bdd;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/* Source handle with special styling to indicate it's reusable */
|
| 146 |
+
.react-flow__handle-right {
|
| 147 |
+
background-color: #34a853;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.react-flow__handle-right:hover {
|
| 151 |
+
background-color: #2d9249;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
/* Enhanced styling for connections */
|
| 155 |
+
.react-flow__edge-path {
|
| 156 |
+
stroke: #1a73e8;
|
| 157 |
+
stroke-width: 2;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.react-flow__connection-path {
|
| 161 |
+
stroke: #34a853;
|
| 162 |
+
stroke-width: 2;
|
| 163 |
}
|
frontend/src/OutputNode.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
-
import { Handle, Position } from 'reactflow';
|
| 3 |
import './OutputNode.css';
|
| 4 |
|
| 5 |
-
const OutputNode = ({ data, isConnectable }) => {
|
| 6 |
const { connectedType, label, result } = data;
|
|
|
|
| 7 |
|
| 8 |
// Log the props received by the component for debugging
|
| 9 |
console.log(`[OutputNode: ${label}] Received data:`, data);
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
// Function to download the output content
|
| 12 |
const downloadOutput = () => {
|
| 13 |
if (!result) return;
|
|
@@ -175,6 +181,37 @@ const OutputNode = ({ data, isConnectable }) => {
|
|
| 175 |
</svg>
|
| 176 |
);
|
| 177 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
return (
|
| 179 |
<div className={`output-node ${data.status || ''}`}>
|
| 180 |
<Handle
|
|
@@ -186,9 +223,15 @@ const OutputNode = ({ data, isConnectable }) => {
|
|
| 186 |
<div className="output-node-header">
|
| 187 |
<strong>{label || 'Output'}</strong>
|
| 188 |
{data.status && <span className="node-status">{data.status}</span>}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
</div>
|
| 190 |
{renderContent()}
|
| 191 |
-
|
|
|
|
| 192 |
{result !== null && result !== undefined && (
|
| 193 |
<div className="output-actions">
|
| 194 |
<button
|
|
@@ -200,11 +243,15 @@ const OutputNode = ({ data, isConnectable }) => {
|
|
| 200 |
</button>
|
| 201 |
</div>
|
| 202 |
)}
|
|
|
|
|
|
|
| 203 |
<Handle
|
| 204 |
type="source"
|
| 205 |
position={Position.Right}
|
| 206 |
id="source-result"
|
| 207 |
isConnectable={isConnectable}
|
|
|
|
|
|
|
| 208 |
/>
|
| 209 |
</div>
|
| 210 |
);
|
|
|
|
| 1 |
+
import React, { useEffect } from 'react';
|
| 2 |
+
import { Handle, Position, useUpdateNodeInternals } from 'reactflow';
|
| 3 |
import './OutputNode.css';
|
| 4 |
|
| 5 |
+
const OutputNode = ({ data, isConnectable, id }) => {
|
| 6 |
const { connectedType, label, result } = data;
|
| 7 |
+
const updateNodeInternals = useUpdateNodeInternals();
|
| 8 |
|
| 9 |
// Log the props received by the component for debugging
|
| 10 |
console.log(`[OutputNode: ${label}] Received data:`, data);
|
| 11 |
|
| 12 |
+
// Make sure node internals update when result changes
|
| 13 |
+
useEffect(() => {
|
| 14 |
+
updateNodeInternals(id);
|
| 15 |
+
}, [result, id, updateNodeInternals]);
|
| 16 |
+
|
| 17 |
// Function to download the output content
|
| 18 |
const downloadOutput = () => {
|
| 19 |
if (!result) return;
|
|
|
|
| 181 |
</svg>
|
| 182 |
);
|
| 183 |
|
| 184 |
+
// Reuse icon (simple SVG)
|
| 185 |
+
const ReusableIcon = () => (
|
| 186 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 187 |
+
<path d="M17 2L21 6L17 10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
| 188 |
+
<path d="M3 11V9C3 7.89543 3.89543 7 5 7H21" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
| 189 |
+
<path d="M7 22L3 18L7 14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
| 190 |
+
<path d="M21 13V15C21 16.1046 20.1046 17 19 17H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
| 191 |
+
</svg>
|
| 192 |
+
);
|
| 193 |
+
|
| 194 |
+
// Get the correct data type for the source handle
|
| 195 |
+
const getOutputDataType = () => {
|
| 196 |
+
switch (connectedType) {
|
| 197 |
+
case 'textbox':
|
| 198 |
+
case 'markdown':
|
| 199 |
+
return 'string';
|
| 200 |
+
case 'json':
|
| 201 |
+
return 'object';
|
| 202 |
+
case 'image':
|
| 203 |
+
return 'image';
|
| 204 |
+
case 'audio':
|
| 205 |
+
return 'audio';
|
| 206 |
+
case 'video':
|
| 207 |
+
return 'video';
|
| 208 |
+
case 'file':
|
| 209 |
+
return 'file';
|
| 210 |
+
default:
|
| 211 |
+
return 'any';
|
| 212 |
+
}
|
| 213 |
+
};
|
| 214 |
+
|
| 215 |
return (
|
| 216 |
<div className={`output-node ${data.status || ''}`}>
|
| 217 |
<Handle
|
|
|
|
| 223 |
<div className="output-node-header">
|
| 224 |
<strong>{label || 'Output'}</strong>
|
| 225 |
{data.status && <span className="node-status">{data.status}</span>}
|
| 226 |
+
{result !== null && result !== undefined && (
|
| 227 |
+
<span className="reusable-indicator" title="This output can be connected to other nodes">
|
| 228 |
+
<ReusableIcon />
|
| 229 |
+
</span>
|
| 230 |
+
)}
|
| 231 |
</div>
|
| 232 |
{renderContent()}
|
| 233 |
+
|
| 234 |
+
{/* Action buttons */}
|
| 235 |
{result !== null && result !== undefined && (
|
| 236 |
<div className="output-actions">
|
| 237 |
<button
|
|
|
|
| 243 |
</button>
|
| 244 |
</div>
|
| 245 |
)}
|
| 246 |
+
|
| 247 |
+
{/* Source handle with data type and result */}
|
| 248 |
<Handle
|
| 249 |
type="source"
|
| 250 |
position={Position.Right}
|
| 251 |
id="source-result"
|
| 252 |
isConnectable={isConnectable}
|
| 253 |
+
data-type={getOutputDataType()}
|
| 254 |
+
data-result={typeof result === 'object' ? JSON.stringify(result) : String(result || '')}
|
| 255 |
/>
|
| 256 |
</div>
|
| 257 |
);
|
frontend/src/RunNode.js
CHANGED
|
@@ -3,8 +3,13 @@ import { Handle, Position } from 'reactflow';
|
|
| 3 |
import { WorkflowContext } from './App'; // Import context from App.js
|
| 4 |
import './RunNode.css';
|
| 5 |
|
| 6 |
-
const RunNode = ({ data, isConnectable }) => {
|
| 7 |
-
const {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
return (
|
| 10 |
<div className="run-node">
|
|
@@ -13,9 +18,12 @@ const RunNode = ({ data, isConnectable }) => {
|
|
| 13 |
</div>
|
| 14 |
<div className="run-node-content">
|
| 15 |
<p>Connect this node to the start of your flow and click the button to begin.</p>
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
| 19 |
</button>
|
| 20 |
</div>
|
| 21 |
<Handle
|
|
@@ -28,4 +36,4 @@ const RunNode = ({ data, isConnectable }) => {
|
|
| 28 |
);
|
| 29 |
};
|
| 30 |
|
| 31 |
-
export default RunNode;
|
|
|
|
| 3 |
import { WorkflowContext } from './App'; // Import context from App.js
|
| 4 |
import './RunNode.css';
|
| 5 |
|
| 6 |
+
const RunNode = ({ data, isConnectable, id }) => {
|
| 7 |
+
const { handleRunFromNode, isRunning } = useContext(WorkflowContext); // Get the specific run function and running state
|
| 8 |
+
|
| 9 |
+
// Run only this specific workflow branch
|
| 10 |
+
const runThisWorkflow = () => {
|
| 11 |
+
handleRunFromNode(id);
|
| 12 |
+
};
|
| 13 |
|
| 14 |
return (
|
| 15 |
<div className="run-node">
|
|
|
|
| 18 |
</div>
|
| 19 |
<div className="run-node-content">
|
| 20 |
<p>Connect this node to the start of your flow and click the button to begin.</p>
|
| 21 |
+
<button
|
| 22 |
+
onClick={runThisWorkflow}
|
| 23 |
+
className="run-button"
|
| 24 |
+
disabled={isRunning}
|
| 25 |
+
>
|
| 26 |
+
{isRunning ? '⏳ Running...' : '▶ Run Flow'}
|
| 27 |
</button>
|
| 28 |
</div>
|
| 29 |
<Handle
|
|
|
|
| 36 |
);
|
| 37 |
};
|
| 38 |
|
| 39 |
+
export default RunNode;
|