mcpgraph-ux 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/app/api/execution/breakpoints/route.ts +106 -0
- package/app/api/execution/context/route.ts +69 -0
- package/app/api/execution/controller/route.ts +121 -0
- package/app/api/execution/history/route.ts +40 -0
- package/app/api/execution/history-with-indices/route.ts +57 -0
- package/app/api/execution/stream/route.ts +34 -0
- package/app/api/graph/route.ts +99 -33
- package/app/api/tools/[toolName]/route.ts +338 -21
- package/app/api/tools/route.ts +1 -16
- package/app/page.module.css +64 -18
- package/app/page.tsx +60 -26
- package/components/DebugControls.module.css +124 -0
- package/components/DebugControls.tsx +209 -0
- package/components/ExecutionHistory.module.css +371 -0
- package/components/ExecutionHistory.tsx +272 -0
- package/components/GraphVisualization.module.css +11 -0
- package/components/GraphVisualization.tsx +353 -70
- package/components/InputForm.module.css +146 -0
- package/components/InputForm.tsx +282 -0
- package/components/ServerDetails.module.css +118 -0
- package/components/ServerDetails.tsx +116 -0
- package/components/TelemetryDashboard.module.css +177 -0
- package/components/TelemetryDashboard.tsx +154 -0
- package/components/ToolList.module.css +2 -2
- package/components/ToolTester.module.css +54 -110
- package/components/ToolTester.tsx +746 -235
- package/package.json +8 -9
|
@@ -18,22 +18,94 @@ import dagre from 'dagre';
|
|
|
18
18
|
import 'reactflow/dist/style.css';
|
|
19
19
|
import styles from './GraphVisualization.module.css';
|
|
20
20
|
|
|
21
|
+
export type NodeExecutionState = 'pending' | 'running' | 'completed' | 'error' | 'paused' | 'stopped';
|
|
22
|
+
|
|
23
|
+
export interface NodeExecutionStatus {
|
|
24
|
+
nodeId: string;
|
|
25
|
+
state: NodeExecutionState;
|
|
26
|
+
startTime?: number;
|
|
27
|
+
endTime?: number;
|
|
28
|
+
duration?: number;
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface NodeData {
|
|
33
|
+
label: string;
|
|
34
|
+
nodeType: string;
|
|
35
|
+
tool?: string;
|
|
36
|
+
server?: string;
|
|
37
|
+
args?: Record<string, unknown>;
|
|
38
|
+
transform?: { expr: string };
|
|
39
|
+
conditions?: Array<{ rule?: unknown; target: string }>;
|
|
40
|
+
executionState?: NodeExecutionState;
|
|
41
|
+
isHighlighted?: boolean;
|
|
42
|
+
isCurrentNode?: boolean;
|
|
43
|
+
hasBreakpoint?: boolean;
|
|
44
|
+
onToggleBreakpoint?: (nodeId: string) => void;
|
|
45
|
+
onNodeClick?: (nodeId: string) => void;
|
|
46
|
+
nodeId: string;
|
|
47
|
+
[key: string]: unknown;
|
|
48
|
+
}
|
|
49
|
+
|
|
21
50
|
interface GraphVisualizationProps {
|
|
22
51
|
nodes: Node[];
|
|
23
52
|
edges: Edge[];
|
|
24
53
|
selectedTool?: string | null;
|
|
54
|
+
executionState?: Map<string, NodeExecutionStatus>;
|
|
55
|
+
highlightedNode?: string | null;
|
|
56
|
+
breakpoints?: Set<string>;
|
|
57
|
+
onToggleBreakpoint?: (nodeId: string) => void;
|
|
58
|
+
onNodeClick?: (nodeId: string) => void;
|
|
59
|
+
currentNodeId?: string | null;
|
|
25
60
|
}
|
|
26
61
|
|
|
27
|
-
// Custom node styles based on node type
|
|
28
|
-
const getNodeStyle = (nodeType: string) => {
|
|
29
|
-
const baseStyle = {
|
|
62
|
+
// Custom node styles based on node type and execution state
|
|
63
|
+
const getNodeStyle = (nodeType: string, executionState?: NodeExecutionState) => {
|
|
64
|
+
const baseStyle: React.CSSProperties = {
|
|
30
65
|
padding: '10px',
|
|
31
66
|
borderRadius: '8px',
|
|
32
|
-
|
|
67
|
+
borderStyle: 'solid',
|
|
68
|
+
borderWidth: '2px',
|
|
33
69
|
fontSize: '12px',
|
|
34
70
|
fontWeight: 500,
|
|
71
|
+
transition: 'all 0.3s ease',
|
|
72
|
+
boxSizing: 'border-box', // Include border in width/height to prevent shifting
|
|
35
73
|
};
|
|
36
74
|
|
|
75
|
+
// Override colors based on execution state
|
|
76
|
+
if (executionState === 'running') {
|
|
77
|
+
return {
|
|
78
|
+
...baseStyle,
|
|
79
|
+
backgroundColor: '#fff9c4',
|
|
80
|
+
borderColor: '#fbc02d',
|
|
81
|
+
color: '#f57f17',
|
|
82
|
+
boxShadow: '0 0 10px rgba(251, 192, 45, 0.5)',
|
|
83
|
+
animation: 'pulse 1.5s ease-in-out infinite',
|
|
84
|
+
};
|
|
85
|
+
} else if (executionState === 'paused') {
|
|
86
|
+
return {
|
|
87
|
+
...baseStyle,
|
|
88
|
+
backgroundColor: '#e0f2f7',
|
|
89
|
+
borderColor: '#00bcd4',
|
|
90
|
+
color: '#006064',
|
|
91
|
+
boxShadow: '0 0 0 3px #00bcd4',
|
|
92
|
+
};
|
|
93
|
+
} else if (executionState === 'completed') {
|
|
94
|
+
return {
|
|
95
|
+
...baseStyle,
|
|
96
|
+
backgroundColor: '#c8e6c9',
|
|
97
|
+
borderColor: '#66bb6a',
|
|
98
|
+
color: '#2e7d32',
|
|
99
|
+
};
|
|
100
|
+
} else if (executionState === 'error') {
|
|
101
|
+
return {
|
|
102
|
+
...baseStyle,
|
|
103
|
+
backgroundColor: '#ffcdd2',
|
|
104
|
+
borderColor: '#ef5350',
|
|
105
|
+
color: '#c62828',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
37
109
|
switch (nodeType) {
|
|
38
110
|
case 'entry':
|
|
39
111
|
return {
|
|
@@ -80,27 +152,235 @@ const getNodeStyle = (nodeType: string) => {
|
|
|
80
152
|
}
|
|
81
153
|
};
|
|
82
154
|
|
|
83
|
-
//
|
|
84
|
-
function
|
|
155
|
+
// Node type icon component
|
|
156
|
+
function NodeTypeIcon({ nodeType }: { nodeType: string }) {
|
|
157
|
+
const iconStyle: React.CSSProperties = {
|
|
158
|
+
width: '1.5em',
|
|
159
|
+
height: '1.5em',
|
|
160
|
+
color: '#000',
|
|
161
|
+
flexShrink: 0,
|
|
162
|
+
display: 'inline-block',
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
switch (nodeType) {
|
|
166
|
+
case 'mcp':
|
|
167
|
+
return (
|
|
168
|
+
<svg
|
|
169
|
+
fill="currentColor"
|
|
170
|
+
fillRule="evenodd"
|
|
171
|
+
height="1.5em"
|
|
172
|
+
style={iconStyle}
|
|
173
|
+
viewBox="0 0 24 24"
|
|
174
|
+
width="1.5em"
|
|
175
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
176
|
+
>
|
|
177
|
+
<title>ModelContextProtocol</title>
|
|
178
|
+
<path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z"></path>
|
|
179
|
+
<path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z"></path>
|
|
180
|
+
</svg>
|
|
181
|
+
);
|
|
182
|
+
case 'entry':
|
|
183
|
+
return (
|
|
184
|
+
<svg
|
|
185
|
+
fill="currentColor"
|
|
186
|
+
height="1.5em"
|
|
187
|
+
style={iconStyle}
|
|
188
|
+
viewBox="0 0 24 24"
|
|
189
|
+
width="1.5em"
|
|
190
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
191
|
+
>
|
|
192
|
+
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeWidth="2"/>
|
|
193
|
+
<path d="M10 8l6 4-6 4V8z" />
|
|
194
|
+
</svg>
|
|
195
|
+
);
|
|
196
|
+
case 'exit':
|
|
197
|
+
return (
|
|
198
|
+
<svg
|
|
199
|
+
fill="currentColor"
|
|
200
|
+
height="1.5em"
|
|
201
|
+
style={iconStyle}
|
|
202
|
+
viewBox="0 0 24 24"
|
|
203
|
+
width="1.5em"
|
|
204
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
205
|
+
>
|
|
206
|
+
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeWidth="2"/>
|
|
207
|
+
<rect x="9" y="9" width="6" height="6" fill="currentColor"/>
|
|
208
|
+
</svg>
|
|
209
|
+
);
|
|
210
|
+
case 'transform':
|
|
211
|
+
return (
|
|
212
|
+
<svg
|
|
213
|
+
fill="currentColor"
|
|
214
|
+
height="1.5em"
|
|
215
|
+
style={iconStyle}
|
|
216
|
+
viewBox="0 0 24 24"
|
|
217
|
+
width="1.5em"
|
|
218
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
219
|
+
>
|
|
220
|
+
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" />
|
|
221
|
+
</svg>
|
|
222
|
+
);
|
|
223
|
+
case 'switch':
|
|
224
|
+
return (
|
|
225
|
+
<svg
|
|
226
|
+
fill="currentColor"
|
|
227
|
+
height="1.5em"
|
|
228
|
+
style={iconStyle}
|
|
229
|
+
viewBox="0 0 24 24"
|
|
230
|
+
width="1.5em"
|
|
231
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
232
|
+
>
|
|
233
|
+
<path d="M12 2L2 12l10 10 10-10L12 2zm0 2.83L19.17 12 12 19.17 4.83 12 12 4.83z" />
|
|
234
|
+
</svg>
|
|
235
|
+
);
|
|
236
|
+
default:
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Custom node component with top/bottom handles for vertical flow
|
|
242
|
+
function CustomNode({ data }: { data: NodeData }) {
|
|
85
243
|
const nodeType = data.nodeType || 'unknown';
|
|
86
|
-
const
|
|
244
|
+
const executionState = data.executionState;
|
|
245
|
+
const isHighlighted = data.isHighlighted;
|
|
246
|
+
const isCurrentNode = data.isCurrentNode;
|
|
247
|
+
const hasBreakpoint = data.hasBreakpoint;
|
|
248
|
+
const onToggleBreakpoint = data.onToggleBreakpoint;
|
|
249
|
+
const onNodeClick = data.onNodeClick;
|
|
250
|
+
const nodeId = data.nodeId;
|
|
251
|
+
|
|
252
|
+
const baseStyle = getNodeStyle(nodeType, executionState);
|
|
253
|
+
|
|
254
|
+
// Make current node very obvious with outline (doesn't affect layout)
|
|
255
|
+
let style: React.CSSProperties = { ...baseStyle };
|
|
256
|
+
if (isCurrentNode) {
|
|
257
|
+
// Use outline to create thicker border effect without changing element size
|
|
258
|
+
style = {
|
|
259
|
+
...baseStyle,
|
|
260
|
+
outline: '4px solid',
|
|
261
|
+
outlineColor: baseStyle.borderColor as string || '#333',
|
|
262
|
+
outlineOffset: '-2px', // Overlap with border to create thicker effect
|
|
263
|
+
zIndex: 10,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (isHighlighted) {
|
|
268
|
+
// Use outline for highlight to avoid layout shift (outline doesn't affect box model)
|
|
269
|
+
style = {
|
|
270
|
+
...style,
|
|
271
|
+
outline: isCurrentNode ? '4px solid rgba(255, 193, 7, 0.8)' : '3px solid rgba(255, 193, 7, 0.5)',
|
|
272
|
+
outlineOffset: isCurrentNode ? '-2px' : '2px',
|
|
273
|
+
boxShadow: '0 0 20px rgba(255, 193, 7, 0.3)',
|
|
274
|
+
zIndex: 10,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
87
277
|
const isEntry = nodeType === 'entry';
|
|
88
278
|
const isExit = nodeType === 'exit';
|
|
89
279
|
|
|
280
|
+
// Add status indicator
|
|
281
|
+
const statusIndicator = executionState === 'running' ? '⏳' :
|
|
282
|
+
executionState === 'completed' ? '✓' :
|
|
283
|
+
executionState === 'error' ? '✗' : null;
|
|
284
|
+
|
|
285
|
+
const handleBreakpointClick = (e: React.MouseEvent) => {
|
|
286
|
+
e.stopPropagation();
|
|
287
|
+
if (onToggleBreakpoint && nodeId) {
|
|
288
|
+
onToggleBreakpoint(nodeId);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const handleNodeClick = (e: React.MouseEvent) => {
|
|
293
|
+
// Don't trigger node click if clicking on breakpoint
|
|
294
|
+
if (e.target instanceof HTMLElement && e.target.closest('button')) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (onNodeClick && nodeId) {
|
|
298
|
+
onNodeClick(nodeId);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
90
302
|
return (
|
|
91
|
-
<div style={style}>
|
|
303
|
+
<div style={{ ...style, cursor: onNodeClick ? 'pointer' : 'default' }} onClick={handleNodeClick}>
|
|
92
304
|
{!isEntry && (
|
|
93
305
|
<Handle
|
|
94
306
|
type="target"
|
|
95
|
-
position={Position.
|
|
307
|
+
position={Position.Top}
|
|
96
308
|
style={{ background: '#555' }}
|
|
97
309
|
/>
|
|
98
310
|
)}
|
|
99
|
-
<div
|
|
311
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '5px', width: '100%' }}>
|
|
312
|
+
{/* Node type icon at far left */}
|
|
313
|
+
<NodeTypeIcon nodeType={nodeType} />
|
|
314
|
+
|
|
315
|
+
{/* Label */}
|
|
316
|
+
<span>{data.label}</span>
|
|
317
|
+
|
|
318
|
+
{/* Duration */}
|
|
319
|
+
{typeof data.duration === 'number' && (
|
|
320
|
+
<span style={{ fontSize: '10px', opacity: 0.7 }}>
|
|
321
|
+
({data.duration}ms)
|
|
322
|
+
</span>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
{/* Breakpoint button */}
|
|
326
|
+
{onToggleBreakpoint && (
|
|
327
|
+
<button
|
|
328
|
+
onClick={handleBreakpointClick}
|
|
329
|
+
style={{
|
|
330
|
+
width: 24,
|
|
331
|
+
height: 24,
|
|
332
|
+
borderRadius: '50%',
|
|
333
|
+
cursor: 'pointer',
|
|
334
|
+
padding: 0,
|
|
335
|
+
display: 'flex',
|
|
336
|
+
alignItems: 'center',
|
|
337
|
+
justifyContent: 'center',
|
|
338
|
+
border: 'none',
|
|
339
|
+
background: 'transparent',
|
|
340
|
+
flexShrink: 0,
|
|
341
|
+
}}
|
|
342
|
+
title={hasBreakpoint ? 'Click to remove breakpoint' : 'Click to add breakpoint'}
|
|
343
|
+
>
|
|
344
|
+
<span
|
|
345
|
+
style={{
|
|
346
|
+
width: 12,
|
|
347
|
+
height: 12,
|
|
348
|
+
borderRadius: '50%',
|
|
349
|
+
background: hasBreakpoint ? '#ef5350' : 'transparent',
|
|
350
|
+
border: hasBreakpoint ? '2px solid white' : '1px solid #999',
|
|
351
|
+
display: 'flex',
|
|
352
|
+
alignItems: 'center',
|
|
353
|
+
justifyContent: 'center',
|
|
354
|
+
fontSize: hasBreakpoint ? '8px' : '0',
|
|
355
|
+
boxShadow: hasBreakpoint ? '0 2px 4px rgba(0,0,0,0.2)' : 'none',
|
|
356
|
+
opacity: hasBreakpoint ? 1 : 0.5,
|
|
357
|
+
transition: 'opacity 0.2s, background 0.2s, border 0.2s',
|
|
358
|
+
}}
|
|
359
|
+
onMouseEnter={(e) => {
|
|
360
|
+
if (!hasBreakpoint) {
|
|
361
|
+
e.currentTarget.style.opacity = '1';
|
|
362
|
+
}
|
|
363
|
+
}}
|
|
364
|
+
onMouseLeave={(e) => {
|
|
365
|
+
if (!hasBreakpoint) {
|
|
366
|
+
e.currentTarget.style.opacity = '0.5';
|
|
367
|
+
}
|
|
368
|
+
}}
|
|
369
|
+
>
|
|
370
|
+
{hasBreakpoint && '●'}
|
|
371
|
+
</span>
|
|
372
|
+
</button>
|
|
373
|
+
)}
|
|
374
|
+
|
|
375
|
+
{/* Status indicators (running/completed/error) at far right */}
|
|
376
|
+
{statusIndicator && (
|
|
377
|
+
<span style={{ marginLeft: 'auto' }}>{statusIndicator}</span>
|
|
378
|
+
)}
|
|
379
|
+
</div>
|
|
100
380
|
{!isExit && (
|
|
101
381
|
<Handle
|
|
102
382
|
type="source"
|
|
103
|
-
position={Position.
|
|
383
|
+
position={Position.Bottom}
|
|
104
384
|
style={{ background: '#555' }}
|
|
105
385
|
/>
|
|
106
386
|
)}
|
|
@@ -108,6 +388,7 @@ function CustomNode({ data }: { data: any }) {
|
|
|
108
388
|
);
|
|
109
389
|
}
|
|
110
390
|
|
|
391
|
+
// Define nodeTypes outside component to avoid React Flow warning
|
|
111
392
|
const nodeTypes = {
|
|
112
393
|
custom: CustomNode,
|
|
113
394
|
};
|
|
@@ -118,9 +399,9 @@ const layoutNodes = (nodes: Node[], edges: Edge[]): Node[] => {
|
|
|
118
399
|
const g = new dagre.graphlib.Graph();
|
|
119
400
|
g.setDefaultEdgeLabel(() => ({}));
|
|
120
401
|
g.setGraph({
|
|
121
|
-
rankdir: '
|
|
122
|
-
nodesep:
|
|
123
|
-
ranksep:
|
|
402
|
+
rankdir: 'TB', // Top to bottom layout
|
|
403
|
+
nodesep: 50, // Vertical spacing between nodes
|
|
404
|
+
ranksep: 75, // Horizontal spacing between ranks
|
|
124
405
|
marginx: 50,
|
|
125
406
|
marginy: 50,
|
|
126
407
|
});
|
|
@@ -128,7 +409,8 @@ const layoutNodes = (nodes: Node[], edges: Edge[]): Node[] => {
|
|
|
128
409
|
// Add nodes to dagre graph
|
|
129
410
|
nodes.forEach(node => {
|
|
130
411
|
// Estimate node dimensions (dagre needs width/height)
|
|
131
|
-
const
|
|
412
|
+
const nodeData = node.data as NodeData;
|
|
413
|
+
const nodeType = nodeData?.nodeType || 'unknown';
|
|
132
414
|
const label = node.id;
|
|
133
415
|
// Rough estimate: ~10px per character + padding
|
|
134
416
|
const width = Math.max(150, label.length * 8 + 40);
|
|
@@ -147,7 +429,8 @@ const layoutNodes = (nodes: Node[], edges: Edge[]): Node[] => {
|
|
|
147
429
|
|
|
148
430
|
// Map dagre positions back to React Flow nodes
|
|
149
431
|
const positionedNodes = nodes.map(node => {
|
|
150
|
-
const
|
|
432
|
+
const nodeData = node.data as NodeData;
|
|
433
|
+
const nodeType = nodeData?.nodeType || 'unknown';
|
|
151
434
|
const dagreNode = g.node(node.id);
|
|
152
435
|
|
|
153
436
|
return {
|
|
@@ -171,10 +454,38 @@ export default function GraphVisualization({
|
|
|
171
454
|
nodes,
|
|
172
455
|
edges,
|
|
173
456
|
selectedTool,
|
|
457
|
+
executionState,
|
|
458
|
+
highlightedNode,
|
|
459
|
+
breakpoints,
|
|
460
|
+
onToggleBreakpoint,
|
|
461
|
+
onNodeClick,
|
|
462
|
+
currentNodeId,
|
|
174
463
|
}: GraphVisualizationProps) {
|
|
175
464
|
const layoutedNodes = useMemo(() => layoutNodes(nodes, edges), [nodes, edges]);
|
|
176
465
|
|
|
177
|
-
|
|
466
|
+
// Merge execution state, highlight, and breakpoints into nodes
|
|
467
|
+
const nodesWithExecutionState = useMemo(() => {
|
|
468
|
+
return layoutedNodes.map(node => {
|
|
469
|
+
const status = executionState?.get(node.id);
|
|
470
|
+
const isCurrentNode = currentNodeId === node.id;
|
|
471
|
+
return {
|
|
472
|
+
...node,
|
|
473
|
+
data: {
|
|
474
|
+
...node.data,
|
|
475
|
+
nodeId: node.id,
|
|
476
|
+
executionState: status?.state || 'pending',
|
|
477
|
+
duration: status?.duration,
|
|
478
|
+
isHighlighted: highlightedNode === node.id,
|
|
479
|
+
isCurrentNode, // Mark current node explicitly
|
|
480
|
+
hasBreakpoint: breakpoints?.has(node.id) || false,
|
|
481
|
+
onToggleBreakpoint,
|
|
482
|
+
onNodeClick,
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
});
|
|
486
|
+
}, [layoutedNodes, executionState, highlightedNode, breakpoints, onToggleBreakpoint, onNodeClick, currentNodeId]);
|
|
487
|
+
|
|
488
|
+
const [flowNodes, setNodes, onNodesChange] = useNodesState(nodesWithExecutionState);
|
|
178
489
|
const [flowEdges, setEdges, onEdgesChange] = useEdgesState(
|
|
179
490
|
edges.map(edge => ({
|
|
180
491
|
...edge,
|
|
@@ -189,7 +500,26 @@ export default function GraphVisualization({
|
|
|
189
500
|
|
|
190
501
|
useEffect(() => {
|
|
191
502
|
const layouted = layoutNodes(nodes, edges);
|
|
192
|
-
|
|
503
|
+
// Merge execution state, highlight, and breakpoints into nodes
|
|
504
|
+
const nodesWithState = layouted.map(node => {
|
|
505
|
+
const status = executionState?.get(node.id);
|
|
506
|
+
const isCurrentNode = currentNodeId === node.id;
|
|
507
|
+
return {
|
|
508
|
+
...node,
|
|
509
|
+
data: {
|
|
510
|
+
...node.data,
|
|
511
|
+
nodeId: node.id,
|
|
512
|
+
executionState: status?.state || 'pending',
|
|
513
|
+
duration: status?.duration,
|
|
514
|
+
isHighlighted: highlightedNode === node.id,
|
|
515
|
+
isCurrentNode, // Mark current node explicitly
|
|
516
|
+
hasBreakpoint: breakpoints?.has(node.id) || false,
|
|
517
|
+
onToggleBreakpoint,
|
|
518
|
+
onNodeClick,
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
});
|
|
522
|
+
setNodes(nodesWithState);
|
|
193
523
|
setEdges(
|
|
194
524
|
edges.map(edge => ({
|
|
195
525
|
...edge,
|
|
@@ -201,59 +531,12 @@ export default function GraphVisualization({
|
|
|
201
531
|
style: { strokeWidth: 2 },
|
|
202
532
|
}))
|
|
203
533
|
);
|
|
204
|
-
}, [nodes, edges, setNodes, setEdges]);
|
|
534
|
+
}, [nodes, edges, executionState, highlightedNode, breakpoints, onToggleBreakpoint, onNodeClick, currentNodeId, setNodes, setEdges]);
|
|
205
535
|
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const data = node.data as any;
|
|
211
|
-
return (
|
|
212
|
-
(data.nodeType === 'entry' && data.tool === selectedTool) ||
|
|
213
|
-
(data.nodeType === 'exit' && data.tool === selectedTool) ||
|
|
214
|
-
flowEdges.some(edge => {
|
|
215
|
-
// Include nodes that are reachable from entry or lead to exit
|
|
216
|
-
const entryNode = flowNodes.find(
|
|
217
|
-
n => (n.data as any)?.nodeType === 'entry' && (n.data as any)?.tool === selectedTool
|
|
218
|
-
);
|
|
219
|
-
const exitNode = flowNodes.find(
|
|
220
|
-
n => (n.data as any)?.nodeType === 'exit' && (n.data as any)?.tool === selectedTool
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
if (!entryNode || !exitNode) return false;
|
|
224
|
-
|
|
225
|
-
// Simple reachability check
|
|
226
|
-
const visited = new Set<string>();
|
|
227
|
-
const queue = [entryNode.id];
|
|
228
|
-
visited.add(entryNode.id);
|
|
229
|
-
|
|
230
|
-
while (queue.length > 0) {
|
|
231
|
-
const current = queue.shift()!;
|
|
232
|
-
if (current === node.id) return true;
|
|
233
|
-
|
|
234
|
-
flowEdges
|
|
235
|
-
.filter(e => e.source === current)
|
|
236
|
-
.forEach(e => {
|
|
237
|
-
if (!visited.has(e.target)) {
|
|
238
|
-
visited.add(e.target);
|
|
239
|
-
queue.push(e.target);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return false;
|
|
245
|
-
})
|
|
246
|
-
);
|
|
247
|
-
});
|
|
248
|
-
}, [flowNodes, flowEdges, selectedTool]);
|
|
249
|
-
|
|
250
|
-
const filteredEdges = useMemo(() => {
|
|
251
|
-
if (!selectedTool) return flowEdges;
|
|
252
|
-
const filteredNodeIds = new Set(filteredNodes.map(n => n.id));
|
|
253
|
-
return flowEdges.filter(
|
|
254
|
-
edge => filteredNodeIds.has(edge.source) && filteredNodeIds.has(edge.target)
|
|
255
|
-
);
|
|
256
|
-
}, [flowEdges, filteredNodes, selectedTool]);
|
|
536
|
+
// No need to filter - the API already returns nodes for the selected tool
|
|
537
|
+
// The selectedTool prop is kept for potential future use but filtering is done server-side
|
|
538
|
+
const filteredNodes = flowNodes;
|
|
539
|
+
const filteredEdges = flowEdges;
|
|
257
540
|
|
|
258
541
|
return (
|
|
259
542
|
<div className={styles.container}>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 12px;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.form {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: 16px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.inputs {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
gap: 16px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.field {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: 4px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.label {
|
|
26
|
+
font-weight: 600;
|
|
27
|
+
color: #333;
|
|
28
|
+
font-size: 0.9rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.checkboxLabel {
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
gap: 8px;
|
|
35
|
+
font-weight: 600;
|
|
36
|
+
color: #333;
|
|
37
|
+
font-size: 0.9rem;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.required {
|
|
42
|
+
color: #d32f2f;
|
|
43
|
+
margin-left: 4px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.input {
|
|
47
|
+
padding: 8px 12px;
|
|
48
|
+
border: 1px solid #ddd;
|
|
49
|
+
border-radius: 4px;
|
|
50
|
+
font-size: 0.9rem;
|
|
51
|
+
font-family: inherit;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.input:focus {
|
|
55
|
+
outline: none;
|
|
56
|
+
border-color: #2196f3;
|
|
57
|
+
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.input:disabled {
|
|
61
|
+
background-color: #f5f5f5;
|
|
62
|
+
cursor: not-allowed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.textarea {
|
|
66
|
+
padding: 8px 12px;
|
|
67
|
+
border: 1px solid #ddd;
|
|
68
|
+
border-radius: 4px;
|
|
69
|
+
font-size: 0.9rem;
|
|
70
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
71
|
+
resize: vertical;
|
|
72
|
+
min-height: 60px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.textarea:focus {
|
|
76
|
+
outline: none;
|
|
77
|
+
border-color: #2196f3;
|
|
78
|
+
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.textarea:disabled {
|
|
82
|
+
background-color: #f5f5f5;
|
|
83
|
+
cursor: not-allowed;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.checkbox {
|
|
87
|
+
width: 18px;
|
|
88
|
+
height: 18px;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.checkbox:disabled {
|
|
93
|
+
cursor: not-allowed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.hint {
|
|
97
|
+
font-size: 0.85rem;
|
|
98
|
+
color: #666;
|
|
99
|
+
font-style: italic;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.loading {
|
|
103
|
+
padding: 1rem;
|
|
104
|
+
text-align: center;
|
|
105
|
+
color: #666;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.error {
|
|
109
|
+
padding: 1rem;
|
|
110
|
+
background-color: #ffebee;
|
|
111
|
+
color: #c62828;
|
|
112
|
+
border-radius: 4px;
|
|
113
|
+
border: 1px solid #ffcdd2;
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
justify-content: space-between;
|
|
117
|
+
gap: 12px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.dismissButton {
|
|
121
|
+
background: none;
|
|
122
|
+
border: none;
|
|
123
|
+
color: #c62828;
|
|
124
|
+
font-size: 1.5rem;
|
|
125
|
+
line-height: 1;
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
padding: 0;
|
|
128
|
+
width: 24px;
|
|
129
|
+
height: 24px;
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
justify-content: center;
|
|
133
|
+
border-radius: 4px;
|
|
134
|
+
flex-shrink: 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.dismissButton:hover {
|
|
138
|
+
background-color: rgba(198, 40, 40, 0.1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.dismissButton:focus {
|
|
142
|
+
outline: 2px solid #c62828;
|
|
143
|
+
outline-offset: 2px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|