nodebench-mcp 2.25.0 → 2.27.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.
- package/NODEBENCH_AGENTS.md +5 -4
- package/README.md +145 -16
- package/dist/__tests__/architectComplex.test.js +3 -5
- package/dist/__tests__/architectComplex.test.js.map +1 -1
- package/dist/__tests__/batchAutopilot.test.d.ts +8 -0
- package/dist/__tests__/batchAutopilot.test.js +218 -0
- package/dist/__tests__/batchAutopilot.test.js.map +1 -0
- package/dist/__tests__/cliSubcommands.test.d.ts +1 -0
- package/dist/__tests__/cliSubcommands.test.js +138 -0
- package/dist/__tests__/cliSubcommands.test.js.map +1 -0
- package/dist/__tests__/evalHarness.test.js +1 -1
- package/dist/__tests__/forecastingDogfood.test.d.ts +9 -0
- package/dist/__tests__/forecastingDogfood.test.js +284 -0
- package/dist/__tests__/forecastingDogfood.test.js.map +1 -0
- package/dist/__tests__/forecastingScoring.test.d.ts +9 -0
- package/dist/__tests__/forecastingScoring.test.js +202 -0
- package/dist/__tests__/forecastingScoring.test.js.map +1 -0
- package/dist/__tests__/localDashboard.test.d.ts +1 -0
- package/dist/__tests__/localDashboard.test.js +226 -0
- package/dist/__tests__/localDashboard.test.js.map +1 -0
- package/dist/__tests__/multiHopDogfood.test.js +11 -11
- package/dist/__tests__/multiHopDogfood.test.js.map +1 -1
- package/dist/__tests__/openclawDogfood.test.d.ts +23 -0
- package/dist/__tests__/openclawDogfood.test.js +535 -0
- package/dist/__tests__/openclawDogfood.test.js.map +1 -0
- package/dist/__tests__/openclawMessaging.test.d.ts +14 -0
- package/dist/__tests__/openclawMessaging.test.js +232 -0
- package/dist/__tests__/openclawMessaging.test.js.map +1 -0
- package/dist/__tests__/presetRealWorldBench.test.js +0 -2
- package/dist/__tests__/presetRealWorldBench.test.js.map +1 -1
- package/dist/__tests__/tools.test.js +9 -157
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/__tests__/toolsetGatingEval.test.js +0 -2
- package/dist/__tests__/toolsetGatingEval.test.js.map +1 -1
- package/dist/__tests__/traceabilityDogfood.test.d.ts +12 -0
- package/dist/__tests__/traceabilityDogfood.test.js +241 -0
- package/dist/__tests__/traceabilityDogfood.test.js.map +1 -0
- package/dist/__tests__/webmcpTools.test.d.ts +7 -0
- package/dist/__tests__/webmcpTools.test.js +195 -0
- package/dist/__tests__/webmcpTools.test.js.map +1 -0
- package/dist/dashboard/briefHtml.d.ts +20 -0
- package/dist/dashboard/briefHtml.js +1000 -0
- package/dist/dashboard/briefHtml.js.map +1 -0
- package/dist/dashboard/briefServer.d.ts +18 -0
- package/dist/dashboard/briefServer.js +320 -0
- package/dist/dashboard/briefServer.js.map +1 -0
- package/dist/dashboard/html.js +1470 -1230
- package/dist/dashboard/html.js.map +1 -1
- package/dist/dashboard/server.js +166 -41
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +210 -14
- package/dist/index.js.map +1 -1
- package/dist/tools/critterTools.js +4 -0
- package/dist/tools/critterTools.js.map +1 -1
- package/dist/tools/forecastingTools.d.ts +11 -0
- package/dist/tools/forecastingTools.js +616 -0
- package/dist/tools/forecastingTools.js.map +1 -0
- package/dist/tools/localDashboardTools.d.ts +8 -0
- package/dist/tools/localDashboardTools.js +332 -0
- package/dist/tools/localDashboardTools.js.map +1 -0
- package/dist/tools/metaTools.js +170 -1
- package/dist/tools/metaTools.js.map +1 -1
- package/dist/tools/openclawTools.d.ts +11 -0
- package/dist/tools/openclawTools.js +1017 -0
- package/dist/tools/openclawTools.js.map +1 -0
- package/dist/tools/overstoryTools.d.ts +14 -0
- package/dist/tools/overstoryTools.js +426 -0
- package/dist/tools/overstoryTools.js.map +1 -0
- package/dist/tools/progressiveDiscoveryTools.js +50 -115
- package/dist/tools/progressiveDiscoveryTools.js.map +1 -1
- package/dist/tools/selfEvalTools.js +8 -1
- package/dist/tools/selfEvalTools.js.map +1 -1
- package/dist/tools/sessionMemoryTools.js +14 -2
- package/dist/tools/sessionMemoryTools.js.map +1 -1
- package/dist/tools/toolRegistry.d.ts +1 -15
- package/dist/tools/toolRegistry.js +243 -228
- package/dist/tools/toolRegistry.js.map +1 -1
- package/dist/tools/visualQaTools.d.ts +2 -0
- package/dist/tools/visualQaTools.js +1088 -0
- package/dist/tools/visualQaTools.js.map +1 -0
- package/dist/tools/webmcpTools.d.ts +16 -0
- package/dist/tools/webmcpTools.js +703 -0
- package/dist/tools/webmcpTools.js.map +1 -0
- package/dist/toolsetRegistry.js +6 -2
- package/dist/toolsetRegistry.js.map +1 -1
- package/package.json +2 -2
package/dist/dashboard/html.js
CHANGED
|
@@ -16,1236 +16,1476 @@
|
|
|
16
16
|
* - Deferred rendering for collapsed cards (DOM-light pagination)
|
|
17
17
|
*/
|
|
18
18
|
export function getDashboardHtml() {
|
|
19
|
-
return `<!DOCTYPE html>
|
|
20
|
-
<html lang="en" class="dark">
|
|
21
|
-
<head>
|
|
22
|
-
<meta charset="UTF-8">
|
|
23
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
24
|
-
<title>NodeBench UI
|
|
25
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
26
|
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
27
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
28
|
-
<script>
|
|
29
|
-
tailwind.config = {
|
|
30
|
-
darkMode: 'class',
|
|
31
|
-
theme: {
|
|
32
|
-
extend: {
|
|
33
|
-
fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'] },
|
|
34
|
-
colors: {
|
|
35
|
-
surface: { 0: '#09090b', 1: '#111113', 2: '#18181b', 3: '#1f1f23' },
|
|
36
|
-
border: { DEFAULT: '#27272a', subtle: '#1e1e22', focus: '#6366f1' },
|
|
37
|
-
accent: { DEFAULT: '#818cf8', bright: '#a5b4fc', dim: '#4f46e5' },
|
|
38
|
-
ok: '#34d399', warn: '#fbbf24', err: '#f87171',
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
</script>
|
|
44
|
-
<style>
|
|
45
|
-
/* ── Design Tokens ──────────────────────────────────────── */
|
|
46
|
-
:root {
|
|
47
|
-
--sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 24px; --sp-6: 32px;
|
|
48
|
-
--border-base: #27272a;
|
|
49
|
-
--border-hover: #3f3f46;
|
|
50
|
-
--border-accent: #6366f1;
|
|
51
|
-
--surface-1: #111113;
|
|
52
|
-
--surface-2: #18181b;
|
|
53
|
-
--gradient-accent: linear-gradient(135deg, #1e1b4b, #312e81);
|
|
54
|
-
--shadow-card: 0 0 0 1px rgba(99,102,241,.15), 0 1px 3px rgba(0,0,0,.4);
|
|
55
|
-
--shadow-card-hover: 0 0 0 1px rgba(99,102,241,.35), 0 4px 12px rgba(0,0,0,.5);
|
|
56
|
-
--shadow-lift: 0 8px 30px rgba(99,102,241,.12), 0 2px 8px rgba(0,0,0,.4);
|
|
57
|
-
--radius-sm: 6px; --radius-md: 8px; --radius-lg: 10px; --radius-xl: 12px;
|
|
58
|
-
--transition-fast: .15s ease; --transition-base: .2s ease;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/* ── Reset & Base ───────────────────────────────────────── */
|
|
62
|
-
*, *::before, *::after { box-sizing: border-box; }
|
|
63
|
-
body { font-family: 'Inter', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
|
64
|
-
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
|
|
65
|
-
|
|
66
|
-
/* ── Shared Interactive ─────────────────────────────────── */
|
|
67
|
-
.glass { background: rgba(17,17,19,.72); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); }
|
|
68
|
-
.ring-glow { box-shadow: var(--shadow-card); }
|
|
69
|
-
.ring-glow:hover { box-shadow: var(--shadow-card-hover); }
|
|
70
|
-
|
|
71
|
-
.btn-icon {
|
|
72
|
-
width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
|
|
73
|
-
border-radius: var(--radius-md); border: 1px solid var(--border-base);
|
|
74
|
-
background: transparent; color: #52525b; cursor: pointer;
|
|
75
|
-
transition: all var(--transition-fast);
|
|
76
|
-
}
|
|
77
|
-
.btn-icon:hover { background: var(--surface-2); color: #d4d4d8; border-color: var(--border-hover); }
|
|
78
|
-
.btn-icon.active { background: var(--gradient-accent); border-color: var(--border-accent); color: #c7d2fe; }
|
|
79
|
-
.btn-icon:focus-visible { outline: 2px solid var(--border-accent); outline-offset: 2px; }
|
|
80
|
-
.btn-icon svg { width: 16px; height: 16px; }
|
|
81
|
-
|
|
82
|
-
/* ── Focus-Visible (global) ─────────────────────────────── */
|
|
83
|
-
:focus-visible { outline: 2px solid var(--border-accent); outline-offset: 2px; }
|
|
84
|
-
:focus:not(:focus-visible) { outline: none; }
|
|
85
|
-
|
|
86
|
-
/* ── Animations ─────────────────────────────────────────── */
|
|
87
|
-
@keyframes fadeUp { from { opacity:0; transform: translateY(8px); } to { opacity:1; transform: translateY(0); } }
|
|
88
|
-
.fade-up { animation: fadeUp .35s ease-out both; }
|
|
89
|
-
/* Staggered cascade for lists of cards */
|
|
90
|
-
.fade-up:nth-child(2) { animation-delay: 50ms; }
|
|
91
|
-
.fade-up:nth-child(3) { animation-delay: 100ms; }
|
|
92
|
-
.fade-up:nth-child(n+4) { animation-delay: 150ms; }
|
|
93
|
-
@keyframes pulse2 { 0%,100%{opacity:1} 50%{opacity:.35} }
|
|
94
|
-
.pulse-live { animation: pulse2 2s infinite; }
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.
|
|
141
|
-
.
|
|
142
|
-
.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
.
|
|
165
|
-
.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
.
|
|
172
|
-
.
|
|
173
|
-
.
|
|
174
|
-
.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
.ss-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
.ss-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
.
|
|
198
|
-
.
|
|
199
|
-
.
|
|
200
|
-
.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
.
|
|
211
|
-
.
|
|
212
|
-
.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
.
|
|
224
|
-
.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
.
|
|
242
|
-
.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
.
|
|
252
|
-
.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
.cl-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
.cl-
|
|
261
|
-
.cl-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
.
|
|
268
|
-
.
|
|
269
|
-
.
|
|
270
|
-
.
|
|
271
|
-
.
|
|
272
|
-
.
|
|
273
|
-
.
|
|
274
|
-
.nav-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
.ss-grid { grid-template-columns: repeat(
|
|
312
|
-
.ss-
|
|
313
|
-
.
|
|
314
|
-
.
|
|
315
|
-
.
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if (
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
h.push('
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
h.push('<
|
|
682
|
-
h.push('
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
h.push('
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
h.push('
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
h.push('<
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
if (
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
window._ssAll =
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
if (
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
//
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
if(
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
19
|
+
return `<!DOCTYPE html>
|
|
20
|
+
<html lang="en" class="dark">
|
|
21
|
+
<head>
|
|
22
|
+
<meta charset="UTF-8">
|
|
23
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
24
|
+
<title>NodeBench UI Review</title>
|
|
25
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
26
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
27
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
28
|
+
<script>
|
|
29
|
+
tailwind.config = {
|
|
30
|
+
darkMode: 'class',
|
|
31
|
+
theme: {
|
|
32
|
+
extend: {
|
|
33
|
+
fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'] },
|
|
34
|
+
colors: {
|
|
35
|
+
surface: { 0: '#09090b', 1: '#111113', 2: '#18181b', 3: '#1f1f23' },
|
|
36
|
+
border: { DEFAULT: '#27272a', subtle: '#1e1e22', focus: '#6366f1' },
|
|
37
|
+
accent: { DEFAULT: '#818cf8', bright: '#a5b4fc', dim: '#4f46e5' },
|
|
38
|
+
ok: '#34d399', warn: '#fbbf24', err: '#f87171',
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
<style>
|
|
45
|
+
/* ── Design Tokens ──────────────────────────────────────── */
|
|
46
|
+
:root {
|
|
47
|
+
--sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 24px; --sp-6: 32px;
|
|
48
|
+
--border-base: #27272a;
|
|
49
|
+
--border-hover: #3f3f46;
|
|
50
|
+
--border-accent: #6366f1;
|
|
51
|
+
--surface-1: #111113;
|
|
52
|
+
--surface-2: #18181b;
|
|
53
|
+
--gradient-accent: linear-gradient(135deg, #1e1b4b, #312e81);
|
|
54
|
+
--shadow-card: 0 0 0 1px rgba(99,102,241,.15), 0 1px 3px rgba(0,0,0,.4);
|
|
55
|
+
--shadow-card-hover: 0 0 0 1px rgba(99,102,241,.35), 0 4px 12px rgba(0,0,0,.5);
|
|
56
|
+
--shadow-lift: 0 8px 30px rgba(99,102,241,.12), 0 2px 8px rgba(0,0,0,.4);
|
|
57
|
+
--radius-sm: 6px; --radius-md: 8px; --radius-lg: 10px; --radius-xl: 12px;
|
|
58
|
+
--transition-fast: .15s ease; --transition-base: .2s ease;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ── Reset & Base ───────────────────────────────────────── */
|
|
62
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
63
|
+
body { font-family: 'Inter', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
|
64
|
+
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
|
|
65
|
+
|
|
66
|
+
/* ── Shared Interactive ─────────────────────────────────── */
|
|
67
|
+
.glass { background: rgba(17,17,19,.72); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); }
|
|
68
|
+
.ring-glow { box-shadow: var(--shadow-card); }
|
|
69
|
+
.ring-glow:hover { box-shadow: var(--shadow-card-hover); }
|
|
70
|
+
|
|
71
|
+
.btn-icon {
|
|
72
|
+
width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
|
|
73
|
+
border-radius: var(--radius-md); border: 1px solid var(--border-base);
|
|
74
|
+
background: transparent; color: #52525b; cursor: pointer;
|
|
75
|
+
transition: all var(--transition-fast);
|
|
76
|
+
}
|
|
77
|
+
.btn-icon:hover { background: var(--surface-2); color: #d4d4d8; border-color: var(--border-hover); }
|
|
78
|
+
.btn-icon.active { background: var(--gradient-accent); border-color: var(--border-accent); color: #c7d2fe; }
|
|
79
|
+
.btn-icon:focus-visible { outline: 2px solid var(--border-accent); outline-offset: 2px; }
|
|
80
|
+
.btn-icon svg { width: 16px; height: 16px; }
|
|
81
|
+
|
|
82
|
+
/* ── Focus-Visible (global) ─────────────────────────────── */
|
|
83
|
+
:focus-visible { outline: 2px solid var(--border-accent); outline-offset: 2px; }
|
|
84
|
+
:focus:not(:focus-visible) { outline: none; }
|
|
85
|
+
|
|
86
|
+
/* ── Animations ─────────────────────────────────────────── */
|
|
87
|
+
@keyframes fadeUp { from { opacity:0; transform: translateY(8px); } to { opacity:1; transform: translateY(0); } }
|
|
88
|
+
.fade-up { animation: fadeUp .35s ease-out both; }
|
|
89
|
+
/* Staggered cascade for lists of cards */
|
|
90
|
+
.fade-up:nth-child(2) { animation-delay: 50ms; }
|
|
91
|
+
.fade-up:nth-child(3) { animation-delay: 100ms; }
|
|
92
|
+
.fade-up:nth-child(n+4) { animation-delay: 150ms; }
|
|
93
|
+
@keyframes pulse2 { 0%,100%{opacity:1} 50%{opacity:.35} }
|
|
94
|
+
.pulse-live { animation: pulse2 2s infinite; }
|
|
95
|
+
@keyframes pulseAgent { 0%,100%{opacity:1;box-shadow:0 0 0 0 rgba(52,211,153,.4)} 50%{opacity:.7;box-shadow:0 0 0 4px rgba(52,211,153,0)} }
|
|
96
|
+
.pulse-agent { animation: pulseAgent 2s ease-in-out infinite; }
|
|
97
|
+
|
|
98
|
+
/* ── Agent Monitor ────────────────────────────────────── */
|
|
99
|
+
.agent-lane { border-left: 3px solid var(--border-accent); transition: border-color var(--transition-base); }
|
|
100
|
+
.agent-lane:hover { border-left-color: #818cf8; }
|
|
101
|
+
.agent-budget-bar { height:4px; border-radius:2px; background:var(--surface-2); overflow:hidden; }
|
|
102
|
+
.agent-budget-fill { height:100%; border-radius:2px; transition: width var(--transition-base); }
|
|
103
|
+
.activity-feed { max-height:400px; overflow-y:auto; scrollbar-width:thin; scrollbar-color:var(--border-base) transparent; }
|
|
104
|
+
.activity-feed::-webkit-scrollbar { width:6px; }
|
|
105
|
+
.activity-feed::-webkit-scrollbar-track { background:transparent; }
|
|
106
|
+
.activity-feed::-webkit-scrollbar-thumb { background:var(--border-base); border-radius:3px; }
|
|
107
|
+
.activity-feed::-webkit-scrollbar-thumb:hover { background:var(--border-hover); }
|
|
108
|
+
|
|
109
|
+
/* ── Back to Top ─────────────────────────────────────── */
|
|
110
|
+
.back-to-top {
|
|
111
|
+
position: fixed; bottom: 24px; right: 24px; z-index: 90;
|
|
112
|
+
width: 40px; height: 40px; border-radius: 50%;
|
|
113
|
+
background: var(--gradient-accent); border: 1px solid var(--border-accent);
|
|
114
|
+
color: #c7d2fe; cursor: pointer;
|
|
115
|
+
display: flex; align-items: center; justify-content: center;
|
|
116
|
+
opacity: 0; transform: translateY(12px); pointer-events: none;
|
|
117
|
+
transition: opacity var(--transition-base), transform var(--transition-base);
|
|
118
|
+
box-shadow: 0 4px 12px rgba(99,102,241,.3);
|
|
119
|
+
}
|
|
120
|
+
.back-to-top.visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
|
|
121
|
+
.back-to-top:hover { box-shadow: 0 6px 20px rgba(99,102,241,.5); }
|
|
122
|
+
.back-to-top:focus-visible { outline: 2px solid var(--border-accent); outline-offset: 2px; }
|
|
123
|
+
|
|
124
|
+
/* ── Skeleton Loading ──────────────────────────────────── */
|
|
125
|
+
.skeleton { border-radius: var(--radius-lg); background: linear-gradient(90deg, var(--surface-2) 25%, #1f1f23 50%, var(--surface-2) 75%); background-size: 200% 100%; animation: skeleton-shimmer 1.5s ease-in-out infinite; }
|
|
126
|
+
@keyframes skeleton-shimmer { 0%, 100% { background-position: 200% 0; } 50% { background-position: 0% 0; } }
|
|
127
|
+
|
|
128
|
+
/* ── Reduced Motion ────────────────────────────────────── */
|
|
129
|
+
@media (prefers-reduced-motion: reduce) {
|
|
130
|
+
*, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; }
|
|
131
|
+
.fade-up { animation: none; opacity: 1; transform: none; }
|
|
132
|
+
.pulse-live { animation: none; }
|
|
133
|
+
.pulse-agent { animation: none; }
|
|
134
|
+
.skeleton { animation: none; }
|
|
135
|
+
.ss-card:hover { transform: none; }
|
|
136
|
+
.score-ring .fg { transition: none; }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* ── Score Ring ─────────────────────────────────────────── */
|
|
140
|
+
.score-ring { width:64px; height:64px; }
|
|
141
|
+
.score-ring circle { fill:none; stroke-width:5; stroke-linecap:round; }
|
|
142
|
+
.score-ring .bg { stroke: var(--border-base); }
|
|
143
|
+
.score-ring .fg { transition: stroke-dashoffset .6s ease; }
|
|
144
|
+
pre { white-space: pre-wrap; word-break: break-word; tab-size: 2; }
|
|
145
|
+
|
|
146
|
+
/* ── Severity Badges ───────────────────────────────────── */
|
|
147
|
+
.sev-critical { background:#450a0a; color:#fca5a5; }
|
|
148
|
+
.sev-high { background:#451a03; color:#fde68a; }
|
|
149
|
+
.sev-medium { background:#0c1a3d; color:#93c5fd; }
|
|
150
|
+
.sev-low { background:#052e16; color:#86efac; }
|
|
151
|
+
|
|
152
|
+
/* ── File Chips ────────────────────────────────────────── */
|
|
153
|
+
.file-chip {
|
|
154
|
+
display:inline-block; padding:2px 8px; margin:2px 3px 2px 0; border-radius:4px;
|
|
155
|
+
background: var(--surface-2); border:1px solid var(--border-base);
|
|
156
|
+
font-size:11px; font-family:'SF Mono',Monaco,monospace; color:#a1a1aa;
|
|
157
|
+
max-width:260px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ── Screenshot Grid ───────────────────────────────────── */
|
|
161
|
+
.ss-grid { display:grid; gap: var(--sp-3); }
|
|
162
|
+
.ss-grid.sz-sm { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
|
|
163
|
+
.ss-grid.sz-md { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
|
|
164
|
+
.ss-grid.sz-lg { grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); }
|
|
165
|
+
.ss-card {
|
|
166
|
+
cursor:pointer; transition: transform var(--transition-base), box-shadow var(--transition-base), border-color var(--transition-base);
|
|
167
|
+
border-radius: var(--radius-lg); overflow:hidden; border:1px solid var(--border-base); background: var(--surface-1);
|
|
168
|
+
}
|
|
169
|
+
.ss-card:hover { transform:translateY(-3px); box-shadow: var(--shadow-lift); border-color:#4f46e5; }
|
|
170
|
+
.ss-card:focus-visible { box-shadow: var(--shadow-lift); border-color:#4f46e5; }
|
|
171
|
+
.ss-card img { width:100%; aspect-ratio:16/10; object-fit:cover; display:block; background:linear-gradient(135deg,#18181b 0%,#1f1f23 100%); box-shadow:inset 0 0 0 1px rgba(255,255,255,.06); }
|
|
172
|
+
.ss-card { border-color: #3f3f46; }
|
|
173
|
+
.ss-meta { padding: var(--sp-2) var(--sp-3); border-top:1px solid #1e1e22; }
|
|
174
|
+
.ss-toolbar {
|
|
175
|
+
display:flex; align-items:center; gap: var(--sp-3); flex-wrap:wrap;
|
|
176
|
+
margin-bottom: var(--sp-3); padding: var(--sp-3) 14px;
|
|
177
|
+
background: var(--surface-1); border-radius: var(--radius-lg); border:1px solid #1e1e22;
|
|
178
|
+
}
|
|
179
|
+
.ss-search {
|
|
180
|
+
background:#09090b; border:1px solid var(--border-base); color:#d4d4d8;
|
|
181
|
+
padding:6px 10px 6px 32px; border-radius: var(--radius-md); font-size:12px;
|
|
182
|
+
width:220px; outline:none; transition: border-color var(--transition-fast);
|
|
183
|
+
}
|
|
184
|
+
.ss-search:focus { border-color: var(--border-accent); box-shadow:0 0 0 3px rgba(99,102,241,.1); }
|
|
185
|
+
.ss-search-wrap { position:relative; display:flex; align-items:center; }
|
|
186
|
+
.ss-search-icon { position:absolute; left:9px; top:50%; transform:translateY(-50%); width:14px; height:14px; color:#52525b; pointer-events:none; }
|
|
187
|
+
|
|
188
|
+
/* ── Category Pills ────────────────────────────────────── */
|
|
189
|
+
.cat-pill {
|
|
190
|
+
display:inline-flex; align-items:center; gap:4px; padding:4px 12px;
|
|
191
|
+
border-radius:999px; font-size:11px; font-weight:500; cursor:pointer;
|
|
192
|
+
transition: all var(--transition-fast); border:1px solid var(--border-base);
|
|
193
|
+
color:#a1a1aa; background:transparent; user-select:none;
|
|
194
|
+
font-family:inherit; line-height:1.4;
|
|
195
|
+
}
|
|
196
|
+
.cat-pill:hover { background: var(--surface-2); border-color: var(--border-hover); }
|
|
197
|
+
.cat-pill[aria-pressed="true"] { background: var(--gradient-accent); border-color: var(--border-accent); color:#c7d2fe; box-shadow:0 0 0 1px rgba(99,102,241,.2); }
|
|
198
|
+
.cat-pill:focus-visible { outline:2px solid var(--border-accent); outline-offset:2px; }
|
|
199
|
+
.cat-pill .cat-count { background: var(--border-base); color:#a1a1aa; padding:1px 6px; border-radius:9px; font-size:10px; margin-left:2px; line-height:1.3; }
|
|
200
|
+
.cat-pill[aria-pressed="true"] .cat-count { background:rgba(99,102,241,.3); color:#e0e7ff; }
|
|
201
|
+
|
|
202
|
+
/* ── Grid Size Toggle ──────────────────────────────────── */
|
|
203
|
+
.sz-btn { width:32px; height:32px; display:flex; align-items:center; justify-content:center; border-radius: var(--radius-md); border:1px solid var(--border-base); background:transparent; color:#52525b; cursor:pointer; transition:all var(--transition-fast); }
|
|
204
|
+
.sz-btn:hover { background: var(--surface-2); color:#d4d4d8; border-color: var(--border-hover); }
|
|
205
|
+
.sz-btn[aria-pressed="true"] { background: var(--gradient-accent); border-color: var(--border-accent); color:#c7d2fe; }
|
|
206
|
+
.sz-btn:focus-visible { outline:2px solid var(--border-accent); outline-offset:2px; }
|
|
207
|
+
.sz-btn svg { width:16px; height:16px; }
|
|
208
|
+
|
|
209
|
+
/* ── Screenshot Groups ─────────────────────────────────── */
|
|
210
|
+
.ss-group-hdr { font-size:12px; font-weight:600; color:#a1a1aa; padding: var(--sp-4) 0 var(--sp-2); margin-bottom: var(--sp-3); display:flex; align-items:center; gap: var(--sp-2); border-bottom:1px solid #1e1e22; }
|
|
211
|
+
.ss-group-hdr .g-count { font-weight:400; color:#71717a; font-size:11px; }
|
|
212
|
+
.ss-show-more {
|
|
213
|
+
display:flex; align-items:center; justify-content:center; gap:6px;
|
|
214
|
+
padding:10px var(--sp-5); border-radius: var(--radius-md); border:1px solid var(--border-base);
|
|
215
|
+
background: var(--surface-1); color:#a1a1aa; font-size:12px; font-weight:500;
|
|
216
|
+
cursor:pointer; transition:all var(--transition-fast); margin-top: var(--sp-3);
|
|
217
|
+
}
|
|
218
|
+
.ss-show-more:hover { border-color: var(--border-accent); color:#c7d2fe; background: var(--gradient-accent); }
|
|
219
|
+
.ss-show-more:focus-visible { outline:2px solid var(--border-accent); outline-offset:2px; }
|
|
220
|
+
|
|
221
|
+
/* ── Section Headers ───────────────────────────────────── */
|
|
222
|
+
.sec-hdr { margin-top: var(--sp-6); margin-bottom: var(--sp-4); display:flex; align-items:center; gap: var(--sp-3); }
|
|
223
|
+
.sec-hdr .sec-icon { width:32px; height:32px; border-radius: var(--radius-md); display:flex; align-items:center; justify-content:center; flex-shrink:0; }
|
|
224
|
+
.sec-hdr .sec-icon svg { width:16px; height:16px; }
|
|
225
|
+
.sec-hdr .sec-text h2 { font-size:14px; font-weight:700; color:#fafafa; letter-spacing:-.01em; }
|
|
226
|
+
.sec-hdr .sec-text .sec-sub { font-size:11px; color:#71717a; margin-top:1px; }
|
|
227
|
+
|
|
228
|
+
/* ── Lightbox ───────────────────────────────────────────── */
|
|
229
|
+
.lightbox { position:fixed; inset:0; z-index:200; background:rgba(0,0,0,.92); display:none; align-items:center; justify-content:center; }
|
|
230
|
+
.lightbox.open { display:flex; }
|
|
231
|
+
.lightbox img { max-width:88vw; max-height:85vh; border-radius: var(--radius-md); box-shadow:0 12px 40px rgba(0,0,0,.6); cursor:default; }
|
|
232
|
+
.lb-chrome { position:absolute; bottom:20px; left:50%; transform:translateX(-50%); display:flex; align-items:center; gap: var(--sp-3); background:rgba(17,17,19,.9); padding: var(--sp-2) var(--sp-4); border-radius: var(--radius-lg); border:1px solid var(--border-base); }
|
|
233
|
+
.lb-label { font-size:12px; color:#a1a1aa; max-width:400px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
234
|
+
.lb-counter { font-size:11px; color:#71717a; white-space:nowrap; }
|
|
235
|
+
.lb-nav {
|
|
236
|
+
width:36px; height:36px; border-radius:50%; border:1px solid var(--border-hover);
|
|
237
|
+
background:rgba(17,17,19,.8); color:#d4d4d8; display:flex; align-items:center;
|
|
238
|
+
justify-content:center; cursor:pointer; font-size:18px; transition:all var(--transition-fast);
|
|
239
|
+
position:absolute; top:50%; z-index:201;
|
|
240
|
+
}
|
|
241
|
+
.lb-nav:hover { background:#4f46e5; border-color: var(--border-accent); color:#fff; }
|
|
242
|
+
.lb-nav:focus-visible { outline:2px solid #a5b4fc; outline-offset:2px; }
|
|
243
|
+
.lb-nav.prev { left:16px; transform:translateY(-50%); }
|
|
244
|
+
.lb-nav.next { right:16px; transform:translateY(-50%); }
|
|
245
|
+
.lb-close {
|
|
246
|
+
position:absolute; top:16px; right:16px; width:36px; height:36px; border-radius:50%;
|
|
247
|
+
border:1px solid var(--border-hover); background:rgba(17,17,19,.8); color:#d4d4d8;
|
|
248
|
+
display:flex; align-items:center; justify-content:center; cursor:pointer; font-size:18px;
|
|
249
|
+
z-index:201; transition:all var(--transition-fast);
|
|
250
|
+
}
|
|
251
|
+
.lb-close:hover { background:#dc2626; border-color:#ef4444; color:#fff; }
|
|
252
|
+
.lb-close:focus-visible { outline:2px solid #ef4444; outline-offset:2px; }
|
|
253
|
+
|
|
254
|
+
/* ── Changelog Carousel ────────────────────────────────── */
|
|
255
|
+
.cl-carousel { position:relative; }
|
|
256
|
+
.cl-track {
|
|
257
|
+
display:flex; gap: var(--sp-4); overflow-x:auto; scroll-snap-type:x mandatory;
|
|
258
|
+
-webkit-overflow-scrolling:touch; padding:4px 0 var(--sp-3); scrollbar-width:none;
|
|
259
|
+
}
|
|
260
|
+
.cl-track::-webkit-scrollbar { display:none; }
|
|
261
|
+
.cl-card {
|
|
262
|
+
flex:0 0 min(100%, 420px); scroll-snap-align:start; background: var(--surface-1);
|
|
263
|
+
border:1px solid var(--border-base); border-radius: var(--radius-xl); padding:20px;
|
|
264
|
+
display:flex; flex-direction:column; gap: var(--sp-3);
|
|
265
|
+
transition: border-color var(--transition-base), box-shadow var(--transition-base);
|
|
266
|
+
}
|
|
267
|
+
.cl-card:hover { border-color: var(--border-hover); box-shadow:0 4px 20px rgba(0,0,0,.3); }
|
|
268
|
+
.cl-card:only-child { flex:0 0 100%; }
|
|
269
|
+
.cl-step { display:inline-flex; align-items:center; justify-content:center; width:24px; height:24px; border-radius:50%; background: var(--gradient-accent); color:#c7d2fe; font-size:11px; font-weight:700; flex-shrink:0; }
|
|
270
|
+
.cl-time { font-size:11px; color:#71717a; }
|
|
271
|
+
.cl-desc { font-size:12px; color:#d4d4d8; line-height:1.6; flex:1; }
|
|
272
|
+
.cl-files { display:flex; flex-wrap:wrap; gap:4px; }
|
|
273
|
+
.cl-files .file-chip { max-width: min(260px, calc(100% - 8px)); }
|
|
274
|
+
.cl-nav-btn {
|
|
275
|
+
position:absolute; top:50%; transform:translateY(-50%); width:32px; height:32px;
|
|
276
|
+
border-radius:50%; border:1px solid var(--border-base); background:rgba(9,9,11,.85);
|
|
277
|
+
backdrop-filter:blur(8px); color:#a1a1aa; display:flex; align-items:center;
|
|
278
|
+
justify-content:center; cursor:pointer; transition:all var(--transition-fast); z-index:5;
|
|
279
|
+
}
|
|
280
|
+
.cl-nav-btn:hover:not(.disabled) { background:#4f46e5; border-color: var(--border-accent); color:#fff; }
|
|
281
|
+
.cl-nav-btn:focus-visible { outline:2px solid var(--border-accent); outline-offset:2px; }
|
|
282
|
+
.cl-nav-btn.disabled { opacity:.25; cursor:default; pointer-events:none; }
|
|
283
|
+
.cl-nav-btn.prev { left:-12px; }
|
|
284
|
+
.cl-nav-btn.next { right:-12px; }
|
|
285
|
+
.cl-nav-btn svg { width:14px; height:14px; }
|
|
286
|
+
.cl-dots { display:flex; justify-content:center; gap:6px; padding-top: var(--sp-2); }
|
|
287
|
+
.cl-dot {
|
|
288
|
+
width:6px; height:6px; border-radius:50%; background: var(--border-base);
|
|
289
|
+
transition: width var(--transition-base), background var(--transition-base), border-radius var(--transition-base);
|
|
290
|
+
cursor:pointer; border:none; padding:0;
|
|
291
|
+
}
|
|
292
|
+
.cl-dot:focus-visible { outline:2px solid var(--border-accent); outline-offset:2px; }
|
|
293
|
+
.cl-dot.active { background:#818cf8; width:18px; border-radius:3px; }
|
|
294
|
+
|
|
295
|
+
/* ── Compare Mode ──────────────────────────────────────── */
|
|
296
|
+
.compare-bar { display:flex; align-items:center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-4); background: var(--surface-1); border-radius: var(--radius-md); margin-bottom: var(--sp-3); }
|
|
297
|
+
.compare-bar select { background: var(--surface-2); border:1px solid var(--border-base); color:#d4d4d8; padding:4px 8px; border-radius: var(--radius-sm); font-size:12px; }
|
|
298
|
+
.compare-grid { display:grid; grid-template-columns:1fr 1fr; gap: var(--sp-3); }
|
|
299
|
+
.compare-grid .compare-col { border:1px solid var(--border-base); border-radius: var(--radius-md); overflow:hidden; }
|
|
300
|
+
.compare-grid .compare-col .ch { padding: var(--sp-2) var(--sp-3); background: var(--surface-2); font-size:11px; color:#a1a1aa; font-weight:600; text-transform:uppercase; letter-spacing:.05em; }
|
|
301
|
+
.compare-grid .compare-col img { width:100%; display:block; }
|
|
302
|
+
.empty-state { text-align:center; padding: var(--sp-6) var(--sp-4); color:#71717a; }
|
|
303
|
+
.empty-state .empty-icon { font-size:28px; margin-bottom: var(--sp-2); opacity:.5; }
|
|
304
|
+
.empty-state .empty-hint { font-size:12px; line-height:1.5; max-width:400px; margin:0 auto; }
|
|
305
|
+
.nav-pill { display:inline-flex; align-items:center; gap:4px; padding:3px 10px; border-radius:999px; font-size:11px; font-weight:500; cursor:pointer; transition:all var(--transition-fast); border:1px solid transparent; }
|
|
306
|
+
.nav-pill:hover { background: var(--surface-2); }
|
|
307
|
+
.nav-pill.active { background:#1e1b4b; border-color:#4f46e5; color:#a5b4fc; }
|
|
308
|
+
|
|
309
|
+
/* ── Responsive ────────────────────────────────────────── */
|
|
310
|
+
@media (max-width: 640px) {
|
|
311
|
+
.ss-grid.sz-md { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
|
|
312
|
+
.ss-grid.sz-lg { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
|
|
313
|
+
.ss-search { width: min(220px, calc(100% - 16px)); }
|
|
314
|
+
.ss-toolbar { padding: var(--sp-2) var(--sp-3); gap: var(--sp-2); }
|
|
315
|
+
.cl-card { flex: 0 0 min(100%, 320px); padding: var(--sp-4); }
|
|
316
|
+
.compare-grid { grid-template-columns: 1fr; }
|
|
317
|
+
.lb-chrome { max-width: calc(100vw - 32px); }
|
|
318
|
+
}
|
|
319
|
+
@media (max-width: 480px) {
|
|
320
|
+
main { padding-left: var(--sp-3) !important; padding-right: var(--sp-3) !important; }
|
|
321
|
+
.sec-hdr { margin-top: var(--sp-5); }
|
|
322
|
+
.ss-grid.sz-sm { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/* ── Print Stylesheet ──────────────────────────────────── */
|
|
326
|
+
@media print {
|
|
327
|
+
:root { --surface-1: #f9fafb; --surface-2: #f3f4f6; --border-base: #e5e7eb; }
|
|
328
|
+
html, body { background: #fff; color: #111; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
329
|
+
header, .ss-toolbar, .cat-pill, .sz-btn, .cl-nav-btn, .cl-dots, .lightbox, .ss-show-more, .compare-bar, #compare-btn { display: none !important; }
|
|
330
|
+
main { max-width: 100%; padding: 0 16px; }
|
|
331
|
+
.fade-up, .pulse-live, .skeleton { animation: none; }
|
|
332
|
+
.ring-glow { box-shadow: none; border: 1px solid #d1d5db; }
|
|
333
|
+
.text-white, .text-zinc-200 { color: #111; }
|
|
334
|
+
.text-zinc-300, .text-zinc-400 { color: #4b5563; }
|
|
335
|
+
.text-zinc-500, .text-zinc-600 { color: #6b7280; }
|
|
336
|
+
.text-accent { color: #4f46e5; }
|
|
337
|
+
.text-ok { color: #16a34a; } .text-warn { color: #d97706; } .text-err { color: #dc2626; }
|
|
338
|
+
.bg-surface-0 { background: #fff; } .bg-surface-1, .bg-surface-2 { background: #f3f4f6; }
|
|
339
|
+
.sev-critical { background: #fef2f2; color: #991b1b; border-left: 3px solid #dc2626; }
|
|
340
|
+
.sev-high { background: #fffbeb; color: #92400e; border-left: 3px solid #ea580c; }
|
|
341
|
+
.sev-medium { background: #eff6ff; color: #1e40af; border-left: 3px solid #3b82f6; }
|
|
342
|
+
.sev-low { background: #f0fdf4; color: #166534; border-left: 3px solid #22c55e; }
|
|
343
|
+
.ss-grid { grid-template-columns: repeat(3, 1fr) !important; gap: 8px; }
|
|
344
|
+
.ss-card { page-break-inside: avoid; }
|
|
345
|
+
.sec-hdr { page-break-after: avoid; margin-top: 20px; }
|
|
346
|
+
.glass { background: #fff; backdrop-filter: none; }
|
|
347
|
+
.file-chip { background: #f3f4f6; border-color: #d1d5db; color: #374151; }
|
|
348
|
+
}
|
|
349
|
+
</style>
|
|
350
|
+
</head>
|
|
351
|
+
<body class="bg-surface-0 text-zinc-300 min-h-screen">
|
|
352
|
+
|
|
353
|
+
<!-- Sticky header -->
|
|
354
|
+
<header class="glass border-b border-border sticky top-0 z-50 px-5 py-2 flex items-center justify-between flex-wrap gap-2 min-h-[3.5rem]">
|
|
355
|
+
<div class="flex items-center gap-2.5">
|
|
356
|
+
<div class="w-7 h-7 rounded-md bg-gradient-to-br from-accent-dim to-accent flex items-center justify-center text-white text-xs font-bold shrink-0">N</div>
|
|
357
|
+
<div>
|
|
358
|
+
<span class="text-sm font-semibold text-white tracking-tight" id="hdr-title">UI Review</span>
|
|
359
|
+
<span class="text-[10px] text-zinc-500 ml-1.5" id="hdr-status"></span>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
363
|
+
<span class="flex items-center gap-1.5 text-[10px] text-zinc-500"><span class="w-1.5 h-1.5 rounded-full bg-ok pulse-live" aria-hidden="true"></span><span aria-live="polite">Auto-refresh</span></span>
|
|
364
|
+
<label for="session-picker" class="sr-only">Select session</label>
|
|
365
|
+
<select id="session-picker" class="bg-surface-2 border border-border rounded-md px-2.5 py-1 text-xs text-zinc-300 focus:outline-none focus:ring-1 focus:ring-accent max-w-[300px]">
|
|
366
|
+
<option value="">Loading...</option>
|
|
367
|
+
</select>
|
|
368
|
+
<button type="button" id="compare-btn" onclick="toggleCompare()" class="text-[11px] px-2.5 py-1 rounded-md border border-border text-zinc-400 hover:text-white hover:border-accent transition-colors" title="Pick two sessions and view their scores and screenshots side-by-side">Compare</button>
|
|
369
|
+
</div>
|
|
370
|
+
</header>
|
|
371
|
+
|
|
372
|
+
<!-- Skip Links (screen-reader + keyboard users) -->
|
|
373
|
+
<nav class="sr-only" aria-label="Skip to section" style="position:fixed;top:56px;left:0;z-index:100">
|
|
374
|
+
<a href="#root" style="background:#4f46e5;color:#fff;padding:8px 16px;display:block" onfocus="this.parentElement.style.position='fixed';this.parentElement.classList.remove('sr-only')" onblur="this.parentElement.classList.add('sr-only')">Skip to main content</a>
|
|
375
|
+
</nav>
|
|
376
|
+
|
|
377
|
+
<!-- Agent Monitor (independent refresh cycle) -->
|
|
378
|
+
<div id="agent-monitor" class="max-w-[960px] mx-auto px-5 pt-6" aria-label="Parallel agent monitor" role="region"></div>
|
|
379
|
+
|
|
380
|
+
<!-- Back to Top -->
|
|
381
|
+
<button class="back-to-top" id="back-to-top" onclick="window.scrollTo({top:0,behavior:'smooth'})" aria-label="Back to top" title="Back to top">
|
|
382
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="18 15 12 9 6 15"/></svg>
|
|
383
|
+
</button>
|
|
384
|
+
|
|
385
|
+
<!-- All content rendered here -->
|
|
386
|
+
<main id="root" class="max-w-[960px] mx-auto px-5 pb-20">
|
|
387
|
+
<div class="space-y-4" aria-busy="true" aria-label="Loading dashboard">
|
|
388
|
+
<div class="skeleton" style="height:120px"></div>
|
|
389
|
+
<div class="skeleton" style="height:14px;width:180px;margin-top:24px;border-radius:4px"></div>
|
|
390
|
+
<div class="skeleton" style="height:80px"></div>
|
|
391
|
+
<div class="skeleton" style="height:80px"></div>
|
|
392
|
+
</div>
|
|
393
|
+
</main>
|
|
394
|
+
|
|
395
|
+
<script>
|
|
396
|
+
let SID = null;
|
|
397
|
+
let _t = null;
|
|
398
|
+
let _allSessions = [];
|
|
399
|
+
let _compareMode = false;
|
|
400
|
+
let _lastDataHash = '';
|
|
401
|
+
let _agentHash = '';
|
|
402
|
+
let _agentTimer = null;
|
|
403
|
+
let _failCount = 0;
|
|
404
|
+
let _backoff = 5000;
|
|
405
|
+
// Persistent gallery state (survives auto-refresh re-renders)
|
|
406
|
+
let _activeCat = 'all';
|
|
407
|
+
let _gridSize = 'md';
|
|
408
|
+
let _expandedGroups = new Set();
|
|
409
|
+
let _searchQuery = '';
|
|
410
|
+
const $ = id => document.getElementById(id);
|
|
411
|
+
|
|
412
|
+
async function init() {
|
|
413
|
+
const res = await fetch('/api/sessions');
|
|
414
|
+
_allSessions = await res.json();
|
|
415
|
+
const pk = $('session-picker');
|
|
416
|
+
pk.innerHTML = _allSessions.map(s => {
|
|
417
|
+
const bugs = s.bug_count||0, fixed = s.bugs_resolved||0;
|
|
418
|
+
const tag = bugs===0 ? 'Clean' : fixed>=bugs ? bugs+' fixed' : bugs+' bugs';
|
|
419
|
+
return '<option value="'+esc(s.id)+'">'+esc(s.app_name||'Session')+' '+esc(s.created_at.slice(5,16))+' ['+tag+']</option>';
|
|
420
|
+
}).join('');
|
|
421
|
+
if (_allSessions.length) { SID = _allSessions[0].id; pk.value = SID; }
|
|
422
|
+
pk.onchange = e => { SID = e.target.value; if(!_compareMode) load(); };
|
|
423
|
+
load();
|
|
424
|
+
loadAgents();
|
|
425
|
+
_t = setInterval(() => { if(!_compareMode) load(); }, 5000);
|
|
426
|
+
_agentTimer = setInterval(() => { loadAgents(); }, 2500);
|
|
427
|
+
|
|
428
|
+
// Back-to-top visibility
|
|
429
|
+
var btt = $('back-to-top');
|
|
430
|
+
if (btt) {
|
|
431
|
+
window.addEventListener('scroll', function() {
|
|
432
|
+
btt.classList.toggle('visible', window.scrollY > 600);
|
|
433
|
+
}, { passive: true });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function toggleCompare() {
|
|
438
|
+
_compareMode = !_compareMode;
|
|
439
|
+
const btn = $('compare-btn');
|
|
440
|
+
btn.textContent = _compareMode ? 'Exit Compare' : 'Compare';
|
|
441
|
+
btn.style.borderColor = _compareMode ? '#6366f1' : '';
|
|
442
|
+
btn.style.color = _compareMode ? '#fff' : '';
|
|
443
|
+
if (_compareMode) {
|
|
444
|
+
renderCompareMode();
|
|
445
|
+
} else {
|
|
446
|
+
_lastDataHash = '';
|
|
447
|
+
load();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function renderCompareMode() {
|
|
452
|
+
const opts = _allSessions.map(s =>
|
|
453
|
+
'<option value="'+esc(s.id)+'">'+esc(s.app_name||'Review')+' \\u2014 '+esc(s.created_at.slice(5,16))+'</option>'
|
|
454
|
+
).join('');
|
|
455
|
+
const parts = [];
|
|
456
|
+
parts.push('<div class="fade-up">');
|
|
457
|
+
parts.push('<h2 class="text-lg font-bold text-white mb-4">Session Comparison</h2>');
|
|
458
|
+
parts.push('<div class="compare-bar">');
|
|
459
|
+
parts.push('<label for="cmp-left" class="text-[11px] text-zinc-400 font-semibold uppercase">Left</label>');
|
|
460
|
+
parts.push('<select id="cmp-left" class="flex-1" onchange="loadCompare()">'+opts+'</select>');
|
|
461
|
+
parts.push('<span class="text-[11px] text-zinc-400" aria-hidden="true">vs</span>');
|
|
462
|
+
parts.push('<label for="cmp-right" class="text-[11px] text-zinc-400 font-semibold uppercase">Right</label>');
|
|
463
|
+
parts.push('<select id="cmp-right" class="flex-1" onchange="loadCompare()">'+opts+'</select>');
|
|
464
|
+
parts.push('</div>');
|
|
465
|
+
parts.push('<div id="cmp-content"><p class="text-zinc-500 text-sm py-10 text-center">Select two sessions to compare</p></div>');
|
|
466
|
+
parts.push('</div>');
|
|
467
|
+
$('root').innerHTML = parts.join('');
|
|
468
|
+
if (_allSessions.length >= 2) {
|
|
469
|
+
$('cmp-left').value = _allSessions[1].id;
|
|
470
|
+
$('cmp-right').value = _allSessions[0].id;
|
|
471
|
+
loadCompare();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function loadCompare() {
|
|
476
|
+
const leftId = $('cmp-left').value;
|
|
477
|
+
const rightId = $('cmp-right').value;
|
|
478
|
+
if (!leftId || !rightId) return;
|
|
479
|
+
const [lShots, rShots, lOv, rOv] = await Promise.all([
|
|
480
|
+
fetch('/api/session/'+leftId+'/screenshots').then(r=>r.json()),
|
|
481
|
+
fetch('/api/session/'+rightId+'/screenshots').then(r=>r.json()),
|
|
482
|
+
fetch('/api/session/'+leftId+'/overview').then(r=>r.json()),
|
|
483
|
+
fetch('/api/session/'+rightId+'/overview').then(r=>r.json()),
|
|
484
|
+
]);
|
|
485
|
+
const parts = [];
|
|
486
|
+
const lRev = lOv.latestReview, rRev = rOv.latestReview;
|
|
487
|
+
const lScore = lRev?(lRev.score??0):null, rScore = rRev?(rRev.score??0):null;
|
|
488
|
+
const gradeOf = s => s===null?'\\u2014':s>=90?'A':s>=80?'B':s>=70?'C':s>=60?'D':'F';
|
|
489
|
+
parts.push('<div class="compare-grid mb-6">');
|
|
490
|
+
parts.push('<div class="ring-glow rounded-lg p-4 bg-surface-1 text-center"><div class="text-[10px] text-zinc-500 uppercase mb-1">Score</div><div class="text-3xl font-bold '+(lScore>=80?'text-ok':lScore>=60?'text-warn':'text-err')+'">'+gradeOf(lScore)+'</div><div class="text-sm text-zinc-400">'+(lScore??'\\u2014')+'/100</div><div class="text-[10px] text-zinc-500 mt-1">'+lOv.stats.bugs+' bugs · '+lOv.stats.components+' components</div></div>');
|
|
491
|
+
parts.push('<div class="ring-glow rounded-lg p-4 bg-surface-1 text-center"><div class="text-[10px] text-zinc-500 uppercase mb-1">Score</div><div class="text-3xl font-bold '+(rScore>=80?'text-ok':rScore>=60?'text-warn':'text-err')+'">'+gradeOf(rScore)+'</div><div class="text-sm text-zinc-400">'+(rScore??'\\u2014')+'/100</div><div class="text-[10px] text-zinc-500 mt-1">'+rOv.stats.bugs+' bugs · '+rOv.stats.components+' components</div></div>');
|
|
492
|
+
parts.push('</div>');
|
|
493
|
+
const routeMap = {};
|
|
494
|
+
lShots.forEach(s => { const r = s.route||s.label; if(!routeMap[r]) routeMap[r]={left:null,right:null}; routeMap[r].left = s; });
|
|
495
|
+
rShots.forEach(s => { const r = s.route||s.label; if(!routeMap[r]) routeMap[r]={left:null,right:null}; routeMap[r].right = s; });
|
|
496
|
+
const routes = Object.keys(routeMap);
|
|
497
|
+
if (routes.length === 0) {
|
|
498
|
+
parts.push('<p class="text-zinc-500 text-sm py-8 text-center">No screenshots to compare. Capture some screenshots during your review sessions first.</p>');
|
|
499
|
+
} else {
|
|
500
|
+
parts.push('<div class="space-y-4">');
|
|
501
|
+
routes.forEach(r => {
|
|
502
|
+
const pair = routeMap[r];
|
|
503
|
+
parts.push('<div class="fade-up"><div class="text-[11px] text-zinc-400 font-medium mb-1.5 font-mono">'+esc(r)+'</div>');
|
|
504
|
+
parts.push('<div class="compare-grid">');
|
|
505
|
+
if (pair.left) {
|
|
506
|
+
const src = pair.left.base64_thumbnail ? 'data:image/png;base64,'+pair.left.base64_thumbnail : '/api/screenshot/'+encodeURIComponent(pair.left.id)+'/image';
|
|
507
|
+
parts.push('<div class="compare-col"><img src="'+src+'" alt="Left: '+esc(r)+'" loading="lazy" style="cursor:pointer" data-lb-src="'+esc(src)+'" data-lb-label="Left: '+esc(r)+'"></div>');
|
|
508
|
+
} else {
|
|
509
|
+
parts.push('<div class="compare-col" style="display:flex;align-items:center;justify-content:center;min-height:120px;color:#71717a;font-size:11px">No screenshot</div>');
|
|
510
|
+
}
|
|
511
|
+
if (pair.right) {
|
|
512
|
+
const src = pair.right.base64_thumbnail ? 'data:image/png;base64,'+pair.right.base64_thumbnail : '/api/screenshot/'+encodeURIComponent(pair.right.id)+'/image';
|
|
513
|
+
parts.push('<div class="compare-col"><img src="'+src+'" alt="Right: '+esc(r)+'" loading="lazy" style="cursor:pointer" data-lb-src="'+esc(src)+'" data-lb-label="Right: '+esc(r)+'"></div>');
|
|
514
|
+
} else {
|
|
515
|
+
parts.push('<div class="compare-col" style="display:flex;align-items:center;justify-content:center;min-height:120px;color:#71717a;font-size:11px">No screenshot</div>');
|
|
516
|
+
}
|
|
517
|
+
parts.push('</div></div>');
|
|
518
|
+
});
|
|
519
|
+
parts.push('</div>');
|
|
520
|
+
}
|
|
521
|
+
$('cmp-content').innerHTML = parts.join('');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function simpleHash(str) {
|
|
525
|
+
let h = 0;
|
|
526
|
+
for (let i = 0; i < str.length; i++) {
|
|
527
|
+
h = ((h << 5) - h + str.charCodeAt(i)) | 0;
|
|
528
|
+
}
|
|
529
|
+
return h.toString(36);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function load() {
|
|
533
|
+
if (!SID) return;
|
|
534
|
+
try {
|
|
535
|
+
const [ov, bugs, fixes, comps, locs, logs, tests, revs, shots] = await Promise.all([
|
|
536
|
+
fetch('/api/session/'+SID+'/overview').then(r=>r.json()),
|
|
537
|
+
fetch('/api/session/'+SID+'/bugs').then(r=>r.json()),
|
|
538
|
+
fetch('/api/session/'+SID+'/fixes').then(r=>r.json()),
|
|
539
|
+
fetch('/api/session/'+SID+'/components').then(r=>r.json()),
|
|
540
|
+
fetch('/api/session/'+SID+'/code-locations').then(r=>r.json()),
|
|
541
|
+
fetch('/api/session/'+SID+'/changelogs').then(r=>r.json()),
|
|
542
|
+
fetch('/api/session/'+SID+'/tests').then(r=>r.json()),
|
|
543
|
+
fetch('/api/session/'+SID+'/reviews').then(r=>r.json()),
|
|
544
|
+
fetch('/api/session/'+SID+'/screenshots').then(r=>r.json()),
|
|
545
|
+
]);
|
|
546
|
+
// Skip re-render if data unchanged
|
|
547
|
+
const hash = simpleHash(JSON.stringify([ov,bugs,fixes,comps,locs,logs,tests,revs,shots?.length]));
|
|
548
|
+
if (hash === _lastDataHash) return;
|
|
549
|
+
_lastDataHash = hash;
|
|
550
|
+
render(ov, bugs, fixes, comps, locs, logs, tests, revs, shots);
|
|
551
|
+
_failCount = 0;
|
|
552
|
+
_backoff = 5000;
|
|
553
|
+
updateRefreshIndicator('ok');
|
|
554
|
+
} catch(e) {
|
|
555
|
+
_failCount++;
|
|
556
|
+
updateRefreshIndicator('fail');
|
|
557
|
+
if (_failCount <= 1) {
|
|
558
|
+
$('root').innerHTML = '<div style="text-align:center;padding:40px 16px"><div style="font-size:32px;margin-bottom:12px;opacity:.6">⚠</div><div class="text-zinc-300 text-sm font-medium mb-2">Connection error</div><div class="text-zinc-500 text-xs mb-4">'+esc(e.message)+'</div><button type="button" onclick="_lastDataHash=\\'\\';load()" class="text-[11px] px-3 py-1.5 rounded-md border border-accent text-accent hover:bg-accent/10" style="cursor:pointer">Retry now</button></div>';
|
|
559
|
+
}
|
|
560
|
+
// Exponential backoff: 5s → 7.5s → 11.25s → ... → max 30s
|
|
561
|
+
_backoff = Math.min(30000, _backoff * 1.5);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function updateRefreshIndicator(status) {
|
|
566
|
+
const dot = document.querySelector('.pulse-live');
|
|
567
|
+
const label = document.querySelector('[aria-live="polite"]');
|
|
568
|
+
if (!dot || !label) return;
|
|
569
|
+
if (status === 'fail') {
|
|
570
|
+
dot.style.background = '#fbbf24';
|
|
571
|
+
label.textContent = 'Retrying in '+Math.round(_backoff/1000)+'s';
|
|
572
|
+
} else {
|
|
573
|
+
dot.style.background = '';
|
|
574
|
+
label.textContent = 'Auto-refresh';
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ── Agent Monitor ──────────────────────────────────────────────
|
|
579
|
+
|
|
580
|
+
function timeSince(iso) {
|
|
581
|
+
const diff = Math.floor((Date.now() - new Date(iso+'Z').getTime()) / 1000);
|
|
582
|
+
if (diff < 0) return 'now';
|
|
583
|
+
if (diff < 60) return diff + 's';
|
|
584
|
+
if (diff < 3600) return Math.floor(diff / 60) + 'm';
|
|
585
|
+
if (diff < 86400) return Math.floor(diff / 3600) + 'h';
|
|
586
|
+
return Math.floor(diff / 86400) + 'd';
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function loadAgents() {
|
|
590
|
+
try {
|
|
591
|
+
const [status, activity, mail] = await Promise.all([
|
|
592
|
+
fetch('/api/agents/status').then(r => r.json()),
|
|
593
|
+
fetch('/api/agents/activity').then(r => r.json()),
|
|
594
|
+
fetch('/api/agents/mailbox').then(r => r.json()),
|
|
595
|
+
]);
|
|
596
|
+
const hash = simpleHash(JSON.stringify([status, activity, mail]));
|
|
597
|
+
if (hash === _agentHash) return;
|
|
598
|
+
_agentHash = hash;
|
|
599
|
+
const el = $('agent-monitor');
|
|
600
|
+
if (el) el.innerHTML = renderAgentMonitor(status, activity, mail);
|
|
601
|
+
} catch(e) {
|
|
602
|
+
// Agent endpoints may not have data yet — silently ignore
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function renderAgentMonitor(status, activity, mail) {
|
|
607
|
+
const h = [];
|
|
608
|
+
const agents = status.agents || [];
|
|
609
|
+
const calls = (activity.calls || []).slice(0, 20);
|
|
610
|
+
const msgs = mail.messages || [];
|
|
611
|
+
const sum = status.summary || {};
|
|
612
|
+
const hasContent = agents.length > 0 || calls.length > 0;
|
|
613
|
+
|
|
614
|
+
if (!hasContent) {
|
|
615
|
+
h.push('<div class="fade-up ring-glow rounded-xl bg-surface-1 p-5 mb-6">');
|
|
616
|
+
h.push('<div style="text-align:center;padding:12px 0">');
|
|
617
|
+
h.push('<div style="font-size:28px;opacity:.25;filter:grayscale(1);margin-bottom:8px" aria-hidden="true">🤖</div>');
|
|
618
|
+
h.push('<div class="text-zinc-500 text-sm font-medium">No parallel agents active</div>');
|
|
619
|
+
h.push('<div class="text-zinc-600 text-xs mt-1">Start a multi-agent session to see live activity here.</div>');
|
|
620
|
+
h.push('</div></div>');
|
|
621
|
+
return h.join('');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ── Summary Bar ─────────────────────────────────────
|
|
625
|
+
h.push('<div class="fade-up ring-glow rounded-xl bg-surface-1 p-4 mb-4">');
|
|
626
|
+
h.push('<div class="flex items-center justify-between flex-wrap gap-3">');
|
|
627
|
+
h.push('<div class="flex items-center gap-2">');
|
|
628
|
+
h.push('<span class="w-2 h-2 rounded-full pulse-agent" style="background:#34d399" aria-label="Live"></span>');
|
|
629
|
+
h.push('<span class="text-sm font-semibold text-white">Parallel Agents</span>');
|
|
630
|
+
h.push('</div>');
|
|
631
|
+
h.push('<div class="flex items-center gap-4 text-[11px] text-zinc-400">');
|
|
632
|
+
h.push('<span><span class="text-white font-semibold">'+(sum.activeAgents||agents.length)+'</span> agents</span>');
|
|
633
|
+
h.push('<span><span class="text-white font-semibold">'+(sum.activeTasks||0)+'</span> active tasks</span>');
|
|
634
|
+
if (sum.completedTasks > 0) h.push('<span><span class="text-ok font-semibold">'+sum.completedTasks+'</span> completed</span>');
|
|
635
|
+
h.push('<span><span class="text-white font-semibold">'+(sum.totalCalls||0)+'</span> total calls</span>');
|
|
636
|
+
if (msgs.length > 0) h.push('<span style="color:#818cf8"><span class="font-semibold">'+msgs.length+'</span> messages</span>');
|
|
637
|
+
h.push('</div>');
|
|
638
|
+
h.push('</div></div>');
|
|
639
|
+
|
|
640
|
+
// ── Swim Lanes ──────────────────────────────────────
|
|
641
|
+
if (agents.length > 0) {
|
|
642
|
+
h.push('<div class="fade-up mb-5">');
|
|
643
|
+
|
|
644
|
+
agents.forEach(function(ag, idx) {
|
|
645
|
+
var pct = ag.tokenBudget.percent;
|
|
646
|
+
var budgetClr = pct >= 80 ? '#f87171' : pct >= 60 ? '#fbbf24' : '#34d399';
|
|
647
|
+
var isStale = ag.lastCallAt && (Date.now() - new Date(ag.lastCallAt+'Z').getTime()) > 7200000;
|
|
648
|
+
|
|
649
|
+
h.push('<div class="agent-lane ring-glow rounded-lg bg-surface-1 p-4 mb-2 fade-up" style="animation-delay:'+(idx*50)+'ms">');
|
|
650
|
+
|
|
651
|
+
// Header
|
|
652
|
+
h.push('<div class="flex items-center justify-between mb-2">');
|
|
653
|
+
h.push('<div class="flex items-center gap-2">');
|
|
654
|
+
if (isStale) {
|
|
655
|
+
h.push('<span class="w-2 h-2 rounded-full" style="background:#fbbf24" title="No activity for 2+ hours" aria-label="Stale agent"></span>');
|
|
656
|
+
} else {
|
|
657
|
+
h.push('<span class="w-2 h-2 rounded-full pulse-agent" style="background:#34d399" aria-label="Active agent"></span>');
|
|
658
|
+
}
|
|
659
|
+
h.push('<span class="text-sm font-semibold text-white">'+esc(ag.role)+'</span>');
|
|
660
|
+
if (ag.focusArea) h.push('<span class="text-xs text-zinc-500"> · '+esc(ag.focusArea)+'</span>');
|
|
661
|
+
h.push('</div>');
|
|
662
|
+
h.push('<span class="text-[10px] text-zinc-600 font-mono">'+esc(ag.sessionId.slice(-8))+'</span>');
|
|
663
|
+
h.push('</div>');
|
|
664
|
+
|
|
665
|
+
// Task
|
|
666
|
+
if (ag.currentTask) {
|
|
667
|
+
h.push('<div class="flex items-start gap-2 mb-2">');
|
|
668
|
+
h.push('<span class="text-[10px] px-1.5 py-0.5 rounded font-medium" style="background:rgba(99,102,241,.12);color:#a5b4fc">Task</span>');
|
|
669
|
+
h.push('<div class="flex-1 min-w-0">');
|
|
670
|
+
h.push('<div class="text-xs text-zinc-300 font-medium">'+esc(ag.currentTask.key)+'</div>');
|
|
671
|
+
if (ag.currentTask.description) h.push('<div class="text-[11px] text-zinc-500 mt-0.5">'+esc(ag.currentTask.description)+'</div>');
|
|
672
|
+
h.push('</div>');
|
|
673
|
+
h.push('<span class="text-[10px] text-zinc-500 shrink-0">'+timeSince(ag.currentTask.claimedAt)+'</span>');
|
|
674
|
+
h.push('</div>');
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Stats row
|
|
678
|
+
h.push('<div class="flex items-center gap-4 text-[11px] text-zinc-400 mb-2">');
|
|
679
|
+
h.push('<span title="Tool calls in last 30 minutes">'+ag.toolCallCount+' calls</span>');
|
|
680
|
+
h.push('<span title="Context budget usage" style="color:'+budgetClr+'">'+pct+'% budget</span>');
|
|
681
|
+
if (ag.unreadMessages > 0) h.push('<span style="color:#818cf8" title="Unread messages">'+ag.unreadMessages+' msg</span>');
|
|
682
|
+
h.push('</div>');
|
|
683
|
+
|
|
684
|
+
// Budget bar
|
|
685
|
+
h.push('<div class="agent-budget-bar"><div class="agent-budget-fill" style="width:'+Math.min(pct,100)+'%;background:'+budgetClr+'"></div></div>');
|
|
686
|
+
|
|
687
|
+
h.push('</div>');
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
h.push('</div>');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ── Activity Feed ───────────────────────────────────
|
|
694
|
+
if (calls.length > 0) {
|
|
695
|
+
h.push('<div class="fade-up mb-5">');
|
|
696
|
+
h.push('<div class="flex items-center gap-2.5 mb-3">');
|
|
697
|
+
h.push('<div class="w-7 h-7 rounded-lg flex items-center justify-center" style="background:#1e1b4b">');
|
|
698
|
+
h.push('<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#818cf8" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>');
|
|
699
|
+
h.push('</div>');
|
|
700
|
+
h.push('<div><div class="text-sm font-semibold text-white">Recent Activity</div>');
|
|
701
|
+
var totalC = sum.totalCalls || activity.totalCalls || calls.length;
|
|
702
|
+
h.push('<div class="text-[11px] text-zinc-500">Latest '+calls.length+(totalC > calls.length ? ' of '+totalC : '')+' calls</div></div>');
|
|
703
|
+
h.push('</div>');
|
|
704
|
+
|
|
705
|
+
h.push('<div class="ring-glow rounded-lg bg-surface-1 activity-feed" role="log" aria-label="Recent tool call activity">');
|
|
706
|
+
calls.forEach(function(c) {
|
|
707
|
+
var elapsed = timeSince(c.created_at);
|
|
708
|
+
var icon = c.result_status === 'success'
|
|
709
|
+
? '<span style="color:#34d399" aria-label="Success">✓</span>'
|
|
710
|
+
: '<span style="color:#f87171" aria-label="Error">✗</span>';
|
|
711
|
+
var dur = c.duration_ms >= 1000 ? (c.duration_ms/1000).toFixed(1)+'s' : c.duration_ms+'ms';
|
|
712
|
+
|
|
713
|
+
h.push('<div class="flex items-center gap-2.5 px-4 py-2 text-xs" style="border-bottom:1px solid rgba(39,39,42,.4)">');
|
|
714
|
+
h.push('<span class="text-zinc-600 font-mono text-[10px] w-10 text-right shrink-0">'+elapsed+'</span>');
|
|
715
|
+
h.push('<span class="text-zinc-600 font-mono text-[10px] w-12 shrink-0">'+esc(c.session_id.slice(-6))+'</span>');
|
|
716
|
+
h.push('<span class="font-mono flex-1 min-w-0 truncate" style="color:#a5b4fc">'+esc(c.tool_name)+'</span>');
|
|
717
|
+
h.push('<span class="text-zinc-500 text-[10px] shrink-0">'+dur+'</span>');
|
|
718
|
+
h.push(icon);
|
|
719
|
+
h.push('</div>');
|
|
720
|
+
});
|
|
721
|
+
h.push('</div></div>');
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ── Mailbox ─────────────────────────────────────────
|
|
725
|
+
if (msgs.length > 0) {
|
|
726
|
+
h.push('<div class="fade-up mb-5">');
|
|
727
|
+
h.push('<div class="flex items-center gap-2.5 mb-3">');
|
|
728
|
+
h.push('<div class="w-7 h-7 rounded-lg flex items-center justify-center" style="background:#7c2d12">');
|
|
729
|
+
h.push('<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fdba74" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>');
|
|
730
|
+
h.push('</div>');
|
|
731
|
+
h.push('<div><div class="text-sm font-semibold text-white">Agent Messages</div>');
|
|
732
|
+
h.push('<div class="text-[11px] text-zinc-500">'+msgs.length+' unread</div></div>');
|
|
733
|
+
h.push('</div>');
|
|
734
|
+
|
|
735
|
+
msgs.forEach(function(m) {
|
|
736
|
+
h.push('<div class="ring-glow rounded-lg bg-surface-1 p-4 mb-2 fade-up">');
|
|
737
|
+
h.push('<div class="flex items-center gap-2 mb-1.5">');
|
|
738
|
+
if (m.priority === 'critical') h.push('<span class="sev-critical text-[10px] px-1.5 py-0.5 rounded font-medium">CRITICAL</span>');
|
|
739
|
+
else if (m.priority === 'high') h.push('<span class="sev-high text-[10px] px-1.5 py-0.5 rounded font-medium">HIGH</span>');
|
|
740
|
+
h.push('<span class="text-[10px] px-1.5 py-0.5 rounded font-medium" style="background:var(--surface-2);color:#a1a1aa">'+esc(m.category)+'</span>');
|
|
741
|
+
h.push('<span class="text-[10px] text-zinc-600 font-mono">from '+esc(m.sender_id.slice(-8))+'</span>');
|
|
742
|
+
h.push('</div>');
|
|
743
|
+
h.push('<div class="text-[13px] font-medium text-white mb-1">'+esc(m.subject)+'</div>');
|
|
744
|
+
var body = m.body.length > 200 ? m.body.slice(0,200)+'\\u2026' : m.body;
|
|
745
|
+
h.push('<div class="text-xs text-zinc-400 leading-relaxed">'+esc(body)+'</div>');
|
|
746
|
+
h.push('</div>');
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
h.push('</div>');
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return h.join('');
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function render(ov, bugs, fixes, comps, locs, logs, tests, revs, shots) {
|
|
756
|
+
const s = ov.stats, sess = ov.session;
|
|
757
|
+
$('hdr-title').textContent = sess.app_name || 'UI Review';
|
|
758
|
+
$('hdr-status').textContent = sess.status === 'completed' ? 'Completed' : 'In Progress';
|
|
759
|
+
const ssMap = {};
|
|
760
|
+
(shots||[]).forEach(ss => { ssMap[ss.id] = ss; });
|
|
761
|
+
|
|
762
|
+
const h = [];
|
|
763
|
+
const hasBugs = bugs.length > 0 || fixes.length > 0;
|
|
764
|
+
const hasReview = revs.length > 0;
|
|
765
|
+
|
|
766
|
+
// ── Hero Card ─────────────────────────────────────────────
|
|
767
|
+
const rev = ov.latestReview;
|
|
768
|
+
const score = rev ? (rev.score??0) : null;
|
|
769
|
+
const grade = score!==null ? (score>=90?'A':score>=80?'B':score>=70?'C':score>=60?'D':'F') : '?';
|
|
770
|
+
const gradeClr = score===null?'text-zinc-500':score>=80?'text-ok':score>=60?'text-warn':'text-err';
|
|
771
|
+
const pct = score!==null ? score/100 : 0;
|
|
772
|
+
const circ = 2 * Math.PI * 28;
|
|
773
|
+
const dashOff = circ - (circ * pct);
|
|
774
|
+
|
|
775
|
+
h.push('<div class="fade-up ring-glow rounded-xl bg-surface-1 p-5 mb-6">');
|
|
776
|
+
h.push('<div class="flex items-start gap-5">');
|
|
777
|
+
h.push('<div class="relative shrink-0" title="'+(score!==null?'Quality score: '+score+'/100':'No review yet. Run a quality review to get a score.')+'">');
|
|
778
|
+
h.push('<svg class="score-ring" viewBox="0 0 64 64" role="img" aria-label="Quality score: '+(score!==null?score+'%':'not rated')+'"><circle class="bg" cx="32" cy="32" r="28"/>');
|
|
779
|
+
if(score!==null) h.push('<circle class="fg" cx="32" cy="32" r="28" stroke="'+(score>=80?'#34d399':score>=60?'#fbbf24':'#f87171')+'" stroke-dasharray="'+circ.toFixed(1)+'" stroke-dashoffset="'+dashOff.toFixed(1)+'" transform="rotate(-90 32 32)"/>');
|
|
780
|
+
h.push('</svg>');
|
|
781
|
+
h.push('<div class="absolute inset-0 flex items-center justify-center"><span class="text-lg font-bold '+gradeClr+'" aria-hidden="true">'+grade+'</span></div>');
|
|
782
|
+
h.push('</div>');
|
|
783
|
+
h.push('<div class="flex-1 min-w-0">');
|
|
784
|
+
h.push('<div class="text-sm font-semibold text-white mb-1">'+esc(sess.app_name||'UI Review Session')+'</div>');
|
|
785
|
+
h.push('<div class="text-[11px] text-zinc-500 mb-3">'+esc(sess.app_url||'')+(sess.created_at?' · Started '+esc(sess.created_at.slice(0,16)):'')+'</div>');
|
|
786
|
+
h.push('<div class="flex flex-wrap gap-1.5">');
|
|
787
|
+
if(s.bugs>0) h.push('<span class="text-[11px] px-2 py-0.5 rounded-full '+(s.bugsOpen>0?'bg-red-950/50 text-red-300':'bg-emerald-950/50 text-emerald-300')+'">'+s.bugs+' bug'+(s.bugs!==1?'s':'')+((s.bugsResolved>0)?' · '+s.bugsResolved+' fixed':'')+'</span>');
|
|
788
|
+
if(s.fixes>0) h.push('<span class="text-[11px] px-2 py-0.5 rounded-full bg-blue-950/50 text-blue-300">'+s.fixes+' fix'+(s.fixes!==1?'es':'')+'</span>');
|
|
789
|
+
h.push('<span class="text-[11px] px-2 py-0.5 rounded-full bg-zinc-800/60 text-zinc-400">'+s.components+' component'+(s.components!==1?'s':'')+'</span>');
|
|
790
|
+
if(s.codeLocations>0) h.push('<span class="text-[11px] px-2 py-0.5 rounded-full bg-zinc-800/60 text-zinc-400">'+s.codeLocations+' file'+(s.codeLocations!==1?'s':'')+' reviewed</span>');
|
|
791
|
+
if(s.generatedTests>0) h.push('<span class="text-[11px] px-2 py-0.5 rounded-full bg-violet-950/50 text-violet-300">'+s.generatedTests+' test'+(s.generatedTests!==1?'s':'')+'</span>');
|
|
792
|
+
if(s.codeReviews>0) h.push('<span class="text-[11px] px-2 py-0.5 rounded-full bg-violet-950/50 text-violet-300">'+s.codeReviews+' review'+(s.codeReviews!==1?'s':'')+'</span>');
|
|
793
|
+
h.push('</div>');
|
|
794
|
+
h.push('</div></div>');
|
|
795
|
+
|
|
796
|
+
if (!hasBugs && !hasReview && logs.length===0 && (!shots||shots.length===0)) {
|
|
797
|
+
h.push('<div class="empty-state mt-4 mb-2"><div class="text-zinc-400 text-sm font-medium mb-2">Looking good so far</div>');
|
|
798
|
+
h.push('<div class="empty-hint text-zinc-500">No bugs found and no review yet. Try testing interactions, logging any issues you find, or running a quality review to get a score.</div></div>');
|
|
799
|
+
}
|
|
800
|
+
h.push('</div>');
|
|
801
|
+
|
|
802
|
+
// ── Bugs & Fixes ──────────────────────────────────────────
|
|
803
|
+
if (hasBugs) {
|
|
804
|
+
h.push(sec('Bugs & Fixes', 'Found '+bugs.length+' issue'+(bugs.length!==1?'s':'')+', applied '+fixes.length+' fix'+(fixes.length!==1?'es':'')));
|
|
805
|
+
bugs.forEach(b => {
|
|
806
|
+
const fix = fixes.find(f => f.bug_id === b.id);
|
|
807
|
+
h.push('<div class="ring-glow rounded-lg p-4 mb-2.5 bg-surface-1 fade-up">');
|
|
808
|
+
h.push('<div class="flex items-center gap-2 flex-wrap">' + sevBadge(b.severity) + statusBadge(b.status) +
|
|
809
|
+
'<span class="text-[13px] font-medium text-white leading-snug">'+esc(b.title)+'</span></div>');
|
|
810
|
+
if (b.description) h.push('<p class="text-xs text-zinc-400 mt-2 leading-relaxed">'+esc(b.description)+'</p>');
|
|
811
|
+
if (b.expected||b.actual) {
|
|
812
|
+
h.push('<div class="mt-2.5 grid grid-cols-2 gap-3 text-[11px]">');
|
|
813
|
+
if(b.expected) h.push('<div class="rounded-md bg-surface-0 p-2.5 border border-border"><span class="text-ok font-semibold text-[10px] uppercase tracking-wide">Expected</span><div class="text-zinc-400 mt-1 leading-relaxed">'+esc(b.expected)+'</div></div>');
|
|
814
|
+
if(b.actual) h.push('<div class="rounded-md bg-surface-0 p-2.5 border border-border"><span class="text-err font-semibold text-[10px] uppercase tracking-wide">Actual</span><div class="text-zinc-400 mt-1 leading-relaxed">'+esc(b.actual)+'</div></div>');
|
|
815
|
+
h.push('</div>');
|
|
816
|
+
}
|
|
817
|
+
if (fix) {
|
|
818
|
+
h.push('<div class="mt-3 border-t border-border pt-3">');
|
|
819
|
+
h.push('<div class="flex items-center gap-2 mb-1.5">' +
|
|
820
|
+
(fix.verified?'<span class="text-[10px] px-1.5 py-0.5 rounded bg-ok/10 text-ok font-medium">Verified Fix</span>':
|
|
821
|
+
'<span class="text-[10px] px-1.5 py-0.5 rounded bg-warn/10 text-warn font-medium">Pending Fix</span>') + '</div>');
|
|
822
|
+
h.push('<p class="text-xs text-zinc-400 leading-relaxed">'+esc(fix.fix_description)+'</p>');
|
|
823
|
+
if (fix.files_changed) h.push('<div class="mt-1.5 flex flex-wrap">'+fileChips(fix.files_changed)+'</div>');
|
|
824
|
+
if (fix.verification_notes) h.push('<div class="mt-1.5 text-[11px] text-zinc-500 italic leading-relaxed">'+esc(fix.verification_notes)+'</div>');
|
|
825
|
+
h.push('</div>');
|
|
826
|
+
}
|
|
827
|
+
h.push('</div>');
|
|
828
|
+
});
|
|
829
|
+
const bugIds = new Set(bugs.map(b=>b.id));
|
|
830
|
+
fixes.filter(f=>!bugIds.has(f.bug_id)).forEach(f => {
|
|
831
|
+
h.push('<div class="ring-glow rounded-lg p-4 mb-2.5 bg-surface-1 fade-up">');
|
|
832
|
+
h.push('<div class="flex items-center gap-2">' + sevBadge(f.bug_severity||'medium') +
|
|
833
|
+
(f.verified?'<span class="text-[10px] px-1.5 py-0.5 rounded bg-ok/10 text-ok font-medium">Verified</span>':
|
|
834
|
+
'<span class="text-[10px] px-1.5 py-0.5 rounded bg-warn/10 text-warn font-medium">Pending</span>') +
|
|
835
|
+
'<span class="text-[13px] font-medium text-white">'+esc(f.bug_title||f.bug_id)+'</span></div>');
|
|
836
|
+
h.push('<p class="text-xs text-zinc-400 mt-2">'+esc(f.fix_description)+'</p>');
|
|
837
|
+
if (f.files_changed) h.push('<div class="mt-1.5 flex flex-wrap">'+fileChips(f.files_changed)+'</div>');
|
|
838
|
+
h.push('</div>');
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ── Code Review (only unique findings, not duplicating bugs) ─
|
|
843
|
+
if (hasReview) {
|
|
844
|
+
const r = revs[0];
|
|
845
|
+
const sc = r.score??0;
|
|
846
|
+
const gr = sc>=90?'A':sc>=80?'B':sc>=70?'C':sc>=60?'D':'F';
|
|
847
|
+
let sev = {};
|
|
848
|
+
try { sev = typeof r.severity_counts==='string'?JSON.parse(r.severity_counts):(r.severity_counts||{}); } catch{}
|
|
849
|
+
let findings = [];
|
|
850
|
+
try { findings = typeof r.findings==='string'?JSON.parse(r.findings):(r.findings||[]); } catch{}
|
|
851
|
+
const bugTitles = new Set(bugs.map(b=>(b.title||'').toLowerCase().trim()));
|
|
852
|
+
const uniqueFindings = findings.filter(f => !bugTitles.has((f.title||'').toLowerCase().trim()));
|
|
853
|
+
|
|
854
|
+
const hasFindings = sev.critical || sev.high || sev.medium || sev.low;
|
|
855
|
+
h.push(sec('Code Review', sc+'/100 quality score'+(uniqueFindings.length>0?' · '+uniqueFindings.length+' additional finding'+(uniqueFindings.length!==1?'s':''):'')));
|
|
856
|
+
|
|
857
|
+
if (!hasFindings && uniqueFindings.length === 0) {
|
|
858
|
+
// Compact single-line for clean reviews
|
|
859
|
+
h.push('<div class="ring-glow rounded-lg bg-surface-1 px-5 py-3 fade-up">');
|
|
860
|
+
h.push('<div class="flex items-center gap-3">');
|
|
861
|
+
h.push('<span class="text-lg font-bold text-ok">'+gr+'</span>');
|
|
862
|
+
h.push('<span class="text-sm font-semibold text-white">'+sc+'/100</span>');
|
|
863
|
+
h.push('<span class="text-[11px] text-zinc-500">No findings</span>');
|
|
864
|
+
h.push('</div></div>');
|
|
865
|
+
} else {
|
|
866
|
+
h.push('<div class="ring-glow rounded-lg bg-surface-1 p-5 fade-up">');
|
|
867
|
+
h.push('<div class="flex items-center justify-between">');
|
|
868
|
+
h.push('<div class="flex items-center gap-3"><span class="text-2xl font-bold '+(sc>=80?'text-ok':sc>=60?'text-warn':'text-err')+'">'+gr+'</span>');
|
|
869
|
+
h.push('<div><div class="text-sm font-semibold text-white">'+sc+'/100</div><div class="text-[11px] text-zinc-500">Overall quality</div></div></div>');
|
|
870
|
+
h.push('<div class="flex gap-4 text-center text-[11px]">');
|
|
871
|
+
['critical','high','medium','low'].forEach(sv => {
|
|
872
|
+
const v = sev[sv]||0;
|
|
873
|
+
if(v===0) return;
|
|
874
|
+
const c = {critical:'text-err',high:'text-warn',medium:'text-accent',low:'text-ok'}[sv];
|
|
875
|
+
h.push('<div><div class="text-base font-bold '+c+'">'+v+'</div><div class="text-zinc-500 capitalize">'+sv+'</div></div>');
|
|
876
|
+
});
|
|
877
|
+
h.push('</div></div>');
|
|
878
|
+
if (uniqueFindings.length) {
|
|
879
|
+
h.push('<div class="space-y-2 mt-4">');
|
|
880
|
+
uniqueFindings.forEach(f => {
|
|
881
|
+
h.push('<div class="flex items-start gap-2.5 text-xs">');
|
|
882
|
+
h.push(sevBadge(f.severity));
|
|
883
|
+
h.push('<div class="flex-1 min-w-0">');
|
|
884
|
+
h.push('<div class="font-medium text-zinc-200">'+esc(f.title)+'</div>');
|
|
885
|
+
h.push('<div class="text-zinc-500 mt-0.5 leading-relaxed">'+truncWords(esc(f.description),200)+'</div>');
|
|
886
|
+
if (f.codeFile) h.push('<span class="file-chip mt-1">'+esc(shortPath(f.codeFile))+(f.codeLine?' '+esc(f.codeLine):'')+'</span>');
|
|
887
|
+
h.push('</div>');
|
|
888
|
+
h.push(statusBadge(f.status));
|
|
889
|
+
h.push('</div>');
|
|
890
|
+
});
|
|
891
|
+
h.push('</div>');
|
|
892
|
+
}
|
|
893
|
+
h.push('</div>');
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// ── Changelog (Carousel) — shown BEFORE screenshots ──────
|
|
898
|
+
if (logs.length) {
|
|
899
|
+
function nearestShot(changeTs) {
|
|
900
|
+
if (!shots || !shots.length || !changeTs) return null;
|
|
901
|
+
let best = null, bestDiff = Infinity;
|
|
902
|
+
const ct = new Date(changeTs).getTime();
|
|
903
|
+
if (isNaN(ct)) return null;
|
|
904
|
+
shots.forEach(ss => {
|
|
905
|
+
const st = new Date(ss.created_at).getTime();
|
|
906
|
+
if (isNaN(st)) return;
|
|
907
|
+
const diff = Math.abs(ct - st);
|
|
908
|
+
if (diff < bestDiff) { bestDiff = diff; best = ss; }
|
|
909
|
+
});
|
|
910
|
+
return best;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
h.push(sec('Changelog', logs.length+' change'+(logs.length!==1?'s':'')+' during this session'));
|
|
914
|
+
h.push('<div class="cl-carousel fade-up" id="cl-carousel" role="region" aria-label="Changelog carousel" tabindex="0">');
|
|
915
|
+
if (logs.length > 1) {
|
|
916
|
+
h.push('<button type="button" class="cl-nav-btn prev disabled" id="cl-prev" onclick="clNav(-1)" aria-label="Previous change"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg></button>');
|
|
917
|
+
h.push('<button type="button" class="cl-nav-btn next" id="cl-next" onclick="clNav(1)" aria-label="Next change"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg></button>');
|
|
918
|
+
}
|
|
919
|
+
h.push('<div class="cl-track" id="cl-track">');
|
|
920
|
+
logs.forEach((c, idx) => {
|
|
921
|
+
h.push('<div class="cl-card" role="group" aria-label="Change '+(idx+1)+' of '+logs.length+'">');
|
|
922
|
+
h.push('<div class="flex items-center gap-3">');
|
|
923
|
+
h.push('<span class="cl-step" aria-hidden="true">'+(idx+1)+'</span>');
|
|
924
|
+
h.push('<span class="cl-time">'+esc(c.created_at)+'</span>');
|
|
925
|
+
h.push('</div>');
|
|
926
|
+
h.push('<div class="cl-desc">'+truncWords(esc(c.description), 280)+'</div>');
|
|
927
|
+
if (c.files_changed) h.push('<div class="cl-files">'+fileChips(c.files_changed)+'</div>');
|
|
928
|
+
const bef = c.before_screenshot_id ? ssMap[c.before_screenshot_id] : null;
|
|
929
|
+
const aft = c.after_screenshot_id ? ssMap[c.after_screenshot_id] : null;
|
|
930
|
+
if (bef || aft) {
|
|
931
|
+
h.push('<div class="compare-grid mt-1">');
|
|
932
|
+
if (bef) {
|
|
933
|
+
const bSrc = bef.base64_thumbnail ? 'data:image/png;base64,'+bef.base64_thumbnail : '/api/screenshot/'+encodeURIComponent(bef.id)+'/image';
|
|
934
|
+
h.push('<div class="compare-col"><div class="ch">Before</div><img src="'+bSrc+'" alt="Before state" loading="lazy" data-lb-src="'+esc(bSrc)+'" data-lb-label="Before" style="cursor:pointer"></div>');
|
|
935
|
+
}
|
|
936
|
+
if (aft) {
|
|
937
|
+
const aSrc = aft.base64_thumbnail ? 'data:image/png;base64,'+aft.base64_thumbnail : '/api/screenshot/'+encodeURIComponent(aft.id)+'/image';
|
|
938
|
+
h.push('<div class="compare-col"><div class="ch">After</div><img src="'+aSrc+'" alt="After state" loading="lazy" data-lb-src="'+esc(aSrc)+'" data-lb-label="After" style="cursor:pointer"></div>');
|
|
939
|
+
}
|
|
940
|
+
h.push('</div>');
|
|
941
|
+
} else {
|
|
942
|
+
const nearest = nearestShot(c.created_at);
|
|
943
|
+
if (nearest) {
|
|
944
|
+
const nSrc = nearest.base64_thumbnail ? 'data:image/png;base64,'+nearest.base64_thumbnail : '/api/screenshot/'+encodeURIComponent(nearest.id)+'/image';
|
|
945
|
+
h.push('<div class="mt-2 rounded-lg overflow-hidden border border-zinc-800">');
|
|
946
|
+
h.push('<div class="text-[10px] text-zinc-500 px-3 py-1.5 bg-zinc-900/50 flex items-center gap-1.5">');
|
|
947
|
+
h.push('<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0022.5 18.75V5.25A2.25 2.25 0 0020.25 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg>');
|
|
948
|
+
h.push('Nearest capture: '+esc(nearest.label||'screenshot')+'</div>');
|
|
949
|
+
h.push('<img src="'+nSrc+'" alt="'+esc(nearest.label||'Nearest screenshot')+'" loading="lazy" style="width:100%;display:block;max-height:200px;object-fit:cover;cursor:pointer" data-lb-src="'+esc(nSrc)+'" data-lb-label="'+esc(nearest.label||'')+'">');
|
|
950
|
+
h.push('</div>');
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
h.push('</div>');
|
|
954
|
+
});
|
|
955
|
+
h.push('</div>');
|
|
956
|
+
if (logs.length > 1) {
|
|
957
|
+
h.push('<div class="cl-dots" id="cl-dots" role="tablist" aria-label="Changelog navigation">');
|
|
958
|
+
logs.forEach((_, idx) => {
|
|
959
|
+
h.push('<button type="button" class="cl-dot'+(idx===0?' active':'')+'" data-idx="'+idx+'" onclick="clGoTo('+idx+')" role="tab" aria-selected="'+(idx===0?'true':'false')+'" aria-label="Go to change '+(idx+1)+'"></button>');
|
|
960
|
+
});
|
|
961
|
+
h.push('</div>');
|
|
962
|
+
}
|
|
963
|
+
h.push('</div>');
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// ── Screenshots Gallery (interactive) ────────────────────
|
|
967
|
+
if (shots && shots.length) {
|
|
968
|
+
window._ssAll = shots.map(ss => ({
|
|
969
|
+
src: ss.base64_thumbnail ? 'data:image/png;base64,'+ss.base64_thumbnail : '/api/screenshot/'+encodeURIComponent(ss.id)+'/image',
|
|
970
|
+
label: ss.label||'screenshot',
|
|
971
|
+
route: ss.route||'',
|
|
972
|
+
time: ss.created_at?.slice(5,16)||'',
|
|
973
|
+
}));
|
|
974
|
+
// Raw shot data for deferred card rendering on expand
|
|
975
|
+
window._ssRaw = shots.map((ss, idx) => ({ ...ss, _idx: idx }));
|
|
976
|
+
|
|
977
|
+
// Priority-scored category detection
|
|
978
|
+
const catMap = {};
|
|
979
|
+
shots.forEach((ss, idx) => {
|
|
980
|
+
const lbl = ss.label||'screenshot';
|
|
981
|
+
const cat = classifyScreenshot(lbl);
|
|
982
|
+
if (!catMap[cat]) catMap[cat] = [];
|
|
983
|
+
catMap[cat].push({ ...ss, _idx: idx });
|
|
984
|
+
});
|
|
985
|
+
const MIN_CAT_SIZE = 3;
|
|
986
|
+
const tinyKeys = Object.keys(catMap).filter(k => catMap[k].length < MIN_CAT_SIZE && k !== 'General');
|
|
987
|
+
if (tinyKeys.length > 1) {
|
|
988
|
+
if (!catMap['Other']) catMap['Other'] = [];
|
|
989
|
+
tinyKeys.forEach(k => { catMap['Other'].push(...catMap[k]); delete catMap[k]; });
|
|
990
|
+
}
|
|
991
|
+
const cats = Object.keys(catMap).sort((a,b) => catMap[b].length - catMap[a].length);
|
|
992
|
+
|
|
993
|
+
h.push(sec('Screenshots', shots.length+' captured image'+(shots.length!==1?'s':'')));
|
|
994
|
+
|
|
995
|
+
h.push('<div class="ss-toolbar" role="toolbar" aria-label="Screenshot controls">');
|
|
996
|
+
h.push('<div class="ss-search-wrap"><svg class="ss-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg><label for="ss-search" class="sr-only">Filter screenshots</label><input type="text" class="ss-search" id="ss-search" placeholder="Filter screenshots..." oninput="filterScreenshots()"></div>');
|
|
997
|
+
h.push('<div class="flex-1"></div>');
|
|
998
|
+
h.push('<button type="button" class="sz-btn" data-sz="sm" onclick="setGridSize('sm')" aria-label="Compact grid" aria-pressed="false"><svg viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="1" width="6" height="6" rx="1"/><rect x="9" y="1" width="6" height="6" rx="1"/><rect x="1" y="9" width="6" height="6" rx="1"/><rect x="9" y="9" width="6" height="6" rx="1"/></svg></button>');
|
|
999
|
+
h.push('<button type="button" class="sz-btn" data-sz="md" onclick="setGridSize('md')" aria-label="Medium grid" aria-pressed="true"><svg viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="1" width="6.5" height="6.5" rx="1.5"/><rect x="8.5" y="1" width="6.5" height="6.5" rx="1.5"/><rect x="1" y="8.5" width="6.5" height="6.5" rx="1.5"/><rect x="8.5" y="8.5" width="6.5" height="6.5" rx="1.5"/></svg></button>');
|
|
1000
|
+
h.push('<button type="button" class="sz-btn" data-sz="lg" onclick="setGridSize('lg')" aria-label="Large grid" aria-pressed="false"><svg viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="1" width="14" height="6.5" rx="1.5"/><rect x="1" y="8.5" width="14" height="6.5" rx="1.5"/></svg></button>');
|
|
1001
|
+
h.push('</div>');
|
|
1002
|
+
|
|
1003
|
+
h.push('<div class="flex flex-wrap gap-1.5 mb-4" id="ss-cat-bar" role="toolbar" aria-label="Category filter">');
|
|
1004
|
+
h.push('<button type="button" class="cat-pill" data-cat="all" onclick="filterCat('all')" aria-pressed="true">All<span class="cat-count">'+shots.length+'</span></button>');
|
|
1005
|
+
cats.forEach(cat => {
|
|
1006
|
+
h.push('<button type="button" class="cat-pill" data-cat="'+esc(cat)+'" onclick="filterCat(''+esc(cat)+'')" aria-pressed="false">'+esc(cat)+'<span class="cat-count">'+catMap[cat].length+'</span></button>');
|
|
1007
|
+
});
|
|
1008
|
+
h.push('</div>');
|
|
1009
|
+
|
|
1010
|
+
h.push('<div id="ss-gallery">');
|
|
1011
|
+
const INITIAL_SHOW = 8;
|
|
1012
|
+
cats.forEach(cat => {
|
|
1013
|
+
const items = catMap[cat];
|
|
1014
|
+
h.push('<div class="ss-group" data-cat="'+esc(cat)+'">');
|
|
1015
|
+
h.push('<div class="ss-group-hdr">'+esc(cat)+' <span class="g-count">('+items.length+')</span></div>');
|
|
1016
|
+
h.push('<div class="ss-grid sz-'+_gridSize+'">');
|
|
1017
|
+
// Render only visible cards; deferred cards rendered on expand
|
|
1018
|
+
const visibleCount = Math.min(items.length, INITIAL_SHOW);
|
|
1019
|
+
items.slice(0, visibleCount).forEach((ss, i) => {
|
|
1020
|
+
h.push(renderSsCard(ss));
|
|
1021
|
+
});
|
|
1022
|
+
h.push('</div>');
|
|
1023
|
+
if (items.length > INITIAL_SHOW) {
|
|
1024
|
+
h.push('<button type="button" class="ss-show-more" data-cat="'+esc(cat)+'" data-items=\\''+esc(JSON.stringify(items.slice(INITIAL_SHOW).map(x=>x._idx)))+'\\' onclick="toggleGroupExpand(this)">Show '+(items.length - INITIAL_SHOW)+' more</button>');
|
|
1025
|
+
}
|
|
1026
|
+
h.push('</div>');
|
|
1027
|
+
});
|
|
1028
|
+
h.push('</div>');
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// ── Generated Tests ──────────────────────────────────────
|
|
1032
|
+
if (tests.length) {
|
|
1033
|
+
h.push(sec('Generated Tests', 'Tests created automatically from findings'));
|
|
1034
|
+
tests.forEach(t => {
|
|
1035
|
+
h.push('<div class="ring-glow rounded-lg p-4 mb-2 bg-surface-1 fade-up">');
|
|
1036
|
+
h.push('<div class="flex items-center gap-2">');
|
|
1037
|
+
h.push('<span class="text-[10px] px-1.5 py-0.5 rounded bg-ok/10 text-ok font-medium">'+esc(t.test_framework)+'</span>');
|
|
1038
|
+
h.push('<span class="text-xs text-white font-medium">'+esc(t.description||'Automated tests')+'</span></div>');
|
|
1039
|
+
if (t.test_file_path) h.push('<span class="file-chip mt-1.5">'+esc(shortPath(t.test_file_path))+'</span>');
|
|
1040
|
+
if (t.test_code) {
|
|
1041
|
+
h.push('<details class="mt-2"><summary class="text-[11px] text-accent cursor-pointer select-none">View source</summary>');
|
|
1042
|
+
h.push('<pre class="mt-1.5 text-[11px] bg-surface-0 rounded-md p-3 text-zinc-400 max-h-64 overflow-auto leading-relaxed border border-border">'+esc(t.test_code)+'</pre></details>');
|
|
1043
|
+
}
|
|
1044
|
+
h.push('</div>');
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// ── Components (grouped by type) ─────────────────────────
|
|
1049
|
+
if (comps.length) {
|
|
1050
|
+
h.push(sec('Components', s.components+' found in this application'));
|
|
1051
|
+
const groups = {};
|
|
1052
|
+
comps.forEach(c => {
|
|
1053
|
+
const t = c.component_type || 'other';
|
|
1054
|
+
if(!groups[t]) groups[t] = [];
|
|
1055
|
+
groups[t].push(c);
|
|
1056
|
+
});
|
|
1057
|
+
const typeOrder = ['page','sidebar','header','hero','section','card','list','form','modal','panel','popup','nav','table','other'];
|
|
1058
|
+
const sortedTypes = Object.keys(groups).sort((a,b) => {
|
|
1059
|
+
const ai = typeOrder.indexOf(a), bi = typeOrder.indexOf(b);
|
|
1060
|
+
return (ai===-1?99:ai) - (bi===-1?99:bi);
|
|
1061
|
+
});
|
|
1062
|
+
sortedTypes.forEach(type => {
|
|
1063
|
+
const items = groups[type];
|
|
1064
|
+
h.push('<div class="mb-4 fade-up">');
|
|
1065
|
+
h.push('<div class="text-[10px] text-zinc-500 uppercase tracking-wider font-semibold mb-1.5">'+esc(type)+'s <span class="text-zinc-500 normal-case font-normal">('+items.length+')</span></div>');
|
|
1066
|
+
h.push('<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-1.5">');
|
|
1067
|
+
items.forEach(c => {
|
|
1068
|
+
const hasBugs = (c.bug_count||0) > 0;
|
|
1069
|
+
h.push('<div class="rounded-md px-3 py-2 bg-surface-1 border border-border hover:border-zinc-600 transition-colors'+(hasBugs?' border-l-2 border-l-err':'')+'">');
|
|
1070
|
+
h.push('<div class="text-xs font-medium text-zinc-200 truncate" title="'+esc(c.name)+'">'+esc(c.name)+'</div>');
|
|
1071
|
+
if (hasBugs) h.push('<div class="text-[10px] text-err mt-0.5">'+c.bug_count+' bug'+(c.bug_count>1?'s':'')+'</div>');
|
|
1072
|
+
h.push('</div>');
|
|
1073
|
+
});
|
|
1074
|
+
h.push('</div></div>');
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// ── Code Locations ───────────────────────────────────────
|
|
1079
|
+
if (locs.length) {
|
|
1080
|
+
h.push(sec('Files Reviewed', locs.length+' files visited during this session'));
|
|
1081
|
+
h.push('<details class="fade-up"><summary class="text-xs text-accent cursor-pointer select-none mb-2">Show '+locs.length+' files</summary>');
|
|
1082
|
+
h.push('<div class="space-y-1">');
|
|
1083
|
+
locs.forEach(l => {
|
|
1084
|
+
h.push('<div class="flex items-center gap-2 text-[11px] py-1.5 px-2.5 rounded bg-surface-1 border border-border">');
|
|
1085
|
+
h.push('<span class="file-chip" style="margin:0" title="'+esc(l.file_path)+'">'+esc(shortPath(l.file_path))+'</span>');
|
|
1086
|
+
if (l.line_start) h.push('<span class="text-zinc-500 text-[10px]">L'+l.line_start+(l.line_end&&l.line_end!==l.line_start?'-'+l.line_end:'')+'</span>');
|
|
1087
|
+
if (l.search_query) h.push('<span class="text-accent text-[10px] truncate max-w-[140px]" title="'+esc(l.search_query)+'">'+esc(l.search_query)+'</span>');
|
|
1088
|
+
h.push('</div>');
|
|
1089
|
+
});
|
|
1090
|
+
h.push('</div></details>');
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
$('root').innerHTML = h.join('');
|
|
1094
|
+
restoreGalleryState();
|
|
1095
|
+
initCarousel();
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function renderSsCard(ss) {
|
|
1099
|
+
const src = ss.base64_thumbnail ? 'data:image/png;base64,'+ss.base64_thumbnail : '/api/screenshot/'+encodeURIComponent(ss.id)+'/image';
|
|
1100
|
+
const lbl = esc(ss.label||'screenshot');
|
|
1101
|
+
const rt = ss.route ? ' - '+esc(ss.route) : '';
|
|
1102
|
+
return '<div class="ss-card ring-glow fade-up" data-ss-idx="'+ss._idx+'" data-ss-label="'+lbl.toLowerCase()+'" tabindex="0" role="button" aria-label="View screenshot: '+lbl+'">' +
|
|
1103
|
+
'<img src="'+src+'" alt="'+lbl+'" loading="lazy" onerror="this.style.display='none';this.nextElementSibling.insertAdjacentHTML('afterbegin','<div style=\\"padding:24px;text-align:center;color:#71717a;font-size:11px\\">Image unavailable</div>')">' +
|
|
1104
|
+
'<div class="ss-meta"><div class="text-[11px] text-zinc-300 truncate" title="'+lbl+'">'+lbl+'</div>' +
|
|
1105
|
+
'<div class="text-[10px] text-zinc-500">'+esc(ss.created_at?.slice(5,16)||'')+(rt?rt:'')+'</div>' +
|
|
1106
|
+
'</div></div>';
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function restoreGalleryState() {
|
|
1110
|
+
if (_gridSize !== 'md') setGridSize(_gridSize);
|
|
1111
|
+
if (_activeCat !== 'all') filterCat(_activeCat);
|
|
1112
|
+
_expandedGroups.forEach(cat => {
|
|
1113
|
+
const btn = document.querySelector('.ss-show-more[data-cat="'+cat+'"]');
|
|
1114
|
+
if (btn) toggleGroupExpand(btn);
|
|
1115
|
+
});
|
|
1116
|
+
if (_searchQuery) {
|
|
1117
|
+
const el = document.getElementById('ss-search');
|
|
1118
|
+
if (el) { el.value = _searchQuery; filterScreenshots(); }
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// ── Slideshow Lightbox ──────────────────────────────────────
|
|
1123
|
+
let _lbIdx = 0;
|
|
1124
|
+
let _lbVisible = [];
|
|
1125
|
+
|
|
1126
|
+
function buildLightbox() {
|
|
1127
|
+
let lb = document.getElementById('lightbox');
|
|
1128
|
+
if (lb) return lb;
|
|
1129
|
+
lb = document.createElement('div');
|
|
1130
|
+
lb.id = 'lightbox';
|
|
1131
|
+
lb.className = 'lightbox';
|
|
1132
|
+
lb.setAttribute('role', 'dialog');
|
|
1133
|
+
lb.setAttribute('aria-label', 'Screenshot viewer');
|
|
1134
|
+
lb.innerHTML =
|
|
1135
|
+
'<button type="button" class="lb-nav prev" id="lb-prev" aria-label="Previous screenshot">‹</button>' +
|
|
1136
|
+
'<img id="lb-img" src="" alt="Screenshot preview">' +
|
|
1137
|
+
'<button type="button" class="lb-nav next" id="lb-next" aria-label="Next screenshot">›</button>' +
|
|
1138
|
+
'<button type="button" class="lb-close" id="lb-close" aria-label="Close viewer">×</button>' +
|
|
1139
|
+
'<div class="lb-chrome"><span class="lb-label" id="lb-label"></span><span class="lb-counter" id="lb-counter"></span></div>';
|
|
1140
|
+
lb.addEventListener('click', e => {
|
|
1141
|
+
if (e.target === lb) closeLightbox();
|
|
1142
|
+
});
|
|
1143
|
+
lb.querySelector('#lb-close').onclick = closeLightbox;
|
|
1144
|
+
lb.querySelector('#lb-prev').onclick = e => { e.stopPropagation(); lbNav(-1); };
|
|
1145
|
+
lb.querySelector('#lb-next').onclick = e => { e.stopPropagation(); lbNav(1); };
|
|
1146
|
+
document.body.appendChild(lb);
|
|
1147
|
+
document.addEventListener('keydown', e => {
|
|
1148
|
+
const lbEl = document.getElementById('lightbox');
|
|
1149
|
+
if (!lbEl || !lbEl.classList.contains('open')) return;
|
|
1150
|
+
if (e.key === 'Escape') closeLightbox();
|
|
1151
|
+
else if (e.key === 'ArrowLeft') lbNav(-1);
|
|
1152
|
+
else if (e.key === 'ArrowRight') lbNav(1);
|
|
1153
|
+
});
|
|
1154
|
+
return lb;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function openLightbox(idx) {
|
|
1158
|
+
const lb = buildLightbox();
|
|
1159
|
+
_lbVisible = [];
|
|
1160
|
+
document.querySelectorAll('.ss-card:not([style*="display:none"]):not([style*="display: none"])').forEach(card => {
|
|
1161
|
+
const i = parseInt(card.dataset.ssIdx);
|
|
1162
|
+
if (!isNaN(i)) _lbVisible.push(i);
|
|
1163
|
+
});
|
|
1164
|
+
if (_lbVisible.length === 0 && window._ssAll) _lbVisible = window._ssAll.map((_, i) => i);
|
|
1165
|
+
_lbIdx = _lbVisible.indexOf(idx);
|
|
1166
|
+
if (_lbIdx === -1) _lbIdx = 0;
|
|
1167
|
+
renderLb();
|
|
1168
|
+
lb.classList.add('open');
|
|
1169
|
+
lb.querySelector('#lb-close').focus();
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function closeLightbox() {
|
|
1173
|
+
const lb = document.getElementById('lightbox');
|
|
1174
|
+
if (lb) lb.classList.remove('open');
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function lbNav(dir) {
|
|
1178
|
+
_lbIdx = (_lbIdx + dir + _lbVisible.length) % _lbVisible.length;
|
|
1179
|
+
renderLb();
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function renderLb() {
|
|
1183
|
+
if (!window._ssAll || _lbVisible.length === 0) return;
|
|
1184
|
+
const ss = window._ssAll[_lbVisible[_lbIdx]];
|
|
1185
|
+
if (!ss) return;
|
|
1186
|
+
const img = document.getElementById('lb-img');
|
|
1187
|
+
const lbl = document.getElementById('lb-label');
|
|
1188
|
+
const ctr = document.getElementById('lb-counter');
|
|
1189
|
+
img.src = ss.src;
|
|
1190
|
+
img.alt = ss.label + (ss.route ? ' \\u2014 ' + ss.route : '');
|
|
1191
|
+
lbl.textContent = ss.label + (ss.route ? ' \\u2014 ' + ss.route : '');
|
|
1192
|
+
ctr.textContent = (_lbIdx + 1) + ' / ' + _lbVisible.length;
|
|
1193
|
+
document.getElementById('lb-prev').style.display = _lbVisible.length > 1 ? '' : 'none';
|
|
1194
|
+
document.getElementById('lb-next').style.display = _lbVisible.length > 1 ? '' : 'none';
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Click delegation for screenshot cards
|
|
1198
|
+
document.addEventListener('click', e => {
|
|
1199
|
+
const card = e.target.closest('.ss-card[data-ss-idx]');
|
|
1200
|
+
if (card) {
|
|
1201
|
+
openLightbox(parseInt(card.dataset.ssIdx));
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const el = e.target.closest('[data-lb-src]');
|
|
1205
|
+
if (el) {
|
|
1206
|
+
const lb = buildLightbox();
|
|
1207
|
+
_lbVisible = [];
|
|
1208
|
+
window._ssAll = window._ssAll || [{ src: el.dataset.lbSrc, label: el.dataset.lbLabel||'', route: '', time: '' }];
|
|
1209
|
+
_lbVisible = [window._ssAll.length - 1];
|
|
1210
|
+
_lbIdx = 0;
|
|
1211
|
+
document.getElementById('lb-img').src = el.dataset.lbSrc;
|
|
1212
|
+
document.getElementById('lb-label').textContent = el.dataset.lbLabel || '';
|
|
1213
|
+
document.getElementById('lb-counter').textContent = '';
|
|
1214
|
+
document.getElementById('lb-prev').style.display = 'none';
|
|
1215
|
+
document.getElementById('lb-next').style.display = 'none';
|
|
1216
|
+
lb.classList.add('open');
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
// Enter/Space on screenshot cards + keyboard shortcuts
|
|
1221
|
+
document.addEventListener('keydown', e => {
|
|
1222
|
+
// Ctrl/Cmd+K: focus search
|
|
1223
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
1224
|
+
e.preventDefault();
|
|
1225
|
+
const search = document.getElementById('ss-search');
|
|
1226
|
+
if (search) { search.focus(); search.select(); }
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
1230
|
+
const card = e.target.closest('.ss-card[data-ss-idx]');
|
|
1231
|
+
if (card) { e.preventDefault(); openLightbox(parseInt(card.dataset.ssIdx)); }
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
// ── Gallery interaction functions ─────────────────────────────
|
|
1236
|
+
function filterScreenshots() {
|
|
1237
|
+
const q = (document.getElementById('ss-search')?.value || '').toLowerCase().trim();
|
|
1238
|
+
_searchQuery = q;
|
|
1239
|
+
document.querySelectorAll('.ss-card').forEach(card => {
|
|
1240
|
+
const lbl = card.dataset.ssLabel || '';
|
|
1241
|
+
const match = !q || lbl.includes(q);
|
|
1242
|
+
card.style.display = match ? '' : 'none';
|
|
1243
|
+
});
|
|
1244
|
+
if (q) {
|
|
1245
|
+
document.querySelectorAll('.ss-group').forEach(g => g.style.display = '');
|
|
1246
|
+
document.querySelectorAll('.ss-card[data-collapsed]').forEach(c => {
|
|
1247
|
+
if ((c.dataset.ssLabel||'').includes(q)) c.style.display = '';
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function filterCat(cat) {
|
|
1253
|
+
_activeCat = cat;
|
|
1254
|
+
_searchQuery = '';
|
|
1255
|
+
document.querySelectorAll('#ss-cat-bar .cat-pill').forEach(p => {
|
|
1256
|
+
const isActive = p.dataset.cat === cat;
|
|
1257
|
+
p.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
|
1258
|
+
});
|
|
1259
|
+
document.querySelectorAll('.ss-group').forEach(g => {
|
|
1260
|
+
g.style.display = (cat === 'all' || g.dataset.cat === cat) ? '' : 'none';
|
|
1261
|
+
});
|
|
1262
|
+
const searchEl = document.getElementById('ss-search');
|
|
1263
|
+
if (searchEl) searchEl.value = '';
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function setGridSize(sz) {
|
|
1267
|
+
_gridSize = sz;
|
|
1268
|
+
document.querySelectorAll('.sz-btn').forEach(b => {
|
|
1269
|
+
const isActive = b.dataset.sz === sz;
|
|
1270
|
+
b.classList.toggle('active', isActive);
|
|
1271
|
+
b.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
|
1272
|
+
});
|
|
1273
|
+
document.querySelectorAll('.ss-grid').forEach(g => {
|
|
1274
|
+
g.classList.remove('sz-sm', 'sz-md', 'sz-lg');
|
|
1275
|
+
g.classList.add('sz-' + sz);
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function toggleGroupExpand(btn) {
|
|
1280
|
+
const group = btn.closest('.ss-group');
|
|
1281
|
+
if (!group) return;
|
|
1282
|
+
const grid = group.querySelector('.ss-grid');
|
|
1283
|
+
if (!grid) return;
|
|
1284
|
+
|
|
1285
|
+
if (btn.dataset.expanded) {
|
|
1286
|
+
// Collapse: remove cards beyond initial 8
|
|
1287
|
+
const cards = grid.querySelectorAll('.ss-card');
|
|
1288
|
+
let count = 0;
|
|
1289
|
+
cards.forEach(c => {
|
|
1290
|
+
count++;
|
|
1291
|
+
if (count > 8) c.remove();
|
|
1292
|
+
});
|
|
1293
|
+
delete btn.dataset.expanded;
|
|
1294
|
+
_expandedGroups.delete(group.dataset.cat);
|
|
1295
|
+
const hiddenIndices = btn.dataset.items ? JSON.parse(btn.dataset.items) : [];
|
|
1296
|
+
btn.textContent = 'Show ' + hiddenIndices.length + ' more';
|
|
1297
|
+
} else {
|
|
1298
|
+
// Expand: render deferred cards into DOM
|
|
1299
|
+
const hiddenIndices = btn.dataset.items ? JSON.parse(btn.dataset.items) : [];
|
|
1300
|
+
if (window._ssAll && hiddenIndices.length) {
|
|
1301
|
+
const allShots = window._ssRaw || [];
|
|
1302
|
+
hiddenIndices.forEach(idx => {
|
|
1303
|
+
const ss = allShots[idx];
|
|
1304
|
+
if (ss) {
|
|
1305
|
+
grid.insertAdjacentHTML('beforeend', renderSsCard(ss));
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
btn.textContent = 'Show less';
|
|
1310
|
+
btn.dataset.expanded = '1';
|
|
1311
|
+
_expandedGroups.add(group.dataset.cat);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// ── Changelog Carousel ─────────────────────────────────────
|
|
1316
|
+
let _clObserver = null;
|
|
1317
|
+
|
|
1318
|
+
function initCarousel() {
|
|
1319
|
+
const track = document.getElementById('cl-track');
|
|
1320
|
+
if (!track) return;
|
|
1321
|
+
|
|
1322
|
+
// Attach scroll-based arrow + dot updates
|
|
1323
|
+
track.addEventListener('scroll', () => {
|
|
1324
|
+
clUpdateDots();
|
|
1325
|
+
clUpdateArrows();
|
|
1326
|
+
}, { passive: true });
|
|
1327
|
+
|
|
1328
|
+
// Keyboard navigation on carousel focus
|
|
1329
|
+
const carousel = document.getElementById('cl-carousel');
|
|
1330
|
+
if (carousel) {
|
|
1331
|
+
carousel.addEventListener('keydown', e => {
|
|
1332
|
+
if (e.key === 'ArrowLeft') { e.preventDefault(); clNav(-1); }
|
|
1333
|
+
else if (e.key === 'ArrowRight') { e.preventDefault(); clNav(1); }
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// IntersectionObserver for accurate dot tracking
|
|
1338
|
+
const dots = document.getElementById('cl-dots');
|
|
1339
|
+
if (dots && 'IntersectionObserver' in window) {
|
|
1340
|
+
_clObserver = new IntersectionObserver(entries => {
|
|
1341
|
+
entries.forEach(entry => {
|
|
1342
|
+
if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
|
|
1343
|
+
const cards = Array.from(track.querySelectorAll('.cl-card'));
|
|
1344
|
+
const idx = cards.indexOf(entry.target);
|
|
1345
|
+
if (idx >= 0) {
|
|
1346
|
+
dots.querySelectorAll('.cl-dot').forEach((d, i) => {
|
|
1347
|
+
const isActive = i === idx;
|
|
1348
|
+
d.classList.toggle('active', isActive);
|
|
1349
|
+
d.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
}, { root: track, threshold: 0.5 });
|
|
1355
|
+
track.querySelectorAll('.cl-card').forEach(card => _clObserver.observe(card));
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
clUpdateArrows();
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function clNav(dir) {
|
|
1362
|
+
const track = document.getElementById('cl-track');
|
|
1363
|
+
if (!track) return;
|
|
1364
|
+
const card = track.querySelector('.cl-card');
|
|
1365
|
+
if (!card) return;
|
|
1366
|
+
const gap = parseFloat(getComputedStyle(track).gap) || 16;
|
|
1367
|
+
const w = card.offsetWidth + gap;
|
|
1368
|
+
track.scrollBy({ left: dir * w, behavior: 'smooth' });
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function clGoTo(idx) {
|
|
1372
|
+
const track = document.getElementById('cl-track');
|
|
1373
|
+
if (!track) return;
|
|
1374
|
+
const card = track.querySelector('.cl-card');
|
|
1375
|
+
if (!card) return;
|
|
1376
|
+
const gap = parseFloat(getComputedStyle(track).gap) || 16;
|
|
1377
|
+
const w = card.offsetWidth + gap;
|
|
1378
|
+
track.scrollTo({ left: idx * w, behavior: 'smooth' });
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function clUpdateDots() {
|
|
1382
|
+
const track = document.getElementById('cl-track');
|
|
1383
|
+
const dots = document.getElementById('cl-dots');
|
|
1384
|
+
if (!track || !dots) return;
|
|
1385
|
+
// Fallback for browsers without IntersectionObserver
|
|
1386
|
+
if (!_clObserver) {
|
|
1387
|
+
const card = track.querySelector('.cl-card');
|
|
1388
|
+
if (!card) return;
|
|
1389
|
+
const gap = parseFloat(getComputedStyle(track).gap) || 16;
|
|
1390
|
+
const w = card.offsetWidth + gap;
|
|
1391
|
+
const idx = Math.round(track.scrollLeft / w);
|
|
1392
|
+
dots.querySelectorAll('.cl-dot').forEach((d, i) => {
|
|
1393
|
+
const isActive = i === idx;
|
|
1394
|
+
d.classList.toggle('active', isActive);
|
|
1395
|
+
d.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function clUpdateArrows() {
|
|
1401
|
+
const track = document.getElementById('cl-track');
|
|
1402
|
+
const prev = document.getElementById('cl-prev');
|
|
1403
|
+
const next = document.getElementById('cl-next');
|
|
1404
|
+
if (!track || !prev || !next) return;
|
|
1405
|
+
const atStart = track.scrollLeft <= 1;
|
|
1406
|
+
const atEnd = track.scrollLeft + track.clientWidth >= track.scrollWidth - 1;
|
|
1407
|
+
prev.classList.toggle('disabled', atStart);
|
|
1408
|
+
prev.setAttribute('aria-disabled', atStart ? 'true' : 'false');
|
|
1409
|
+
next.classList.toggle('disabled', atEnd);
|
|
1410
|
+
next.setAttribute('aria-disabled', atEnd ? 'true' : 'false');
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// ── Category Classification (priority scoring) ─────────────
|
|
1414
|
+
const CAT_RULES = [
|
|
1415
|
+
{ pattern: /^trace\\s+qa/i, cat: 'Quality Checks', priority: 10 },
|
|
1416
|
+
{ pattern: /^trace/i, cat: 'Activity', priority: 9 },
|
|
1417
|
+
{ pattern: /^mcp/i, cat: 'Activity', priority: 9 },
|
|
1418
|
+
{ pattern: /^benchmarks?/i, cat: 'Performance Tests', priority: 9 },
|
|
1419
|
+
{ pattern: /^page\\s+index/i, cat: 'Pages', priority: 10 },
|
|
1420
|
+
{ pattern: /^redesign/i, cat: 'Redesigns', priority: 8 },
|
|
1421
|
+
{ pattern: /^final/i, cat: 'Final Results', priority: 8 },
|
|
1422
|
+
{ pattern: /landing|home|signin|main\\s+page|navigation/i, cat: 'Navigation', priority: 5 },
|
|
1423
|
+
{ pattern: /fast\\s+agent|agent/i, cat: 'Assistant', priority: 5 },
|
|
1424
|
+
];
|
|
1425
|
+
|
|
1426
|
+
function classifyScreenshot(label) {
|
|
1427
|
+
let bestCat = 'General', bestPriority = -1;
|
|
1428
|
+
for (const rule of CAT_RULES) {
|
|
1429
|
+
if (rule.pattern.test(label) && rule.priority > bestPriority) {
|
|
1430
|
+
bestCat = rule.cat;
|
|
1431
|
+
bestPriority = rule.priority;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return bestCat;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// ── Helpers ─────────────────────────────────────────────────
|
|
1438
|
+
const SEC_ICONS = {
|
|
1439
|
+
'Bugs & Fixes': ['#451a03','#fbbf24','<path d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/>'],
|
|
1440
|
+
'Code Review': ['#1e1b4b','#818cf8','<path d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"/>'],
|
|
1441
|
+
'Screenshots': ['#052e16','#34d399','<path d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0022.5 18.75V5.25A2.25 2.25 0 0020.25 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/>'],
|
|
1442
|
+
'Changelog': ['#172554','#60a5fa','<path d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"/>'],
|
|
1443
|
+
'Generated Tests': ['#14532d','#4ade80','<path d="M4.5 12.75l6 6 9-13.5"/>'],
|
|
1444
|
+
'Components': ['#3b0764','#c084fc','<path d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9"/>'],
|
|
1445
|
+
'Files Reviewed': ['#1c1917','#a8a29e','<path d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/>'],
|
|
1446
|
+
};
|
|
1447
|
+
function sec(title, subtitle) {
|
|
1448
|
+
const icon = SEC_ICONS[title];
|
|
1449
|
+
const parts = ['<div class="sec-hdr">'];
|
|
1450
|
+
if (icon) {
|
|
1451
|
+
parts.push('<div class="sec-icon" style="background:'+icon[0]+';color:'+icon[1]+'" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">'+icon[2]+'</svg></div>');
|
|
1452
|
+
}
|
|
1453
|
+
parts.push('<div class="sec-text"><h2>'+title+'</h2>');
|
|
1454
|
+
if (subtitle) parts.push('<div class="sec-sub">'+subtitle+'</div>');
|
|
1455
|
+
parts.push('</div></div>');
|
|
1456
|
+
return parts.join('');
|
|
1457
|
+
}
|
|
1458
|
+
function sevBadge(sev) {
|
|
1459
|
+
const c = {critical:'sev-critical',high:'sev-high',medium:'sev-medium',low:'sev-low'}[sev]||'sev-medium';
|
|
1460
|
+
return '<span class="text-[10px] px-1.5 py-0.5 rounded font-medium shrink-0 '+c+'">'+sev+'</span>';
|
|
1461
|
+
}
|
|
1462
|
+
function statusBadge(st) {
|
|
1463
|
+
if(st==='resolved'||st==='fixed') return '<span class="text-[10px] px-1.5 py-0.5 rounded bg-ok/10 text-ok shrink-0">fixed</span>';
|
|
1464
|
+
if(st==='open') return '<span class="text-[10px] px-1.5 py-0.5 rounded bg-err/10 text-err shrink-0">open</span>';
|
|
1465
|
+
return '';
|
|
1466
|
+
}
|
|
1467
|
+
function shortPath(p) {
|
|
1468
|
+
if(!p) return '';
|
|
1469
|
+
const parts = p.replace(/\\\\/g,'/').split('/');
|
|
1470
|
+
return parts.length>3 ? '\\u2026/'+parts.slice(-3).join('/') : p;
|
|
1471
|
+
}
|
|
1472
|
+
function fileChips(raw) {
|
|
1473
|
+
if(!raw) return '';
|
|
1474
|
+
let files = [];
|
|
1475
|
+
try { files = JSON.parse(raw); } catch { files = [raw]; }
|
|
1476
|
+
if(!Array.isArray(files)) files = [String(files)];
|
|
1477
|
+
return files.map(f => '<span class="file-chip" title="'+esc(f)+'">'+esc(shortPath(f))+'</span>').join('');
|
|
1478
|
+
}
|
|
1479
|
+
function truncWords(s, max) {
|
|
1480
|
+
if(!s || s.length<=max) return s||'';
|
|
1481
|
+
const cut = s.lastIndexOf(' ', max);
|
|
1482
|
+
return s.slice(0, cut>0?cut:max) + '\\u2026';
|
|
1483
|
+
}
|
|
1484
|
+
function esc(s) { return s==null?'':String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
1485
|
+
|
|
1486
|
+
init();
|
|
1487
|
+
</script>
|
|
1488
|
+
</body>
|
|
1249
1489
|
</html>`;
|
|
1250
1490
|
}
|
|
1251
1491
|
//# sourceMappingURL=html.js.map
|