@orka-js/devtools 1.2.0 → 1.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.
@@ -0,0 +1,613 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>OrkaJS DevTools</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script>
9
+ tailwind.config = {
10
+ darkMode: 'class',
11
+ theme: {
12
+ extend: {
13
+ animation: {
14
+ 'fade-in': 'fadeIn 0.3s ease-out',
15
+ 'slide-up': 'slideUp 0.3s ease-out',
16
+ 'pulse-slow': 'pulse 3s infinite',
17
+ }
18
+ }
19
+ }
20
+ }
21
+ </script>
22
+ <style>
23
+ .tree-line { border-left: 2px solid #e2e8f0; }
24
+ .dark .tree-line { border-left-color: #334155; }
25
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
26
+ @keyframes slideUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
27
+ .animate-fade-in { animation: fadeIn 0.3s ease-out; }
28
+ .animate-slide-up { animation: slideUp 0.3s ease-out; }
29
+ .scrollbar-thin::-webkit-scrollbar { width: 6px; height: 6px; }
30
+ .scrollbar-thin::-webkit-scrollbar-track { background: transparent; }
31
+ .scrollbar-thin::-webkit-scrollbar-thumb { background: #64748b; border-radius: 3px; }
32
+ .dark .scrollbar-thin::-webkit-scrollbar-thumb { background: #475569; }
33
+ .glass { backdrop-filter: blur(12px); background: rgba(255,255,255,0.8); }
34
+ .dark .glass { background: rgba(15,23,42,0.8); }
35
+ .gradient-border { background: linear-gradient(135deg, #8b5cf6, #ec4899, #8b5cf6); background-size: 200% 200%; animation: gradient 3s ease infinite; }
36
+ @keyframes gradient { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } }
37
+ .run-card { transition: all 0.2s ease; }
38
+ .run-card:hover { transform: translateX(4px); }
39
+ .tooltip { position: relative; }
40
+ .tooltip::after { content: attr(data-tip); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); padding: 4px 8px; background: #1e293b; color: white; font-size: 12px; border-radius: 4px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s; }
41
+ .tooltip:hover::after { opacity: 1; }
42
+ .kbd { display: inline-flex; align-items: center; padding: 2px 6px; font-size: 11px; font-family: monospace; background: #e2e8f0; border-radius: 4px; border: 1px solid #cbd5e1; }
43
+ .dark .kbd { background: #334155; border-color: #475569; }
44
+ </style>
45
+ </head>
46
+ <body class="bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-white min-h-screen transition-colors duration-300">
47
+ <div id="app" class="max-w-7xl mx-auto p-6">
48
+ <header class="flex items-center justify-between mb-8">
49
+ <div class="flex items-center gap-3">
50
+ <img src="https://devtools.orkajs.com/orka-devtools.png" alt="OrkaJS DevTools" class="w-10 h-10 rounded-lg" onerror="this.style.display='none'" />
51
+ <div>
52
+ <h1 class="text-2xl font-bold">OrkaJS DevTools</h1>
53
+ <p class="text-sm text-slate-500">Real-time LLM observability</p>
54
+ </div>
55
+ </div>
56
+ <div class="flex items-center gap-3">
57
+ <div class="relative">
58
+ <input type="text" id="searchInput" placeholder="Search traces... (⌘K)"
59
+ class="w-64 px-4 py-2 pl-10 text-sm bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all" />
60
+ <svg class="absolute left-3 top-2.5 w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
61
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
62
+ </svg>
63
+ </div>
64
+
65
+ <select id="typeFilter" onchange="applyFilters()"
66
+ class="px-3 py-2 text-sm bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500">
67
+ <option value="">All Types</option>
68
+ <option value="agent">Agent</option>
69
+ <option value="llm">LLM</option>
70
+ <option value="tool">Tool</option>
71
+ <option value="chain">Chain</option>
72
+ <option value="retrieval">Retrieval</option>
73
+ </select>
74
+
75
+ <span id="status" class="flex items-center gap-2 text-sm px-3 py-1.5 bg-green-500/10 rounded-lg">
76
+ <span class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
77
+ <span class="text-green-600 dark:text-green-400">Live</span>
78
+ </span>
79
+
80
+ <button onclick="toggleTheme()" id="themeToggle" class="p-2 rounded-lg bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors tooltip" data-tip="Toggle theme (⌘D)">
81
+ <svg id="sunIcon" class="w-5 h-5 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
82
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
83
+ </svg>
84
+ <svg id="moonIcon" class="w-5 h-5 block dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
85
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
86
+ </svg>
87
+ </button>
88
+
89
+ <button onclick="clearTraces()" class="px-3 py-2 text-sm bg-red-500/10 text-red-600 dark:text-red-400 rounded-lg hover:bg-red-500/20 transition-colors flex items-center gap-2">
90
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
91
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
92
+ </svg>
93
+ Clear
94
+ </button>
95
+
96
+ <button onclick="exportTraces()" class="px-3 py-2 text-sm bg-purple-500/10 text-purple-600 dark:text-purple-400 rounded-lg hover:bg-purple-500/20 transition-colors flex items-center gap-2">
97
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
98
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
99
+ </svg>
100
+ Export
101
+ </button>
102
+ </div>
103
+ </header>
104
+
105
+ <!-- Metrics -->
106
+ <div id="metrics" class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 mb-8">
107
+ <div class="bg-white dark:bg-slate-800 rounded-xl p-4 shadow-sm border border-slate-100 dark:border-slate-700 hover:shadow-md transition-shadow">
108
+ <div class="flex items-center justify-between mb-2">
109
+ <p class="text-sm text-slate-500">Total Runs</p>
110
+ <div class="p-1.5 bg-blue-500/10 rounded-lg">
111
+ <svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
112
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
113
+ </svg>
114
+ </div>
115
+ </div>
116
+ <p id="metric-runs" class="text-2xl font-bold">0</p>
117
+ </div>
118
+ <div class="bg-white dark:bg-slate-800 rounded-xl p-4 shadow-sm border border-slate-100 dark:border-slate-700 hover:shadow-md transition-shadow">
119
+ <div class="flex items-center justify-between mb-2">
120
+ <p class="text-sm text-slate-500">Avg Latency</p>
121
+ <div class="p-1.5 bg-yellow-500/10 rounded-lg">
122
+ <svg class="w-4 h-4 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
123
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
124
+ </svg>
125
+ </div>
126
+ </div>
127
+ <p id="metric-latency" class="text-2xl font-bold">0ms</p>
128
+ </div>
129
+ <div class="bg-white dark:bg-slate-800 rounded-xl p-4 shadow-sm border border-slate-100 dark:border-slate-700 hover:shadow-md transition-shadow">
130
+ <div class="flex items-center justify-between mb-2">
131
+ <p class="text-sm text-slate-500">Total Tokens</p>
132
+ <div class="p-1.5 bg-purple-500/10 rounded-lg">
133
+ <svg class="w-4 h-4 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
134
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
135
+ </svg>
136
+ </div>
137
+ </div>
138
+ <p id="metric-tokens" class="text-2xl font-bold">0</p>
139
+ </div>
140
+ <div class="bg-white dark:bg-slate-800 rounded-xl p-4 shadow-sm border border-slate-100 dark:border-slate-700 hover:shadow-md transition-shadow">
141
+ <div class="flex items-center justify-between mb-2">
142
+ <p class="text-sm text-slate-500">Error Rate</p>
143
+ <div class="p-1.5 bg-red-500/10 rounded-lg">
144
+ <svg class="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
145
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
146
+ </svg>
147
+ </div>
148
+ </div>
149
+ <p id="metric-errors" class="text-2xl font-bold">0%</p>
150
+ </div>
151
+ <div class="bg-white dark:bg-slate-800 rounded-xl p-4 shadow-sm border border-slate-100 dark:border-slate-700 hover:shadow-md transition-shadow">
152
+ <div class="flex items-center justify-between mb-2">
153
+ <p class="text-sm text-slate-500">Est. Cost</p>
154
+ <div class="p-1.5 bg-green-500/10 rounded-lg">
155
+ <svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
156
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
157
+ </svg>
158
+ </div>
159
+ </div>
160
+ <p id="metric-cost" class="text-2xl font-bold">$0.00</p>
161
+ </div>
162
+ <div class="bg-white dark:bg-slate-800 rounded-xl p-4 shadow-sm border border-slate-100 dark:border-slate-700 hover:shadow-md transition-shadow">
163
+ <div class="flex items-center justify-between mb-2">
164
+ <p class="text-sm text-slate-500">Sessions</p>
165
+ <div class="p-1.5 bg-indigo-500/10 rounded-lg">
166
+ <svg class="w-4 h-4 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
167
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
168
+ </svg>
169
+ </div>
170
+ </div>
171
+ <p id="metric-sessions" class="text-2xl font-bold">0</p>
172
+ </div>
173
+ </div>
174
+
175
+ <!-- Sessions & Traces -->
176
+ <div class="grid grid-cols-12 gap-6">
177
+ <div class="col-span-3">
178
+ <div class="flex items-center justify-between mb-4">
179
+ <h2 class="text-lg font-semibold">Sessions</h2>
180
+ <span id="sessionCount" class="text-xs px-2 py-1 bg-slate-100 dark:bg-slate-700 rounded-full">0</span>
181
+ </div>
182
+ <div id="sessions" class="space-y-2 max-h-[600px] overflow-y-auto scrollbar-thin pr-2"></div>
183
+ </div>
184
+
185
+ <div class="col-span-5">
186
+ <div class="flex items-center justify-between mb-4">
187
+ <h2 class="text-lg font-semibold">Trace Viewer</h2>
188
+ <div class="flex items-center gap-2">
189
+ <button onclick="expandAll()" class="text-xs px-2 py-1 bg-slate-100 dark:bg-slate-700 rounded hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors">Expand All</button>
190
+ <button onclick="collapseAll()" class="text-xs px-2 py-1 bg-slate-100 dark:bg-slate-700 rounded hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors">Collapse All</button>
191
+ </div>
192
+ </div>
193
+ <div id="traces" class="bg-white dark:bg-slate-800 rounded-xl p-4 shadow-sm border border-slate-100 dark:border-slate-700 min-h-[500px] max-h-[600px] overflow-y-auto scrollbar-thin">
194
+ <div class="flex flex-col items-center justify-center h-full text-center py-12">
195
+ <svg class="w-16 h-16 text-slate-300 dark:text-slate-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
196
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
197
+ </svg>
198
+ <p class="text-slate-500 dark:text-slate-400 font-medium">No session selected</p>
199
+ <p class="text-sm text-slate-400 dark:text-slate-500 mt-1">Select a session from the left panel to view traces</p>
200
+ </div>
201
+ </div>
202
+ </div>
203
+
204
+ <div class="col-span-4">
205
+ <div class="flex items-center justify-between mb-4">
206
+ <h2 class="text-lg font-semibold">Run Details</h2>
207
+ <button onclick="closeDetails()" id="closeDetailsBtn" class="hidden text-xs px-2 py-1 bg-slate-100 dark:bg-slate-700 rounded hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors">Close</button>
208
+ </div>
209
+ <div id="runDetails" class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-100 dark:border-slate-700 min-h-[500px] max-h-[600px] overflow-hidden">
210
+ <div class="flex flex-col items-center justify-center h-full text-center py-12">
211
+ <svg class="w-16 h-16 text-slate-300 dark:text-slate-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
212
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
213
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
214
+ </svg>
215
+ <p class="text-slate-500 dark:text-slate-400 font-medium">No run selected</p>
216
+ <p class="text-sm text-slate-400 dark:text-slate-500 mt-1">Click on a trace to view details</p>
217
+ </div>
218
+ </div>
219
+ </div>
220
+ </div>
221
+
222
+ <div id="toastContainer" class="fixed bottom-6 right-6 z-50 space-y-2"></div>
223
+
224
+ <div id="commandPalette" class="hidden fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-start justify-center pt-[20vh]">
225
+ <div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl w-full max-w-xl border border-slate-200 dark:border-slate-700 overflow-hidden animate-slide-up">
226
+ <div class="flex items-center gap-3 px-4 py-3 border-b border-slate-200 dark:border-slate-700">
227
+ <svg class="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
228
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
229
+ </svg>
230
+ <input type="text" id="commandInput" placeholder="Search traces, sessions, or type a command..."
231
+ class="flex-1 bg-transparent border-none outline-none text-lg" autofocus />
232
+ <span class="kbd">ESC</span>
233
+ </div>
234
+ <div id="commandResults" class="max-h-80 overflow-y-auto p-2">
235
+ <p class="text-sm text-slate-500 text-center py-4">Start typing to search...</p>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ </div>
240
+
241
+ <script>
242
+ // State
243
+ let selectedSession = null;
244
+ let selectedRun = null;
245
+ let allSessions = [];
246
+ let currentRuns = [];
247
+ let searchQuery = '';
248
+ let typeFilter = '';
249
+ let expandedRuns = new Set();
250
+
251
+ // Theme management
252
+ function initTheme() {
253
+ const saved = localStorage.getItem('orka-devtools-theme');
254
+ if (saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
255
+ document.documentElement.classList.add('dark');
256
+ }
257
+ }
258
+ initTheme();
259
+
260
+ function toggleTheme() {
261
+ const isDark = document.documentElement.classList.toggle('dark');
262
+ localStorage.setItem('orka-devtools-theme', isDark ? 'dark' : 'light');
263
+ showToast(isDark ? '🌙 Dark mode enabled' : '☀️ Light mode enabled', 'info');
264
+ }
265
+
266
+ // Toast notifications
267
+ function showToast(message, type) {
268
+ type = type || 'info';
269
+ const colors = { info: 'bg-blue-500', success: 'bg-green-500', error: 'bg-red-500', warning: 'bg-yellow-500' };
270
+ const toast = document.createElement('div');
271
+ toast.className = colors[type] + ' text-white px-4 py-3 rounded-lg shadow-lg animate-slide-up flex items-center gap-2';
272
+ toast.innerHTML = '<span>' + message + '</span>';
273
+ document.getElementById('toastContainer').appendChild(toast);
274
+ setTimeout(function() { toast.remove(); }, 3000);
275
+ }
276
+
277
+ // SSE connection
278
+ let eventSource;
279
+ function connectSSE() {
280
+ eventSource = new EventSource('/api/events');
281
+ eventSource.onmessage = function(e) {
282
+ const event = JSON.parse(e.data);
283
+ handleEvent(event);
284
+ refreshData();
285
+ };
286
+ eventSource.onerror = function() {
287
+ document.getElementById('status').innerHTML = '<span class="w-2 h-2 bg-red-500 rounded-full"></span><span class="text-red-600 dark:text-red-400">Disconnected</span>';
288
+ setTimeout(connectSSE, 3000);
289
+ };
290
+ eventSource.onopen = function() {
291
+ document.getElementById('status').innerHTML = '<span class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span><span class="text-green-600 dark:text-green-400">Live</span>';
292
+ };
293
+ }
294
+ connectSSE();
295
+
296
+ function handleEvent(event) {
297
+ if (event.type === 'run:start') {
298
+ showToast('▶️ ' + (event.run && event.run.type || 'Run') + ' started: ' + (event.run && event.run.name || 'Unknown'), 'info');
299
+ } else if (event.type === 'run:end') {
300
+ const status = event.run && event.run.status === 'success' ? '✅' : '❌';
301
+ showToast(status + ' ' + (event.run && event.run.type || 'Run') + ' completed', event.run && event.run.status === 'success' ? 'success' : 'error');
302
+ }
303
+ }
304
+
305
+ async function refreshData() {
306
+ try {
307
+ const [metrics, sessions] = await Promise.all([
308
+ fetch('/api/metrics').then(function(r) { return r.json(); }),
309
+ fetch('/api/sessions').then(function(r) { return r.json(); })
310
+ ]);
311
+
312
+ document.getElementById('metric-runs').textContent = metrics.totalRuns.toLocaleString();
313
+ document.getElementById('metric-latency').textContent = Math.round(metrics.avgLatencyMs) + 'ms';
314
+ document.getElementById('metric-tokens').textContent = metrics.totalTokens.toLocaleString();
315
+ document.getElementById('metric-errors').textContent = (metrics.errorRate * 100).toFixed(1) + '%';
316
+ document.getElementById('metric-cost').textContent = '$' + (metrics.totalCost || 0).toFixed(4);
317
+ document.getElementById('metric-sessions').textContent = sessions.length;
318
+ document.getElementById('sessionCount').textContent = sessions.length;
319
+
320
+ allSessions = sessions;
321
+ renderSessions(sessions);
322
+
323
+ if (selectedSession) {
324
+ const session = sessions.find(function(s) { return s.id === selectedSession; });
325
+ if (session) {
326
+ currentRuns = session.runs;
327
+ renderTraces(applyFilters(session.runs));
328
+ }
329
+ }
330
+ } catch (err) {
331
+ console.error('Failed to refresh:', err);
332
+ }
333
+ }
334
+
335
+ function renderSessions(sessions) {
336
+ const container = document.getElementById('sessions');
337
+ if (sessions.length === 0) {
338
+ container.innerHTML = '<div class="text-center py-8 text-slate-500"><p>No sessions yet</p><p class="text-sm mt-1">Start using your LLM to see traces</p></div>';
339
+ return;
340
+ }
341
+
342
+ container.innerHTML = sessions.map(function(s) {
343
+ const duration = s.endTime ? ((s.endTime - s.startTime) / 1000).toFixed(1) + 's' : 'active';
344
+ const isSelected = selectedSession === s.id;
345
+ const cls = isSelected ? 'bg-purple-500/10 border-purple-500 shadow-sm' : 'bg-white dark:bg-slate-800 border-transparent hover:border-slate-200 dark:hover:border-slate-600 hover:shadow-sm';
346
+ const durCls = s.endTime ? 'bg-slate-100 dark:bg-slate-700' : 'bg-green-500/10 text-green-600';
347
+ return '<div onclick="selectSession(\'' + s.id + '\')" class="p-3 rounded-lg cursor-pointer transition-all border ' + cls + '">' +
348
+ '<div class="flex items-center justify-between">' +
349
+ '<p class="font-medium truncate">' + (s.name || 'Session') + '</p>' +
350
+ '<span class="text-xs px-1.5 py-0.5 rounded ' + durCls + '">' + duration + '</span>' +
351
+ '</div>' +
352
+ '<div class="flex items-center gap-2 mt-1 text-xs text-slate-500">' +
353
+ '<span>' + s.runs.length + ' runs</span><span>•</span><span>' + new Date(s.startTime).toLocaleTimeString() + '</span>' +
354
+ '</div></div>';
355
+ }).join('');
356
+ }
357
+
358
+ function selectSession(id) {
359
+ selectedSession = id;
360
+ selectedRun = null;
361
+ refreshData();
362
+ }
363
+
364
+ function applyFilters(runs) {
365
+ if (!runs) return [];
366
+ let filtered = runs;
367
+ if (typeFilter) filtered = filterByType(filtered, typeFilter);
368
+ if (searchQuery) filtered = filterBySearch(filtered, searchQuery);
369
+ return filtered;
370
+ }
371
+
372
+ function filterByType(runs, type) {
373
+ return runs.filter(function(r) {
374
+ if (r.type === type) return true;
375
+ if (r.children && r.children.length) {
376
+ r.children = filterByType(r.children, type);
377
+ return r.children.length > 0;
378
+ }
379
+ return false;
380
+ });
381
+ }
382
+
383
+ function filterBySearch(runs, query) {
384
+ const q = query.toLowerCase();
385
+ return runs.filter(function(r) {
386
+ const matches = r.name.toLowerCase().includes(q) || r.type.toLowerCase().includes(q);
387
+ if (matches) return true;
388
+ if (r.children && r.children.length) {
389
+ r.children = filterBySearch(r.children, query);
390
+ return r.children.length > 0;
391
+ }
392
+ return false;
393
+ });
394
+ }
395
+
396
+ function renderTraces(runs) {
397
+ const container = document.getElementById('traces');
398
+ if (!runs || runs.length === 0) {
399
+ container.innerHTML = '<div class="flex flex-col items-center justify-center h-full text-center py-12"><svg class="w-12 h-12 text-slate-300 dark:text-slate-600 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg><p class="text-slate-500">No traces found</p></div>';
400
+ return;
401
+ }
402
+ container.innerHTML = runs.map(function(run) { return renderRun(run, 0); }).join('');
403
+ }
404
+
405
+ function renderRun(run, depth) {
406
+ const statusColors = { success: 'bg-green-500', error: 'bg-red-500', running: 'bg-yellow-500 animate-pulse' };
407
+ const typeColors = {
408
+ llm: 'bg-purple-500/10 text-purple-600 dark:text-purple-400',
409
+ agent: 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
410
+ tool: 'bg-orange-500/10 text-orange-600 dark:text-orange-400',
411
+ retrieval: 'bg-green-500/10 text-green-600 dark:text-green-400',
412
+ chain: 'bg-pink-500/10 text-pink-600 dark:text-pink-400',
413
+ workflow: 'bg-indigo-500/10 text-indigo-600 dark:text-indigo-400',
414
+ graph: 'bg-cyan-500/10 text-cyan-600 dark:text-cyan-400'
415
+ };
416
+ const hasChildren = run.children && run.children.length > 0;
417
+ const isExpanded = expandedRuns.has(run.id);
418
+ const isSelected = selectedRun && selectedRun.id === run.id;
419
+ const statusCls = statusColors[run.status] || 'bg-slate-400';
420
+ const typeCls = typeColors[run.type] || 'bg-slate-100 dark:bg-slate-700';
421
+ const selectedCls = isSelected ? 'bg-purple-500/10 ring-1 ring-purple-500' : 'hover:bg-slate-100 dark:hover:bg-slate-700/50';
422
+ const expandIcon = hasChildren ? '<button onclick="event.stopPropagation(); toggleExpand(\'' + run.id + '\')" class="p-0.5 hover:bg-slate-200 dark:hover:bg-slate-600 rounded"><svg class="w-4 h-4 text-slate-400 transition-transform ' + (isExpanded ? 'rotate-90' : '') + '" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg></button>' : '<span class="w-5"></span>';
423
+ const tokens = run.metadata && run.metadata.totalTokens ? '<span class="text-xs text-slate-400">' + run.metadata.totalTokens + ' tok</span>' : '';
424
+
425
+ let html = '<div class="run-card mb-1" style="margin-left: ' + (depth * 16) + 'px">' +
426
+ '<div onclick="selectRun(\'' + run.id + '\')" class="flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ' + selectedCls + '">' +
427
+ expandIcon +
428
+ '<span class="w-2 h-2 rounded-full ' + statusCls + '"></span>' +
429
+ '<span class="text-xs font-medium px-1.5 py-0.5 rounded ' + typeCls + '">' + run.type + '</span>' +
430
+ '<span class="font-medium text-sm truncate flex-1">' + run.name + '</span>' +
431
+ '<span class="text-xs text-slate-500">' + (run.latencyMs ? run.latencyMs + 'ms' : '...') + '</span>' +
432
+ tokens +
433
+ '</div>';
434
+
435
+ if (hasChildren && isExpanded) {
436
+ html += run.children.map(function(c) { return renderRun(c, depth + 1); }).join('');
437
+ }
438
+ html += '</div>';
439
+ return html;
440
+ }
441
+
442
+ function toggleExpand(runId) {
443
+ if (expandedRuns.has(runId)) {
444
+ expandedRuns.delete(runId);
445
+ } else {
446
+ expandedRuns.add(runId);
447
+ }
448
+ renderTraces(applyFilters(currentRuns));
449
+ }
450
+
451
+ function expandAll() {
452
+ function addAll(runs) {
453
+ runs.forEach(function(r) {
454
+ if (r.children && r.children.length) {
455
+ expandedRuns.add(r.id);
456
+ addAll(r.children);
457
+ }
458
+ });
459
+ }
460
+ addAll(currentRuns);
461
+ renderTraces(applyFilters(currentRuns));
462
+ }
463
+
464
+ function collapseAll() {
465
+ expandedRuns.clear();
466
+ renderTraces(applyFilters(currentRuns));
467
+ }
468
+
469
+ async function selectRun(runId) {
470
+ try {
471
+ const run = await fetch('/api/runs/' + runId + '?sessionId=' + selectedSession).then(function(r) { return r.json(); });
472
+ selectedRun = run;
473
+ renderRunDetails(run);
474
+ document.getElementById('closeDetailsBtn').classList.remove('hidden');
475
+ renderTraces(applyFilters(currentRuns));
476
+ } catch (err) {
477
+ console.error('Failed to fetch run:', err);
478
+ }
479
+ }
480
+
481
+ function renderRunDetails(run) {
482
+ const container = document.getElementById('runDetails');
483
+ const statusColors = { success: 'text-green-500', error: 'text-red-500', running: 'text-yellow-500' };
484
+ const statusCls = statusColors[run.status] || '';
485
+
486
+ let html = '<div class="h-full flex flex-col">' +
487
+ '<div class="p-4 border-b border-slate-100 dark:border-slate-700">' +
488
+ '<div class="flex items-center gap-2 mb-2">' +
489
+ '<span class="font-semibold text-lg">' + run.name + '</span>' +
490
+ '<span class="text-xs px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-700">' + run.type + '</span>' +
491
+ '<span class="' + statusCls + ' text-sm ml-auto">' + run.status + '</span>' +
492
+ '</div>' +
493
+ '<div class="flex items-center gap-4 text-sm text-slate-500">' +
494
+ '<span>⏱️ ' + (run.latencyMs || 0) + 'ms</span>';
495
+
496
+ if (run.metadata && run.metadata.totalTokens) {
497
+ html += '<span>🎫 ' + run.metadata.totalTokens + ' tokens</span>';
498
+ }
499
+ if (run.metadata && run.metadata.model) {
500
+ html += '<span>🤖 ' + run.metadata.model + '</span>';
501
+ }
502
+ html += '</div></div>';
503
+
504
+ html += '<div class="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-thin">';
505
+
506
+ if (run.input) {
507
+ html += '<div><h4 class="text-sm font-medium text-slate-500 mb-2 flex items-center gap-2"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7"/></svg>Input</h4><pre class="text-sm bg-slate-50 dark:bg-slate-900 p-3 rounded-lg overflow-x-auto">' + formatJSON(run.input) + '</pre></div>';
508
+ }
509
+
510
+ if (run.output) {
511
+ html += '<div><h4 class="text-sm font-medium text-slate-500 mb-2 flex items-center gap-2"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"/></svg>Output</h4><pre class="text-sm bg-slate-50 dark:bg-slate-900 p-3 rounded-lg overflow-x-auto">' + formatJSON(run.output) + '</pre></div>';
512
+ }
513
+
514
+ if (run.error) {
515
+ html += '<div><h4 class="text-sm font-medium text-red-500 mb-2 flex items-center gap-2"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>Error</h4><pre class="text-sm bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-3 rounded-lg overflow-x-auto">' + run.error + '</pre></div>';
516
+ }
517
+
518
+ if (run.metadata && Object.keys(run.metadata).length) {
519
+ html += '<div><h4 class="text-sm font-medium text-slate-500 mb-2 flex items-center gap-2"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/></svg>Metadata</h4><pre class="text-sm bg-slate-50 dark:bg-slate-900 p-3 rounded-lg overflow-x-auto">' + formatJSON(run.metadata) + '</pre></div>';
520
+ }
521
+
522
+ html += '</div>';
523
+ html += '<div class="p-3 border-t border-slate-100 dark:border-slate-700 flex gap-2"><button onclick="copyRunData()" class="flex-1 px-3 py-1.5 text-sm bg-slate-100 dark:bg-slate-700 rounded-lg hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors">📋 Copy JSON</button></div>';
524
+ html += '</div>';
525
+
526
+ container.innerHTML = html;
527
+ }
528
+
529
+ function formatJSON(data) {
530
+ try {
531
+ if (typeof data === 'string') return escapeHtml(data);
532
+ return escapeHtml(JSON.stringify(data, null, 2));
533
+ } catch (e) {
534
+ return escapeHtml(String(data));
535
+ }
536
+ }
537
+
538
+ function escapeHtml(str) {
539
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
540
+ }
541
+
542
+ function closeDetails() {
543
+ selectedRun = null;
544
+ document.getElementById('closeDetailsBtn').classList.add('hidden');
545
+ document.getElementById('runDetails').innerHTML = '<div class="flex flex-col items-center justify-center h-full text-center py-12"><svg class="w-16 h-16 text-slate-300 dark:text-slate-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg><p class="text-slate-500 dark:text-slate-400 font-medium">No run selected</p><p class="text-sm text-slate-400 dark:text-slate-500 mt-1">Click on a trace to view details</p></div>';
546
+ renderTraces(applyFilters(currentRuns));
547
+ }
548
+
549
+ function copyRunData() {
550
+ if (selectedRun) {
551
+ navigator.clipboard.writeText(JSON.stringify(selectedRun, null, 2));
552
+ showToast('📋 Copied to clipboard', 'success');
553
+ }
554
+ }
555
+
556
+ async function clearTraces() {
557
+ if (!confirm('Clear all traces? This cannot be undone.')) return;
558
+ await fetch('/api/sessions', { method: 'DELETE' });
559
+ selectedSession = null;
560
+ selectedRun = null;
561
+ currentRuns = [];
562
+ refreshData();
563
+ closeDetails();
564
+ showToast('🗑️ All traces cleared', 'success');
565
+ }
566
+
567
+ function exportTraces() {
568
+ window.open('/api/export', '_blank');
569
+ showToast('📤 Exporting traces...', 'info');
570
+ }
571
+
572
+ document.getElementById('searchInput').addEventListener('input', function(e) {
573
+ searchQuery = e.target.value;
574
+ renderTraces(applyFilters(currentRuns));
575
+ });
576
+
577
+ document.getElementById('typeFilter').addEventListener('change', function(e) {
578
+ typeFilter = e.target.value;
579
+ renderTraces(applyFilters(currentRuns));
580
+ });
581
+
582
+ function openCommandPalette() {
583
+ document.getElementById('commandPalette').classList.remove('hidden');
584
+ document.getElementById('commandInput').focus();
585
+ }
586
+
587
+ function closeCommandPalette() {
588
+ document.getElementById('commandPalette').classList.add('hidden');
589
+ document.getElementById('commandInput').value = '';
590
+ }
591
+
592
+ document.getElementById('commandPalette').addEventListener('click', function(e) {
593
+ if (e.target.id === 'commandPalette') closeCommandPalette();
594
+ });
595
+
596
+ document.addEventListener('keydown', function(e) {
597
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
598
+ e.preventDefault();
599
+ openCommandPalette();
600
+ }
601
+ if (e.key === 'Escape') {
602
+ closeCommandPalette();
603
+ }
604
+ if ((e.metaKey || e.ctrlKey) && e.key === 'd') {
605
+ e.preventDefault();
606
+ toggleTheme();
607
+ }
608
+ });
609
+
610
+ refreshData();
611
+ </script>
612
+ </body>
613
+ </html>