victor HF Staff commited on
Commit
7c56675
·
verified ·
1 Parent(s): de0a4ee

Upload index.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +140 -175
index.html CHANGED
@@ -10,91 +10,95 @@
10
  @import url('https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;500;600;700&family=Source+Code+Pro:wght@400;500&display=swap');
11
  body { font-family: 'Source Sans 3', ui-sans-serif, system-ui, sans-serif; }
12
  code, pre, .mono { font-family: 'Source Code Pro', monospace; }
13
- .fade-in { animation: fadeIn 0.15s ease-out; }
14
- @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
15
  pre { white-space: pre-wrap; word-break: break-word; }
16
  details summary { cursor: pointer; user-select: none; }
17
  details summary::-webkit-details-marker { display: none; }
18
- .drop-zone.drag-over { border-color: #6366f1; background: rgba(99, 102, 241, 0.04); }
19
  </style>
20
  </head>
21
  <body class="bg-white text-gray-900 min-h-screen">
22
- <!-- Header -->
23
- <header class="sticky top-0 z-50 bg-white/80 backdrop-blur border-b border-gray-100 px-4 py-2.5">
24
- <div class="max-w-4xl mx-auto flex items-center justify-between">
25
- <div class="flex items-center gap-2.5">
26
- <h1 class="text-sm font-semibold text-gray-900">Session Viewer</h1>
27
- <span id="meta-badge" class="hidden text-[10px] px-1.5 py-0.5 rounded font-semibold"></span>
28
- <span id="meta-info" class="hidden text-xs text-gray-400"></span>
29
- </div>
30
- <div class="flex items-center gap-2">
31
- <input type="text" id="url-input" placeholder="Paste session URL…"
32
- class="text-xs px-2.5 py-1.5 rounded-lg bg-white border border-gray-200 text-gray-700 placeholder-gray-400 w-56 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-400 mono">
33
- <label class="cursor-pointer text-xs px-3 py-1.5 rounded-lg bg-gray-900 hover:bg-gray-800 transition text-white font-medium">
34
  Load File
35
  <input type="file" accept=".jsonl,.json" class="hidden" id="file-input">
36
  </label>
37
  </div>
38
- </div>
39
- </header>
40
-
41
- <!-- Drop zone / Empty state -->
42
- <div id="empty-state" class="max-w-4xl mx-auto mt-24 px-4">
43
- <div id="drop-zone" class="drop-zone border-2 border-dashed border-gray-200 rounded-xl p-12 text-center transition-colors">
44
- <div class="text-gray-500 text-base mb-1.5">Drop a session file or paste a URL above</div>
45
- <div class="text-gray-400 text-sm mb-8">Supports Claude Code, Pi, Codex, and OpenCode JSONL formats</div>
46
- <div class="text-gray-400 text-[10px] uppercase tracking-widest mb-3 font-semibold">Sample Sessions</div>
47
- <div class="flex flex-wrap justify-center gap-2">
48
  <button onclick="loadUrl('https://huggingface.co/datasets/victor/claude-sample-session/resolve/main/claude-code-session-2026-03-16.jsonl')"
49
- class="text-xs px-3 py-1.5 rounded-lg bg-orange-50 border border-orange-200 text-orange-700 hover:bg-orange-100 transition font-medium">Claude Code</button>
50
  <button onclick="loadUrl('https://huggingface.co/datasets/victor/pi-sample-session/resolve/main/pi-session-2026-03-16.jsonl')"
51
- class="text-xs px-3 py-1.5 rounded-lg bg-emerald-50 border border-emerald-200 text-emerald-700 hover:bg-emerald-100 transition font-medium">Pi</button>
52
  <button onclick="loadUrl('https://huggingface.co/datasets/victor/codex-sample-session/resolve/main/codex-session-hello-2026-03-16.jsonl')"
53
- class="text-xs px-3 py-1.5 rounded-lg bg-blue-50 border border-blue-200 text-blue-700 hover:bg-blue-100 transition font-medium">Codex</button>
54
  <button onclick="loadUrl('https://huggingface.co/datasets/victor/opencode-sample-session/resolve/main/opencode-session-2026-03-16.jsonl')"
55
- class="text-xs px-3 py-1.5 rounded-lg bg-purple-50 border border-purple-200 text-purple-700 hover:bg-purple-100 transition font-medium">OpenCode</button>
56
  </div>
57
  </div>
58
  </div>
59
 
60
- <!-- Session view -->
61
- <main id="session-view" class="hidden max-w-4xl mx-auto px-4 py-5 pb-20">
62
- <div id="session-header" class="mb-5 p-3.5 rounded-lg bg-gray-50 border border-gray-200">
63
- <div class="flex items-center gap-2 mb-1.5">
64
- <span id="sh-source" class="text-[10px] px-1.5 py-0.5 rounded font-semibold"></span>
65
- <span id="sh-model" class="text-[11px] px-1.5 py-0.5 rounded bg-gray-100 text-gray-500 mono"></span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  </div>
67
- <div id="sh-title" class="text-sm text-gray-500"></div>
68
- <div id="sh-cwd" class="text-xs text-gray-400 mono mt-0.5"></div>
69
- </div>
70
- <div id="messages-container"></div>
71
- </main>
72
-
73
- <!-- Stats footer -->
74
- <footer id="stats-footer" class="hidden fixed bottom-0 left-0 right-0 bg-white/80 backdrop-blur border-t border-gray-100 px-4 py-2">
75
- <div class="max-w-4xl mx-auto flex items-center justify-between text-[11px] text-gray-400 mono">
76
- <span id="stat-messages"></span>
77
- <span id="stat-tokens"></span>
78
  </div>
79
- </footer>
80
 
81
  <script>
82
  const fileInput = document.getElementById('file-input');
 
83
  const emptyState = document.getElementById('empty-state');
84
- const sessionView = document.getElementById('session-view');
85
- const dropZone = document.getElementById('drop-zone');
86
  const messagesContainer = document.getElementById('messages-container');
87
- const statsFooter = document.getElementById('stats-footer');
88
 
89
  const SOURCE_COLORS = {
90
- 'claude-code': { bg: 'bg-orange-50', text: 'text-orange-700', border: 'border-orange-200', label: 'Claude Code' },
91
- 'pi': { bg: 'bg-emerald-50', text: 'text-emerald-700', border: 'border-emerald-200', label: 'Pi' },
92
- 'codex': { bg: 'bg-blue-50', text: 'text-blue-700', border: 'border-blue-200', label: 'Codex' },
93
- 'opencode': { bg: 'bg-purple-50', text: 'text-purple-700', border: 'border-purple-200', label: 'OpenCode' },
94
  };
95
 
96
- // URL input
 
97
  const urlInput = document.getElementById('url-input');
 
 
 
98
  urlInput.addEventListener('keydown', (e) => {
99
  if (e.key === 'Enter' && urlInput.value.trim()) loadUrl(urlInput.value.trim());
100
  });
@@ -105,84 +109,60 @@
105
  url = `https://huggingface.co/datasets/${hfMatch[1]}/resolve/main/${hfMatch[2]}`;
106
  } else if (url.includes('huggingface.co/datasets/') && !url.includes('/resolve/')) {
107
  const parts = url.match(/huggingface\.co\/datasets\/([^/]+\/[^/]+)\/?$/);
108
- if (parts) {
109
- alert('Paste the URL of a specific .jsonl file, not the dataset root.');
110
- return;
111
- }
112
  }
113
- urlInput.disabled = true;
114
- urlInput.value = 'Loading\u2026';
115
  try {
116
  const res = await fetch(url);
117
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
118
  const text = await res.text();
119
- const session = parseSession(text);
120
- renderSession(session);
121
- } catch (err) {
122
- alert(`Load error: ${err.message}`);
123
- } finally {
124
- urlInput.disabled = false;
125
- urlInput.value = '';
126
- }
127
  }
128
 
129
  const params = new URLSearchParams(location.search);
130
  if (params.get('url')) loadUrl(params.get('url'));
131
 
132
- fileInput.addEventListener('change', (e) => {
133
- if (e.target.files[0]) loadFile(e.target.files[0]);
134
- });
135
 
 
 
136
  dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
137
  dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
138
  dropZone.addEventListener('drop', (e) => {
139
- e.preventDefault();
140
- dropZone.classList.remove('drag-over');
141
  if (e.dataTransfer.files[0]) loadFile(e.dataTransfer.files[0]);
142
  });
143
 
144
  function loadFile(file) {
145
  const reader = new FileReader();
146
  reader.onload = (e) => {
147
- try {
148
- const session = parseSession(e.target.result);
149
- renderSession(session);
150
- } catch (err) {
151
- alert(`Parse error: ${err.message}`);
152
- }
153
  };
154
  reader.readAsText(file);
155
  }
156
 
157
  function renderSession(session) {
158
  emptyState.classList.add('hidden');
159
- sessionView.classList.remove('hidden');
160
- statsFooter.classList.remove('hidden');
161
 
162
  const sc = SOURCE_COLORS[session.source] || SOURCE_COLORS['claude-code'];
163
  const srcEl = document.getElementById('sh-source');
164
  srcEl.textContent = sc.label;
165
  srcEl.className = `text-[10px] px-1.5 py-0.5 rounded font-semibold ${sc.bg} ${sc.text}`;
166
 
167
- document.getElementById('sh-model').textContent = session.model || 'unknown model';
168
  document.getElementById('sh-title').textContent = session.title || session.startedAt || '';
169
  document.getElementById('sh-cwd').textContent = session.cwd || '';
170
 
171
- const badge = document.getElementById('meta-badge');
172
- badge.textContent = sc.label;
173
- badge.className = `text-[10px] px-1.5 py-0.5 rounded font-semibold ${sc.bg} ${sc.text}`;
174
- badge.classList.remove('hidden');
175
-
176
- const metaInfo = document.getElementById('meta-info');
177
- metaInfo.textContent = `${session.messages.length} messages`;
178
- metaInfo.classList.remove('hidden');
179
-
180
  messagesContainer.innerHTML = '';
181
  let totalIn = 0, totalOut = 0;
182
 
183
  for (const msg of session.messages) {
184
- const el = renderMessage(msg);
185
- messagesContainer.appendChild(el);
186
  if (msg.usage) {
187
  totalIn += msg.usage.inputTokens || 0;
188
  totalOut += msg.usage.outputTokens || 0;
@@ -191,73 +171,64 @@
191
 
192
  document.getElementById('stat-messages').textContent = `${session.messages.length} messages`;
193
  document.getElementById('stat-tokens').textContent =
194
- totalIn || totalOut
195
- ? `${totalIn.toLocaleString()} in / ${totalOut.toLocaleString()} out tokens`
196
- : '';
197
  }
198
 
199
  function renderMessage(msg) {
200
- const wrapper = document.createElement('div');
201
- wrapper.className = 'fade-in mb-3';
202
-
203
  const isUser = msg.role === 'user';
204
  const isSystem = msg.role === 'system';
205
 
206
- // Role bar
207
- const roleBar = document.createElement('div');
208
- roleBar.className = 'flex items-center gap-2 mb-1';
 
 
209
 
210
  const roleLabel = document.createElement('span');
211
  roleLabel.className = `text-[11px] font-bold uppercase tracking-wide ${
212
- isUser ? 'text-blue-600' : isSystem ? 'text-gray-400' : 'text-emerald-600'
213
  }`;
214
  roleLabel.textContent = msg.role;
215
- roleBar.appendChild(roleLabel);
216
 
217
  if (msg.model) {
218
- const modelTag = document.createElement('span');
219
- modelTag.className = 'text-[11px] text-gray-400 mono';
220
- modelTag.textContent = msg.model;
221
- roleBar.appendChild(modelTag);
222
  }
223
 
224
  if (msg.timestamp) {
225
  const ts = document.createElement('span');
226
  ts.className = 'text-[11px] text-gray-300';
227
  ts.textContent = new Date(msg.timestamp).toLocaleTimeString();
228
- roleBar.appendChild(ts);
229
  }
230
 
231
- wrapper.appendChild(roleBar);
232
 
233
- // Content card
234
- const card = document.createElement('div');
235
- card.className = `rounded-lg px-4 py-3 ${
236
- isUser
237
- ? 'bg-blue-50/60 border border-blue-100'
238
- : isSystem
239
- ? 'bg-gray-50/60 border border-gray-100'
240
- : 'bg-gray-50 border border-gray-200'
241
- }`;
242
 
243
  for (const block of msg.blocks) {
244
- card.appendChild(renderBlock(block));
245
  }
246
 
247
  // Usage
248
  if (msg.usage && (msg.usage.inputTokens || msg.usage.outputTokens)) {
249
- const usagePill = document.createElement('div');
250
- usagePill.className = 'mt-2 text-[11px] text-gray-400 mono';
251
  const parts = [];
252
  if (msg.usage.inputTokens) parts.push(`${msg.usage.inputTokens.toLocaleString()} in`);
253
  if (msg.usage.outputTokens) parts.push(`${msg.usage.outputTokens.toLocaleString()} out`);
254
  if (msg.usage.cacheRead) parts.push(`${msg.usage.cacheRead.toLocaleString()} cached`);
255
- usagePill.textContent = parts.join(' \u00b7 ');
256
- card.appendChild(usagePill);
257
  }
258
 
259
- wrapper.appendChild(card);
260
- return wrapper;
261
  }
262
 
263
  function renderBlock(block) {
@@ -265,74 +236,68 @@
265
 
266
  switch (block.type) {
267
  case 'text':
268
- el.className = 'text-[13px] leading-relaxed text-gray-800 whitespace-pre-wrap';
269
  el.innerHTML = renderMarkdown(block.text);
270
  break;
271
 
272
  case 'thinking':
273
- el.className = 'my-1.5';
274
- const details = document.createElement('details');
275
- const summary = document.createElement('summary');
276
- summary.className = 'text-[11px] text-gray-400 hover:text-gray-600 flex items-center gap-1';
277
- summary.innerHTML = '<span class="text-[10px]">\u25b6</span> Thinking';
278
- details.appendChild(summary);
279
- const thinkContent = document.createElement('div');
280
- thinkContent.className = 'mt-1.5 pl-3 border-l-2 border-gray-200 text-[11px] text-gray-500 italic whitespace-pre-wrap';
281
- thinkContent.textContent = block.text;
282
- details.appendChild(thinkContent);
283
- el.appendChild(details);
284
  break;
285
 
286
  case 'tool_call':
287
- el.className = 'my-1.5';
288
- const tcDetails = document.createElement('details');
289
- const tcSummary = document.createElement('summary');
290
- tcSummary.className = 'text-[11px] flex items-center gap-1.5 py-1 px-2 rounded-md bg-amber-50 border border-amber-200 text-amber-800 hover:bg-amber-100 transition';
291
- tcSummary.innerHTML = `<span class="font-semibold">${escapeHtml(block.toolName)}</span>`;
292
- tcDetails.appendChild(tcSummary);
293
- const tcContent = document.createElement('pre');
294
- tcContent.className = 'mt-1 p-2.5 rounded-md bg-gray-50 border border-gray-200 text-[11px] text-gray-600 overflow-x-auto mono';
295
- tcContent.textContent = typeof block.input === 'string'
296
- ? block.input
297
- : JSON.stringify(block.input, null, 2);
298
- tcDetails.appendChild(tcContent);
299
- el.appendChild(tcDetails);
300
  break;
301
 
302
  case 'tool_result':
303
- el.className = 'my-1.5';
304
- const trDetails = document.createElement('details');
305
- const trSummary = document.createElement('summary');
306
  const isErr = block.isError;
307
- trSummary.className = `text-[11px] flex items-center gap-1.5 py-1 px-2 rounded-md border transition ${
308
- isErr
309
- ? 'bg-red-50 border-red-200 text-red-700 hover:bg-red-100'
310
- : 'bg-sky-50 border-sky-200 text-sky-700 hover:bg-sky-100'
311
  }`;
312
- const label = isErr ? 'Error' : 'Result';
313
- const preview = (block.content || '').slice(0, 80).replace(/\n/g, ' ');
314
- trSummary.innerHTML = `<span class="font-semibold">${label}</span><span class="text-gray-400 truncate">${escapeHtml(preview)}</span>`;
315
- trDetails.appendChild(trSummary);
316
- const trContent = document.createElement('pre');
317
- trContent.className = 'mt-1 p-2.5 rounded-md bg-gray-50 border border-gray-200 text-[11px] text-gray-600 overflow-x-auto max-h-80 overflow-y-auto mono';
318
- trContent.textContent = block.content;
319
- trDetails.appendChild(trContent);
320
- el.appendChild(trDetails);
321
  break;
322
  }
323
-
324
  return el;
325
  }
326
 
327
  function renderMarkdown(text) {
328
  if (!text) return '';
329
  return escapeHtml(text)
330
- .replace(/```(\w*)\n([\s\S]*?)```/g, '<pre class="my-2 p-2.5 rounded-md bg-gray-100 border border-gray-200 text-[11px] text-gray-700 overflow-x-auto mono"><code>$2</code></pre>')
331
- .replace(/`([^`]+)`/g, '<code class="px-1 py-0.5 rounded bg-gray-100 border border-gray-200 text-[11px] text-gray-700 mono">$1</code>')
332
- .replace(/\*\*(.+?)\*\*/g, '<strong class="font-semibold text-gray-900">$1</strong>')
333
- .replace(/^### (.+)$/gm, '<div class="text-sm font-semibold text-gray-900 mt-3 mb-1">$1</div>')
334
- .replace(/^## (.+)$/gm, '<div class="text-base font-semibold text-gray-900 mt-3 mb-1">$1</div>')
335
- .replace(/^# (.+)$/gm, '<div class="text-lg font-semibold text-gray-900 mt-3 mb-1">$1</div>')
336
  .replace(/\n/g, '<br>');
337
  }
338
 
 
10
  @import url('https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;500;600;700&family=Source+Code+Pro:wght@400;500&display=swap');
11
  body { font-family: 'Source Sans 3', ui-sans-serif, system-ui, sans-serif; }
12
  code, pre, .mono { font-family: 'Source Code Pro', monospace; }
 
 
13
  pre { white-space: pre-wrap; word-break: break-word; }
14
  details summary { cursor: pointer; user-select: none; }
15
  details summary::-webkit-details-marker { display: none; }
16
+ .drop-zone.drag-over { border-color: #6366f1; background: rgba(99, 102, 241, 0.03); }
17
  </style>
18
  </head>
19
  <body class="bg-white text-gray-900 min-h-screen">
20
+
21
+ <!-- Empty state -->
22
+ <div id="empty-state" class="max-w-5xl mx-auto mt-16 px-4">
23
+ <div id="drop-zone" class="drop-zone border border-gray-200 rounded-lg p-10 text-center transition-colors">
24
+ <div class="text-gray-500 text-sm mb-1">Drop a session file or paste a URL</div>
25
+ <div class="text-gray-400 text-xs mb-6">Supports Claude Code, Pi, Codex, and OpenCode JSONL formats</div>
26
+ <div class="flex items-center justify-center gap-2 mb-6">
27
+ <input type="text" id="url-input-empty" placeholder="Paste session URL…"
28
+ class="text-xs px-2.5 py-1.5 rounded-md bg-white border border-gray-200 text-gray-700 placeholder-gray-400 w-72 focus:outline-none focus:ring-1 focus:ring-gray-300 mono">
29
+ <label class="cursor-pointer text-xs px-3 py-1.5 rounded-md border border-gray-200 bg-white hover:bg-gray-50 transition text-gray-700 font-medium">
 
 
30
  Load File
31
  <input type="file" accept=".jsonl,.json" class="hidden" id="file-input">
32
  </label>
33
  </div>
34
+ <div class="text-gray-400 text-[10px] uppercase tracking-widest mb-2 font-semibold">Samples</div>
35
+ <div class="flex flex-wrap justify-center gap-1.5">
 
 
 
 
 
 
 
 
36
  <button onclick="loadUrl('https://huggingface.co/datasets/victor/claude-sample-session/resolve/main/claude-code-session-2026-03-16.jsonl')"
37
+ class="text-[11px] px-2 py-1 rounded border border-gray-200 text-gray-600 hover:bg-gray-50 transition">Claude Code</button>
38
  <button onclick="loadUrl('https://huggingface.co/datasets/victor/pi-sample-session/resolve/main/pi-session-2026-03-16.jsonl')"
39
+ class="text-[11px] px-2 py-1 rounded border border-gray-200 text-gray-600 hover:bg-gray-50 transition">Pi</button>
40
  <button onclick="loadUrl('https://huggingface.co/datasets/victor/codex-sample-session/resolve/main/codex-session-hello-2026-03-16.jsonl')"
41
+ class="text-[11px] px-2 py-1 rounded border border-gray-200 text-gray-600 hover:bg-gray-50 transition">Codex</button>
42
  <button onclick="loadUrl('https://huggingface.co/datasets/victor/opencode-sample-session/resolve/main/opencode-session-2026-03-16.jsonl')"
43
+ class="text-[11px] px-2 py-1 rounded border border-gray-200 text-gray-600 hover:bg-gray-50 transition">OpenCode</button>
44
  </div>
45
  </div>
46
  </div>
47
 
48
+ <!-- Session panel (single bordered container like HF Dataset Viewer) -->
49
+ <div id="session-panel" class="hidden max-w-5xl mx-auto my-4 mx-4">
50
+ <!-- Panel header -->
51
+ <div class="border border-gray-200 rounded-lg overflow-hidden">
52
+ <!-- Toolbar row -->
53
+ <div class="flex items-center justify-between px-4 py-2.5 border-b border-gray-200 bg-white">
54
+ <div class="flex items-center gap-2">
55
+ <span class="text-sm font-semibold text-gray-900">Session Viewer</span>
56
+ <span id="sh-source" class="text-[10px] px-1.5 py-0.5 rounded font-semibold"></span>
57
+ <span id="sh-model" class="text-[11px] text-gray-400 mono"></span>
58
+ <span id="stat-messages" class="text-[11px] text-gray-400"></span>
59
+ </div>
60
+ <div class="flex items-center gap-2">
61
+ <span id="stat-tokens" class="text-[11px] text-gray-400 mono"></span>
62
+ <input type="text" id="url-input" placeholder="Paste session URL…"
63
+ class="text-[11px] px-2 py-1 rounded-md border border-gray-200 text-gray-600 placeholder-gray-400 w-48 focus:outline-none focus:ring-1 focus:ring-gray-300 mono">
64
+ <label class="cursor-pointer text-[11px] px-2.5 py-1 rounded-md border border-gray-200 bg-white hover:bg-gray-50 transition text-gray-600 font-medium">
65
+ Load File
66
+ <input type="file" accept=".jsonl,.json" class="hidden" id="file-input-2">
67
+ </label>
68
+ </div>
69
  </div>
70
+
71
+ <!-- Session info row -->
72
+ <div id="session-info" class="px-4 py-2 border-b border-gray-200 bg-gray-50/50">
73
+ <div id="sh-title" class="text-[13px] text-gray-600"></div>
74
+ <div id="sh-cwd" class="text-[11px] text-gray-400 mono"></div>
75
+ </div>
76
+
77
+ <!-- Messages -->
78
+ <div id="messages-container"></div>
 
 
79
  </div>
80
+ </div>
81
 
82
  <script>
83
  const fileInput = document.getElementById('file-input');
84
+ const fileInput2 = document.getElementById('file-input-2');
85
  const emptyState = document.getElementById('empty-state');
86
+ const sessionPanel = document.getElementById('session-panel');
 
87
  const messagesContainer = document.getElementById('messages-container');
 
88
 
89
  const SOURCE_COLORS = {
90
+ 'claude-code': { bg: 'bg-orange-50', text: 'text-orange-600', label: 'Claude Code' },
91
+ 'pi': { bg: 'bg-emerald-50', text: 'text-emerald-600', label: 'Pi' },
92
+ 'codex': { bg: 'bg-blue-50', text: 'text-blue-600', label: 'Codex' },
93
+ 'opencode': { bg: 'bg-purple-50', text: 'text-purple-600', label: 'OpenCode' },
94
  };
95
 
96
+ // URL inputs (both empty state and panel)
97
+ const urlInputEmpty = document.getElementById('url-input-empty');
98
  const urlInput = document.getElementById('url-input');
99
+ urlInputEmpty.addEventListener('keydown', (e) => {
100
+ if (e.key === 'Enter' && urlInputEmpty.value.trim()) loadUrl(urlInputEmpty.value.trim());
101
+ });
102
  urlInput.addEventListener('keydown', (e) => {
103
  if (e.key === 'Enter' && urlInput.value.trim()) loadUrl(urlInput.value.trim());
104
  });
 
109
  url = `https://huggingface.co/datasets/${hfMatch[1]}/resolve/main/${hfMatch[2]}`;
110
  } else if (url.includes('huggingface.co/datasets/') && !url.includes('/resolve/')) {
111
  const parts = url.match(/huggingface\.co\/datasets\/([^/]+\/[^/]+)\/?$/);
112
+ if (parts) { alert('Paste the URL of a specific .jsonl file.'); return; }
 
 
 
113
  }
114
+ for (const inp of [urlInputEmpty, urlInput]) { inp.disabled = true; inp.value = 'Loading\u2026'; }
 
115
  try {
116
  const res = await fetch(url);
117
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
118
  const text = await res.text();
119
+ renderSession(parseSession(text));
120
+ } catch (err) { alert(`Load error: ${err.message}`); }
121
+ finally { for (const inp of [urlInputEmpty, urlInput]) { inp.disabled = false; inp.value = ''; } }
 
 
 
 
 
122
  }
123
 
124
  const params = new URLSearchParams(location.search);
125
  if (params.get('url')) loadUrl(params.get('url'));
126
 
127
+ fileInput.addEventListener('change', (e) => { if (e.target.files[0]) loadFile(e.target.files[0]); });
128
+ fileInput2.addEventListener('change', (e) => { if (e.target.files[0]) loadFile(e.target.files[0]); });
 
129
 
130
+ // Drag and drop on empty state
131
+ const dropZone = document.getElementById('drop-zone');
132
  dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
133
  dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
134
  dropZone.addEventListener('drop', (e) => {
135
+ e.preventDefault(); dropZone.classList.remove('drag-over');
 
136
  if (e.dataTransfer.files[0]) loadFile(e.dataTransfer.files[0]);
137
  });
138
 
139
  function loadFile(file) {
140
  const reader = new FileReader();
141
  reader.onload = (e) => {
142
+ try { renderSession(parseSession(e.target.result)); }
143
+ catch (err) { alert(`Parse error: ${err.message}`); }
 
 
 
 
144
  };
145
  reader.readAsText(file);
146
  }
147
 
148
  function renderSession(session) {
149
  emptyState.classList.add('hidden');
150
+ sessionPanel.classList.remove('hidden');
 
151
 
152
  const sc = SOURCE_COLORS[session.source] || SOURCE_COLORS['claude-code'];
153
  const srcEl = document.getElementById('sh-source');
154
  srcEl.textContent = sc.label;
155
  srcEl.className = `text-[10px] px-1.5 py-0.5 rounded font-semibold ${sc.bg} ${sc.text}`;
156
 
157
+ document.getElementById('sh-model').textContent = session.model || '';
158
  document.getElementById('sh-title').textContent = session.title || session.startedAt || '';
159
  document.getElementById('sh-cwd').textContent = session.cwd || '';
160
 
 
 
 
 
 
 
 
 
 
161
  messagesContainer.innerHTML = '';
162
  let totalIn = 0, totalOut = 0;
163
 
164
  for (const msg of session.messages) {
165
+ messagesContainer.appendChild(renderMessage(msg));
 
166
  if (msg.usage) {
167
  totalIn += msg.usage.inputTokens || 0;
168
  totalOut += msg.usage.outputTokens || 0;
 
171
 
172
  document.getElementById('stat-messages').textContent = `${session.messages.length} messages`;
173
  document.getElementById('stat-tokens').textContent =
174
+ totalIn || totalOut ? `${totalIn.toLocaleString()} in / ${totalOut.toLocaleString()} out` : '';
 
 
175
  }
176
 
177
  function renderMessage(msg) {
178
+ const row = document.createElement('div');
 
 
179
  const isUser = msg.role === 'user';
180
  const isSystem = msg.role === 'system';
181
 
182
+ row.className = 'border-b border-gray-200 px-4 py-2.5';
183
+
184
+ // Role + meta line
185
+ const meta = document.createElement('div');
186
+ meta.className = 'flex items-center gap-1.5 mb-1';
187
 
188
  const roleLabel = document.createElement('span');
189
  roleLabel.className = `text-[11px] font-bold uppercase tracking-wide ${
190
+ isUser ? 'text-blue-600' : isSystem ? 'text-gray-400' : 'text-green-600'
191
  }`;
192
  roleLabel.textContent = msg.role;
193
+ meta.appendChild(roleLabel);
194
 
195
  if (msg.model) {
196
+ const m = document.createElement('span');
197
+ m.className = 'text-[11px] text-gray-400 mono';
198
+ m.textContent = msg.model;
199
+ meta.appendChild(m);
200
  }
201
 
202
  if (msg.timestamp) {
203
  const ts = document.createElement('span');
204
  ts.className = 'text-[11px] text-gray-300';
205
  ts.textContent = new Date(msg.timestamp).toLocaleTimeString();
206
+ meta.appendChild(ts);
207
  }
208
 
209
+ row.appendChild(meta);
210
 
211
+ // Content
212
+ const content = document.createElement('div');
 
 
 
 
 
 
 
213
 
214
  for (const block of msg.blocks) {
215
+ content.appendChild(renderBlock(block));
216
  }
217
 
218
  // Usage
219
  if (msg.usage && (msg.usage.inputTokens || msg.usage.outputTokens)) {
220
+ const u = document.createElement('div');
221
+ u.className = 'text-[11px] text-gray-400 mono mt-1';
222
  const parts = [];
223
  if (msg.usage.inputTokens) parts.push(`${msg.usage.inputTokens.toLocaleString()} in`);
224
  if (msg.usage.outputTokens) parts.push(`${msg.usage.outputTokens.toLocaleString()} out`);
225
  if (msg.usage.cacheRead) parts.push(`${msg.usage.cacheRead.toLocaleString()} cached`);
226
+ u.textContent = parts.join(' \u00b7 ');
227
+ content.appendChild(u);
228
  }
229
 
230
+ row.appendChild(content);
231
+ return row;
232
  }
233
 
234
  function renderBlock(block) {
 
236
 
237
  switch (block.type) {
238
  case 'text':
239
+ el.className = 'text-[13px] leading-snug text-gray-800 whitespace-pre-wrap';
240
  el.innerHTML = renderMarkdown(block.text);
241
  break;
242
 
243
  case 'thinking':
244
+ el.className = 'my-1';
245
+ const d = document.createElement('details');
246
+ const s = document.createElement('summary');
247
+ s.className = 'text-[11px] text-gray-400 hover:text-gray-500 flex items-center gap-1';
248
+ s.innerHTML = '<span class="text-[9px]">\u25b6</span> Thinking';
249
+ d.appendChild(s);
250
+ const c = document.createElement('div');
251
+ c.className = 'mt-1 pl-2.5 border-l border-gray-200 text-[11px] text-gray-500 italic whitespace-pre-wrap';
252
+ c.textContent = block.text;
253
+ d.appendChild(c);
254
+ el.appendChild(d);
255
  break;
256
 
257
  case 'tool_call':
258
+ el.className = 'my-1';
259
+ const td = document.createElement('details');
260
+ const ts = document.createElement('summary');
261
+ ts.className = 'text-[11px] inline-flex items-center gap-1 py-0.5 px-1.5 rounded bg-amber-50 border border-amber-200 text-amber-700 hover:bg-amber-100 transition';
262
+ ts.innerHTML = `<span class="font-medium">${escapeHtml(block.toolName)}</span>`;
263
+ td.appendChild(ts);
264
+ const tc = document.createElement('pre');
265
+ tc.className = 'mt-1 p-2 rounded bg-gray-50 border border-gray-200 text-[11px] text-gray-600 overflow-x-auto mono';
266
+ tc.textContent = typeof block.input === 'string' ? block.input : JSON.stringify(block.input, null, 2);
267
+ td.appendChild(tc);
268
+ el.appendChild(td);
 
 
269
  break;
270
 
271
  case 'tool_result':
272
+ el.className = 'my-1';
273
+ const rd = document.createElement('details');
274
+ const rs = document.createElement('summary');
275
  const isErr = block.isError;
276
+ rs.className = `text-[11px] inline-flex items-center gap-1 py-0.5 px-1.5 rounded border transition ${
277
+ isErr ? 'bg-red-50 border-red-200 text-red-600 hover:bg-red-100' : 'bg-gray-50 border-gray-200 text-gray-500 hover:bg-gray-100'
 
 
278
  }`;
279
+ const preview = (block.content || '').slice(0, 60).replace(/\n/g, ' ');
280
+ rs.innerHTML = `<span class="font-medium">${isErr ? 'Error' : 'Result'}</span><span class="text-gray-400 truncate ml-1">${escapeHtml(preview)}</span>`;
281
+ rd.appendChild(rs);
282
+ const rc = document.createElement('pre');
283
+ rc.className = 'mt-1 p-2 rounded bg-gray-50 border border-gray-200 text-[11px] text-gray-600 overflow-x-auto max-h-64 overflow-y-auto mono';
284
+ rc.textContent = block.content;
285
+ rd.appendChild(rc);
286
+ el.appendChild(rd);
 
287
  break;
288
  }
 
289
  return el;
290
  }
291
 
292
  function renderMarkdown(text) {
293
  if (!text) return '';
294
  return escapeHtml(text)
295
+ .replace(/```(\w*)\n([\s\S]*?)```/g, '<pre class="my-1 p-2 rounded bg-gray-50 border border-gray-200 text-[11px] text-gray-700 overflow-x-auto mono"><code>$2</code></pre>')
296
+ .replace(/`([^`]+)`/g, '<code class="px-0.5 py-px rounded bg-gray-100 text-[11px] text-gray-700 mono">$1</code>')
297
+ .replace(/\*\*(.+?)\*\*/g, '<strong class="font-semibold">$1</strong>')
298
+ .replace(/^### (.+)$/gm, '<div class="text-[13px] font-semibold mt-2 mb-0.5">$1</div>')
299
+ .replace(/^## (.+)$/gm, '<div class="text-sm font-semibold mt-2 mb-0.5">$1</div>')
300
+ .replace(/^# (.+)$/gm, '<div class="text-[15px] font-semibold mt-2 mb-0.5">$1</div>')
301
  .replace(/\n/g, '<br>');
302
  }
303