mishig HF Staff commited on
Commit
c3f575b
·
verified ·
1 Parent(s): f30617c

Update Svelte source

Browse files
Files changed (1) hide show
  1. src/lib/TraceViewer.svelte +176 -121
src/lib/TraceViewer.svelte CHANGED
@@ -1,11 +1,16 @@
1
  <script>
2
- import { tick } from 'svelte';
3
  import { parseJsonl, toRawUrl } from './parse.js';
4
 
 
 
 
 
5
  let url = $state(
6
  'https://huggingface.co/datasets/mishig/traces/blob/main/36c3db7c-71e3-4ee6-b528-e2a64c9cad77.jsonl'
7
  );
8
  let loading = $state(false);
 
9
  let error = $state('');
10
  let messages = $state([]);
11
  let focusedIdx = $state(-1);
@@ -16,11 +21,13 @@
16
  error = '';
17
  messages = [];
18
  focusedIdx = -1;
 
19
  try {
20
  const res = await fetch(toRawUrl(url));
21
  if (!res.ok) throw new Error(`Failed to fetch (HTTP ${res.status})`);
22
  const text = await res.text();
23
  messages = parseJsonl(text);
 
24
  if (messages.length === 0) {
25
  error = 'No messages parsed from this file.';
26
  } else {
@@ -79,13 +86,23 @@
79
  }
80
  }
81
 
82
- const roleBadge = {
83
- user: 'bg-[#dceafe] text-[#1e40af]',
84
- assistant: 'bg-[#dcfce7] text-[#166534]',
85
- tool: 'bg-[#f3e8ff] text-[#6b21a8]',
86
- system: 'bg-[#fef3c7] text-[#92400e]',
87
- meta: 'bg-[#f1f1ec] text-[#6a6a66]',
88
- unknown: 'bg-[#f1f1ec] text-[#6a6a66]',
 
 
 
 
 
 
 
 
 
 
89
  };
90
  </script>
91
 
@@ -95,7 +112,7 @@
95
  class="frame-bg frame-shadow w-[960px] max-w-[calc(100vw-48px)] rounded-[20px] p-[3px]"
96
  >
97
  <div
98
- class="w-full h-[85vh] bg-[#fbfbf9] rounded-[17px] overflow-hidden flex flex-col text-[#232323]"
99
  >
100
  <!-- chrome -->
101
  <div class="flex items-center gap-2 pt-4 px-[18px] pb-2 shrink-0">
@@ -107,83 +124,114 @@
107
  >
108
  </div>
109
 
110
- <!-- url input -->
111
- <div class="flex gap-2 px-5 py-3 border-b border-[#eeeae0] shrink-0">
 
 
 
112
  <input
113
  type="url"
114
  bind:value={url}
115
  onkeydown={(e) => {
116
  if (e.key === 'Enter') load();
117
  }}
118
- placeholder="https://huggingface.co/datasets/<user>/<repo>/blob/main/<file>.jsonl"
119
- class="flex-1 bg-[#f5f5f2] border border-[#e5e5e0] rounded-md px-3 py-2 text-[12px] font-mono focus:outline-none focus:border-[#ffd21e] focus:bg-white"
120
  />
121
  <button
122
  type="button"
123
  onclick={load}
124
  disabled={loading}
125
- class="px-4 py-2 bg-[#ffd21e] rounded-md text-[13px] font-semibold hover:bg-[#ffbb1a] disabled:opacity-50 cursor-pointer"
126
  >
127
- {loading ? 'Loading…' : 'Load'}
128
  </button>
129
  </div>
130
 
131
- <!-- messages -->
132
- <div
133
- bind:this={listEl}
134
- class="flex-1 overflow-y-auto px-5 py-4 space-y-3"
135
- >
136
- {#if error}
137
- <div
138
- class="text-[#991b1b] bg-[#fef2f2] border border-[#fecaca] rounded-md p-3 text-[13px]"
139
- >
140
- {error}
141
  </div>
142
- {:else if messages.length === 0 && !loading}
143
- <div class="text-[#888] text-[13px] leading-relaxed">
144
- Paste a Hugging Face dataset <code
145
- class="bg-[#f5f5f2] px-1 rounded">.jsonl</code
146
- > URL above and press
147
- <kbd
148
- class="px-1.5 py-0.5 bg-[#f0f0ed] rounded border border-[#ddd] text-[11px]"
149
- >Load</kbd
150
- >.<br />
151
- Then use
152
- <kbd
153
- class="px-1.5 py-0.5 bg-[#f0f0ed] rounded border border-[#ddd] text-[11px]"
154
- >↑</kbd
155
  >
156
- <kbd
157
- class="px-1.5 py-0.5 bg-[#f0f0ed] rounded border border-[#ddd] text-[11px]"
158
- >↓</kbd
159
- > to jump between sections,
160
- <kbd
161
- class="px-1.5 py-0.5 bg-[#f0f0ed] rounded border border-[#ddd] text-[11px]"
162
- >Home</kbd
163
- >/
164
- <kbd
165
- class="px-1.5 py-0.5 bg-[#f0f0ed] rounded border border-[#ddd] text-[11px]"
166
- >End</kbd
167
- > for start/end.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  </div>
169
  {/if}
170
 
171
  {#each messages as msg, i (i)}
 
172
  <!-- svelte-ignore a11y_click_events_have_key_events -->
173
  <!-- svelte-ignore a11y_no_static_element_interactions -->
174
  <div
175
  data-idx={i}
176
  onclick={() => (focusedIdx = i)}
177
- class="border rounded-lg px-4 py-3 transition-colors cursor-default {i ===
178
- focusedIdx
179
- ? 'border-[#ffd21e] bg-[#fffbe6] shadow-[0_0_0_2px_rgba(255,210,30,0.25)]'
180
- : 'border-[#eaeae5] bg-white hover:border-[#d9d9d2]'}"
181
  >
182
- <div class="flex items-center gap-2 mb-2 flex-wrap">
 
 
 
 
 
 
 
 
183
  <span
184
- class="text-[10px] uppercase tracking-wider font-semibold px-2 py-0.5 rounded {roleBadge[
185
  msg.role
186
- ] || roleBadge.unknown}"
187
  >
188
  {msg.role}
189
  </span>
@@ -192,73 +240,80 @@
192
  >{msg.title}</span
193
  >
194
  {/if}
195
- {#if msg.model}
196
- <span class="text-[11px] text-[#888] font-mono ml-auto"
197
- >{msg.model}</span
198
- >
199
- {/if}
200
- <span class="text-[11px] text-[#aaa] font-mono {msg.model ? '' : 'ml-auto'}"
201
- >#{i}</span
202
- >
203
  </div>
204
 
205
- {#each msg.blocks as block}
206
- {#if block.kind === 'text'}
207
- <pre
208
- class="whitespace-pre-wrap break-words text-[13px] text-[#232323] leading-relaxed font-sans">{block.text}</pre>
209
- {:else if block.kind === 'thinking'}
210
- <details class="my-2 border-l-2 border-[#c084fc] pl-3">
211
- <summary
212
- class="cursor-pointer text-[11px] text-[#6b21a8] font-semibold select-none"
213
- >thinking</summary
214
- >
215
- <pre
216
- class="whitespace-pre-wrap break-words text-[12px] text-[#555] mt-1 leading-relaxed font-sans">{block.text}</pre>
217
- </details>
218
- {:else if block.kind === 'tool_call'}
219
- <div
220
- class="my-2 bg-[#faf5ff] border border-[#e9d5ff] rounded px-3 py-2"
221
  >
222
- <div class="text-[11px] font-semibold text-[#6b21a8] mb-1">
223
- tool call · <span class="font-mono">{block.name}</span>
224
- </div>
225
- <pre
226
- class="text-[12px] text-[#3a3a38] whitespace-pre-wrap break-words font-mono max-h-[240px] overflow-auto">{formatJson(
227
- block.input
228
- )}</pre>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  </div>
230
- {:else if block.kind === 'tool_result'}
231
- <div
232
- class="my-2 border rounded px-3 py-2 {block.isError
233
- ? 'bg-[#fef2f2] border-[#fecaca]'
234
- : 'bg-[#f5f5f0] border-[#e5e5e0]'}"
235
- >
236
- <div
237
- class="text-[11px] font-semibold mb-1 {block.isError
238
- ? 'text-[#991b1b]'
239
- : 'text-[#6a6a66]'}"
240
- >
241
- tool result{block.isError ? ' · error' : ''}
242
- </div>
243
- <pre
244
- class="text-[12px] text-[#3a3a38] whitespace-pre-wrap break-words font-mono max-h-[280px] overflow-auto">{block.text}</pre>
245
- </div>
246
- {:else if block.kind === 'image'}
247
- <div class="text-[12px] text-[#6a6a66] italic my-1">
248
- [image attachment]
249
- </div>
250
- {:else if block.kind === 'raw'}
251
- <details class="my-1">
252
- <summary
253
- class="cursor-pointer text-[11px] text-[#888] select-none"
254
- >raw</summary
255
- >
256
- <pre
257
- class="text-[11px] text-[#555] whitespace-pre-wrap break-words font-mono mt-1 max-h-[200px] overflow-auto">{formatJson(
258
- block.json
259
- )}</pre>
260
- </details>
261
- {/if}
262
  {/each}
263
  </div>
264
  {/each}
@@ -269,7 +324,7 @@
269
  class="flex items-center gap-4 px-5 py-2 border-t border-[#eeeae0] text-[11px] text-[#888] shrink-0"
270
  >
271
  {#if messages.length > 0}
272
- <span class="font-mono">{focusedIdx + 1} / {messages.length}</span>
273
  {:else}
274
  <span>ready</span>
275
  {/if}
 
1
  <script>
2
+ import { tick, onMount, onDestroy } from 'svelte';
3
  import { parseJsonl, toRawUrl } from './parse.js';
4
 
5
+ const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
6
+ let spinnerFrame = $state(0);
7
+ let spinnerInterval;
8
+
9
  let url = $state(
10
  'https://huggingface.co/datasets/mishig/traces/blob/main/36c3db7c-71e3-4ee6-b528-e2a64c9cad77.jsonl'
11
  );
12
  let loading = $state(false);
13
+ let loadedCount = $state(0);
14
  let error = $state('');
15
  let messages = $state([]);
16
  let focusedIdx = $state(-1);
 
21
  error = '';
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 {
 
86
  }
87
  }
88
 
89
+ onMount(() => {
90
+ spinnerInterval = setInterval(() => {
91
+ spinnerFrame = (spinnerFrame + 1) % spinnerFrames.length;
92
+ }, 90);
93
+ });
94
+
95
+ onDestroy(() => {
96
+ clearInterval(spinnerInterval);
97
+ });
98
+
99
+ const roleColor = {
100
+ user: 'text-[#1e40af]',
101
+ assistant: 'text-[#0f5a2a]',
102
+ tool: 'text-[#6b21a8]',
103
+ system: 'text-[#92400e]',
104
+ meta: 'text-[#6a6a66]',
105
+ unknown: 'text-[#6a6a66]',
106
  };
107
  </script>
108
 
 
112
  class="frame-bg frame-shadow w-[960px] max-w-[calc(100vw-48px)] rounded-[20px] p-[3px]"
113
  >
114
  <div
115
+ class="w-full h-[85vh] bg-[#fbfbf9] rounded-[17px] overflow-hidden flex flex-col font-mono text-[14px] leading-[1.7] text-[#232323]"
116
  >
117
  <!-- chrome -->
118
  <div class="flex items-center gap-2 pt-4 px-[18px] pb-2 shrink-0">
 
124
  >
125
  </div>
126
 
127
+ <!-- url prompt -->
128
+ <div
129
+ class="flex items-center gap-2 px-5 py-3 border-b border-[#eeeae0] shrink-0"
130
+ >
131
+ <span class="text-[#6a6a66] select-none">›</span>
132
  <input
133
  type="url"
134
  bind:value={url}
135
  onkeydown={(e) => {
136
  if (e.key === 'Enter') load();
137
  }}
138
+ placeholder="paste .jsonl dataset URL and press Enter"
139
+ class="flex-1 bg-transparent border-none outline-none text-[13px] text-[#222220] placeholder:text-[#b0b0aa]"
140
  />
141
  <button
142
  type="button"
143
  onclick={load}
144
  disabled={loading}
145
+ class="px-3 py-1 bg-[#ffd21e] rounded text-[12px] font-semibold hover:bg-[#ffbb1a] disabled:opacity-50 cursor-pointer"
146
  >
147
+ load
148
  </button>
149
  </div>
150
 
151
+ <!-- messages / log -->
152
+ <div bind:this={listEl} class="flex-1 overflow-y-auto px-5 py-3">
153
+ {#if loading}
154
+ <div class="flex items-baseline gap-2">
155
+ <span class="w-[1ch] text-center text-[#5f5f5c]"
156
+ >{spinnerFrames[spinnerFrame]}</span
157
+ >
158
+ <span class="text-[#333331]">Fetching traces...</span>
 
 
159
  </div>
160
+ {:else if error}
161
+ <div class="flex items-baseline gap-2">
162
+ <span class="w-[1ch] text-center text-[#991b1b]">✗</span>
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">
177
+ <div class="flex items-baseline gap-2 mb-1">
178
+ <span class="w-[1ch] text-center text-[#6b6b68]">○</span>
179
+ <span>waiting for input</span>
180
+ </div>
181
+ <div class="pl-[2.2ch] text-[#888]">
182
+ paste a <code class="text-[#8b5cf6]">.jsonl</code> dataset URL above,
183
+ press
184
+ <kbd
185
+ class="px-1 py-px bg-[#f5f5f2] rounded border border-[#e5e5e0] text-[11px]"
186
+ >Enter</kbd
187
+ > to load.
188
+ </div>
189
+ <div class="pl-[2.2ch] text-[#888] mt-1">
190
+ then <kbd
191
+ class="px-1 py-px bg-[#f5f5f2] rounded border border-[#e5e5e0] text-[11px]"
192
+ >↑</kbd
193
+ >
194
+ <kbd
195
+ class="px-1 py-px bg-[#f5f5f2] rounded border border-[#e5e5e0] text-[11px]"
196
+ >↓</kbd
197
+ >
198
+ to navigate sections,
199
+ <kbd
200
+ class="px-1 py-px bg-[#f5f5f2] rounded border border-[#e5e5e0] text-[11px]"
201
+ >Home</kbd
202
+ >/
203
+ <kbd
204
+ class="px-1 py-px bg-[#f5f5f2] rounded border border-[#e5e5e0] text-[11px]"
205
+ >End</kbd
206
+ > for start/end.
207
+ </div>
208
  </div>
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>
 
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}
 
324
  class="flex items-center gap-4 px-5 py-2 border-t border-[#eeeae0] text-[11px] text-[#888] shrink-0"
325
  >
326
  {#if messages.length > 0}
327
+ <span>{focusedIdx + 1} / {messages.length}</span>
328
  {:else}
329
  <span>ready</span>
330
  {/if}