mcpgraph-ux 0.1.1 → 0.1.3
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 +2 -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 +63 -29
- package/app/api/tools/[toolName]/route.ts +268 -17
- package/app/api/tools/route.ts +3 -15
- package/app/page.module.css +64 -18
- package/app/page.tsx +38 -15
- package/components/DebugControls.module.css +124 -0
- package/components/DebugControls.tsx +209 -0
- package/components/ExecutionHistory.module.css +268 -0
- package/components/ExecutionHistory.tsx +197 -0
- package/components/GraphVisualization.module.css +11 -0
- package/components/GraphVisualization.tsx +350 -21
- package/components/InputForm.module.css +115 -0
- package/components/InputForm.tsx +271 -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 +24 -119
- package/components/ToolTester.tsx +627 -229
- package/package.json +2 -2
- package/server.ts +24 -10
|
@@ -18,22 +18,87 @@ 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
|
+
[key: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
21
43
|
interface GraphVisualizationProps {
|
|
22
44
|
nodes: Node[];
|
|
23
45
|
edges: Edge[];
|
|
24
46
|
selectedTool?: string | null;
|
|
47
|
+
executionState?: Map<string, NodeExecutionStatus>;
|
|
48
|
+
highlightedNode?: string | null;
|
|
49
|
+
breakpoints?: Set<string>;
|
|
50
|
+
onToggleBreakpoint?: (nodeId: string) => void;
|
|
51
|
+
onNodeClick?: (nodeId: string) => void;
|
|
52
|
+
currentNodeId?: string | null;
|
|
25
53
|
}
|
|
26
54
|
|
|
27
|
-
// Custom node styles based on node type
|
|
28
|
-
const getNodeStyle = (nodeType: string) => {
|
|
29
|
-
const baseStyle = {
|
|
55
|
+
// Custom node styles based on node type and execution state
|
|
56
|
+
const getNodeStyle = (nodeType: string, executionState?: NodeExecutionState) => {
|
|
57
|
+
const baseStyle: React.CSSProperties = {
|
|
30
58
|
padding: '10px',
|
|
31
59
|
borderRadius: '8px',
|
|
32
|
-
|
|
60
|
+
borderStyle: 'solid',
|
|
61
|
+
borderWidth: '2px',
|
|
33
62
|
fontSize: '12px',
|
|
34
63
|
fontWeight: 500,
|
|
64
|
+
transition: 'all 0.3s ease',
|
|
65
|
+
boxSizing: 'border-box', // Include border in width/height to prevent shifting
|
|
35
66
|
};
|
|
36
67
|
|
|
68
|
+
// Override colors based on execution state
|
|
69
|
+
if (executionState === 'running') {
|
|
70
|
+
return {
|
|
71
|
+
...baseStyle,
|
|
72
|
+
backgroundColor: '#fff9c4',
|
|
73
|
+
borderColor: '#fbc02d',
|
|
74
|
+
color: '#f57f17',
|
|
75
|
+
boxShadow: '0 0 10px rgba(251, 192, 45, 0.5)',
|
|
76
|
+
animation: 'pulse 1.5s ease-in-out infinite',
|
|
77
|
+
};
|
|
78
|
+
} else if (executionState === 'paused') {
|
|
79
|
+
return {
|
|
80
|
+
...baseStyle,
|
|
81
|
+
backgroundColor: '#e0f2f7',
|
|
82
|
+
borderColor: '#00bcd4',
|
|
83
|
+
color: '#006064',
|
|
84
|
+
boxShadow: '0 0 0 3px #00bcd4',
|
|
85
|
+
};
|
|
86
|
+
} else if (executionState === 'completed') {
|
|
87
|
+
return {
|
|
88
|
+
...baseStyle,
|
|
89
|
+
backgroundColor: '#c8e6c9',
|
|
90
|
+
borderColor: '#66bb6a',
|
|
91
|
+
color: '#2e7d32',
|
|
92
|
+
};
|
|
93
|
+
} else if (executionState === 'error') {
|
|
94
|
+
return {
|
|
95
|
+
...baseStyle,
|
|
96
|
+
backgroundColor: '#ffcdd2',
|
|
97
|
+
borderColor: '#ef5350',
|
|
98
|
+
color: '#c62828',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
37
102
|
switch (nodeType) {
|
|
38
103
|
case 'entry':
|
|
39
104
|
return {
|
|
@@ -80,27 +145,235 @@ const getNodeStyle = (nodeType: string) => {
|
|
|
80
145
|
}
|
|
81
146
|
};
|
|
82
147
|
|
|
83
|
-
//
|
|
148
|
+
// Node type icon component
|
|
149
|
+
function NodeTypeIcon({ nodeType }: { nodeType: string }) {
|
|
150
|
+
const iconStyle: React.CSSProperties = {
|
|
151
|
+
width: '1.5em',
|
|
152
|
+
height: '1.5em',
|
|
153
|
+
color: '#000',
|
|
154
|
+
flexShrink: 0,
|
|
155
|
+
display: 'inline-block',
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
switch (nodeType) {
|
|
159
|
+
case 'mcp':
|
|
160
|
+
return (
|
|
161
|
+
<svg
|
|
162
|
+
fill="currentColor"
|
|
163
|
+
fillRule="evenodd"
|
|
164
|
+
height="1.5em"
|
|
165
|
+
style={iconStyle}
|
|
166
|
+
viewBox="0 0 24 24"
|
|
167
|
+
width="1.5em"
|
|
168
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
169
|
+
>
|
|
170
|
+
<title>ModelContextProtocol</title>
|
|
171
|
+
<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>
|
|
172
|
+
<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>
|
|
173
|
+
</svg>
|
|
174
|
+
);
|
|
175
|
+
case 'entry':
|
|
176
|
+
return (
|
|
177
|
+
<svg
|
|
178
|
+
fill="currentColor"
|
|
179
|
+
height="1.5em"
|
|
180
|
+
style={iconStyle}
|
|
181
|
+
viewBox="0 0 24 24"
|
|
182
|
+
width="1.5em"
|
|
183
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
184
|
+
>
|
|
185
|
+
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeWidth="2"/>
|
|
186
|
+
<path d="M10 8l6 4-6 4V8z" />
|
|
187
|
+
</svg>
|
|
188
|
+
);
|
|
189
|
+
case 'exit':
|
|
190
|
+
return (
|
|
191
|
+
<svg
|
|
192
|
+
fill="currentColor"
|
|
193
|
+
height="1.5em"
|
|
194
|
+
style={iconStyle}
|
|
195
|
+
viewBox="0 0 24 24"
|
|
196
|
+
width="1.5em"
|
|
197
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
198
|
+
>
|
|
199
|
+
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeWidth="2"/>
|
|
200
|
+
<rect x="9" y="9" width="6" height="6" fill="currentColor"/>
|
|
201
|
+
</svg>
|
|
202
|
+
);
|
|
203
|
+
case 'transform':
|
|
204
|
+
return (
|
|
205
|
+
<svg
|
|
206
|
+
fill="currentColor"
|
|
207
|
+
height="1.5em"
|
|
208
|
+
style={iconStyle}
|
|
209
|
+
viewBox="0 0 24 24"
|
|
210
|
+
width="1.5em"
|
|
211
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
212
|
+
>
|
|
213
|
+
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" />
|
|
214
|
+
</svg>
|
|
215
|
+
);
|
|
216
|
+
case 'switch':
|
|
217
|
+
return (
|
|
218
|
+
<svg
|
|
219
|
+
fill="currentColor"
|
|
220
|
+
height="1.5em"
|
|
221
|
+
style={iconStyle}
|
|
222
|
+
viewBox="0 0 24 24"
|
|
223
|
+
width="1.5em"
|
|
224
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
225
|
+
>
|
|
226
|
+
<path d="M12 2L2 12l10 10 10-10L12 2zm0 2.83L19.17 12 12 19.17 4.83 12 12 4.83z" />
|
|
227
|
+
</svg>
|
|
228
|
+
);
|
|
229
|
+
default:
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Custom node component with top/bottom handles for vertical flow
|
|
84
235
|
function CustomNode({ data }: { data: any }) {
|
|
85
236
|
const nodeType = data.nodeType || 'unknown';
|
|
86
|
-
const
|
|
237
|
+
const executionState = data.executionState as NodeExecutionState | undefined;
|
|
238
|
+
const isHighlighted = data.isHighlighted as boolean | undefined;
|
|
239
|
+
const isCurrentNode = data.isCurrentNode as boolean | undefined;
|
|
240
|
+
const hasBreakpoint = data.hasBreakpoint as boolean | undefined;
|
|
241
|
+
const onToggleBreakpoint = data.onToggleBreakpoint as ((nodeId: string) => void) | undefined;
|
|
242
|
+
const onNodeClick = data.onNodeClick as ((nodeId: string) => void) | undefined;
|
|
243
|
+
const nodeId = data.nodeId as string;
|
|
244
|
+
|
|
245
|
+
const baseStyle = getNodeStyle(nodeType, executionState);
|
|
246
|
+
|
|
247
|
+
// Make current node very obvious with outline (doesn't affect layout)
|
|
248
|
+
let style: React.CSSProperties = { ...baseStyle };
|
|
249
|
+
if (isCurrentNode) {
|
|
250
|
+
// Use outline to create thicker border effect without changing element size
|
|
251
|
+
style = {
|
|
252
|
+
...baseStyle,
|
|
253
|
+
outline: '4px solid',
|
|
254
|
+
outlineColor: baseStyle.borderColor as string || '#333',
|
|
255
|
+
outlineOffset: '-2px', // Overlap with border to create thicker effect
|
|
256
|
+
zIndex: 10,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (isHighlighted) {
|
|
261
|
+
// Use outline for highlight to avoid layout shift (outline doesn't affect box model)
|
|
262
|
+
style = {
|
|
263
|
+
...style,
|
|
264
|
+
outline: isCurrentNode ? '4px solid rgba(255, 193, 7, 0.8)' : '3px solid rgba(255, 193, 7, 0.5)',
|
|
265
|
+
outlineOffset: isCurrentNode ? '-2px' : '2px',
|
|
266
|
+
boxShadow: '0 0 20px rgba(255, 193, 7, 0.3)',
|
|
267
|
+
zIndex: 10,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
87
270
|
const isEntry = nodeType === 'entry';
|
|
88
271
|
const isExit = nodeType === 'exit';
|
|
89
272
|
|
|
273
|
+
// Add status indicator
|
|
274
|
+
const statusIndicator = executionState === 'running' ? '⏳' :
|
|
275
|
+
executionState === 'completed' ? '✓' :
|
|
276
|
+
executionState === 'error' ? '✗' : null;
|
|
277
|
+
|
|
278
|
+
const handleBreakpointClick = (e: React.MouseEvent) => {
|
|
279
|
+
e.stopPropagation();
|
|
280
|
+
if (onToggleBreakpoint && nodeId) {
|
|
281
|
+
onToggleBreakpoint(nodeId);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const handleNodeClick = (e: React.MouseEvent) => {
|
|
286
|
+
// Don't trigger node click if clicking on breakpoint
|
|
287
|
+
if ((e.target as HTMLElement).closest('button')) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (onNodeClick && nodeId) {
|
|
291
|
+
onNodeClick(nodeId);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
90
295
|
return (
|
|
91
|
-
<div style={style}>
|
|
296
|
+
<div style={{ ...style, cursor: onNodeClick ? 'pointer' : 'default' }} onClick={handleNodeClick}>
|
|
92
297
|
{!isEntry && (
|
|
93
298
|
<Handle
|
|
94
299
|
type="target"
|
|
95
|
-
position={Position.
|
|
300
|
+
position={Position.Top}
|
|
96
301
|
style={{ background: '#555' }}
|
|
97
302
|
/>
|
|
98
303
|
)}
|
|
99
|
-
<div
|
|
304
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '5px', width: '100%' }}>
|
|
305
|
+
{/* Node type icon at far left */}
|
|
306
|
+
<NodeTypeIcon nodeType={nodeType} />
|
|
307
|
+
|
|
308
|
+
{/* Label */}
|
|
309
|
+
<span>{data.label}</span>
|
|
310
|
+
|
|
311
|
+
{/* Duration */}
|
|
312
|
+
{data.duration !== undefined && (
|
|
313
|
+
<span style={{ fontSize: '10px', opacity: 0.7 }}>
|
|
314
|
+
({data.duration}ms)
|
|
315
|
+
</span>
|
|
316
|
+
)}
|
|
317
|
+
|
|
318
|
+
{/* Breakpoint button */}
|
|
319
|
+
{onToggleBreakpoint && (
|
|
320
|
+
<button
|
|
321
|
+
onClick={handleBreakpointClick}
|
|
322
|
+
style={{
|
|
323
|
+
width: 24,
|
|
324
|
+
height: 24,
|
|
325
|
+
borderRadius: '50%',
|
|
326
|
+
cursor: 'pointer',
|
|
327
|
+
padding: 0,
|
|
328
|
+
display: 'flex',
|
|
329
|
+
alignItems: 'center',
|
|
330
|
+
justifyContent: 'center',
|
|
331
|
+
border: 'none',
|
|
332
|
+
background: 'transparent',
|
|
333
|
+
flexShrink: 0,
|
|
334
|
+
}}
|
|
335
|
+
title={hasBreakpoint ? 'Click to remove breakpoint' : 'Click to add breakpoint'}
|
|
336
|
+
>
|
|
337
|
+
<span
|
|
338
|
+
style={{
|
|
339
|
+
width: 12,
|
|
340
|
+
height: 12,
|
|
341
|
+
borderRadius: '50%',
|
|
342
|
+
background: hasBreakpoint ? '#ef5350' : 'transparent',
|
|
343
|
+
border: hasBreakpoint ? '2px solid white' : '1px solid #999',
|
|
344
|
+
display: 'flex',
|
|
345
|
+
alignItems: 'center',
|
|
346
|
+
justifyContent: 'center',
|
|
347
|
+
fontSize: hasBreakpoint ? '8px' : '0',
|
|
348
|
+
boxShadow: hasBreakpoint ? '0 2px 4px rgba(0,0,0,0.2)' : 'none',
|
|
349
|
+
opacity: hasBreakpoint ? 1 : 0.5,
|
|
350
|
+
transition: 'opacity 0.2s, background 0.2s, border 0.2s',
|
|
351
|
+
}}
|
|
352
|
+
onMouseEnter={(e) => {
|
|
353
|
+
if (!hasBreakpoint) {
|
|
354
|
+
e.currentTarget.style.opacity = '1';
|
|
355
|
+
}
|
|
356
|
+
}}
|
|
357
|
+
onMouseLeave={(e) => {
|
|
358
|
+
if (!hasBreakpoint) {
|
|
359
|
+
e.currentTarget.style.opacity = '0.5';
|
|
360
|
+
}
|
|
361
|
+
}}
|
|
362
|
+
>
|
|
363
|
+
{hasBreakpoint && '●'}
|
|
364
|
+
</span>
|
|
365
|
+
</button>
|
|
366
|
+
)}
|
|
367
|
+
|
|
368
|
+
{/* Status indicators (running/completed/error) at far right */}
|
|
369
|
+
{statusIndicator && (
|
|
370
|
+
<span style={{ marginLeft: 'auto' }}>{statusIndicator}</span>
|
|
371
|
+
)}
|
|
372
|
+
</div>
|
|
100
373
|
{!isExit && (
|
|
101
374
|
<Handle
|
|
102
375
|
type="source"
|
|
103
|
-
position={Position.
|
|
376
|
+
position={Position.Bottom}
|
|
104
377
|
style={{ background: '#555' }}
|
|
105
378
|
/>
|
|
106
379
|
)}
|
|
@@ -108,6 +381,7 @@ function CustomNode({ data }: { data: any }) {
|
|
|
108
381
|
);
|
|
109
382
|
}
|
|
110
383
|
|
|
384
|
+
// Define nodeTypes outside component to avoid React Flow warning
|
|
111
385
|
const nodeTypes = {
|
|
112
386
|
custom: CustomNode,
|
|
113
387
|
};
|
|
@@ -118,9 +392,9 @@ const layoutNodes = (nodes: Node[], edges: Edge[]): Node[] => {
|
|
|
118
392
|
const g = new dagre.graphlib.Graph();
|
|
119
393
|
g.setDefaultEdgeLabel(() => ({}));
|
|
120
394
|
g.setGraph({
|
|
121
|
-
rankdir: '
|
|
122
|
-
nodesep:
|
|
123
|
-
ranksep:
|
|
395
|
+
rankdir: 'TB', // Top to bottom layout
|
|
396
|
+
nodesep: 50, // Vertical spacing between nodes
|
|
397
|
+
ranksep: 75, // Horizontal spacing between ranks
|
|
124
398
|
marginx: 50,
|
|
125
399
|
marginy: 50,
|
|
126
400
|
});
|
|
@@ -128,7 +402,8 @@ const layoutNodes = (nodes: Node[], edges: Edge[]): Node[] => {
|
|
|
128
402
|
// Add nodes to dagre graph
|
|
129
403
|
nodes.forEach(node => {
|
|
130
404
|
// Estimate node dimensions (dagre needs width/height)
|
|
131
|
-
const
|
|
405
|
+
const nodeData = node.data as NodeData;
|
|
406
|
+
const nodeType = nodeData?.nodeType || 'unknown';
|
|
132
407
|
const label = node.id;
|
|
133
408
|
// Rough estimate: ~10px per character + padding
|
|
134
409
|
const width = Math.max(150, label.length * 8 + 40);
|
|
@@ -147,7 +422,8 @@ const layoutNodes = (nodes: Node[], edges: Edge[]): Node[] => {
|
|
|
147
422
|
|
|
148
423
|
// Map dagre positions back to React Flow nodes
|
|
149
424
|
const positionedNodes = nodes.map(node => {
|
|
150
|
-
const
|
|
425
|
+
const nodeData = node.data as NodeData;
|
|
426
|
+
const nodeType = nodeData?.nodeType || 'unknown';
|
|
151
427
|
const dagreNode = g.node(node.id);
|
|
152
428
|
|
|
153
429
|
return {
|
|
@@ -171,10 +447,38 @@ export default function GraphVisualization({
|
|
|
171
447
|
nodes,
|
|
172
448
|
edges,
|
|
173
449
|
selectedTool,
|
|
450
|
+
executionState,
|
|
451
|
+
highlightedNode,
|
|
452
|
+
breakpoints,
|
|
453
|
+
onToggleBreakpoint,
|
|
454
|
+
onNodeClick,
|
|
455
|
+
currentNodeId,
|
|
174
456
|
}: GraphVisualizationProps) {
|
|
175
457
|
const layoutedNodes = useMemo(() => layoutNodes(nodes, edges), [nodes, edges]);
|
|
176
458
|
|
|
177
|
-
|
|
459
|
+
// Merge execution state, highlight, and breakpoints into nodes
|
|
460
|
+
const nodesWithExecutionState = useMemo(() => {
|
|
461
|
+
return layoutedNodes.map(node => {
|
|
462
|
+
const status = executionState?.get(node.id);
|
|
463
|
+
const isCurrentNode = currentNodeId === node.id;
|
|
464
|
+
return {
|
|
465
|
+
...node,
|
|
466
|
+
data: {
|
|
467
|
+
...node.data,
|
|
468
|
+
nodeId: node.id,
|
|
469
|
+
executionState: status?.state || 'pending',
|
|
470
|
+
duration: status?.duration,
|
|
471
|
+
isHighlighted: highlightedNode === node.id,
|
|
472
|
+
isCurrentNode, // Mark current node explicitly
|
|
473
|
+
hasBreakpoint: breakpoints?.has(node.id) || false,
|
|
474
|
+
onToggleBreakpoint,
|
|
475
|
+
onNodeClick,
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
});
|
|
479
|
+
}, [layoutedNodes, executionState, highlightedNode, breakpoints, onToggleBreakpoint, onNodeClick, currentNodeId]);
|
|
480
|
+
|
|
481
|
+
const [flowNodes, setNodes, onNodesChange] = useNodesState(nodesWithExecutionState);
|
|
178
482
|
const [flowEdges, setEdges, onEdgesChange] = useEdgesState(
|
|
179
483
|
edges.map(edge => ({
|
|
180
484
|
...edge,
|
|
@@ -189,7 +493,26 @@ export default function GraphVisualization({
|
|
|
189
493
|
|
|
190
494
|
useEffect(() => {
|
|
191
495
|
const layouted = layoutNodes(nodes, edges);
|
|
192
|
-
|
|
496
|
+
// Merge execution state, highlight, and breakpoints into nodes
|
|
497
|
+
const nodesWithState = layouted.map(node => {
|
|
498
|
+
const status = executionState?.get(node.id);
|
|
499
|
+
const isCurrentNode = currentNodeId === node.id;
|
|
500
|
+
return {
|
|
501
|
+
...node,
|
|
502
|
+
data: {
|
|
503
|
+
...node.data,
|
|
504
|
+
nodeId: node.id,
|
|
505
|
+
executionState: status?.state || 'pending',
|
|
506
|
+
duration: status?.duration,
|
|
507
|
+
isHighlighted: highlightedNode === node.id,
|
|
508
|
+
isCurrentNode, // Mark current node explicitly
|
|
509
|
+
hasBreakpoint: breakpoints?.has(node.id) || false,
|
|
510
|
+
onToggleBreakpoint,
|
|
511
|
+
onNodeClick,
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
});
|
|
515
|
+
setNodes(nodesWithState);
|
|
193
516
|
setEdges(
|
|
194
517
|
edges.map(edge => ({
|
|
195
518
|
...edge,
|
|
@@ -201,23 +524,29 @@ export default function GraphVisualization({
|
|
|
201
524
|
style: { strokeWidth: 2 },
|
|
202
525
|
}))
|
|
203
526
|
);
|
|
204
|
-
}, [nodes, edges, setNodes, setEdges]);
|
|
527
|
+
}, [nodes, edges, executionState, highlightedNode, breakpoints, onToggleBreakpoint, onNodeClick, currentNodeId, setNodes, setEdges]);
|
|
205
528
|
|
|
206
529
|
// Filter nodes/edges for selected tool if provided
|
|
207
530
|
const filteredNodes = useMemo(() => {
|
|
208
531
|
if (!selectedTool) return flowNodes;
|
|
209
532
|
return flowNodes.filter(node => {
|
|
210
|
-
const data = node.data as
|
|
533
|
+
const data = node.data as NodeData;
|
|
211
534
|
return (
|
|
212
535
|
(data.nodeType === 'entry' && data.tool === selectedTool) ||
|
|
213
536
|
(data.nodeType === 'exit' && data.tool === selectedTool) ||
|
|
214
537
|
flowEdges.some(edge => {
|
|
215
538
|
// Include nodes that are reachable from entry or lead to exit
|
|
216
539
|
const entryNode = flowNodes.find(
|
|
217
|
-
n =>
|
|
540
|
+
n => {
|
|
541
|
+
const nData = n.data as NodeData;
|
|
542
|
+
return nData?.nodeType === 'entry' && nData?.tool === selectedTool;
|
|
543
|
+
}
|
|
218
544
|
);
|
|
219
545
|
const exitNode = flowNodes.find(
|
|
220
|
-
n =>
|
|
546
|
+
n => {
|
|
547
|
+
const nData = n.data as NodeData;
|
|
548
|
+
return nData?.nodeType === 'exit' && nData?.tool === selectedTool;
|
|
549
|
+
}
|
|
221
550
|
);
|
|
222
551
|
|
|
223
552
|
if (!entryNode || !exitNode) return false;
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
}
|
|
115
|
+
|