mishig HF Staff commited on
Commit
a74678d
Β·
verified Β·
1 Parent(s): 33fa589

Update source

Browse files
Files changed (1) hide show
  1. src/lib/TraceViewer.svelte +245 -129
src/lib/TraceViewer.svelte CHANGED
@@ -14,6 +14,8 @@
14
  let error = $state('');
15
  let messages = $state([]);
16
  let focusedIdx = $state(-1);
 
 
17
  let listEl;
18
 
19
  async function load() {
@@ -22,18 +24,30 @@
22
  messages = [];
23
  focusedIdx = -1;
24
  loadedCount = 0;
 
 
 
25
  try {
26
  const res = await fetch(toRawUrl(url));
27
  if (!res.ok) throw new Error(`Failed to fetch (HTTP ${res.status})`);
28
  const text = await res.text();
29
- messages = parseJsonl(text);
30
- loadedCount = messages.length;
31
- if (messages.length === 0) {
 
 
 
 
 
 
 
 
 
 
32
  error = 'No messages parsed from this file.';
33
  } else {
34
- focusedIdx = 0;
35
  await tick();
36
- scrollToFocused('auto');
37
  }
38
  } catch (e) {
39
  error = e?.message || String(e);
@@ -42,6 +56,72 @@
42
  }
43
  }
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  async function handleKey(e) {
46
  const t = e.target;
47
  if (
@@ -51,8 +131,17 @@
51
  t.isContentEditable)
52
  )
53
  return;
54
- if (messages.length === 0) return;
55
 
 
 
 
 
 
 
 
 
 
 
56
  if (e.key === 'ArrowDown' || e.key === 'j') {
57
  e.preventDefault();
58
  focusedIdx = Math.min(messages.length - 1, focusedIdx + 1);
@@ -163,14 +252,23 @@
163
  <span class="text-[#991b1b]">{error}</span>
164
  </div>
165
  {:else if messages.length > 0}
166
- <div class="flex items-baseline gap-2 mb-3">
167
  <span
168
- class="w-[1ch] text-center text-[#0f7a3a] animate-ready-pulse"
169
- >●</span
 
170
  >
171
- <span class="text-[#0f5a2a] font-semibold"
172
- >Loaded {loadedCount} messages</span
 
 
 
 
173
  >
 
 
 
 
174
  </div>
175
  {:else}
176
  <div class="text-[#888] text-[13px] leading-relaxed">
@@ -209,113 +307,123 @@
209
  {/if}
210
 
211
  {#each messages as msg, i (i)}
212
- {@const focused = i === focusedIdx}
213
- <!-- svelte-ignore a11y_click_events_have_key_events -->
214
- <!-- svelte-ignore a11y_no_static_element_interactions -->
215
- <div
216
- data-idx={i}
217
- onclick={() => (focusedIdx = i)}
218
- class="py-1 cursor-default rounded transition-colors {focused
219
- ? 'bg-[#fffbe6]'
220
- : 'hover:bg-[#faf9f5]'}"
221
- >
222
- <!-- header -->
223
- <div class="flex items-baseline gap-2 px-2">
224
- <span
225
- class="w-[1ch] text-center {focused
226
- ? 'text-[#0f7a3a] animate-ready-pulse'
227
- : 'text-[#6b6b68]'}"
228
- >
229
- {focused ? '●' : 'β—‹'}
230
- </span>
231
- <span
232
- class="text-[11px] uppercase tracking-wider font-semibold {roleColor[
233
- msg.role
234
- ] || roleColor.unknown}"
235
- >
236
- {msg.role}
237
- </span>
238
- {#if msg.title}
239
- <span class="text-[12px] text-[#6a6a66] truncate"
240
- >{msg.title}</span
241
  >
242
- {/if}
243
- <span class="ml-auto flex items-baseline gap-3">
244
- {#if msg.model}
245
- <span class="text-[11px] text-[#888]">{msg.model}</span>
246
- {/if}
247
- <span class="text-[11px] text-[#aaa]">#{i}</span>
248
- </span>
249
- </div>
250
-
251
- <!-- blocks -->
252
- {#each msg.blocks as block, bi}
253
- {@const isLast = bi === msg.blocks.length - 1}
254
- <div class="flex items-start gap-2 px-2">
255
- <span class="w-[1ch] text-[#b3b3ad] shrink-0 mt-[2px]"
256
- >{isLast ? 'β””' : 'β”œ'}</span
257
  >
258
- <div class="flex-1 min-w-0">
259
- {#if block.kind === 'text'}
260
- <pre
261
- class="whitespace-pre-wrap break-words text-[13px] text-[#232323] leading-[1.65] font-mono">{block.text}</pre>
262
- {:else if block.kind === 'thinking'}
263
- <details class="py-0.5">
264
- <summary
265
- class="cursor-pointer text-[11px] text-[#8b5cf6] font-semibold select-none hover:underline"
266
- >thinking</summary
267
- >
268
- <pre
269
- class="whitespace-pre-wrap break-words text-[12px] text-[#6b21a8] mt-1 leading-[1.65] pl-[1ch] border-l border-[#e9d5ff]">{block.text}</pre>
270
- </details>
271
- {:else if block.kind === 'tool_call'}
272
- <div class="py-0.5">
273
- <div class="text-[12px] text-[#6b21a8]">
274
- <span class="text-[#aaa]">tool</span>
275
- <span class="font-semibold">{block.name}</span>
276
- </div>
277
- <pre
278
- class="text-[12px] text-[#3a3a38] whitespace-pre-wrap break-words max-h-[240px] overflow-auto mt-0.5 pl-[1ch] border-l border-[#e9d5ff]">{formatJson(
279
- block.input
280
- )}</pre>
281
- </div>
282
- {:else if block.kind === 'tool_result'}
283
- <div class="py-0.5">
284
- <div
285
- class="text-[12px] {block.isError
286
- ? 'text-[#991b1b]'
287
- : 'text-[#6a6a66]'}"
288
- >
289
- <span class="text-[#aaa]">result</span>
290
- {#if block.isError}<span class="font-semibold"
291
- >Β· error</span
292
- >{/if}
293
- </div>
294
- <pre
295
- class="text-[12px] text-[#3a3a38] whitespace-pre-wrap break-words max-h-[280px] overflow-auto mt-0.5 pl-[1ch] border-l {block.isError
296
- ? 'border-[#fecaca]'
297
- : 'border-[#e5e5e0]'}">{block.text}</pre>
298
- </div>
299
- {:else if block.kind === 'image'}
300
- <div class="text-[12px] text-[#6a6a66] italic">
301
- [image attachment]
302
- </div>
303
- {:else if block.kind === 'raw'}
304
- <details>
305
- <summary
306
- class="cursor-pointer text-[11px] text-[#888] select-none hover:underline"
307
- >raw</summary
308
- >
309
- <pre
310
- class="text-[11px] text-[#555] whitespace-pre-wrap break-words mt-1 max-h-[200px] overflow-auto pl-[1ch] border-l border-[#e5e5e0]">{formatJson(
311
- block.json
312
- )}</pre>
313
- </details>
314
  {/if}
315
- </div>
 
316
  </div>
317
- {/each}
318
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  {/each}
320
  </div>
321
 
@@ -328,22 +436,30 @@
328
  {:else}
329
  <span>ready</span>
330
  {/if}
331
- <span class="flex items-center gap-1">
332
- <kbd class="px-1 py-px bg-[#f5f5f2] rounded border border-[#e5e5e0]"
333
- >↑</kbd
334
- >
335
- <kbd class="px-1 py-px bg-[#f5f5f2] rounded border border-[#e5e5e0]"
336
- >↓</kbd
337
- > navigate
338
- </span>
339
- <span class="flex items-center gap-1">
340
- <kbd class="px-1 py-px bg-[#f5f5f2] rounded border border-[#e5e5e0]"
341
- >Home</kbd
342
  >
343
- <kbd class="px-1 py-px bg-[#f5f5f2] rounded border border-[#e5e5e0]"
344
- >End</kbd
345
- > jump
346
- </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  </div>
348
  </div>
349
  </div>
 
14
  let error = $state('');
15
  let messages = $state([]);
16
  let focusedIdx = $state(-1);
17
+ let playing = $state(false);
18
+ let skipFlag = false;
19
  let listEl;
20
 
21
  async function load() {
 
24
  messages = [];
25
  focusedIdx = -1;
26
  loadedCount = 0;
27
+ playing = false;
28
+ skipFlag = false;
29
+
30
  try {
31
  const res = await fetch(toRawUrl(url));
32
  if (!res.ok) throw new Error(`Failed to fetch (HTTP ${res.status})`);
33
  const text = await res.text();
34
+ const parsed = parseJsonl(text);
35
+ for (const msg of parsed) {
36
+ msg._visible = false;
37
+ msg._visibleBlocks = 0;
38
+ for (const block of msg.blocks) {
39
+ block._typedText = '';
40
+ block._typing = false;
41
+ }
42
+ }
43
+ messages = parsed;
44
+ loadedCount = parsed.length;
45
+
46
+ if (parsed.length === 0) {
47
  error = 'No messages parsed from this file.';
48
  } else {
 
49
  await tick();
50
+ playback();
51
  }
52
  } catch (e) {
53
  error = e?.message || String(e);
 
56
  }
57
  }
58
 
59
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
60
+
61
+ async function playback() {
62
+ playing = true;
63
+ skipFlag = false;
64
+ for (let mi = 0; mi < messages.length; mi++) {
65
+ if (skipFlag) break;
66
+ const msg = messages[mi];
67
+ msg._visible = true;
68
+ focusedIdx = mi;
69
+ await tick();
70
+ scrollToFocused('smooth');
71
+ await wait(40);
72
+
73
+ for (let bi = 0; bi < msg.blocks.length; bi++) {
74
+ if (skipFlag) break;
75
+ msg._visibleBlocks = bi + 1;
76
+ const block = msg.blocks[bi];
77
+ if (block.kind === 'text' || block.kind === 'thinking') {
78
+ await typeBlock(block);
79
+ } else {
80
+ await wait(90);
81
+ }
82
+ }
83
+ if (skipFlag) break;
84
+ await wait(120);
85
+ }
86
+
87
+ if (skipFlag) revealAll();
88
+ playing = false;
89
+ }
90
+
91
+ async function typeBlock(block) {
92
+ const full = block.text || '';
93
+ const len = full.length;
94
+ if (len === 0) return;
95
+ const totalMs = Math.max(250, Math.min(1400, len * 10));
96
+ const tickMs = 16;
97
+ const totalTicks = Math.ceil(totalMs / tickMs);
98
+ const step = Math.max(1, Math.ceil(len / totalTicks));
99
+
100
+ block._typing = true;
101
+ for (let c = step; c < len; c += step) {
102
+ if (skipFlag) break;
103
+ block._typedText = full.slice(0, c);
104
+ await wait(tickMs);
105
+ }
106
+ block._typedText = full;
107
+ block._typing = false;
108
+ }
109
+
110
+ function revealAll() {
111
+ for (const msg of messages) {
112
+ msg._visible = true;
113
+ msg._visibleBlocks = msg.blocks.length;
114
+ for (const block of msg.blocks) {
115
+ if ('_typedText' in block) block._typedText = block.text || '';
116
+ block._typing = false;
117
+ }
118
+ }
119
+ }
120
+
121
+ function skip() {
122
+ skipFlag = true;
123
+ }
124
+
125
  async function handleKey(e) {
126
  const t = e.target;
127
  if (
 
131
  t.isContentEditable)
132
  )
133
  return;
 
134
 
135
+ if (
136
+ playing &&
137
+ ['ArrowDown', 'ArrowUp', 'Home', 'End', 'Escape', ' '].includes(e.key)
138
+ ) {
139
+ e.preventDefault();
140
+ skip();
141
+ return;
142
+ }
143
+
144
+ if (messages.length === 0) return;
145
  if (e.key === 'ArrowDown' || e.key === 'j') {
146
  e.preventDefault();
147
  focusedIdx = Math.min(messages.length - 1, focusedIdx + 1);
 
252
  <span class="text-[#991b1b]">{error}</span>
253
  </div>
254
  {:else if messages.length > 0}
255
+ <div class="flex items-baseline gap-2 mb-3 animate-fade-in">
256
  <span
257
+ class="w-[1ch] text-center {playing
258
+ ? 'text-[#5f5f5c]'
259
+ : 'text-[#0f7a3a] animate-ready-pulse'}"
260
  >
261
+ {playing ? spinnerFrames[spinnerFrame] : '●'}
262
+ </span>
263
+ <span
264
+ class="{playing
265
+ ? 'text-[#333331]'
266
+ : 'text-[#0f5a2a]'} font-semibold"
267
  >
268
+ {playing
269
+ ? `Streaming ${focusedIdx + 1} / ${loadedCount}...`
270
+ : `Loaded ${loadedCount} messages`}
271
+ </span>
272
  </div>
273
  {:else}
274
  <div class="text-[#888] text-[13px] leading-relaxed">
 
307
  {/if}
308
 
309
  {#each messages as msg, i (i)}
310
+ {#if msg._visible}
311
+ {@const focused = i === focusedIdx}
312
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
313
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
314
+ <div
315
+ data-idx={i}
316
+ onclick={() => (focusedIdx = i)}
317
+ class="py-1 cursor-default rounded transition-colors animate-fade-in {focused
318
+ ? 'bg-[#fffbe6]'
319
+ : 'hover:bg-[#faf9f5]'}"
320
+ >
321
+ <!-- header -->
322
+ <div class="flex items-baseline gap-2 px-2">
323
+ <span
324
+ class="w-[1ch] text-center {focused
325
+ ? 'text-[#0f7a3a] animate-ready-pulse'
326
+ : 'text-[#6b6b68]'}"
 
 
 
 
 
 
 
 
 
 
 
 
327
  >
328
+ {focused ? '●' : 'β—‹'}
329
+ </span>
330
+ <span
331
+ class="text-[11px] uppercase tracking-wider font-semibold {roleColor[
332
+ msg.role
333
+ ] || roleColor.unknown}"
 
 
 
 
 
 
 
 
 
334
  >
335
+ {msg.role}
336
+ </span>
337
+ {#if msg.title}
338
+ <span class="text-[12px] text-[#6a6a66] truncate"
339
+ >{msg.title}</span
340
+ >
341
+ {/if}
342
+ <span class="ml-auto flex items-baseline gap-3">
343
+ {#if msg.model}
344
+ <span class="text-[11px] text-[#888]">{msg.model}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  {/if}
346
+ <span class="text-[11px] text-[#aaa]">#{i}</span>
347
+ </span>
348
  </div>
349
+
350
+ <!-- blocks (only show revealed ones) -->
351
+ {#each msg.blocks as block, bi}
352
+ {#if bi < msg._visibleBlocks}
353
+ {@const isLast = bi === msg.blocks.length - 1}
354
+ <div class="flex items-start gap-2 px-2 animate-fade-in">
355
+ <span class="w-[1ch] text-[#b3b3ad] shrink-0 mt-[2px]"
356
+ >{isLast ? 'β””' : 'β”œ'}</span
357
+ >
358
+ <div class="flex-1 min-w-0">
359
+ {#if block.kind === 'text'}
360
+ <pre
361
+ class="whitespace-pre-wrap break-words text-[13px] text-[#232323] leading-[1.65] font-mono">{block._typedText}{#if block._typing}<span
362
+ class="animate-blink text-[#8b5cf6]"
363
+ aria-hidden="true">β–Ž</span
364
+ >{/if}</pre>
365
+ {:else if block.kind === 'thinking'}
366
+ <details class="py-0.5" open>
367
+ <summary
368
+ class="cursor-pointer text-[11px] text-[#8b5cf6] font-semibold select-none hover:underline"
369
+ >thinking</summary
370
+ >
371
+ <pre
372
+ class="whitespace-pre-wrap break-words text-[12px] text-[#6b21a8] mt-1 leading-[1.65] pl-[1ch] border-l border-[#e9d5ff]">{block._typedText}{#if block._typing}<span
373
+ class="animate-blink text-[#8b5cf6]"
374
+ aria-hidden="true">β–Ž</span
375
+ >{/if}</pre>
376
+ </details>
377
+ {:else if block.kind === 'tool_call'}
378
+ <div class="py-0.5">
379
+ <div class="text-[12px] text-[#6b21a8]">
380
+ <span class="text-[#aaa]">tool</span>
381
+ <span class="font-semibold">{block.name}</span>
382
+ </div>
383
+ <pre
384
+ class="text-[12px] text-[#3a3a38] whitespace-pre-wrap break-words max-h-[240px] overflow-auto mt-0.5 pl-[1ch] border-l border-[#e9d5ff]">{formatJson(
385
+ block.input
386
+ )}</pre>
387
+ </div>
388
+ {:else if block.kind === 'tool_result'}
389
+ <div class="py-0.5">
390
+ <div
391
+ class="text-[12px] {block.isError
392
+ ? 'text-[#991b1b]'
393
+ : 'text-[#6a6a66]'}"
394
+ >
395
+ <span class="text-[#aaa]">result</span>
396
+ {#if block.isError}<span class="font-semibold"
397
+ >Β· error</span
398
+ >{/if}
399
+ </div>
400
+ <pre
401
+ class="text-[12px] text-[#3a3a38] whitespace-pre-wrap break-words max-h-[280px] overflow-auto mt-0.5 pl-[1ch] border-l {block.isError
402
+ ? 'border-[#fecaca]'
403
+ : 'border-[#e5e5e0]'}">{block.text}</pre>
404
+ </div>
405
+ {:else if block.kind === 'image'}
406
+ <div class="text-[12px] text-[#6a6a66] italic">
407
+ [image attachment]
408
+ </div>
409
+ {:else if block.kind === 'raw'}
410
+ <details>
411
+ <summary
412
+ class="cursor-pointer text-[11px] text-[#888] select-none hover:underline"
413
+ >raw</summary
414
+ >
415
+ <pre
416
+ class="text-[11px] text-[#555] whitespace-pre-wrap break-words mt-1 max-h-[200px] overflow-auto pl-[1ch] border-l border-[#e5e5e0]">{formatJson(
417
+ block.json
418
+ )}</pre>
419
+ </details>
420
+ {/if}
421
+ </div>
422
+ </div>
423
+ {/if}
424
+ {/each}
425
+ </div>
426
+ {/if}
427
  {/each}
428
  </div>
429
 
 
436
  {:else}
437
  <span>ready</span>
438
  {/if}
439
+ {#if playing}
440
+ <button
441
+ onclick={skip}
442
+ class="px-2 py-0.5 bg-[#f5f5f2] rounded border border-[#e5e5e0] text-[11px] hover:bg-[#eeeae0] cursor-pointer"
443
+ >skip</button
 
 
 
 
 
 
444
  >
445
+ {:else}
446
+ <span class="flex items-center gap-1">
447
+ <kbd class="px-1 py-px bg-[#f5f5f2] rounded border border-[#e5e5e0]"
448
+ >↑</kbd
449
+ >
450
+ <kbd class="px-1 py-px bg-[#f5f5f2] rounded border border-[#e5e5e0]"
451
+ >↓</kbd
452
+ > navigate
453
+ </span>
454
+ <span class="flex items-center gap-1">
455
+ <kbd class="px-1 py-px bg-[#f5f5f2] rounded border border-[#e5e5e0]"
456
+ >Home</kbd
457
+ >
458
+ <kbd class="px-1 py-px bg-[#f5f5f2] rounded border border-[#e5e5e0]"
459
+ >End</kbd
460
+ > jump
461
+ </span>
462
+ {/if}
463
  </div>
464
  </div>
465
  </div>