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
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import styles from './DebugControls.module.css';
|
|
5
|
+
|
|
6
|
+
export type ExecutionStatus = 'not_started' | 'running' | 'paused' | 'finished' | 'error' | 'stopped';
|
|
7
|
+
|
|
8
|
+
interface DebugControlsProps {
|
|
9
|
+
executionId: string | null;
|
|
10
|
+
status: ExecutionStatus;
|
|
11
|
+
currentNodeId: string | null;
|
|
12
|
+
onStatusChange?: (status: ExecutionStatus) => void;
|
|
13
|
+
onRun?: () => void;
|
|
14
|
+
onStepFromStart?: () => void;
|
|
15
|
+
onClear?: () => void;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function DebugControls({
|
|
20
|
+
executionId,
|
|
21
|
+
status,
|
|
22
|
+
currentNodeId,
|
|
23
|
+
onStatusChange,
|
|
24
|
+
onRun,
|
|
25
|
+
onStepFromStart,
|
|
26
|
+
onClear,
|
|
27
|
+
disabled,
|
|
28
|
+
}: DebugControlsProps) {
|
|
29
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
30
|
+
|
|
31
|
+
const handlePause = async () => {
|
|
32
|
+
if (!executionId || status !== 'running') return;
|
|
33
|
+
|
|
34
|
+
setIsProcessing(true);
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetch('/api/execution/controller', {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
body: JSON.stringify({ executionId, action: 'pause' }),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
if (data.error) {
|
|
44
|
+
console.error('Error pausing:', data.error);
|
|
45
|
+
}
|
|
46
|
+
// Don't set status here - onPause hook will send stateUpdate with actual state
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('Error pausing execution:', error);
|
|
49
|
+
} finally {
|
|
50
|
+
setIsProcessing(false);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleResume = async () => {
|
|
55
|
+
if (!executionId || status !== 'paused') return;
|
|
56
|
+
|
|
57
|
+
setIsProcessing(true);
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch('/api/execution/controller', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({ executionId, action: 'resume' }),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const data = await response.json();
|
|
66
|
+
if (data.error) {
|
|
67
|
+
console.error('Error resuming:', data.error);
|
|
68
|
+
}
|
|
69
|
+
// Don't set status here - hooks will tell us the actual state
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Error resuming execution:', error);
|
|
72
|
+
} finally {
|
|
73
|
+
setIsProcessing(false);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const handleStep = async () => {
|
|
78
|
+
if (!executionId || status !== 'paused') return;
|
|
79
|
+
|
|
80
|
+
setIsProcessing(true);
|
|
81
|
+
try {
|
|
82
|
+
const response = await fetch('/api/execution/controller', {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: { 'Content-Type': 'application/json' },
|
|
85
|
+
body: JSON.stringify({ executionId, action: 'step' }),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const data = await response.json();
|
|
89
|
+
if (data.error) {
|
|
90
|
+
console.error('Error stepping:', data.error);
|
|
91
|
+
}
|
|
92
|
+
// Don't set status here - onPause hook will send stateUpdate with actual state after step
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Error stepping execution:', error);
|
|
95
|
+
} finally {
|
|
96
|
+
setIsProcessing(false);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleRun = () => {
|
|
101
|
+
if (onRun) {
|
|
102
|
+
onRun();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleStop = async () => {
|
|
107
|
+
if (!executionId || (status !== 'running' && status !== 'paused')) return;
|
|
108
|
+
|
|
109
|
+
setIsProcessing(true);
|
|
110
|
+
try {
|
|
111
|
+
const response = await fetch('/api/execution/controller', {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: { 'Content-Type': 'application/json' },
|
|
114
|
+
body: JSON.stringify({ executionId, action: 'stop' }),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const data = await response.json();
|
|
118
|
+
if (data.error) {
|
|
119
|
+
console.error('Error stopping:', data.error);
|
|
120
|
+
}
|
|
121
|
+
// Don't set status here - executionStopped event will be sent and handled in ToolTester
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('Error stopping execution:', error);
|
|
124
|
+
} finally {
|
|
125
|
+
setIsProcessing(false);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const handleClear = () => {
|
|
130
|
+
if (onClear) {
|
|
131
|
+
onClear();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className={styles.container}>
|
|
137
|
+
<div className={styles.controlsRow}>
|
|
138
|
+
<div className={styles.controls}>
|
|
139
|
+
<button
|
|
140
|
+
onClick={handleRun}
|
|
141
|
+
disabled={disabled || isProcessing || status === 'running' || status === 'paused'}
|
|
142
|
+
className={styles.button}
|
|
143
|
+
title="Run execution"
|
|
144
|
+
>
|
|
145
|
+
▶ Run
|
|
146
|
+
</button>
|
|
147
|
+
<button
|
|
148
|
+
onClick={status === 'paused' ? handleStep : (onStepFromStart || handleRun)}
|
|
149
|
+
disabled={isProcessing || (status !== 'paused' && status !== 'not_started' && status !== 'finished' && status !== 'error' && status !== 'stopped') || (status === 'not_started' && disabled)}
|
|
150
|
+
className={styles.button}
|
|
151
|
+
title={status === 'paused' ? 'Step to next node' : 'Start execution and pause at first node'}
|
|
152
|
+
>
|
|
153
|
+
⏭ Step
|
|
154
|
+
</button>
|
|
155
|
+
<button
|
|
156
|
+
onClick={handlePause}
|
|
157
|
+
disabled={isProcessing || status !== 'running'}
|
|
158
|
+
className={styles.button}
|
|
159
|
+
title="Pause execution"
|
|
160
|
+
>
|
|
161
|
+
⏸ Pause
|
|
162
|
+
</button>
|
|
163
|
+
<button
|
|
164
|
+
onClick={handleResume}
|
|
165
|
+
disabled={isProcessing || status !== 'paused'}
|
|
166
|
+
className={styles.button}
|
|
167
|
+
title="Resume execution"
|
|
168
|
+
>
|
|
169
|
+
▶ Resume
|
|
170
|
+
</button>
|
|
171
|
+
<button
|
|
172
|
+
onClick={handleStop}
|
|
173
|
+
disabled={isProcessing || (status !== 'running' && status !== 'paused')}
|
|
174
|
+
className={styles.button}
|
|
175
|
+
title="Stop/cancel execution"
|
|
176
|
+
>
|
|
177
|
+
⏹ Stop
|
|
178
|
+
</button>
|
|
179
|
+
<button
|
|
180
|
+
onClick={handleClear}
|
|
181
|
+
disabled={isProcessing || (status !== 'finished' && status !== 'error' && status !== 'stopped')}
|
|
182
|
+
className={styles.button}
|
|
183
|
+
title="Clear execution history and reset state"
|
|
184
|
+
>
|
|
185
|
+
× Clear
|
|
186
|
+
</button>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{(status === 'running' || status === 'paused' || status === 'finished' || status === 'error' || status === 'stopped') && (
|
|
190
|
+
<div className={styles.statusInfo}>
|
|
191
|
+
{currentNodeId && (
|
|
192
|
+
<>
|
|
193
|
+
<span className={styles.currentNode}>
|
|
194
|
+
Current: <code>{currentNodeId}</code>
|
|
195
|
+
</span>
|
|
196
|
+
<span className={styles.separator}>•</span>
|
|
197
|
+
</>
|
|
198
|
+
)}
|
|
199
|
+
<span className={styles.statusLabel}>Status:</span>
|
|
200
|
+
<span className={`${styles.statusBadge} ${styles[status]}`}>
|
|
201
|
+
{status.toUpperCase()}
|
|
202
|
+
</span>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
width: 100%;
|
|
3
|
+
height: 100%;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
background: white;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
.empty {
|
|
12
|
+
padding: 24px;
|
|
13
|
+
text-align: center;
|
|
14
|
+
color: #999;
|
|
15
|
+
font-style: italic;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.list {
|
|
19
|
+
overflow-y: auto;
|
|
20
|
+
flex: 1;
|
|
21
|
+
min-height: 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.item {
|
|
25
|
+
border-bottom: 1px solid #e0e0e0;
|
|
26
|
+
transition: background-color 0.2s;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.item:hover {
|
|
30
|
+
background-color: #f9f9f9;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.item.error {
|
|
34
|
+
border-left: 4px solid #ef5350;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.header {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: space-between;
|
|
41
|
+
padding: 12px 16px;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
user-select: none;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.nodeInfo {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: 8px;
|
|
50
|
+
flex: 1;
|
|
51
|
+
min-width: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.nodeId {
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
color: #333;
|
|
57
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
58
|
+
font-size: 13px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.nodeType {
|
|
62
|
+
font-size: 11px;
|
|
63
|
+
color: #666;
|
|
64
|
+
background: #e8e8e8;
|
|
65
|
+
padding: 2px 6px;
|
|
66
|
+
border-radius: 4px;
|
|
67
|
+
text-transform: uppercase;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.errorBadge {
|
|
71
|
+
font-size: 10px;
|
|
72
|
+
color: white;
|
|
73
|
+
background: #ef5350;
|
|
74
|
+
padding: 2px 6px;
|
|
75
|
+
border-radius: 4px;
|
|
76
|
+
font-weight: 600;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.timing {
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: 12px;
|
|
83
|
+
margin-left: 12px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.duration {
|
|
87
|
+
font-weight: 600;
|
|
88
|
+
color: #2196f3;
|
|
89
|
+
font-size: 12px;
|
|
90
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.time {
|
|
94
|
+
font-size: 11px;
|
|
95
|
+
color: #999;
|
|
96
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.expandButton {
|
|
100
|
+
background: none;
|
|
101
|
+
border: none;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
font-size: 10px;
|
|
104
|
+
color: #666;
|
|
105
|
+
padding: 4px 8px;
|
|
106
|
+
margin-left: 8px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.expandButton:hover {
|
|
110
|
+
color: #333;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.details {
|
|
114
|
+
padding: 12px 16px;
|
|
115
|
+
background: #fafafa;
|
|
116
|
+
border-top: 1px solid #e0e0e0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.errorSection {
|
|
120
|
+
margin-bottom: 16px;
|
|
121
|
+
padding: 12px;
|
|
122
|
+
background: #ffebee;
|
|
123
|
+
border-radius: 4px;
|
|
124
|
+
border-left: 4px solid #ef5350;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.errorSection strong {
|
|
128
|
+
display: block;
|
|
129
|
+
margin-bottom: 8px;
|
|
130
|
+
color: #c62828;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.errorMessage {
|
|
134
|
+
margin: 0;
|
|
135
|
+
font-size: 12px;
|
|
136
|
+
color: #c62828;
|
|
137
|
+
white-space: pre-wrap;
|
|
138
|
+
word-break: break-word;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.stackTrace {
|
|
142
|
+
margin-top: 8px;
|
|
143
|
+
font-size: 11px;
|
|
144
|
+
color: #999;
|
|
145
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.dataSection {
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-direction: column;
|
|
151
|
+
gap: 12px;
|
|
152
|
+
margin-bottom: 12px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.dataItem {
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-direction: column;
|
|
158
|
+
gap: 4px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.dataItem strong {
|
|
162
|
+
font-size: 12px;
|
|
163
|
+
color: #666;
|
|
164
|
+
text-transform: uppercase;
|
|
165
|
+
letter-spacing: 0.5px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.jsonData {
|
|
169
|
+
margin: 0;
|
|
170
|
+
padding: 8px;
|
|
171
|
+
background: white;
|
|
172
|
+
border: 1px solid #e0e0e0;
|
|
173
|
+
border-radius: 4px;
|
|
174
|
+
font-size: 11px;
|
|
175
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
176
|
+
overflow-x: auto;
|
|
177
|
+
max-height: 200px;
|
|
178
|
+
overflow-y: auto;
|
|
179
|
+
white-space: pre-wrap;
|
|
180
|
+
word-break: break-word;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.metadata {
|
|
184
|
+
display: flex;
|
|
185
|
+
gap: 16px;
|
|
186
|
+
font-size: 11px;
|
|
187
|
+
color: #666;
|
|
188
|
+
padding-top: 8px;
|
|
189
|
+
border-top: 1px solid #e0e0e0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.metadata div {
|
|
193
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* Result item - styled to stand out at the bottom */
|
|
197
|
+
.resultItem {
|
|
198
|
+
border-top: 3px solid #4caf50;
|
|
199
|
+
background: linear-gradient(to bottom, #f1f8f4 0%, #ffffff 100%);
|
|
200
|
+
margin-top: auto;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.resultHeader {
|
|
204
|
+
display: flex;
|
|
205
|
+
justify-content: space-between;
|
|
206
|
+
align-items: center;
|
|
207
|
+
padding: 16px;
|
|
208
|
+
border-bottom: 2px solid #4caf50;
|
|
209
|
+
flex-wrap: wrap;
|
|
210
|
+
gap: 12px;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.resultTitle {
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
gap: 8px;
|
|
217
|
+
font-size: 14px;
|
|
218
|
+
color: #2e7d32;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.resultIcon {
|
|
222
|
+
font-size: 18px;
|
|
223
|
+
color: #4caf50;
|
|
224
|
+
font-weight: bold;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.resultStats {
|
|
228
|
+
display: flex;
|
|
229
|
+
gap: 16px;
|
|
230
|
+
flex-wrap: wrap;
|
|
231
|
+
font-size: 12px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.statItem {
|
|
235
|
+
color: #666;
|
|
236
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.statItem strong {
|
|
240
|
+
color: #333;
|
|
241
|
+
margin-right: 4px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.errorStat {
|
|
245
|
+
color: #d32f2f;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.resultContent {
|
|
249
|
+
padding: 16px;
|
|
250
|
+
background: white;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.resultPre {
|
|
254
|
+
margin: 0;
|
|
255
|
+
padding: 12px;
|
|
256
|
+
background: #fafafa;
|
|
257
|
+
border: 1px solid #e0e0e0;
|
|
258
|
+
border-radius: 4px;
|
|
259
|
+
font-size: 12px;
|
|
260
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
261
|
+
overflow-x: auto;
|
|
262
|
+
max-height: 300px;
|
|
263
|
+
overflow-y: auto;
|
|
264
|
+
white-space: pre-wrap;
|
|
265
|
+
word-break: break-word;
|
|
266
|
+
color: #333;
|
|
267
|
+
}
|
|
268
|
+
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import styles from './ExecutionHistory.module.css';
|
|
5
|
+
|
|
6
|
+
export interface NodeExecutionRecord {
|
|
7
|
+
nodeId: string;
|
|
8
|
+
nodeType: string;
|
|
9
|
+
startTime: number;
|
|
10
|
+
endTime?: number; // Optional for pending/running nodes
|
|
11
|
+
duration?: number; // Optional for pending/running nodes
|
|
12
|
+
input: unknown;
|
|
13
|
+
output?: unknown; // Optional for pending/running nodes
|
|
14
|
+
error?: Error;
|
|
15
|
+
executionIndex?: number; // For tracking specific node instances
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ExecutionHistoryProps {
|
|
19
|
+
history: NodeExecutionRecord[];
|
|
20
|
+
onNodeClick?: (nodeId: string) => void;
|
|
21
|
+
result?: unknown;
|
|
22
|
+
telemetry?: {
|
|
23
|
+
totalDuration: number;
|
|
24
|
+
nodeCounts: Record<string, number>;
|
|
25
|
+
errorCount: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function ExecutionHistory({ history, onNodeClick, result, telemetry }: ExecutionHistoryProps) {
|
|
30
|
+
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
|
|
31
|
+
|
|
32
|
+
const toggleExpand = (nodeId: string) => {
|
|
33
|
+
const newExpanded = new Set(expandedNodes);
|
|
34
|
+
if (newExpanded.has(nodeId)) {
|
|
35
|
+
newExpanded.delete(nodeId);
|
|
36
|
+
} else {
|
|
37
|
+
newExpanded.add(nodeId);
|
|
38
|
+
}
|
|
39
|
+
setExpandedNodes(newExpanded);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const formatTime = (timestamp: number) => {
|
|
43
|
+
return new Date(timestamp).toLocaleTimeString('en-US', {
|
|
44
|
+
hour12: false,
|
|
45
|
+
hour: '2-digit',
|
|
46
|
+
minute: '2-digit',
|
|
47
|
+
second: '2-digit',
|
|
48
|
+
fractionalSecondDigits: 3,
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const formatDuration = (ms: number) => {
|
|
53
|
+
if (ms < 1) return `${(ms * 1000).toFixed(0)}μs`;
|
|
54
|
+
if (ms < 1000) return `${ms.toFixed(2)}ms`;
|
|
55
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const formatJSON = (data: unknown) => {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.stringify(data, null, 2);
|
|
61
|
+
} catch {
|
|
62
|
+
return String(data);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (history.length === 0 && !result) {
|
|
67
|
+
return (
|
|
68
|
+
<div className={styles.container}>
|
|
69
|
+
<div className={styles.empty}>No execution history available</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className={styles.container}>
|
|
76
|
+
<div className={styles.list}>
|
|
77
|
+
{history.map((record, index) => {
|
|
78
|
+
const isExpanded = expandedNodes.has(record.nodeId);
|
|
79
|
+
const hasError = !!record.error;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
key={`${record.nodeId}-${index}`}
|
|
84
|
+
className={`${styles.item} ${hasError ? styles.error : ''}`}
|
|
85
|
+
>
|
|
86
|
+
<div
|
|
87
|
+
className={styles.header}
|
|
88
|
+
onClick={() => {
|
|
89
|
+
toggleExpand(record.nodeId);
|
|
90
|
+
onNodeClick?.(record.nodeId);
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
<div className={styles.nodeInfo}>
|
|
94
|
+
<span className={styles.nodeId}>{record.nodeId}</span>
|
|
95
|
+
<span className={styles.nodeType}>{record.nodeType}</span>
|
|
96
|
+
{hasError && <span className={styles.errorBadge}>ERROR</span>}
|
|
97
|
+
</div>
|
|
98
|
+
<div className={styles.timing}>
|
|
99
|
+
{record.duration !== undefined ? (
|
|
100
|
+
<span className={styles.duration}>{formatDuration(record.duration)}</span>
|
|
101
|
+
) : (
|
|
102
|
+
<span className={styles.duration} style={{ fontStyle: 'italic', color: '#666' }}>pending</span>
|
|
103
|
+
)}
|
|
104
|
+
<span className={styles.time}>{formatTime(record.startTime)}</span>
|
|
105
|
+
</div>
|
|
106
|
+
<button className={styles.expandButton}>
|
|
107
|
+
{isExpanded ? '▼' : '▶'}
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{isExpanded && (
|
|
112
|
+
<div className={styles.details}>
|
|
113
|
+
{hasError && (
|
|
114
|
+
<div className={styles.errorSection}>
|
|
115
|
+
<strong>Error:</strong>
|
|
116
|
+
<pre className={styles.errorMessage}>
|
|
117
|
+
{record.error?.message || 'Unknown error'}
|
|
118
|
+
{record.error?.stack && (
|
|
119
|
+
<div className={styles.stackTrace}>
|
|
120
|
+
{record.error.stack}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</pre>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
<div className={styles.dataSection}>
|
|
128
|
+
<div className={styles.dataItem}>
|
|
129
|
+
<strong>Input:</strong>
|
|
130
|
+
<pre className={styles.jsonData}>{formatJSON(record.input)}</pre>
|
|
131
|
+
</div>
|
|
132
|
+
{!hasError && record.output !== undefined && (
|
|
133
|
+
<div className={styles.dataItem}>
|
|
134
|
+
<strong>Output:</strong>
|
|
135
|
+
<pre className={styles.jsonData}>{formatJSON(record.output)}</pre>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
{!hasError && record.output === undefined && (
|
|
139
|
+
<div className={styles.dataItem}>
|
|
140
|
+
<strong>Output:</strong>
|
|
141
|
+
<div style={{ fontStyle: 'italic', color: '#666' }}>Pending execution</div>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div className={styles.metadata}>
|
|
147
|
+
<div>Start: {formatTime(record.startTime)}</div>
|
|
148
|
+
{record.endTime !== undefined && (
|
|
149
|
+
<div>End: {formatTime(record.endTime)}</div>
|
|
150
|
+
)}
|
|
151
|
+
{record.duration !== undefined && (
|
|
152
|
+
<div>Duration: {formatDuration(record.duration)}</div>
|
|
153
|
+
)}
|
|
154
|
+
{(record.endTime === undefined || record.duration === undefined) && (
|
|
155
|
+
<div style={{ fontStyle: 'italic', color: '#666' }}>Status: Pending</div>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
})}
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* Result display at the bottom - always expanded and styled to stand out */}
|
|
166
|
+
{result !== null && result !== undefined && (
|
|
167
|
+
<div className={styles.resultItem}>
|
|
168
|
+
<div className={styles.resultHeader}>
|
|
169
|
+
<div className={styles.resultTitle}>
|
|
170
|
+
<span className={styles.resultIcon}>✓</span>
|
|
171
|
+
<strong>Final Result</strong>
|
|
172
|
+
</div>
|
|
173
|
+
{telemetry && (
|
|
174
|
+
<div className={styles.resultStats}>
|
|
175
|
+
<span className={styles.statItem}>
|
|
176
|
+
<strong>Elapsed:</strong> {formatDuration(telemetry.totalDuration)}
|
|
177
|
+
</span>
|
|
178
|
+
<span className={styles.statItem}>
|
|
179
|
+
<strong>Nodes:</strong> {Object.values(telemetry.nodeCounts).reduce((sum, count) => sum + count, 0)}
|
|
180
|
+
</span>
|
|
181
|
+
{telemetry.errorCount > 0 && (
|
|
182
|
+
<span className={`${styles.statItem} ${styles.errorStat}`}>
|
|
183
|
+
<strong>Errors:</strong> {telemetry.errorCount}
|
|
184
|
+
</span>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
<div className={styles.resultContent}>
|
|
190
|
+
<pre className={styles.resultPre}>{formatJSON(result)}</pre>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|