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.
@@ -0,0 +1,272 @@
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
+ <div className={styles.dataSection}>
114
+ <div className={styles.dataItem}>
115
+ <strong>Input:</strong>
116
+ <pre className={styles.jsonData}>{formatJSON(record.input)}</pre>
117
+ </div>
118
+ {hasError ? (
119
+ <div className={styles.dataItem}>
120
+ <strong>Error:</strong>
121
+ <pre className={styles.errorOutput}>
122
+ {(() => {
123
+ const err = record.error;
124
+ if (!err) return 'Unknown error';
125
+
126
+ // Extract error properties
127
+ const errorCode = 'code' in err && typeof err.code === 'number' ? err.code : null;
128
+ const errorData = 'data' in err ? err.data : null;
129
+ const errorType = 'errorType' in err && typeof err.errorType === 'string' ? err.errorType : 'unknown';
130
+ const stderr = 'stderr' in err && Array.isArray(err.stderr) ? err.stderr : null;
131
+ const result = 'result' in err ? err.result : null;
132
+
133
+ // Clean the message - remove stderr part if stderr is available as separate property
134
+ let errorText = err.message || 'Unknown error';
135
+ if (stderr && stderr.length > 0) {
136
+ // Remove the stderr part that was concatenated into the message
137
+ errorText = errorText.split('\n\nServer stderr output:')[0].trim();
138
+ }
139
+
140
+ // Prepend error code if available
141
+ if (errorCode !== null) {
142
+ errorText = `[Error ${errorCode}] ${errorText}`;
143
+ }
144
+
145
+ return (
146
+ <>
147
+ <div className={styles.errorTypeBadge}>
148
+ {errorType === 'mcp' && 'ðŸ”ī MCP Protocol Error'}
149
+ {errorType === 'tool' && 'ðŸŸĄ Tool Error'}
150
+ {errorType === 'unknown' && '❌ Error'}
151
+ </div>
152
+ {errorText}
153
+
154
+ {/* Show stderr for MCP errors */}
155
+ {stderr && stderr.length > 0 && (
156
+ <div className={styles.errorSection}>
157
+ <strong>Server stderr output:</strong>
158
+ <pre className={styles.stderrOutput}>{stderr.join('\n')}</pre>
159
+ </div>
160
+ )}
161
+
162
+ {/* Show error data for MCP errors */}
163
+ {errorData !== null && errorData !== undefined && (
164
+ <div className={styles.errorSection}>
165
+ <strong>Error Details:</strong>
166
+ <pre className={styles.errorDataPre}>{formatJSON(errorData)}</pre>
167
+ </div>
168
+ )}
169
+
170
+ {/* Show result for tool errors */}
171
+ {result !== null && result !== undefined && (
172
+ <div className={styles.errorSection}>
173
+ <strong>Tool Call Result:</strong>
174
+ <pre className={styles.errorDataPre}>{formatJSON(result)}</pre>
175
+ </div>
176
+ )}
177
+
178
+ {err.stack && (
179
+ <div className={styles.errorSection}>
180
+ <strong>Stack Trace:</strong>
181
+ <pre className={styles.errorDataPre}>{err.stack}</pre>
182
+ </div>
183
+ )}
184
+ </>
185
+ );
186
+ })()}
187
+ </pre>
188
+ </div>
189
+ ) : record.output !== undefined ? (
190
+ <div className={styles.dataItem}>
191
+ <strong>Output:</strong>
192
+ <pre className={styles.jsonData}>{formatJSON(record.output)}</pre>
193
+ </div>
194
+ ) : (
195
+ <div className={styles.dataItem}>
196
+ <strong>Output:</strong>
197
+ <div style={{ fontStyle: 'italic', color: '#666' }}>Pending execution</div>
198
+ </div>
199
+ )}
200
+ </div>
201
+
202
+ <div className={styles.metadata}>
203
+ <div>Start: {formatTime(record.startTime)}</div>
204
+ {record.endTime !== undefined && (
205
+ <div>End: {formatTime(record.endTime)}</div>
206
+ )}
207
+ {record.duration !== undefined && (
208
+ <div>Duration: {formatDuration(record.duration)}</div>
209
+ )}
210
+ {(record.endTime === undefined || record.duration === undefined) && (
211
+ <div style={{ fontStyle: 'italic', color: '#666' }}>Status: Pending</div>
212
+ )}
213
+ </div>
214
+ </div>
215
+ )}
216
+ </div>
217
+ );
218
+ })}
219
+ </div>
220
+
221
+ {/* Result display at the bottom - always expanded and styled to stand out */}
222
+ {result !== null && result !== undefined && (
223
+ <div className={`${styles.resultItem} ${result && typeof result === 'object' && 'error' in result ? styles.errorResultItem : ''}`}>
224
+ <div className={styles.resultHeader}>
225
+ <div className={styles.resultTitle}>
226
+ {result && typeof result === 'object' && 'error' in result ? (
227
+ <>
228
+ <span className={styles.errorIcon}>✗</span>
229
+ <strong style={{ color: '#c62828' }}>Execution Error</strong>
230
+ </>
231
+ ) : (
232
+ <>
233
+ <span className={styles.resultIcon}>✓</span>
234
+ <strong>Final Result</strong>
235
+ </>
236
+ )}
237
+ </div>
238
+ {telemetry && (
239
+ <div className={styles.resultStats}>
240
+ <span className={styles.statItem}>
241
+ <strong>Elapsed:</strong> {formatDuration(telemetry.totalDuration)}
242
+ </span>
243
+ <span className={styles.statItem}>
244
+ <strong>Nodes:</strong> {Object.values(telemetry.nodeCounts).reduce((sum, count) => sum + count, 0)}
245
+ </span>
246
+ {telemetry.errorCount > 0 && (
247
+ <span className={`${styles.statItem} ${styles.errorStat}`}>
248
+ <strong>Errors:</strong> {telemetry.errorCount}
249
+ </span>
250
+ )}
251
+ </div>
252
+ )}
253
+ </div>
254
+ <div className={styles.resultContent}>
255
+ {result && typeof result === 'object' && 'error' in result ? (
256
+ <div className={styles.errorResult}>
257
+ <pre className={styles.errorPre}>
258
+ {typeof result.error === 'string'
259
+ ? result.error
260
+ : 'Execution failed - see node errors above for details'}
261
+ </pre>
262
+ </div>
263
+ ) : (
264
+ <pre className={styles.resultPre}>{formatJSON(result)}</pre>
265
+ )}
266
+ </div>
267
+ </div>
268
+ )}
269
+ </div>
270
+ );
271
+ }
272
+
@@ -4,3 +4,14 @@
4
4
  flex: 1;
5
5
  }
6
6
 
7
+ @keyframes pulse {
8
+ 0%, 100% {
9
+ opacity: 1;
10
+ transform: scale(1);
11
+ }
12
+ 50% {
13
+ opacity: 0.8;
14
+ transform: scale(1.02);
15
+ }
16
+ }
17
+