agentflow-dashboard 0.1.4 → 0.3.0
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/bin/dashboard.js +7 -5
- package/dist/chunk-2FTN742J.js +1168 -0
- package/dist/cli.cjs +1349 -0
- package/dist/cli.js +151 -0
- package/dist/index.cjs +839 -157
- package/dist/index.js +5 -483
- package/dist/public/dashboard.js +1487 -289
- package/dist/public/index.html +1016 -163
- package/package.json +5 -5
- package/public/dashboard.js +1487 -289
- package/public/index.html +1016 -163
package/public/dashboard.js
CHANGED
|
@@ -1,342 +1,1540 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentFlow Dashboard — Production monitoring UI for AI agent infrastructure.
|
|
3
|
+
* Connects to the Express backend via REST + WebSocket.
|
|
4
|
+
* Handles 860+ traces efficiently with DOM limiting and lazy loading.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function escapeHtml(str) {
|
|
8
|
+
if (typeof str !== 'string') return str == null ? '' : String(str);
|
|
9
|
+
return str
|
|
10
|
+
.replace(/&/g, '&')
|
|
11
|
+
.replace(/</g, '<')
|
|
12
|
+
.replace(/>/g, '>')
|
|
13
|
+
.replace(/"/g, '"')
|
|
14
|
+
.replace(/'/g, ''');
|
|
15
|
+
}
|
|
16
|
+
|
|
1
17
|
class AgentFlowDashboard {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
18
|
+
constructor() {
|
|
19
|
+
this.ws = null;
|
|
20
|
+
this.reconnectAttempts = 0;
|
|
21
|
+
this.maxReconnectAttempts = 20;
|
|
22
|
+
this.reconnectDelay = 1000;
|
|
23
|
+
|
|
24
|
+
this.traces = [];
|
|
25
|
+
this.stats = null;
|
|
26
|
+
this.processHealth = null;
|
|
27
|
+
this.selectedTrace = null;
|
|
28
|
+
this.selectedTraceData = null;
|
|
29
|
+
this.activeTab = 'timeline';
|
|
30
|
+
this.searchFilter = '';
|
|
31
|
+
this.statusFilter = 'all';
|
|
32
|
+
this.timeRangeFilter = 'all';
|
|
33
|
+
this.isLive = true;
|
|
34
|
+
|
|
35
|
+
this.cy = null;
|
|
36
|
+
|
|
37
|
+
this.init();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Initialization
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
init() {
|
|
44
|
+
this.connectWebSocket();
|
|
45
|
+
this.loadInitialData();
|
|
46
|
+
this.loadProcessHealth();
|
|
47
|
+
this.setupEventListeners();
|
|
48
|
+
this._healthInterval = setInterval(() => this.loadProcessHealth(), 10000);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// WebSocket with auto-reconnect + exponential backoff
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
connectWebSocket() {
|
|
55
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
56
|
+
const wsUrl = protocol + '//' + window.location.host;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
this.ws = new WebSocket(wsUrl);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error('WebSocket creation failed:', e);
|
|
62
|
+
this.updateConnectionStatus(false);
|
|
63
|
+
this.attemptReconnect();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.ws.onopen = () => {
|
|
68
|
+
this.reconnectAttempts = 0;
|
|
69
|
+
this.updateConnectionStatus(true);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
this.ws.onmessage = (event) => {
|
|
73
|
+
try {
|
|
74
|
+
var message = JSON.parse(event.data);
|
|
75
|
+
this.handleWebSocketMessage(message);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.error('WS parse error:', e);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
this.ws.onclose = () => {
|
|
82
|
+
this.updateConnectionStatus(false);
|
|
83
|
+
this.attemptReconnect();
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
this.ws.onerror = () => {
|
|
87
|
+
this.updateConnectionStatus(false);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
attemptReconnect() {
|
|
92
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
|
|
93
|
+
this.reconnectAttempts++;
|
|
94
|
+
var delay = this.reconnectDelay * Math.min(Math.pow(1.5, this.reconnectAttempts - 1), 30);
|
|
95
|
+
setTimeout(() => this.connectWebSocket(), delay);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
handleWebSocketMessage(msg) {
|
|
99
|
+
switch (msg.type) {
|
|
100
|
+
case 'init':
|
|
101
|
+
if (msg.data && msg.data.traces) this.traces = msg.data.traces;
|
|
102
|
+
if (msg.data && msg.data.stats) this.stats = msg.data.stats;
|
|
103
|
+
this.renderTraceList();
|
|
104
|
+
this.renderStatsOverview();
|
|
105
|
+
if (this.traces.length > 0 && !this.selectedTrace) {
|
|
106
|
+
this.selectTrace(this.traces[0].filename);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
case 'trace-added':
|
|
110
|
+
if (this.isLive) {
|
|
111
|
+
this.traces.unshift(msg.data);
|
|
112
|
+
this.renderTraceList();
|
|
113
|
+
this.refreshStats();
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
case 'trace-updated': {
|
|
117
|
+
var idx = this.traces.findIndex(function(t) { return t.filename === msg.data.filename; });
|
|
118
|
+
if (idx >= 0) this.traces[idx] = msg.data;
|
|
119
|
+
this.renderTraceList();
|
|
120
|
+
if (this.selectedTrace && this.selectedTrace.filename === msg.data.filename) {
|
|
121
|
+
this.selectedTrace = msg.data;
|
|
122
|
+
this.selectedTraceData = msg.data;
|
|
123
|
+
this.renderActiveTab();
|
|
58
124
|
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case 'stats-updated':
|
|
128
|
+
this.stats = msg.data;
|
|
129
|
+
this.renderStatsOverview();
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
updateConnectionStatus(connected) {
|
|
135
|
+
var dot = document.getElementById('connectionDot');
|
|
136
|
+
var text = document.getElementById('connectionText');
|
|
137
|
+
var liveInd = document.getElementById('liveIndicator');
|
|
138
|
+
if (connected) {
|
|
139
|
+
dot.className = 'status-dot connected';
|
|
140
|
+
text.textContent = 'Connected';
|
|
141
|
+
if (liveInd) liveInd.className = 'live-indicator active';
|
|
142
|
+
} else {
|
|
143
|
+
dot.className = 'status-dot';
|
|
144
|
+
text.textContent = 'Disconnected';
|
|
145
|
+
if (liveInd) liveInd.className = 'live-indicator';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Data loading
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
async loadInitialData() {
|
|
153
|
+
try {
|
|
154
|
+
var results = await Promise.all([
|
|
155
|
+
fetch('/api/traces'),
|
|
156
|
+
fetch('/api/stats')
|
|
157
|
+
]);
|
|
158
|
+
if (results[0].ok) this.traces = await results[0].json();
|
|
159
|
+
if (results[1].ok) this.stats = await results[1].json();
|
|
160
|
+
this.renderTraceList();
|
|
161
|
+
this.renderStatsOverview();
|
|
162
|
+
// Auto-select first trace
|
|
163
|
+
if (this.traces.length > 0 && !this.selectedTrace) {
|
|
164
|
+
this.selectTrace(this.traces[0].filename);
|
|
165
|
+
}
|
|
166
|
+
} catch (e) {
|
|
167
|
+
console.error('Initial data load failed:', e);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async refreshStats() {
|
|
172
|
+
try {
|
|
173
|
+
var res = await fetch('/api/stats');
|
|
174
|
+
if (res.ok) {
|
|
175
|
+
this.stats = await res.json();
|
|
176
|
+
this.renderStatsOverview();
|
|
177
|
+
}
|
|
178
|
+
} catch (e) {
|
|
179
|
+
console.error('Stats refresh failed:', e);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async loadTraceDetail(filename) {
|
|
184
|
+
try {
|
|
185
|
+
var res = await fetch('/api/traces/' + encodeURIComponent(filename));
|
|
186
|
+
if (res.ok) {
|
|
187
|
+
this.selectedTraceData = await res.json();
|
|
188
|
+
this.renderActiveTab();
|
|
189
|
+
}
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.error('Trace detail load failed:', e);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async loadProcessHealth() {
|
|
196
|
+
try {
|
|
197
|
+
var res = await fetch('/api/process-health');
|
|
198
|
+
if (!res.ok) return;
|
|
199
|
+
this.processHealth = await res.json();
|
|
200
|
+
this.renderProcessHealth();
|
|
201
|
+
} catch (e) {
|
|
202
|
+
// silent — endpoint may not always be available
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Event listeners
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
setupEventListeners() {
|
|
210
|
+
var self = this;
|
|
211
|
+
|
|
212
|
+
// Tab switching
|
|
213
|
+
document.querySelectorAll('.tab').forEach(function(tab) {
|
|
214
|
+
tab.addEventListener('click', function() {
|
|
215
|
+
self.activeTab = tab.dataset.tab;
|
|
216
|
+
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
|
|
217
|
+
tab.classList.add('active');
|
|
218
|
+
document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
|
|
219
|
+
document.getElementById('panel-' + self.activeTab).classList.add('active');
|
|
220
|
+
self.renderActiveTab();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Search
|
|
225
|
+
document.getElementById('traceSearch').addEventListener('input', function(e) {
|
|
226
|
+
self.searchFilter = e.target.value.toLowerCase();
|
|
227
|
+
self.renderTraceList();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Status filter dropdown
|
|
231
|
+
document.getElementById('statusFilter').addEventListener('change', function(e) {
|
|
232
|
+
self.statusFilter = e.target.value;
|
|
233
|
+
self.renderTraceList();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Time range filter dropdown
|
|
237
|
+
document.getElementById('timeRangeFilter').addEventListener('change', function(e) {
|
|
238
|
+
self.timeRangeFilter = e.target.value;
|
|
239
|
+
self.renderTraceList();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Toolbar buttons
|
|
243
|
+
document.getElementById('btnFit').addEventListener('click', function() {
|
|
244
|
+
if (self.cy) self.cy.fit(50);
|
|
245
|
+
});
|
|
246
|
+
document.getElementById('btnLayout').addEventListener('click', function() {
|
|
247
|
+
self.runCytoscapeLayout();
|
|
248
|
+
});
|
|
249
|
+
document.getElementById('btnExportPng').addEventListener('click', function() {
|
|
250
|
+
self.exportGraphPNG();
|
|
251
|
+
});
|
|
252
|
+
document.getElementById('btnRefresh').addEventListener('click', function() {
|
|
253
|
+
self.loadInitialData();
|
|
254
|
+
self.loadProcessHealth();
|
|
255
|
+
});
|
|
256
|
+
document.getElementById('btnPlayPause').addEventListener('click', function() {
|
|
257
|
+
self.isLive = !self.isLive;
|
|
258
|
+
var btn = document.getElementById('btnPlayPause');
|
|
259
|
+
btn.innerHTML = self.isLive ? '⏸' : '▶';
|
|
260
|
+
btn.title = self.isLive ? 'Pause live tail' : 'Resume live tail';
|
|
261
|
+
var liveInd = document.getElementById('liveIndicator');
|
|
262
|
+
if (self.isLive && self.ws && self.ws.readyState === WebSocket.OPEN) {
|
|
263
|
+
liveInd.className = 'live-indicator active';
|
|
264
|
+
} else {
|
|
265
|
+
liveInd.className = 'live-indicator';
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Node detail close
|
|
270
|
+
document.getElementById('nodeDetailClose').addEventListener('click', function() {
|
|
271
|
+
document.getElementById('nodeDetailPanel').classList.remove('active');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Trace list click delegation
|
|
275
|
+
document.getElementById('traceList').addEventListener('click', function(e) {
|
|
276
|
+
var item = e.target.closest('.session-item');
|
|
277
|
+
if (!item) return;
|
|
278
|
+
var filename = item.dataset.filename;
|
|
279
|
+
self.selectTrace(filename);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Auto-refresh stats every 30s
|
|
283
|
+
setInterval(function() {
|
|
284
|
+
if (self.ws && self.ws.readyState === WebSocket.OPEN) {
|
|
285
|
+
self.refreshStats();
|
|
286
|
+
}
|
|
287
|
+
}, 30000);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Trace selection
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
selectTrace(filename) {
|
|
294
|
+
var trace = this.traces.find(function(t) { return t.filename === filename; });
|
|
295
|
+
if (!trace) return;
|
|
296
|
+
|
|
297
|
+
this.selectedTrace = trace;
|
|
298
|
+
this.selectedTraceData = trace;
|
|
299
|
+
|
|
300
|
+
// Update sidebar selection
|
|
301
|
+
document.querySelectorAll('.session-item').forEach(function(el) { el.classList.remove('active'); });
|
|
302
|
+
var activeEl = document.querySelector('.session-item[data-filename="' + CSS.escape(filename) + '"]');
|
|
303
|
+
if (activeEl) {
|
|
304
|
+
activeEl.classList.add('active');
|
|
305
|
+
// Scroll into view if needed
|
|
306
|
+
activeEl.scrollIntoView({ block: 'nearest' });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Load full detail
|
|
310
|
+
this.loadTraceDetail(filename);
|
|
311
|
+
|
|
312
|
+
// Render current tab immediately with list data
|
|
313
|
+
this.renderActiveTab();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Rendering: Stats overview bar
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
renderStatsOverview() {
|
|
320
|
+
if (!this.stats) return;
|
|
321
|
+
var s = this.stats;
|
|
322
|
+
document.getElementById('statAgents').textContent = s.totalAgents || 0;
|
|
323
|
+
document.getElementById('statExecutions').textContent = (s.totalExecutions || 0).toLocaleString();
|
|
324
|
+
var rate = Math.round((s.globalSuccessRate || 0) * 10) / 10;
|
|
325
|
+
var rateEl = document.getElementById('statSuccessRate');
|
|
326
|
+
rateEl.textContent = rate + '%';
|
|
327
|
+
rateEl.className = 'metric-value ' + (rate >= 90 ? 'success' : rate >= 70 ? 'warning' : 'error');
|
|
328
|
+
document.getElementById('statActive').textContent = s.activeAgents || 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// Rendering: Process Health (above metrics, not a tab)
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
renderProcessHealth() {
|
|
335
|
+
var section = document.getElementById('processHealthSection');
|
|
336
|
+
if (!this.processHealth) {
|
|
337
|
+
section.style.display = 'none';
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
var r = this.processHealth;
|
|
342
|
+
var hasContent = r.pidFile || r.systemd || r.workers || (r.orphans && r.orphans.length > 0) || (r.problems && r.problems.length > 0);
|
|
343
|
+
if (!hasContent) {
|
|
344
|
+
section.style.display = 'none';
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
section.style.display = '';
|
|
349
|
+
var html = '<h4>Process Health</h4>';
|
|
350
|
+
|
|
351
|
+
if (r.pidFile) {
|
|
352
|
+
var pf = r.pidFile;
|
|
353
|
+
var cls = pf.alive && pf.matchesProcess ? 'ok' : pf.stale ? 'bad' : 'warn';
|
|
354
|
+
html += '<div class="ph-row">';
|
|
355
|
+
html += '<span class="ph-label">PID File</span>';
|
|
356
|
+
html += '<span class="ph-value ' + cls + '">';
|
|
357
|
+
html += pf.pid ? ('PID ' + pf.pid + (pf.alive ? ' (alive)' : ' (dead)')) : 'No PID';
|
|
358
|
+
html += '</span></div>';
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (r.systemd) {
|
|
362
|
+
var sd = r.systemd;
|
|
363
|
+
var sdCls = sd.activeState === 'active' ? 'ok' : sd.failed ? 'bad' : 'warn';
|
|
364
|
+
html += '<div class="ph-row">';
|
|
365
|
+
html += '<span class="ph-label">Systemd</span>';
|
|
366
|
+
html += '<span class="ph-value ' + sdCls + '">';
|
|
367
|
+
html += escapeHtml(sd.unit) + ' \u2014 ' + escapeHtml(sd.activeState) + ' (' + escapeHtml(sd.subState) + ')';
|
|
368
|
+
if (sd.restarts > 0) html += ' [' + sd.restarts + ' restarts]';
|
|
369
|
+
html += '</span></div>';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (r.workers && r.workers.workers) {
|
|
373
|
+
html += '<div class="ph-row">';
|
|
374
|
+
html += '<span class="ph-label">Workers</span>';
|
|
375
|
+
html += '<div class="worker-dots">';
|
|
376
|
+
for (var i = 0; i < r.workers.workers.length; i++) {
|
|
377
|
+
var worker = r.workers.workers[i];
|
|
378
|
+
var dotCls = worker.alive ? 'alive' : worker.stale ? 'stale' : 'unknown';
|
|
379
|
+
html += '<span class="worker-dot ' + dotCls + '" title="' + escapeHtml(worker.name) + ' (pid ' + (worker.pid || '-') + ') \u2014 ' + escapeHtml(worker.declaredStatus) + '"></span>';
|
|
380
|
+
html += '<span class="worker-dot-label">' + escapeHtml(worker.name) + '</span>';
|
|
381
|
+
}
|
|
382
|
+
html += '</div></div>';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (r.orphans && r.orphans.length > 0) {
|
|
386
|
+
html += '<div class="ph-row" style="flex-direction:column;align-items:flex-start;gap:0.3rem;">';
|
|
387
|
+
html += '<span class="ph-label">Orphans (' + r.orphans.length + ')</span>';
|
|
388
|
+
html += '<table class="orphan-table"><thead><tr>';
|
|
389
|
+
html += '<th>PID</th><th>CPU%</th><th>MEM%</th><th>Uptime</th><th>Command</th>';
|
|
390
|
+
html += '</tr></thead><tbody>';
|
|
391
|
+
for (var j = 0; j < r.orphans.length; j++) {
|
|
392
|
+
var o = r.orphans[j];
|
|
393
|
+
html += '<tr>';
|
|
394
|
+
html += '<td>' + o.pid + '</td>';
|
|
395
|
+
html += '<td>' + escapeHtml(o.cpu) + '</td>';
|
|
396
|
+
html += '<td>' + escapeHtml(o.mem) + '</td>';
|
|
397
|
+
html += '<td>' + escapeHtml(o.elapsed) + '</td>';
|
|
398
|
+
html += '<td title="' + escapeHtml(o.cmdline || o.command) + '">' + escapeHtml(o.command) + '</td>';
|
|
399
|
+
html += '</tr>';
|
|
400
|
+
}
|
|
401
|
+
html += '</tbody></table></div>';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (r.problems && r.problems.length > 0) {
|
|
405
|
+
html += '<ul class="problems-list">';
|
|
406
|
+
for (var k = 0; k < r.problems.length; k++) {
|
|
407
|
+
html += '<li>' + escapeHtml(r.problems[k]) + '</li>';
|
|
408
|
+
}
|
|
409
|
+
html += '</ul>';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
section.innerHTML = html;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
// Rendering: Trace list (limit to 100 most recent for perf)
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
renderTraceList() {
|
|
419
|
+
var container = document.getElementById('traceList');
|
|
420
|
+
var countEl = document.getElementById('traceCount');
|
|
421
|
+
var self = this;
|
|
422
|
+
|
|
423
|
+
var filtered = this.traces;
|
|
424
|
+
|
|
425
|
+
// Search filter
|
|
426
|
+
if (this.searchFilter) {
|
|
427
|
+
var sf = this.searchFilter;
|
|
428
|
+
filtered = filtered.filter(function(t) {
|
|
429
|
+
return (t.agentId || '').toLowerCase().indexOf(sf) >= 0 ||
|
|
430
|
+
(t.name || '').toLowerCase().indexOf(sf) >= 0 ||
|
|
431
|
+
(t.filename || '').toLowerCase().indexOf(sf) >= 0;
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Time range filter
|
|
436
|
+
if (this.timeRangeFilter !== 'all') {
|
|
437
|
+
var now = Date.now();
|
|
438
|
+
var cutoff;
|
|
439
|
+
switch (this.timeRangeFilter) {
|
|
440
|
+
case '1h': cutoff = now - 3600000; break;
|
|
441
|
+
case '24h': cutoff = now - 86400000; break;
|
|
442
|
+
case '7d': cutoff = now - 604800000; break;
|
|
443
|
+
default: cutoff = 0;
|
|
444
|
+
}
|
|
445
|
+
filtered = filtered.filter(function(t) {
|
|
446
|
+
var ts = t.timestamp ? new Date(t.timestamp).getTime() : (t.startTime || t.lastModified || 0);
|
|
447
|
+
return ts >= cutoff;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Status filter
|
|
452
|
+
if (this.statusFilter !== 'all') {
|
|
453
|
+
var statusTarget = this.statusFilter;
|
|
454
|
+
filtered = filtered.filter(function(t) {
|
|
455
|
+
return self.getTraceStatus(t) === statusTarget;
|
|
456
|
+
});
|
|
457
|
+
}
|
|
59
458
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
459
|
+
countEl.textContent = filtered.length + ' of ' + this.traces.length + ' traces';
|
|
460
|
+
|
|
461
|
+
// Render max 100 items for performance
|
|
462
|
+
var visible = filtered.slice(0, 100);
|
|
463
|
+
|
|
464
|
+
if (visible.length === 0) {
|
|
465
|
+
container.innerHTML = '<div class="empty-state" style="height:120px;"><div class="empty-state-text">No traces match the filter.</div></div>';
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
var html = '';
|
|
470
|
+
for (var i = 0; i < visible.length; i++) {
|
|
471
|
+
var trace = visible[i];
|
|
472
|
+
var status = this.getTraceStatus(trace);
|
|
473
|
+
var isActive = this.selectedTrace && this.selectedTrace.filename === trace.filename;
|
|
474
|
+
var name = trace.name || trace.agentId || trace.filename;
|
|
475
|
+
var ts = this.formatTimestamp(trace.timestamp || trace.startTime || trace.lastModified);
|
|
476
|
+
var badgeClass = status === 'success' ? 'badge-success' : status === 'failure' ? 'badge-error' : status === 'running' ? 'badge-running' : 'badge-unknown';
|
|
477
|
+
var badgeText = status === 'success' ? 'OK' : status === 'failure' ? 'FAIL' : status === 'running' ? 'LIVE' : '?';
|
|
478
|
+
|
|
479
|
+
// Compute node stats for this trace
|
|
480
|
+
var traceNodes = this.getNodesArray(trace);
|
|
481
|
+
var nodeCount = traceNodes.length;
|
|
482
|
+
var agentCount = 0, toolCount = 0, subagentCount = 0, otherCount = 0;
|
|
483
|
+
for (var j = 0; j < traceNodes.length; j++) {
|
|
484
|
+
var nt = traceNodes[j].type;
|
|
485
|
+
if (nt === 'agent') agentCount++;
|
|
486
|
+
else if (nt === 'tool') toolCount++;
|
|
487
|
+
else if (nt === 'subagent') subagentCount++;
|
|
488
|
+
else otherCount++;
|
|
489
|
+
}
|
|
490
|
+
var traceDuration = this.computeDuration(trace.startTime, traceNodes.length > 0 ? Math.max.apply(null, traceNodes.map(function(n) { return n.endTime ? new Date(n.endTime).getTime() : 0; }).filter(function(v) { return v > 0; })) || null : null);
|
|
491
|
+
var sourceLabel = trace.sourceType === 'session' ? 'session' : 'trace';
|
|
492
|
+
|
|
493
|
+
html += '<div class="session-item' + (isActive ? ' active' : '') + '" data-filename="' + escapeHtml(trace.filename) + '">';
|
|
494
|
+
html += '<div class="session-id" title="' + escapeHtml(trace.filename) + '">' + escapeHtml(name.length > 45 ? name.substring(0, 42) + '...' : name) + '</div>';
|
|
495
|
+
html += '<div class="session-meta">';
|
|
496
|
+
html += '<span class="session-agent">' + escapeHtml(trace.agentId || '') + '</span>';
|
|
497
|
+
html += '<span>' + escapeHtml(ts) + '</span>';
|
|
498
|
+
html += '<span class="badge ' + badgeClass + '">' + badgeText + '</span>';
|
|
499
|
+
html += '</div>';
|
|
500
|
+
// Node type breakdown + duration
|
|
501
|
+
html += '<div class="session-meta" style="margin-top:3px;">';
|
|
502
|
+
html += '<span style="font-size:0.7rem;color:var(--accent-primary);">' + nodeCount + ' nodes</span>';
|
|
503
|
+
if (agentCount > 0) html += '<span class="badge badge-type badge-agent">' + agentCount + ' agent</span>';
|
|
504
|
+
if (toolCount > 0) html += '<span class="badge badge-type badge-tool">' + toolCount + ' tool</span>';
|
|
505
|
+
if (subagentCount > 0) html += '<span class="badge badge-type badge-subagent">' + subagentCount + ' sub</span>';
|
|
506
|
+
if (otherCount > 0) html += '<span class="badge badge-type badge-other">' + otherCount + ' other</span>';
|
|
507
|
+
if (traceDuration !== '--') html += '<span style="font-size:0.7rem;color:var(--text-secondary);">' + escapeHtml(traceDuration) + '</span>';
|
|
508
|
+
if (trace.tokenUsage && trace.tokenUsage.total > 0) {
|
|
509
|
+
html += '<span style="font-size:0.7rem;color:#bc8cff;">' + (trace.tokenUsage.total > 1000 ? Math.round(trace.tokenUsage.total/1000) + 'k' : trace.tokenUsage.total) + ' tok</span>';
|
|
510
|
+
if (trace.tokenUsage.cost > 0) {
|
|
511
|
+
html += '<span style="font-size:0.7rem;color:#f0883e;">$' + trace.tokenUsage.cost.toFixed(4) + '</span>';
|
|
99
512
|
}
|
|
513
|
+
}
|
|
514
|
+
html += '</div>';
|
|
515
|
+
html += '</div>';
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
container.innerHTML = html;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
getTraceStatus(trace) {
|
|
522
|
+
if (!trace.nodes) return 'unknown';
|
|
523
|
+
var nodes = this.getNodesArray(trace);
|
|
524
|
+
if (nodes.length === 0) return 'unknown';
|
|
525
|
+
var hasFailed = nodes.some(function(n) { return n.status === 'failed' || (n.metadata && n.metadata.error); });
|
|
526
|
+
if (hasFailed) return 'failure';
|
|
527
|
+
var hasRunning = nodes.some(function(n) { return n.status === 'running'; });
|
|
528
|
+
if (hasRunning) return 'running';
|
|
529
|
+
var hasCompleted = nodes.some(function(n) { return n.status === 'completed' || n.endTime; });
|
|
530
|
+
if (hasCompleted) return 'success';
|
|
531
|
+
return 'unknown';
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
getNodesArray(trace) {
|
|
535
|
+
if (!trace.nodes) return [];
|
|
536
|
+
if (Array.isArray(trace.nodes)) {
|
|
537
|
+
return trace.nodes.map(function(entry) { return Array.isArray(entry) ? entry[1] : entry; });
|
|
100
538
|
}
|
|
539
|
+
if (trace.nodes instanceof Map) return Array.from(trace.nodes.values());
|
|
540
|
+
return Object.values(trace.nodes);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
formatTimestamp(ts) {
|
|
544
|
+
if (!ts) return '';
|
|
545
|
+
var d = new Date(ts);
|
|
546
|
+
if (isNaN(d.getTime())) return String(ts);
|
|
547
|
+
var now = new Date();
|
|
548
|
+
var diffMs = now - d;
|
|
549
|
+
if (diffMs < 60000) return 'just now';
|
|
550
|
+
if (diffMs < 3600000) return Math.floor(diffMs / 60000) + 'm ago';
|
|
551
|
+
if (diffMs < 86400000) return Math.floor(diffMs / 3600000) + 'h ago';
|
|
552
|
+
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
553
|
+
}
|
|
101
554
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
555
|
+
computeDuration(startTime, endTime) {
|
|
556
|
+
if (!startTime || !endTime) return '--';
|
|
557
|
+
var ms = new Date(endTime).getTime() - new Date(startTime).getTime();
|
|
558
|
+
if (isNaN(ms) || ms < 0) return '--';
|
|
559
|
+
if (ms < 1000) return ms + 'ms';
|
|
560
|
+
if (ms < 60000) return (ms / 1000).toFixed(1) + 's';
|
|
561
|
+
return (ms / 60000).toFixed(1) + 'm';
|
|
562
|
+
}
|
|
108
563
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
564
|
+
formatDuration(ms) {
|
|
565
|
+
if (!ms || ms <= 0) return '--';
|
|
566
|
+
if (ms < 1000) return Math.round(ms) + 'ms';
|
|
567
|
+
if (ms < 60000) return (ms / 1000).toFixed(1) + 's';
|
|
568
|
+
return (ms / 60000).toFixed(1) + 'm';
|
|
569
|
+
}
|
|
112
570
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
571
|
+
// ---------------------------------------------------------------------------
|
|
572
|
+
// Render active tab
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
renderActiveTab() {
|
|
575
|
+
switch (this.activeTab) {
|
|
576
|
+
case 'timeline': this.renderTimeline(); break;
|
|
577
|
+
case 'metrics': this.renderMetrics(); break;
|
|
578
|
+
case 'graph': this.renderGraph(); break;
|
|
579
|
+
case 'heatmap': this.renderHeatmap(); break;
|
|
580
|
+
case 'state': this.renderStateMachine(); break;
|
|
581
|
+
case 'summary': this.renderSummary(); break;
|
|
582
|
+
case 'transcript': this.renderTranscript(); break;
|
|
583
|
+
}
|
|
584
|
+
this.updateToolbarInfo();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
updateToolbarInfo() {
|
|
588
|
+
var info = document.getElementById('toolbarInfo');
|
|
589
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
590
|
+
if (!trace) {
|
|
591
|
+
info.textContent = '';
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
var nodes = this.getNodesArray(trace);
|
|
595
|
+
info.textContent = nodes.length + ' nodes' + (trace.agentId ? ' | ' + trace.agentId : '');
|
|
596
|
+
}
|
|
116
597
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
598
|
+
// ---------------------------------------------------------------------------
|
|
599
|
+
// Tab 1: Timeline
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
renderTimeline() {
|
|
602
|
+
var container = document.getElementById('timelineContent');
|
|
603
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
604
|
+
if (!trace || !trace.nodes) {
|
|
605
|
+
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">☰</div><div class="empty-state-title">Select a trace</div><div class="empty-state-text">Choose a trace from the sidebar to view its execution timeline.</div></div>';
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// For session traces, render rich session timeline if available
|
|
610
|
+
if (trace.sourceType === 'session') {
|
|
611
|
+
this.renderSessionTimeline(trace, container);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
var nodes = this.getNodesArray(trace);
|
|
616
|
+
if (nodes.length === 0) {
|
|
617
|
+
container.innerHTML = '<div class="empty-state"><div class="empty-state-text">No nodes in this trace.</div></div>';
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Build depth map for tree indentation
|
|
622
|
+
var nodeMap = {};
|
|
623
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
624
|
+
if (nodes[j].id) nodeMap[nodes[j].id] = nodes[j];
|
|
625
|
+
}
|
|
626
|
+
var depthCache = {};
|
|
627
|
+
var getDepth = function(nid, visited) {
|
|
628
|
+
if (!nid || (visited && visited.has(nid))) return 0;
|
|
629
|
+
if (depthCache[nid] !== undefined) return depthCache[nid];
|
|
630
|
+
var nd = nodeMap[nid];
|
|
631
|
+
if (!nd || !nd.parentId) { depthCache[nid] = 0; return 0; }
|
|
632
|
+
var vis = visited || new Set();
|
|
633
|
+
vis.add(nid);
|
|
634
|
+
depthCache[nid] = 1 + getDepth(nd.parentId, vis);
|
|
635
|
+
return depthCache[nid];
|
|
636
|
+
};
|
|
637
|
+
for (var k = 0; k < nodes.length; k++) getDepth(nodes[k].id);
|
|
638
|
+
|
|
639
|
+
// Compute timeline range for duration bars
|
|
640
|
+
var allStarts = nodes.map(function(n) { return n.startTime ? new Date(n.startTime).getTime() : Infinity; }).filter(function(v) { return isFinite(v); });
|
|
641
|
+
var allEnds = nodes.map(function(n) { return n.endTime ? new Date(n.endTime).getTime() : 0; }).filter(function(v) { return v > 0; });
|
|
642
|
+
var timelineStart = allStarts.length > 0 ? Math.min.apply(null, allStarts) : 0;
|
|
643
|
+
var timelineEnd = allEnds.length > 0 ? Math.max.apply(null, allEnds) : 0;
|
|
644
|
+
var timelineSpan = timelineEnd - timelineStart || 1;
|
|
645
|
+
|
|
646
|
+
// Sort by startTime then depth
|
|
647
|
+
var sorted = nodes.slice().sort(function(a, b) {
|
|
648
|
+
var sa = a.startTime ? new Date(a.startTime).getTime() : Infinity;
|
|
649
|
+
var sb = b.startTime ? new Date(b.startTime).getTime() : Infinity;
|
|
650
|
+
if (sa !== sb) return sa - sb;
|
|
651
|
+
return (depthCache[a.id] || 0) - (depthCache[b.id] || 0);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// Type icons
|
|
655
|
+
var typeIcons = { agent: '\ud83e\udd16', tool: '\ud83d\udee0\ufe0f', subagent: '\ud83d\udc64', wait: '\u23f3', decision: '\ud83d\udd00', custom: '\u2b50', exec: '\u25b6\ufe0f' };
|
|
656
|
+
var statusIcons = { completed: '\u2705', failed: '\u274c', running: '\ud83d\udfe2', hung: '\u26a0\ufe0f', timeout: '\u23f0' };
|
|
657
|
+
|
|
658
|
+
var html = '';
|
|
659
|
+
// Summary header
|
|
660
|
+
html += '<div style="display:flex;gap:12px;margin-bottom:12px;flex-wrap:wrap;">';
|
|
661
|
+
var typeCounts = {};
|
|
662
|
+
for (var m = 0; m < sorted.length; m++) { var tt = sorted[m].type || 'unknown'; typeCounts[tt] = (typeCounts[tt] || 0) + 1; }
|
|
663
|
+
html += '<span style="font-size:0.85rem;color:var(--text-secondary);">' + sorted.length + ' nodes</span>';
|
|
664
|
+
var typeEntries = Object.entries(typeCounts);
|
|
665
|
+
for (var p = 0; p < typeEntries.length; p++) {
|
|
666
|
+
var tIcon = typeIcons[typeEntries[p][0]] || '\u25cf';
|
|
667
|
+
html += '<span class="badge badge-type badge-' + escapeHtml(typeEntries[p][0]) + '">' + tIcon + ' ' + typeEntries[p][1] + ' ' + escapeHtml(typeEntries[p][0]) + '</span>';
|
|
668
|
+
}
|
|
669
|
+
if (timelineSpan > 1) {
|
|
670
|
+
html += '<span style="font-size:0.85rem;color:var(--text-secondary);">Total: ' + this.formatDuration(timelineSpan) + '</span>';
|
|
671
|
+
}
|
|
672
|
+
html += '</div>';
|
|
673
|
+
|
|
674
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
675
|
+
var n = sorted[i];
|
|
676
|
+
var depth = depthCache[n.id] || 0;
|
|
677
|
+
var markerClass = n.status === 'failed' ? 'failed' :
|
|
678
|
+
n.status === 'completed' ? 'completed' :
|
|
679
|
+
n.status === 'running' ? 'running' :
|
|
680
|
+
n.status === 'hung' || n.status === 'timeout' ? 'hung' :
|
|
681
|
+
n.type === 'agent' ? 'agent' :
|
|
682
|
+
n.type === 'tool' ? 'tool' :
|
|
683
|
+
n.type === 'subagent' ? 'subagent' : 'agent';
|
|
684
|
+
|
|
685
|
+
var typeIcon = typeIcons[n.type] || '\u25cf';
|
|
686
|
+
var statusIcon = statusIcons[n.status] || '';
|
|
687
|
+
var eventName = escapeHtml(n.name || n.id || 'unnamed');
|
|
688
|
+
var eventTs = n.startTime ? new Date(n.startTime).toLocaleTimeString() : '--';
|
|
689
|
+
var dur = this.computeDuration(n.startTime, n.endTime);
|
|
690
|
+
var durMs = (n.startTime && n.endTime) ? new Date(n.endTime).getTime() - new Date(n.startTime).getTime() : 0;
|
|
691
|
+
|
|
692
|
+
// Duration bar width proportional to timeline
|
|
693
|
+
var barLeft = 0, barWidth = 0;
|
|
694
|
+
if (n.startTime && timelineSpan > 1) {
|
|
695
|
+
barLeft = ((new Date(n.startTime).getTime() - timelineStart) / timelineSpan) * 100;
|
|
696
|
+
barWidth = Math.max(1, (durMs / timelineSpan) * 100);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
var details = '';
|
|
700
|
+
if (n.metadata) {
|
|
701
|
+
var showKeys = Object.keys(n.metadata).filter(function(k) {
|
|
702
|
+
return k !== 'error' && typeof n.metadata[k] !== 'object';
|
|
703
|
+
});
|
|
704
|
+
if (showKeys.length > 0) {
|
|
705
|
+
details = showKeys.slice(0, 4).map(function(k) {
|
|
706
|
+
return escapeHtml(k) + ': ' + escapeHtml(String(n.metadata[k]).substring(0, 50));
|
|
707
|
+
}).join(' \u00b7 ');
|
|
120
708
|
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
var indent = depth * 24;
|
|
712
|
+
html += '<div class="timeline-item" style="margin-left:' + indent + 'px;">';
|
|
713
|
+
html += '<div class="timeline-marker ' + markerClass + '"></div>';
|
|
714
|
+
html += '<div class="timeline-content">';
|
|
715
|
+
html += '<div class="timeline-header">';
|
|
716
|
+
html += '<span class="event-type">' + typeIcon + ' <span class="badge badge-type badge-' + escapeHtml(n.type || 'unknown') + '" style="font-size:0.7rem;">' + escapeHtml(n.type || 'node') + '</span> ' + eventName + ' ' + statusIcon + '</span>';
|
|
717
|
+
html += '<span class="event-time">' + eventTs;
|
|
718
|
+
if (dur !== '--') html += ' \u00b7 <strong>' + escapeHtml(dur) + '</strong>';
|
|
719
|
+
html += '</span></div>';
|
|
720
|
+
// Duration bar
|
|
721
|
+
if (barWidth > 0) {
|
|
722
|
+
var barColor = n.status === 'failed' ? 'var(--accent-error)' : n.status === 'completed' ? 'var(--accent-success)' : n.status === 'running' ? 'var(--accent-primary)' : 'var(--accent-warning)';
|
|
723
|
+
html += '<div style="position:relative;height:6px;background:var(--bg-tertiary);border-radius:3px;margin:4px 0;">';
|
|
724
|
+
html += '<div style="position:absolute;left:' + barLeft.toFixed(1) + '%;width:' + barWidth.toFixed(1) + '%;height:100%;background:' + barColor + ';border-radius:3px;"></div>';
|
|
725
|
+
html += '</div>';
|
|
726
|
+
}
|
|
727
|
+
if (details) {
|
|
728
|
+
html += '<div class="event-details">' + details + '</div>';
|
|
729
|
+
}
|
|
730
|
+
if (n.metadata && n.metadata.error) {
|
|
731
|
+
html += '<div class="event-details" style="color:var(--accent-error);">\u274c ' + escapeHtml(String(n.metadata.error).substring(0, 120)) + '</div>';
|
|
732
|
+
}
|
|
733
|
+
html += '</div></div>';
|
|
121
734
|
}
|
|
122
735
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
736
|
+
container.innerHTML = html;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// ---------------------------------------------------------------------------
|
|
740
|
+
// Tab 2: Metrics
|
|
741
|
+
// ---------------------------------------------------------------------------
|
|
742
|
+
renderMetrics() {
|
|
743
|
+
var container = document.getElementById('metricsContent');
|
|
744
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
745
|
+
if (!trace || !trace.nodes) {
|
|
746
|
+
container.innerHTML = '<div class="empty-state"><div class="empty-state-text">Select a trace to view metrics.</div></div>';
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
var nodes = this.getNodesArray(trace);
|
|
751
|
+
var totalNodes = nodes.length;
|
|
752
|
+
var completedNodes = nodes.filter(function(n) { return n.status === 'completed'; }).length;
|
|
753
|
+
var failedNodes = nodes.filter(function(n) { return n.status === 'failed'; }).length;
|
|
754
|
+
var runningNodes = nodes.filter(function(n) { return n.status === 'running'; }).length;
|
|
755
|
+
var hungNodes = nodes.filter(function(n) { return n.status === 'hung' || n.status === 'timeout'; }).length;
|
|
756
|
+
var successRate = totalNodes > 0 ? Math.round(completedNodes / totalNodes * 1000) / 10 : 0;
|
|
757
|
+
|
|
758
|
+
// Compute average and max duration
|
|
759
|
+
var totalDur = 0, durCount = 0, maxDur = 0;
|
|
760
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
761
|
+
var n = nodes[i];
|
|
762
|
+
if (n.startTime && n.endTime) {
|
|
763
|
+
var ms = new Date(n.endTime).getTime() - new Date(n.startTime).getTime();
|
|
764
|
+
if (!isNaN(ms) && ms >= 0) {
|
|
765
|
+
totalDur += ms;
|
|
766
|
+
durCount++;
|
|
767
|
+
if (ms > maxDur) maxDur = ms;
|
|
133
768
|
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
var avgDur = durCount > 0 ? totalDur / durCount : 0;
|
|
772
|
+
|
|
773
|
+
// Compute max depth
|
|
774
|
+
var nodeMap = {};
|
|
775
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
776
|
+
if (nodes[j].id) nodeMap[nodes[j].id] = nodes[j];
|
|
777
|
+
}
|
|
778
|
+
var maxDepth = 0;
|
|
779
|
+
var depthOf = function(nid, visited) {
|
|
780
|
+
if (!nid || visited.has(nid)) return 0;
|
|
781
|
+
visited.add(nid);
|
|
782
|
+
var nd = nodeMap[nid];
|
|
783
|
+
if (!nd || !nd.parentId) return 0;
|
|
784
|
+
return 1 + depthOf(nd.parentId, visited);
|
|
785
|
+
};
|
|
786
|
+
for (var k = 0; k < nodes.length; k++) {
|
|
787
|
+
maxDepth = Math.max(maxDepth, depthOf(nodes[k].id, new Set()));
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Type breakdown
|
|
791
|
+
var typeCounts = {};
|
|
792
|
+
for (var m = 0; m < nodes.length; m++) {
|
|
793
|
+
var t = nodes[m].type || 'unknown';
|
|
794
|
+
typeCounts[t] = (typeCounts[t] || 0) + 1;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
var html = '<div class="metrics-grid">';
|
|
798
|
+
html += this.metricCard('Total Nodes', totalNodes, 'primary');
|
|
799
|
+
html += this.metricCard('Success Rate', successRate + '%', successRate >= 90 ? 'success' : successRate >= 70 ? 'warning' : 'error');
|
|
800
|
+
html += this.metricCard('Avg Duration', this.formatDuration(avgDur), 'primary', durCount > 0 ? 'across ' + durCount + ' nodes' : 'no timing data');
|
|
801
|
+
html += this.metricCard('Max Duration', this.formatDuration(maxDur), 'primary', 'tool execution time');
|
|
802
|
+
html += this.metricCard('Max Depth', maxDepth, 'primary');
|
|
803
|
+
html += this.metricCard('Failures', failedNodes, failedNodes > 0 ? 'error' : 'success');
|
|
804
|
+
html += this.metricCard('Running/Active', runningNodes, runningNodes > 0 ? 'warning' : 'primary');
|
|
805
|
+
html += this.metricCard('Completed', completedNodes, 'success');
|
|
806
|
+
html += '</div>';
|
|
807
|
+
|
|
808
|
+
// Token/cost metrics for session traces
|
|
809
|
+
if (trace.tokenUsage && trace.tokenUsage.total > 0) {
|
|
810
|
+
html += '<h4 style="margin:1.5rem 0 0.75rem;font-size:0.85rem;color:var(--text-secondary);">Token Usage</h4>';
|
|
811
|
+
html += '<div class="metrics-grid">';
|
|
812
|
+
html += this.metricCard('Total Tokens', trace.tokenUsage.total > 1000 ? Math.round(trace.tokenUsage.total / 1000) + 'k' : trace.tokenUsage.total, 'primary');
|
|
813
|
+
html += this.metricCard('Input Tokens', trace.tokenUsage.input > 1000 ? Math.round(trace.tokenUsage.input / 1000) + 'k' : trace.tokenUsage.input, 'primary');
|
|
814
|
+
html += this.metricCard('Output Tokens', trace.tokenUsage.output > 1000 ? Math.round(trace.tokenUsage.output / 1000) + 'k' : trace.tokenUsage.output, 'primary');
|
|
815
|
+
html += this.metricCard('Estimated Cost', trace.tokenUsage.cost > 0 ? '$' + trace.tokenUsage.cost.toFixed(4) : '$0', trace.tokenUsage.cost > 0.10 ? 'warning' : 'success');
|
|
816
|
+
if (totalNodes > 0) html += this.metricCard('Tokens/Node', Math.round(trace.tokenUsage.total / totalNodes), 'primary');
|
|
817
|
+
var modelName = (trace.metadata && trace.metadata.model) || '';
|
|
818
|
+
if (modelName) html += this.metricCard('Model', modelName.length > 20 ? modelName.slice(0, 18) + '..' : modelName, 'primary', (trace.metadata && trace.metadata.provider) || '');
|
|
819
|
+
html += '</div>';
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Type breakdown
|
|
823
|
+
html += '<h4 style="margin:1.5rem 0 0.75rem;font-size:0.85rem;color:var(--text-secondary);">Node Type Breakdown</h4>';
|
|
824
|
+
html += '<div class="metrics-grid">';
|
|
825
|
+
var typeEntries = Object.entries(typeCounts).sort(function(a, b) { return b[1] - a[1]; });
|
|
826
|
+
for (var p = 0; p < typeEntries.length; p++) {
|
|
827
|
+
html += this.metricCard(typeEntries[p][0], typeEntries[p][1], 'primary');
|
|
828
|
+
}
|
|
829
|
+
html += '</div>';
|
|
830
|
+
|
|
831
|
+
container.innerHTML = html;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
metricCard(label, value, colorClass, sub) {
|
|
835
|
+
var html = '<div class="metric-card"><div class="metric-label">' + escapeHtml(label) + '</div><div class="metric-value ' + colorClass + '">' + escapeHtml(String(value)) + '</div>';
|
|
836
|
+
if (sub) html += '<div class="metric-sub">' + escapeHtml(sub) + '</div>';
|
|
837
|
+
html += '</div>';
|
|
838
|
+
return html;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// ---------------------------------------------------------------------------
|
|
842
|
+
// Tab 3: Dependency Graph (Cytoscape.js)
|
|
843
|
+
// ---------------------------------------------------------------------------
|
|
844
|
+
renderGraph() {
|
|
845
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
846
|
+
if (!trace || !trace.nodes) {
|
|
847
|
+
document.getElementById('graphEmpty').style.display = '';
|
|
848
|
+
if (this.cy) { this.cy.destroy(); this.cy = null; }
|
|
849
|
+
return;
|
|
134
850
|
}
|
|
135
851
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
852
|
+
document.getElementById('graphEmpty').style.display = 'none';
|
|
853
|
+
|
|
854
|
+
var nodes = this.getNodesArray(trace);
|
|
855
|
+
if (nodes.length === 0) {
|
|
856
|
+
document.getElementById('graphEmpty').style.display = '';
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Build cytoscape elements
|
|
861
|
+
var elements = [];
|
|
862
|
+
var nodeIds = new Set();
|
|
863
|
+
|
|
864
|
+
// Collect valid IDs
|
|
865
|
+
if (typeof trace.nodes === 'object' && !Array.isArray(trace.nodes)) {
|
|
866
|
+
Object.keys(trace.nodes).forEach(function(key) { nodeIds.add(key); });
|
|
140
867
|
}
|
|
868
|
+
nodes.forEach(function(n) { if (n.id) nodeIds.add(n.id); });
|
|
141
869
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
870
|
+
// Add nodes
|
|
871
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
872
|
+
var node = nodes[i];
|
|
873
|
+
var id = node.id || ('n-' + i);
|
|
874
|
+
elements.push({
|
|
875
|
+
group: 'nodes',
|
|
876
|
+
data: {
|
|
877
|
+
id: id,
|
|
878
|
+
label: node.name || node.type || id,
|
|
879
|
+
status: node.status || 'unknown',
|
|
880
|
+
nodeType: node.type || 'custom',
|
|
881
|
+
fullData: node
|
|
150
882
|
}
|
|
883
|
+
});
|
|
151
884
|
}
|
|
152
885
|
|
|
153
|
-
|
|
154
|
-
|
|
886
|
+
// Build edges from parentId relationships
|
|
887
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
888
|
+
var n = nodes[j];
|
|
889
|
+
if (n.parentId && nodeIds.has(n.parentId) && n.id) {
|
|
890
|
+
elements.push({
|
|
891
|
+
group: 'edges',
|
|
892
|
+
data: {
|
|
893
|
+
source: n.parentId,
|
|
894
|
+
target: n.id,
|
|
895
|
+
id: 'e-' + n.parentId + '-' + n.id
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
}
|
|
155
900
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
901
|
+
// Also add explicit trace edges if present
|
|
902
|
+
if (trace.edges && Array.isArray(trace.edges)) {
|
|
903
|
+
for (var k = 0; k < trace.edges.length; k++) {
|
|
904
|
+
var edge = trace.edges[k];
|
|
905
|
+
var src = edge.source || edge.from;
|
|
906
|
+
var tgt = edge.target || edge.to;
|
|
907
|
+
if (src && tgt && nodeIds.has(src) && nodeIds.has(tgt)) {
|
|
908
|
+
var eid = 'e-' + src + '-' + tgt;
|
|
909
|
+
if (!elements.some(function(el) { return el.data && el.data.id === eid; })) {
|
|
910
|
+
elements.push({
|
|
911
|
+
group: 'edges',
|
|
912
|
+
data: { source: src, target: tgt, id: eid, edgeType: edge.type || '' }
|
|
913
|
+
});
|
|
914
|
+
}
|
|
159
915
|
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Destroy previous instance
|
|
920
|
+
if (this.cy) { this.cy.destroy(); this.cy = null; }
|
|
160
921
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
922
|
+
var cyContainer = document.getElementById('cy');
|
|
923
|
+
|
|
924
|
+
this.cy = cytoscape({
|
|
925
|
+
container: cyContainer,
|
|
926
|
+
elements: elements,
|
|
927
|
+
style: [
|
|
928
|
+
{
|
|
929
|
+
selector: 'node',
|
|
930
|
+
style: {
|
|
931
|
+
'label': 'data(label)',
|
|
932
|
+
'width': 45,
|
|
933
|
+
'height': 45,
|
|
934
|
+
'font-size': '10px',
|
|
935
|
+
'text-valign': 'bottom',
|
|
936
|
+
'text-halign': 'center',
|
|
937
|
+
'text-margin-y': 6,
|
|
938
|
+
'color': '#c9d1d9',
|
|
939
|
+
'text-outline-color': '#0d1117',
|
|
940
|
+
'text-outline-width': 2,
|
|
941
|
+
'border-width': 2,
|
|
942
|
+
'border-color': '#30363d',
|
|
943
|
+
'background-color': '#3b82f6'
|
|
944
|
+
}
|
|
945
|
+
},
|
|
946
|
+
{ selector: 'node[status="completed"]', style: { 'background-color': '#10b981', 'border-color': '#2ea043' } },
|
|
947
|
+
{ selector: 'node[status="failed"]', style: { 'background-color': '#ef4444', 'border-color': '#f85149', 'shape': 'diamond' } },
|
|
948
|
+
{ selector: 'node[status="running"]', style: { 'background-color': '#3b82f6', 'border-color': '#79b8ff' } },
|
|
949
|
+
{ selector: 'node[status="hung"]', style: { 'background-color': '#f0883e', 'border-color': '#f5a623' } },
|
|
950
|
+
{ selector: 'node[status="timeout"]', style: { 'background-color': '#f0883e', 'border-color': '#f5a623' } },
|
|
951
|
+
// Shape by type
|
|
952
|
+
{ selector: 'node[nodeType="agent"]', style: { 'shape': 'ellipse', 'width': 50, 'height': 50 } },
|
|
953
|
+
{ selector: 'node[nodeType="tool"]', style: { 'shape': 'round-rectangle', 'width': 50, 'height': 35 } },
|
|
954
|
+
{ selector: 'node[nodeType="subagent"]', style: { 'shape': 'ellipse', 'width': 38, 'height': 38 } },
|
|
955
|
+
{ selector: 'node[nodeType="wait"]', style: { 'shape': 'round-rectangle', 'width': 40, 'height': 30 } },
|
|
956
|
+
{ selector: 'node[nodeType="decision"]', style: { 'shape': 'diamond', 'width': 45, 'height': 45 } },
|
|
957
|
+
{ selector: 'node[nodeType="custom"]', style: { 'shape': 'diamond', 'width': 40, 'height': 40 } },
|
|
958
|
+
// Selected node — gold border
|
|
959
|
+
{ selector: ':selected', style: { 'border-width': 4, 'border-color': '#f59e0b', 'overlay-opacity': 0.08 } },
|
|
960
|
+
// Edges
|
|
961
|
+
{
|
|
962
|
+
selector: 'edge',
|
|
963
|
+
style: {
|
|
964
|
+
'width': 2,
|
|
965
|
+
'line-color': '#6b7280',
|
|
966
|
+
'target-arrow-color': '#6b7280',
|
|
967
|
+
'target-arrow-shape': 'triangle',
|
|
968
|
+
'curve-style': 'bezier',
|
|
969
|
+
'arrow-scale': 0.8
|
|
970
|
+
}
|
|
971
|
+
},
|
|
972
|
+
// Dashed edges for specific types
|
|
973
|
+
{
|
|
974
|
+
selector: 'edge[edgeType]',
|
|
975
|
+
style: {
|
|
976
|
+
'line-style': 'dashed',
|
|
977
|
+
'line-color': '#f0883e',
|
|
978
|
+
'target-arrow-color': '#f0883e'
|
|
979
|
+
}
|
|
189
980
|
}
|
|
981
|
+
],
|
|
982
|
+
layout: { name: 'breadthfirst', directed: true, padding: 40, spacingFactor: 1.4, animate: true, animationDuration: 300 },
|
|
983
|
+
minZoom: 0.2,
|
|
984
|
+
maxZoom: 4,
|
|
985
|
+
wheelSensitivity: 0.3
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
var self = this;
|
|
989
|
+
|
|
990
|
+
// Node tap -> detail panel
|
|
991
|
+
this.cy.on('tap', 'node', function(e) {
|
|
992
|
+
var data = e.target.data();
|
|
993
|
+
self.showNodeDetail(data.fullData);
|
|
994
|
+
});
|
|
190
995
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
996
|
+
// Background tap -> close panel
|
|
997
|
+
this.cy.on('tap', function(e) {
|
|
998
|
+
if (e.target === self.cy) {
|
|
999
|
+
document.getElementById('nodeDetailPanel').classList.remove('active');
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
196
1003
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
1004
|
+
runCytoscapeLayout() {
|
|
1005
|
+
if (!this.cy) return;
|
|
1006
|
+
this.cy.layout({
|
|
1007
|
+
name: 'breadthfirst',
|
|
1008
|
+
directed: true,
|
|
1009
|
+
padding: 40,
|
|
1010
|
+
spacingFactor: 1.4,
|
|
1011
|
+
animate: true,
|
|
1012
|
+
animationDuration: 400
|
|
1013
|
+
}).run();
|
|
1014
|
+
}
|
|
207
1015
|
|
|
208
|
-
|
|
1016
|
+
showNodeDetail(node) {
|
|
1017
|
+
var panel = document.getElementById('nodeDetailPanel');
|
|
1018
|
+
var body = document.getElementById('nodeDetailBody');
|
|
1019
|
+
var title = document.getElementById('nodeDetailTitle');
|
|
1020
|
+
|
|
1021
|
+
title.textContent = node.name || node.id || 'Node';
|
|
1022
|
+
|
|
1023
|
+
var duration = this.computeDuration(node.startTime, node.endTime);
|
|
1024
|
+
|
|
1025
|
+
var html = '';
|
|
1026
|
+
html += this.detailRow('ID', node.id);
|
|
1027
|
+
html += this.detailRow('Type', node.type);
|
|
1028
|
+
html += '<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value status-' + escapeHtml(node.status || '') + '">' + escapeHtml(node.status || 'unknown') + '</span></div>';
|
|
1029
|
+
html += this.detailRow('Duration', duration);
|
|
1030
|
+
if (node.startTime) html += this.detailRow('Start', new Date(node.startTime).toLocaleString());
|
|
1031
|
+
if (node.endTime) html += this.detailRow('End', new Date(node.endTime).toLocaleString());
|
|
1032
|
+
if (node.parentId) html += this.detailRow('Parent', node.parentId);
|
|
1033
|
+
if (node.children && node.children.length) html += this.detailRow('Children', node.children.length);
|
|
1034
|
+
|
|
1035
|
+
if (node.metadata && Object.keys(node.metadata).length > 0) {
|
|
1036
|
+
html += '<div style="margin-top:0.5rem;font-size:0.7rem;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.3px;">Metadata</div>';
|
|
1037
|
+
html += '<div class="detail-metadata">' + escapeHtml(JSON.stringify(node.metadata, null, 2)) + '</div>';
|
|
209
1038
|
}
|
|
210
1039
|
|
|
211
|
-
|
|
212
|
-
|
|
1040
|
+
body.innerHTML = html;
|
|
1041
|
+
panel.classList.add('active');
|
|
1042
|
+
}
|
|
213
1043
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
1044
|
+
detailRow(label, value) {
|
|
1045
|
+
if (value === undefined || value === null || value === '') return '';
|
|
1046
|
+
return '<div class="detail-row"><span class="detail-label">' + escapeHtml(label) + '</span><span class="detail-value">' + escapeHtml(String(value)) + '</span></div>';
|
|
1047
|
+
}
|
|
218
1048
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
<div class="trace-timestamp">${timestamp}</div>
|
|
236
|
-
</div>
|
|
237
|
-
<div class="trace-details">
|
|
238
|
-
<div class="trace-agent">${trace.agentId}</div>
|
|
239
|
-
<div class="trace-trigger">${trace.trigger}</div>
|
|
240
|
-
</div>
|
|
241
|
-
</div>
|
|
242
|
-
`;
|
|
243
|
-
}).join('');
|
|
244
|
-
|
|
245
|
-
tracesList.innerHTML = traceItems || '<div class="loading">No traces found</div>';
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
getTraceStatusClass(trace) {
|
|
249
|
-
// Try to determine status from the trace structure
|
|
250
|
-
if (trace.nodes) {
|
|
251
|
-
const nodes = Array.isArray(trace.nodes) ?
|
|
252
|
-
trace.nodes.map(([, node]) => node) :
|
|
253
|
-
trace.nodes instanceof Map ?
|
|
254
|
-
Array.from(trace.nodes.values()) :
|
|
255
|
-
Object.values(trace.nodes);
|
|
256
|
-
|
|
257
|
-
const hasFailures = nodes.some(node =>
|
|
258
|
-
node.status === 'failed' ||
|
|
259
|
-
node.error ||
|
|
260
|
-
(node.metadata && node.metadata.error)
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
if (hasFailures) return 'status-failure';
|
|
264
|
-
|
|
265
|
-
const hasCompleted = nodes.some(node =>
|
|
266
|
-
node.status === 'completed' ||
|
|
267
|
-
node.endTime ||
|
|
268
|
-
(node.metadata && node.metadata.status === 'success')
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
if (hasCompleted) return 'status-success';
|
|
272
|
-
}
|
|
1049
|
+
exportGraphPNG() {
|
|
1050
|
+
if (!this.cy) return;
|
|
1051
|
+
var png = this.cy.png({ bg: '#0d1117', full: true, maxWidth: 4000, maxHeight: 4000 });
|
|
1052
|
+
var link = document.createElement('a');
|
|
1053
|
+
var traceName = this.selectedTrace ? this.selectedTrace.filename.replace(/\.json$/, '') : 'graph';
|
|
1054
|
+
link.download = 'agentflow-' + traceName + '.png';
|
|
1055
|
+
link.href = png;
|
|
1056
|
+
link.click();
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// ---------------------------------------------------------------------------
|
|
1060
|
+
// Tab 4: Error Heatmap
|
|
1061
|
+
// ---------------------------------------------------------------------------
|
|
1062
|
+
renderHeatmap() {
|
|
1063
|
+
var container = document.getElementById('heatmapContent');
|
|
1064
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
273
1065
|
|
|
274
|
-
|
|
1066
|
+
// Build heatmap from recent traces (not just selected trace)
|
|
1067
|
+
var tracesToUse = this.traces.slice(0, 100);
|
|
1068
|
+
if (tracesToUse.length === 0) {
|
|
1069
|
+
container.innerHTML = '<div class="empty-state"><div class="empty-state-text">No traces available for heatmap.</div></div>';
|
|
1070
|
+
return;
|
|
275
1071
|
}
|
|
276
1072
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
1073
|
+
var html = '<h3 class="heatmap-header">Error Distribution Across Recent Traces</h3>';
|
|
1074
|
+
html += '<div class="heatmap-grid">';
|
|
1075
|
+
|
|
1076
|
+
for (var i = 0; i < Math.min(tracesToUse.length, 100); i++) {
|
|
1077
|
+
var tr = tracesToUse[i];
|
|
1078
|
+
var nodes = this.getNodesArray(tr);
|
|
1079
|
+
var failCount = 0;
|
|
1080
|
+
var warnCount = 0;
|
|
1081
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
1082
|
+
if (nodes[j].status === 'failed') failCount++;
|
|
1083
|
+
if (nodes[j].status === 'hung' || nodes[j].status === 'timeout') warnCount++;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
var color;
|
|
1087
|
+
if (failCount > 2) color = 'rgba(218, 54, 51, 0.9)';
|
|
1088
|
+
else if (failCount > 0) color = 'rgba(218, 54, 51, 0.5)';
|
|
1089
|
+
else if (warnCount > 0) color = 'rgba(240, 136, 62, 0.5)';
|
|
1090
|
+
else color = 'rgba(35, 134, 54, 0.3)';
|
|
1091
|
+
|
|
1092
|
+
var cellLabel = failCount > 0 ? failCount : '';
|
|
1093
|
+
var agentName = escapeHtml(tr.agentId || tr.name || 'unknown');
|
|
1094
|
+
var tooltipText = escapeHtml((tr.name || tr.filename || '').substring(0, 30)) + ' | ' + agentName + ' | ' + failCount + ' errors, ' + warnCount + ' warnings';
|
|
286
1095
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
}, 30000);
|
|
1096
|
+
html += '<div class="heatmap-cell" style="background:' + color + ';" title="' + tooltipText + '">';
|
|
1097
|
+
html += cellLabel;
|
|
1098
|
+
html += '<div class="heatmap-tooltip">' + tooltipText + '</div>';
|
|
1099
|
+
html += '</div>';
|
|
293
1100
|
}
|
|
294
1101
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
1102
|
+
html += '</div>';
|
|
1103
|
+
|
|
1104
|
+
// Legend
|
|
1105
|
+
html += '<div style="display:flex;gap:1.5rem;font-size:0.75rem;color:var(--text-secondary);margin-top:0.5rem;">';
|
|
1106
|
+
html += '<span><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:rgba(35,134,54,0.3);vertical-align:middle;margin-right:4px;"></span>No errors</span>';
|
|
1107
|
+
html += '<span><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:rgba(240,136,62,0.5);vertical-align:middle;margin-right:4px;"></span>Warnings</span>';
|
|
1108
|
+
html += '<span><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:rgba(218,54,51,0.5);vertical-align:middle;margin-right:4px;"></span>1-2 failures</span>';
|
|
1109
|
+
html += '<span><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:rgba(218,54,51,0.9);vertical-align:middle;margin-right:4px;"></span>3+ failures</span>';
|
|
1110
|
+
html += '</div>';
|
|
1111
|
+
|
|
1112
|
+
container.innerHTML = html;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// ---------------------------------------------------------------------------
|
|
1116
|
+
// Tab 5: State Machine
|
|
1117
|
+
// ---------------------------------------------------------------------------
|
|
1118
|
+
renderStateMachine() {
|
|
1119
|
+
var container = document.getElementById('stateContent');
|
|
1120
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
1121
|
+
if (!trace || !trace.nodes) {
|
|
1122
|
+
container.innerHTML = '<div class="empty-state"><div class="empty-state-text">Select a trace to view state machine.</div></div>';
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
var nodes = this.getNodesArray(trace);
|
|
1127
|
+
var pendingCount = 0, runningCount = 0, completedCount = 0, failedCount = 0;
|
|
1128
|
+
|
|
1129
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
1130
|
+
var s = nodes[i].status;
|
|
1131
|
+
if (s === 'completed') completedCount++;
|
|
1132
|
+
else if (s === 'failed') failedCount++;
|
|
1133
|
+
else if (s === 'running') runningCount++;
|
|
1134
|
+
else pendingCount++;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Determine which states are "active" (have nodes)
|
|
1138
|
+
var pendingActive = pendingCount > 0 ? ' pending' : '';
|
|
1139
|
+
var runningActive = runningCount > 0 ? ' running' : '';
|
|
1140
|
+
var completedActive = completedCount > 0 ? ' completed' : '';
|
|
1141
|
+
var failedActive = failedCount > 0 ? ' failed' : '';
|
|
1142
|
+
|
|
1143
|
+
var html = '<div class="state-machine">';
|
|
1144
|
+
|
|
1145
|
+
html += '<div class="state">';
|
|
1146
|
+
html += '<div class="state-circle' + pendingActive + '"><span class="state-count">' + pendingCount + '</span>PENDING</div>';
|
|
1147
|
+
html += '<span class="state-label">Queued</span>';
|
|
1148
|
+
html += '</div>';
|
|
1149
|
+
|
|
1150
|
+
html += '<div class="state-arrow">→</div>';
|
|
1151
|
+
|
|
1152
|
+
html += '<div class="state">';
|
|
1153
|
+
html += '<div class="state-circle' + runningActive + '"><span class="state-count">' + runningCount + '</span>RUNNING</div>';
|
|
1154
|
+
html += '<span class="state-label">Active</span>';
|
|
1155
|
+
html += '</div>';
|
|
1156
|
+
|
|
1157
|
+
html += '<div class="state-arrow">→</div>';
|
|
1158
|
+
|
|
1159
|
+
html += '<div class="state">';
|
|
1160
|
+
html += '<div class="state-circle' + completedActive + '"><span class="state-count">' + completedCount + '</span>COMPLETED</div>';
|
|
1161
|
+
html += '<span class="state-label">Success</span>';
|
|
1162
|
+
html += '</div>';
|
|
1163
|
+
|
|
1164
|
+
html += '<div class="state-arrow">↔</div>';
|
|
1165
|
+
|
|
1166
|
+
html += '<div class="state">';
|
|
1167
|
+
html += '<div class="state-circle' + failedActive + '"><span class="state-count">' + failedCount + '</span>FAILED</div>';
|
|
1168
|
+
html += '<span class="state-label">Error</span>';
|
|
1169
|
+
html += '</div>';
|
|
1170
|
+
|
|
1171
|
+
html += '</div>';
|
|
1172
|
+
|
|
1173
|
+
// State details
|
|
1174
|
+
html += '<div style="padding:1rem;">';
|
|
1175
|
+
html += '<div class="metrics-grid">';
|
|
1176
|
+
html += this.metricCard('Pending', pendingCount, 'primary');
|
|
1177
|
+
html += this.metricCard('Running', runningCount, runningCount > 0 ? 'warning' : 'primary');
|
|
1178
|
+
html += this.metricCard('Completed', completedCount, 'success');
|
|
1179
|
+
html += this.metricCard('Failed', failedCount, failedCount > 0 ? 'error' : 'success');
|
|
1180
|
+
html += '</div></div>';
|
|
1181
|
+
|
|
1182
|
+
container.innerHTML = html;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// ---------------------------------------------------------------------------
|
|
1186
|
+
// Tab 6: Summary
|
|
1187
|
+
// ---------------------------------------------------------------------------
|
|
1188
|
+
renderSummary() {
|
|
1189
|
+
var container = document.getElementById('summaryContent');
|
|
1190
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
1191
|
+
if (!trace || !trace.nodes) {
|
|
1192
|
+
container.innerHTML = '<div class="empty-state"><div class="empty-state-text">Select a trace to view summary.</div></div>';
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Show spinner briefly then generate
|
|
1197
|
+
container.innerHTML = '<div class="empty-state"><div class="spinner"></div><div class="empty-state-text">Generating summary...</div></div>';
|
|
300
1198
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
1199
|
+
var self = this;
|
|
1200
|
+
// Use setTimeout to avoid blocking render
|
|
1201
|
+
setTimeout(function() { self.generateSummary(trace, container); }, 50);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
generateSummary(trace, container) {
|
|
1205
|
+
var nodes = this.getNodesArray(trace);
|
|
1206
|
+
var totalNodes = nodes.length;
|
|
1207
|
+
var completedCount = 0, failedCount = 0, runningCount = 0;
|
|
1208
|
+
var agentNames = new Set();
|
|
1209
|
+
var totalDur = 0, durCount = 0;
|
|
1210
|
+
|
|
1211
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
1212
|
+
var n = nodes[i];
|
|
1213
|
+
if (n.status === 'completed') completedCount++;
|
|
1214
|
+
else if (n.status === 'failed') failedCount++;
|
|
1215
|
+
else if (n.status === 'running') runningCount++;
|
|
1216
|
+
|
|
1217
|
+
if (n.type === 'agent' || n.type === 'subagent') {
|
|
1218
|
+
agentNames.add(n.name || n.id || 'unnamed');
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
if (n.startTime && n.endTime) {
|
|
1222
|
+
var ms = new Date(n.endTime).getTime() - new Date(n.startTime).getTime();
|
|
1223
|
+
if (!isNaN(ms) && ms >= 0) { totalDur += ms; durCount++; }
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
var successRate = totalNodes > 0 ? Math.round(completedCount / totalNodes * 100) : 0;
|
|
1228
|
+
var agentList = Array.from(agentNames);
|
|
1229
|
+
|
|
1230
|
+
// Build summary title
|
|
1231
|
+
var titleText = 'Trace: ' + escapeHtml(trace.name || trace.agentId || trace.filename || 'Unknown');
|
|
1232
|
+
|
|
1233
|
+
// Build summary text
|
|
1234
|
+
var summaryText = 'This trace contains ' + totalNodes + ' node' + (totalNodes !== 1 ? 's' : '') + '. ';
|
|
1235
|
+
summaryText += completedCount + ' completed successfully, ' + failedCount + ' failed';
|
|
1236
|
+
if (runningCount > 0) summaryText += ', and ' + runningCount + ' are still running';
|
|
1237
|
+
summaryText += '. ';
|
|
1238
|
+
if (durCount > 0) {
|
|
1239
|
+
summaryText += 'Average node duration was ' + this.formatDuration(totalDur / durCount) + '. ';
|
|
1240
|
+
summaryText += 'Total execution time: ' + this.formatDuration(totalDur) + '.';
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Build details list
|
|
1244
|
+
var details = [];
|
|
1245
|
+
details.push('Total nodes: ' + totalNodes);
|
|
1246
|
+
details.push('Completed: ' + completedCount);
|
|
1247
|
+
details.push('Failed: ' + failedCount);
|
|
1248
|
+
if (runningCount > 0) details.push('Running: ' + runningCount);
|
|
1249
|
+
if (agentList.length > 0) details.push('Agents involved: ' + agentList.join(', '));
|
|
1250
|
+
if (trace.trigger) details.push('Trigger: ' + trace.trigger);
|
|
1251
|
+
|
|
1252
|
+
// Recommendations
|
|
1253
|
+
var recommendations = '';
|
|
1254
|
+
if (failedCount === 0 && runningCount === 0) {
|
|
1255
|
+
recommendations = '<strong>Status:</strong> All tasks completed successfully. No issues detected.';
|
|
1256
|
+
} else if (failedCount > 0) {
|
|
1257
|
+
recommendations = '<strong>Action needed:</strong> ' + failedCount + ' node' + (failedCount !== 1 ? 's' : '') + ' failed. Investigate the failed nodes in the Timeline or Dependency Graph tabs for error details.';
|
|
1258
|
+
}
|
|
1259
|
+
if (runningCount > 0) {
|
|
1260
|
+
recommendations += (recommendations ? ' ' : '') + '<strong>Note:</strong> ' + runningCount + ' node' + (runningCount !== 1 ? 's are' : ' is') + ' still running. The trace may not be complete yet.';
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
var html = '<div class="summary-card">';
|
|
1264
|
+
html += '<h3 class="summary-title">' + titleText + '</h3>';
|
|
1265
|
+
html += '<p class="summary-text">' + escapeHtml(summaryText) + '</p>';
|
|
1266
|
+
html += '<ul class="summary-details">';
|
|
1267
|
+
for (var j = 0; j < details.length; j++) {
|
|
1268
|
+
html += '<li>' + escapeHtml(details[j]) + '</li>';
|
|
1269
|
+
}
|
|
1270
|
+
html += '</ul>';
|
|
1271
|
+
|
|
1272
|
+
if (recommendations) {
|
|
1273
|
+
html += '<div class="summary-recommendations">' + recommendations + '</div>';
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// Confidence bar based on success rate
|
|
1277
|
+
html += '<div class="confidence-bar">';
|
|
1278
|
+
html += '<span>Confidence:</span>';
|
|
1279
|
+
html += '<div class="bar"><div class="bar-fill" style="width:' + successRate + '%;"></div></div>';
|
|
1280
|
+
html += '<span>' + successRate + '%</span>';
|
|
1281
|
+
html += '</div>';
|
|
1282
|
+
|
|
1283
|
+
html += '</div>';
|
|
1284
|
+
|
|
1285
|
+
container.innerHTML = html;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// ---------------------------------------------------------------------------
|
|
1289
|
+
// Session Timeline (rich event-based timeline for JSONL sessions)
|
|
1290
|
+
// ---------------------------------------------------------------------------
|
|
1291
|
+
async renderSessionTimeline(trace, container) {
|
|
1292
|
+
var filename = trace.filename;
|
|
1293
|
+
var html = '';
|
|
1294
|
+
|
|
1295
|
+
// Try to fetch session events from the API
|
|
1296
|
+
var events = trace.sessionEvents || [];
|
|
1297
|
+
var tokenUsage = trace.tokenUsage || null;
|
|
1298
|
+
|
|
1299
|
+
if (events.length === 0 && filename) {
|
|
1300
|
+
try {
|
|
1301
|
+
var res = await fetch('/api/traces/' + encodeURIComponent(filename) + '/events');
|
|
1302
|
+
if (res.ok) {
|
|
1303
|
+
var data = await res.json();
|
|
1304
|
+
events = data.events || [];
|
|
1305
|
+
tokenUsage = data.tokenUsage || null;
|
|
307
1306
|
}
|
|
1307
|
+
} catch (e) {
|
|
1308
|
+
// fall through to node-based rendering
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
308
1311
|
|
|
309
|
-
|
|
310
|
-
|
|
1312
|
+
if (events.length === 0) {
|
|
1313
|
+
// Fallback: render nodes like a normal trace
|
|
1314
|
+
container.innerHTML = '<div class="empty-state"><div class="empty-state-text">No session events found. Try the node-based timeline.</div></div>';
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
311
1317
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
1318
|
+
// Token usage summary at top
|
|
1319
|
+
if (tokenUsage && tokenUsage.total > 0) {
|
|
1320
|
+
html += '<div style="display:flex;gap:16px;margin-bottom:12px;flex-wrap:wrap;padding:8px 12px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;">';
|
|
1321
|
+
html += '<span style="font-size:0.8rem;color:#bc8cff;">Tokens: ' + (tokenUsage.total > 1000 ? Math.round(tokenUsage.total / 1000) + 'k' : tokenUsage.total) + '</span>';
|
|
1322
|
+
html += '<span style="font-size:0.8rem;color:var(--text-secondary);">In: ' + (tokenUsage.input > 1000 ? Math.round(tokenUsage.input / 1000) + 'k' : tokenUsage.input) + '</span>';
|
|
1323
|
+
html += '<span style="font-size:0.8rem;color:var(--text-secondary);">Out: ' + (tokenUsage.output > 1000 ? Math.round(tokenUsage.output / 1000) + 'k' : tokenUsage.output) + '</span>';
|
|
1324
|
+
if (tokenUsage.cost > 0) html += '<span style="font-size:0.8rem;color:#f0883e;">Cost: $' + tokenUsage.cost.toFixed(4) + '</span>';
|
|
1325
|
+
html += '</div>';
|
|
316
1326
|
}
|
|
317
1327
|
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
1328
|
+
// Summary badges
|
|
1329
|
+
var userCount = 0, assistantCount = 0, toolCount = 0, thinkCount = 0, spawnCount = 0;
|
|
1330
|
+
for (var i = 0; i < events.length; i++) {
|
|
1331
|
+
switch (events[i].type) {
|
|
1332
|
+
case 'user': userCount++; break;
|
|
1333
|
+
case 'assistant': assistantCount++; break;
|
|
1334
|
+
case 'tool_call': toolCount++; break;
|
|
1335
|
+
case 'thinking': thinkCount++; break;
|
|
1336
|
+
case 'spawn': spawnCount++; break;
|
|
1337
|
+
}
|
|
321
1338
|
}
|
|
1339
|
+
html += '<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;">';
|
|
1340
|
+
html += '<span style="font-size:0.8rem;color:var(--text-secondary);">' + events.length + ' events</span>';
|
|
1341
|
+
if (userCount) html += '<span class="badge" style="background:rgba(88,166,255,0.15);color:#58a6ff;">' + userCount + ' user</span>';
|
|
1342
|
+
if (assistantCount) html += '<span class="badge" style="background:rgba(35,134,54,0.15);color:#3fb950;">' + assistantCount + ' assistant</span>';
|
|
1343
|
+
if (toolCount) html += '<span class="badge" style="background:rgba(240,136,62,0.15);color:#f0883e;">' + toolCount + ' tools</span>';
|
|
1344
|
+
if (thinkCount) html += '<span class="badge" style="background:rgba(188,140,255,0.15);color:#bc8cff;">' + thinkCount + ' thinking</span>';
|
|
1345
|
+
if (spawnCount) html += '<span class="badge" style="background:rgba(0,200,200,0.15);color:#00c8c8;">' + spawnCount + ' spawns</span>';
|
|
1346
|
+
html += '</div>';
|
|
1347
|
+
|
|
1348
|
+
// Render events
|
|
1349
|
+
var typeMarkers = {
|
|
1350
|
+
user: { icon: '\ud83e\uddd1', color: '#58a6ff', label: 'User' },
|
|
1351
|
+
assistant: { icon: '\ud83e\udd16', color: '#3fb950', label: 'Assistant' },
|
|
1352
|
+
thinking: { icon: '\ud83d\udcad', color: '#bc8cff', label: 'Thinking' },
|
|
1353
|
+
tool_call: { icon: '\ud83d\udee0\ufe0f', color: '#f0883e', label: 'Tool Call' },
|
|
1354
|
+
tool_result: { icon: '\u2705', color: '#3fb950', label: 'Tool Result' },
|
|
1355
|
+
spawn: { icon: '\ud83d\udc64', color: '#00c8c8', label: 'Subagent' },
|
|
1356
|
+
model_change: { icon: '\u2699\ufe0f', color: '#8b949e', label: 'Model' },
|
|
1357
|
+
system: { icon: '\u2139\ufe0f', color: '#6e7681', label: 'System' },
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
for (var j = 0; j < events.length; j++) {
|
|
1361
|
+
var evt = events[j];
|
|
1362
|
+
var marker = typeMarkers[evt.type] || typeMarkers.system;
|
|
1363
|
+
var evtTime = evt.timestamp ? new Date(evt.timestamp).toLocaleTimeString() : '';
|
|
1364
|
+
var contentPreview = escapeHtml((evt.content || '').substring(0, 300));
|
|
1365
|
+
if ((evt.content || '').length > 300) contentPreview += '...';
|
|
1366
|
+
|
|
1367
|
+
// Tool result with error gets red marker
|
|
1368
|
+
if (evt.type === 'tool_result' && evt.toolError) {
|
|
1369
|
+
marker = { icon: '\u274c', color: '#f85149', label: 'Tool Error' };
|
|
1370
|
+
}
|
|
322
1371
|
|
|
323
|
-
|
|
324
|
-
|
|
1372
|
+
html += '<div class="timeline-item">';
|
|
1373
|
+
html += '<div class="timeline-marker" style="background:' + marker.color + ';"></div>';
|
|
1374
|
+
html += '<div class="timeline-content">';
|
|
1375
|
+
html += '<div class="timeline-header">';
|
|
1376
|
+
html += '<span class="event-type">' + marker.icon + ' <strong>' + escapeHtml(evt.name || marker.label) + '</strong>';
|
|
1377
|
+
if (evt.type === 'tool_call' && evt.toolName) html += ' <code style="font-size:0.75rem;color:#f0883e;">' + escapeHtml(evt.toolName) + '</code>';
|
|
1378
|
+
html += '</span>';
|
|
1379
|
+
html += '<span class="event-time">' + evtTime;
|
|
1380
|
+
if (evt.duration) html += ' · ' + this.formatDuration(evt.duration);
|
|
1381
|
+
if (evt.tokens && evt.tokens.total) html += ' · <span style="color:#bc8cff;">' + (evt.tokens.total > 1000 ? Math.round(evt.tokens.total / 1000) + 'k' : evt.tokens.total) + ' tok</span>';
|
|
1382
|
+
html += '</span></div>';
|
|
1383
|
+
|
|
1384
|
+
if (contentPreview) {
|
|
1385
|
+
html += '<div class="event-details" style="margin-top:4px;">' + contentPreview + '</div>';
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
if (evt.type === 'tool_call' && evt.toolArgs) {
|
|
1389
|
+
var argsStr = typeof evt.toolArgs === 'string' ? evt.toolArgs : JSON.stringify(evt.toolArgs);
|
|
1390
|
+
html += '<div class="event-details" style="margin-top:2px;font-family:monospace;font-size:0.7rem;color:var(--text-secondary);max-height:60px;overflow:hidden;">' + escapeHtml(argsStr.substring(0, 200)) + '</div>';
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
if (evt.type === 'tool_result' && evt.toolResult) {
|
|
1394
|
+
var resultColor = evt.toolError ? 'var(--accent-error)' : 'var(--text-secondary)';
|
|
1395
|
+
html += '<div class="event-details" style="margin-top:2px;font-family:monospace;font-size:0.7rem;color:' + resultColor + ';max-height:80px;overflow:hidden;">' + escapeHtml(evt.toolResult.substring(0, 300)) + '</div>';
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
html += '</div></div>';
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
container.innerHTML = html;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// ---------------------------------------------------------------------------
|
|
1405
|
+
// Tab 7: Transcript (chat bubble UI for session traces)
|
|
1406
|
+
// ---------------------------------------------------------------------------
|
|
1407
|
+
async renderTranscript() {
|
|
1408
|
+
var container = document.getElementById('transcriptContent');
|
|
1409
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
1410
|
+
|
|
1411
|
+
if (!trace) {
|
|
1412
|
+
container.innerHTML = '<div class="empty-state"><div class="empty-state-text">Select a trace to view transcript.</div></div>';
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
if (trace.sourceType !== 'session') {
|
|
1417
|
+
container.innerHTML = '<div class="empty-state"><div class="empty-state-text">Transcript view is only available for session traces (JSONL files).</div></div>';
|
|
1418
|
+
return;
|
|
325
1419
|
}
|
|
326
1420
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
1421
|
+
var events = trace.sessionEvents || [];
|
|
1422
|
+
if (events.length === 0 && trace.filename) {
|
|
1423
|
+
try {
|
|
1424
|
+
var res = await fetch('/api/traces/' + encodeURIComponent(trace.filename) + '/events');
|
|
1425
|
+
if (res.ok) {
|
|
1426
|
+
var data = await res.json();
|
|
1427
|
+
events = data.events || [];
|
|
330
1428
|
}
|
|
331
|
-
|
|
332
|
-
this.connectWebSocket();
|
|
1429
|
+
} catch (e) { /* ignore */ }
|
|
333
1430
|
}
|
|
1431
|
+
|
|
1432
|
+
if (events.length === 0) {
|
|
1433
|
+
container.innerHTML = '<div class="empty-state"><div class="empty-state-text">No session events found.</div></div>';
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
var html = '<div style="display:flex;flex-direction:column;gap:4px;padding:0.5rem;">';
|
|
1438
|
+
|
|
1439
|
+
var thinkingIdx = 0;
|
|
1440
|
+
for (var i = 0; i < events.length; i++) {
|
|
1441
|
+
var evt = events[i];
|
|
1442
|
+
var evtTime = evt.timestamp ? new Date(evt.timestamp).toLocaleTimeString() : '';
|
|
1443
|
+
|
|
1444
|
+
if (evt.type === 'user') {
|
|
1445
|
+
html += '<div class="chat-bubble chat-user">';
|
|
1446
|
+
html += escapeHtml(evt.content || '');
|
|
1447
|
+
html += '<div class="chat-meta">' + evtTime + '</div>';
|
|
1448
|
+
html += '</div>';
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if (evt.type === 'assistant') {
|
|
1452
|
+
html += '<div class="chat-bubble chat-assistant">';
|
|
1453
|
+
html += escapeHtml(evt.content || '');
|
|
1454
|
+
html += '<div class="chat-meta">' + evtTime;
|
|
1455
|
+
if (evt.tokens && evt.tokens.total) {
|
|
1456
|
+
html += ' · <span class="chat-tokens">' + (evt.tokens.total > 1000 ? Math.round(evt.tokens.total / 1000) + 'k' : evt.tokens.total) + ' tokens';
|
|
1457
|
+
if (evt.tokens.cost) html += ' ($' + evt.tokens.cost.toFixed(4) + ')';
|
|
1458
|
+
html += '</span>';
|
|
1459
|
+
}
|
|
1460
|
+
if (evt.model) html += ' · ' + escapeHtml(evt.model);
|
|
1461
|
+
html += '</div></div>';
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
if (evt.type === 'thinking') {
|
|
1465
|
+
thinkingIdx++;
|
|
1466
|
+
var tId = 'thinking-toggle-' + thinkingIdx;
|
|
1467
|
+
html += '<div class="chat-bubble chat-thinking">';
|
|
1468
|
+
html += '<span class="chat-thinking-toggle" onclick="var b=document.getElementById(\'' + tId + '\');b.classList.toggle(\'open\');">\ud83d\udcad Thinking (click to expand)</span>';
|
|
1469
|
+
html += '<div class="chat-thinking-body" id="' + tId + '">' + escapeHtml(evt.content || '') + '</div>';
|
|
1470
|
+
html += '<div class="chat-meta">' + evtTime + '</div>';
|
|
1471
|
+
html += '</div>';
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
if (evt.type === 'tool_call') {
|
|
1475
|
+
html += '<div class="chat-bubble chat-tool">';
|
|
1476
|
+
html += '<strong>\ud83d\udee0\ufe0f ' + escapeHtml(evt.toolName || evt.name || 'Tool') + '</strong>';
|
|
1477
|
+
if (evt.toolArgs) {
|
|
1478
|
+
var argsStr = typeof evt.toolArgs === 'string' ? evt.toolArgs : JSON.stringify(evt.toolArgs, null, 2);
|
|
1479
|
+
html += '<div style="margin-top:4px;max-height:100px;overflow:hidden;font-size:0.75rem;color:var(--text-secondary);">' + escapeHtml(argsStr.substring(0, 300)) + '</div>';
|
|
1480
|
+
}
|
|
1481
|
+
html += '<div class="chat-meta">' + evtTime;
|
|
1482
|
+
if (evt.duration) html += ' · ' + this.formatDuration(evt.duration);
|
|
1483
|
+
html += '</div></div>';
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
if (evt.type === 'tool_result') {
|
|
1487
|
+
var isError = !!evt.toolError;
|
|
1488
|
+
html += '<div class="chat-bubble chat-tool" style="' + (isError ? 'border-color:var(--accent-error);' : 'border-color:rgba(35,134,54,0.3);') + '">';
|
|
1489
|
+
html += '<strong>' + (isError ? '\u274c' : '\u2705') + ' Result</strong>';
|
|
1490
|
+
var resultText = evt.toolError || evt.toolResult || '';
|
|
1491
|
+
html += '<div style="margin-top:4px;max-height:120px;overflow:hidden;font-size:0.75rem;color:' + (isError ? 'var(--accent-error)' : 'var(--text-secondary)') + ';">' + escapeHtml(resultText.substring(0, 400)) + '</div>';
|
|
1492
|
+
html += '<div class="chat-meta">' + evtTime + '</div>';
|
|
1493
|
+
html += '</div>';
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
if (evt.type === 'spawn') {
|
|
1497
|
+
html += '<div class="chat-bubble" style="margin:0 auto;max-width:70%;background:rgba(0,200,200,0.08);border:1px solid rgba(0,200,200,0.25);text-align:center;">';
|
|
1498
|
+
html += '\ud83d\udc64 Subagent spawned';
|
|
1499
|
+
if (evt.content) html += ': <code>' + escapeHtml(evt.content.substring(0, 40)) + '</code>';
|
|
1500
|
+
html += '<div class="chat-meta">' + evtTime + '</div>';
|
|
1501
|
+
html += '</div>';
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
html += '</div>';
|
|
1506
|
+
container.innerHTML = html;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// ---------------------------------------------------------------------------
|
|
1510
|
+
// Alert panel
|
|
1511
|
+
// ---------------------------------------------------------------------------
|
|
1512
|
+
showAlert(messages) {
|
|
1513
|
+
var panel = document.getElementById('alertPanel');
|
|
1514
|
+
var list = document.getElementById('alertList');
|
|
1515
|
+
if (!messages || messages.length === 0) {
|
|
1516
|
+
panel.classList.remove('show');
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
list.innerHTML = messages.map(function(m) { return '<li>' + escapeHtml(m) + '</li>'; }).join('');
|
|
1520
|
+
panel.classList.add('show');
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// ---------------------------------------------------------------------------
|
|
1524
|
+
// Public / debug
|
|
1525
|
+
// ---------------------------------------------------------------------------
|
|
1526
|
+
getStats() { return this.stats; }
|
|
1527
|
+
getTraces() { return this.traces; }
|
|
1528
|
+
reconnect() {
|
|
1529
|
+
if (this.ws) this.ws.close();
|
|
1530
|
+
this.reconnectAttempts = 0;
|
|
1531
|
+
this.connectWebSocket();
|
|
1532
|
+
}
|
|
334
1533
|
}
|
|
335
1534
|
|
|
336
|
-
// Initialize
|
|
337
|
-
document.addEventListener('DOMContentLoaded', ()
|
|
338
|
-
|
|
1535
|
+
// Initialize
|
|
1536
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1537
|
+
window.dashboard = new AgentFlowDashboard();
|
|
339
1538
|
});
|
|
340
1539
|
|
|
341
|
-
|
|
342
|
-
window.AgentFlowDashboard = AgentFlowDashboard;
|
|
1540
|
+
window.AgentFlowDashboard = AgentFlowDashboard;
|