hadinicknam commited on
Commit
d57a1c0
·
1 Parent(s): 2dc2fbc

Resolve a bug related to the Running Node

Browse files
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
- const { status, result, ...restData } = n.data;
277
- return { ...n, data: restData };
 
 
 
278
  }));
279
  };
280
 
@@ -475,22 +499,48 @@ function App() {
475
  }
476
  };
477
 
478
- const handleRun = async () => {
479
- addLog("================== WORKFLOW RUNNING ==================", 'INFO');
480
- resetNodeStatuses();
481
-
482
- const nodesMap = new Map(nodes.map(n => [n.id, n]));
483
- const inDegree = new Map(nodes.map(n => [n.id, 0]));
484
- edges.forEach(edge => {
485
- inDegree.set(edge.target, (inDegree.get(edge.target) || 0) + 1);
486
- });
487
-
488
- const executionResults = new Map();
489
- const initialNodes = nodes.filter(n => inDegree.get(n.id) === 0);
490
 
491
- addLog(`Found ${initialNodes.length} initial node(s) to execute.`);
492
- for (const node of initialNodes) {
493
- await executeNode(node, executionResults, nodesMap);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- {/* Only show download button when there's a result */}
 
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 { handleRun } = useContext(WorkflowContext); // Consume the context
 
 
 
 
 
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
- {/* The button now calls the function directly from the context */}
17
- <button onClick={handleRun} className="run-button">
18
- &#9658; Run Flow
 
 
 
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;