Spaces:
Running
Running
Upload index.html with huggingface_hub
Browse files- 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.
|
| 19 |
</style>
|
| 20 |
</head>
|
| 21 |
<body class="bg-white text-gray-900 min-h-screen">
|
| 22 |
-
|
| 23 |
-
<
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
<
|
| 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 |
-
|
| 39 |
-
|
| 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-
|
| 50 |
<button onclick="loadUrl('https://huggingface.co/datasets/victor/pi-sample-session/resolve/main/pi-session-2026-03-16.jsonl')"
|
| 51 |
-
class="text-
|
| 52 |
<button onclick="loadUrl('https://huggingface.co/datasets/victor/codex-sample-session/resolve/main/codex-session-hello-2026-03-16.jsonl')"
|
| 53 |
-
class="text-
|
| 54 |
<button onclick="loadUrl('https://huggingface.co/datasets/victor/opencode-sample-session/resolve/main/opencode-session-2026-03-16.jsonl')"
|
| 55 |
-
class="text-
|
| 56 |
</div>
|
| 57 |
</div>
|
| 58 |
</div>
|
| 59 |
|
| 60 |
-
<!-- Session
|
| 61 |
-
<
|
| 62 |
-
<
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
</div>
|
| 67 |
-
|
| 68 |
-
<
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
<span id="stat-messages"></span>
|
| 77 |
-
<span id="stat-tokens"></span>
|
| 78 |
</div>
|
| 79 |
-
</
|
| 80 |
|
| 81 |
<script>
|
| 82 |
const fileInput = document.getElementById('file-input');
|
|
|
|
| 83 |
const emptyState = document.getElementById('empty-state');
|
| 84 |
-
const
|
| 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-
|
| 91 |
-
'pi': { bg: 'bg-emerald-50', text: 'text-emerald-
|
| 92 |
-
'codex': { bg: 'bg-blue-50', text: 'text-blue-
|
| 93 |
-
'opencode': { bg: 'bg-purple-50', text: 'text-purple-
|
| 94 |
};
|
| 95 |
|
| 96 |
-
// URL
|
|
|
|
| 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 |
-
|
| 120 |
-
|
| 121 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 || '
|
| 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 |
-
|
| 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
|
| 201 |
-
wrapper.className = 'fade-in mb-3';
|
| 202 |
-
|
| 203 |
const isUser = msg.role === 'user';
|
| 204 |
const isSystem = msg.role === 'system';
|
| 205 |
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
| 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-
|
| 213 |
}`;
|
| 214 |
roleLabel.textContent = msg.role;
|
| 215 |
-
|
| 216 |
|
| 217 |
if (msg.model) {
|
| 218 |
-
const
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 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 |
-
|
| 229 |
}
|
| 230 |
|
| 231 |
-
|
| 232 |
|
| 233 |
-
// Content
|
| 234 |
-
const
|
| 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 |
-
|
| 245 |
}
|
| 246 |
|
| 247 |
// Usage
|
| 248 |
if (msg.usage && (msg.usage.inputTokens || msg.usage.outputTokens)) {
|
| 249 |
-
const
|
| 250 |
-
|
| 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 |
-
|
| 256 |
-
|
| 257 |
}
|
| 258 |
|
| 259 |
-
|
| 260 |
-
return
|
| 261 |
}
|
| 262 |
|
| 263 |
function renderBlock(block) {
|
|
@@ -265,74 +236,68 @@
|
|
| 265 |
|
| 266 |
switch (block.type) {
|
| 267 |
case 'text':
|
| 268 |
-
el.className = 'text-[13px] leading-
|
| 269 |
el.innerHTML = renderMarkdown(block.text);
|
| 270 |
break;
|
| 271 |
|
| 272 |
case 'thinking':
|
| 273 |
-
el.className = 'my-1
|
| 274 |
-
const
|
| 275 |
-
const
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
const
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
el.appendChild(
|
| 284 |
break;
|
| 285 |
|
| 286 |
case 'tool_call':
|
| 287 |
-
el.className = 'my-1
|
| 288 |
-
const
|
| 289 |
-
const
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
const
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
tcDetails.appendChild(tcContent);
|
| 299 |
-
el.appendChild(tcDetails);
|
| 300 |
break;
|
| 301 |
|
| 302 |
case 'tool_result':
|
| 303 |
-
el.className = 'my-1
|
| 304 |
-
const
|
| 305 |
-
const
|
| 306 |
const isErr = block.isError;
|
| 307 |
-
|
| 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
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 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-
|
| 331 |
-
.replace(/`([^`]+)`/g, '<code class="px-
|
| 332 |
-
.replace(/\*\*(.+?)\*\*/g, '<strong class="font-semibold
|
| 333 |
-
.replace(/^### (.+)$/gm, '<div class="text-
|
| 334 |
-
.replace(/^## (.+)$/gm, '<div class="text-
|
| 335 |
-
.replace(/^# (.+)$/gm, '<div class="text-
|
| 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 |
|