claude-session-insights 0.3.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.
@@ -0,0 +1,1560 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>claude-session-insights</title>
7
+ <style>
8
+ :root {
9
+ --radius: 10px;
10
+ --mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
11
+ --shadow-card: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.06);
12
+ --shadow-tooltip: 0 4px 12px rgba(0,0,0,0.1);
13
+ color-scheme: light;
14
+ }
15
+
16
+ /* Light theme (default) */
17
+ :root, [data-theme="light"] {
18
+ --bg: #f5f6f8;
19
+ --surface: #ffffff;
20
+ --surface2: #f0f1f4;
21
+ --surface3: #e4e6eb;
22
+ --border: rgba(0,0,0,0.08);
23
+ --border-hover: rgba(0,0,0,0.16);
24
+ --text: #1a1d23;
25
+ --text2: #5c6370;
26
+ --text3: #8b919a;
27
+ --accent: #2563eb;
28
+ --accent-dim: rgba(37,99,235,0.1);
29
+ --green: #16a34a;
30
+ --green-dim: rgba(22,163,74,0.1);
31
+ --yellow: #ca8a04;
32
+ --yellow-dim: rgba(202,138,4,0.1);
33
+ --red: #dc2626;
34
+ --red-dim: rgba(220,38,38,0.1);
35
+ --purple: #7c3aed;
36
+ --purple-dim: rgba(124,58,237,0.1);
37
+ --chart-score-label: #ffffff;
38
+ --chart-grid: rgba(0,0,0,0.06);
39
+ --chart-axis: #8b919a;
40
+ }
41
+
42
+ /* Dark theme */
43
+ [data-theme="dark"] {
44
+ --bg: #0c0f14;
45
+ --surface: #14181f;
46
+ --surface2: #1c2029;
47
+ --surface3: #242830;
48
+ --border: rgba(255,255,255,0.08);
49
+ --border-hover: rgba(255,255,255,0.15);
50
+ --text: #d8dce4;
51
+ --text2: #8b919a;
52
+ --text3: #5c6370;
53
+ --accent: #6ea4f7;
54
+ --accent-dim: rgba(110,164,247,0.14);
55
+ --green: #56d364;
56
+ --green-dim: rgba(86,211,100,0.14);
57
+ --yellow: #e3b341;
58
+ --yellow-dim: rgba(227,179,65,0.14);
59
+ --red: #f47067;
60
+ --red-dim: rgba(244,112,103,0.14);
61
+ --purple: #d2a8ff;
62
+ --purple-dim: rgba(210,168,255,0.14);
63
+ --shadow-card: 0 1px 3px rgba(0,0,0,0.2);
64
+ --shadow-tooltip: 0 8px 24px rgba(0,0,0,0.5);
65
+ --chart-score-label: #0c0f14;
66
+ --chart-grid: rgba(255,255,255,0.05);
67
+ --chart-axis: #5c6370;
68
+ color-scheme: dark;
69
+ }
70
+
71
+ @media (prefers-color-scheme: dark) {
72
+ :root:not([data-theme="light"]) {
73
+ --bg: #0c0f14;
74
+ --surface: #14181f;
75
+ --surface2: #1c2029;
76
+ --surface3: #242830;
77
+ --border: rgba(255,255,255,0.08);
78
+ --border-hover: rgba(255,255,255,0.15);
79
+ --text: #d8dce4;
80
+ --text2: #8b919a;
81
+ --text3: #5c6370;
82
+ --accent: #6ea4f7;
83
+ --accent-dim: rgba(110,164,247,0.14);
84
+ --green: #56d364;
85
+ --green-dim: rgba(86,211,100,0.14);
86
+ --yellow: #e3b341;
87
+ --yellow-dim: rgba(227,179,65,0.14);
88
+ --red: #f47067;
89
+ --red-dim: rgba(244,112,103,0.14);
90
+ --purple: #d2a8ff;
91
+ --purple-dim: rgba(210,168,255,0.14);
92
+ --shadow-card: 0 1px 3px rgba(0,0,0,0.2);
93
+ --shadow-tooltip: 0 8px 24px rgba(0,0,0,0.5);
94
+ --chart-score-label: #0c0f14;
95
+ --chart-grid: rgba(255,255,255,0.05);
96
+ --chart-axis: #5c6370;
97
+ color-scheme: dark;
98
+ }
99
+ }
100
+ * { margin: 0; padding: 0; box-sizing: border-box; }
101
+ body {
102
+ font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
103
+ background: var(--bg); color: var(--text);
104
+ line-height: 1.55; padding: 28px 32px; max-width: 1360px; margin: 0 auto;
105
+ -webkit-font-smoothing: antialiased;
106
+ }
107
+
108
+ /* Header */
109
+ header {
110
+ display: flex; justify-content: space-between; align-items: center;
111
+ margin-bottom: 36px; padding-bottom: 20px;
112
+ border-bottom: 1px solid var(--border);
113
+ }
114
+ header h1 {
115
+ font-size: 17px; font-weight: 600; letter-spacing: -0.3px;
116
+ display: flex; align-items: center; gap: 10px;
117
+ }
118
+ header h1 span {
119
+ font-family: var(--mono); font-size: 11px; font-weight: 500;
120
+ color: var(--text2); background: var(--surface2);
121
+ padding: 2px 8px; border-radius: 5px;
122
+ }
123
+ .header-actions { display: flex; align-items: center; gap: 8px; }
124
+ .refresh-btn {
125
+ background: var(--surface2); border: 1px solid var(--border); color: var(--text2);
126
+ width: 34px; height: 34px; border-radius: 7px; cursor: pointer;
127
+ display: flex; align-items: center; justify-content: center;
128
+ transition: all 0.2s; font-size: 15px; line-height: 1; padding: 0;
129
+ }
130
+ .refresh-btn:hover {
131
+ background: var(--surface3); color: var(--text); border-color: var(--border-hover);
132
+ }
133
+ .refresh-btn.spinning svg { animation: spin 0.8s linear infinite; }
134
+ @keyframes spin { to { transform: rotate(360deg); } }
135
+ .auto-refresh-toggle {
136
+ display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--text3);
137
+ cursor: pointer; user-select: none; width: 155px; flex-shrink: 0;
138
+ }
139
+ #auto-refresh-label { font-variant-numeric: tabular-nums; }
140
+ .auto-refresh-toggle input { display: none; }
141
+ .auto-refresh-track {
142
+ width: 28px; height: 16px; border-radius: 8px; background: var(--surface3);
143
+ border: 1px solid var(--border); position: relative; transition: all 0.2s;
144
+ }
145
+ .auto-refresh-track::after {
146
+ content: ''; position: absolute; top: 2px; left: 2px;
147
+ width: 10px; height: 10px; border-radius: 50%; background: var(--text3);
148
+ transition: all 0.2s;
149
+ }
150
+ .auto-refresh-toggle input:checked + .auto-refresh-track {
151
+ background: var(--accent-dim); border-color: var(--accent);
152
+ }
153
+ .auto-refresh-toggle input:checked + .auto-refresh-track::after {
154
+ left: 14px; background: var(--accent);
155
+ }
156
+ .theme-toggle {
157
+ background: var(--surface2); border: 1px solid var(--border); color: var(--text2);
158
+ width: 34px; height: 34px; border-radius: 7px; cursor: pointer;
159
+ display: flex; align-items: center; justify-content: center;
160
+ transition: all 0.2s; font-size: 16px; line-height: 1; padding: 0;
161
+ }
162
+ .theme-toggle:hover {
163
+ background: var(--surface3); color: var(--text); border-color: var(--border-hover);
164
+ }
165
+
166
+ /* Hero: two-column layout */
167
+ .hero { display: flex; gap: 20px; margin-bottom: 28px; align-items: stretch; }
168
+ .hero-left { display: flex; flex-direction: column; gap: 14px; width: 340px; flex-shrink: 0; }
169
+ .hero-right { flex: 1; min-width: 0; }
170
+ @media (max-width: 720px) {
171
+ .hero { flex-direction: column; }
172
+ .hero-left { width: auto; min-width: 0; }
173
+ }
174
+ .score-card {
175
+ background: var(--surface); border: 1px solid var(--border);
176
+ border-radius: var(--radius); padding: 28px 36px; text-align: center;
177
+ position: relative; overflow: hidden; box-shadow: var(--shadow-card);
178
+ }
179
+ .score-card::before {
180
+ content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
181
+ background: linear-gradient(90deg, var(--green), var(--accent), var(--purple));
182
+ opacity: 0.7;
183
+ }
184
+ .score-card .score {
185
+ font-family: var(--mono); font-size: 56px; font-weight: 700; line-height: 1;
186
+ color: var(--text); letter-spacing: -2px;
187
+ }
188
+ .score-card .label {
189
+ color: var(--text2); font-size: 12px; margin-top: 6px; font-weight: 500;
190
+ }
191
+ .badges-card {
192
+ background: var(--surface); border: 1px solid var(--border);
193
+ border-radius: var(--radius); padding: 18px 22px; flex: 1;
194
+ overflow: visible; position: relative; z-index: 5; box-shadow: var(--shadow-card);
195
+ }
196
+ .badges-card h3 {
197
+ font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px;
198
+ color: var(--text2); margin-bottom: 12px; font-weight: 600;
199
+ }
200
+ .badges-grid { display: flex; flex-wrap: wrap; gap: 6px; }
201
+ .badge {
202
+ display: flex; align-items: center; gap: 8px; position: relative;
203
+ background: var(--surface2); border: 1px solid var(--border);
204
+ border-radius: 8px; padding: 7px 12px 7px 8px; cursor: default; transition: all 0.15s;
205
+ }
206
+ .badge:hover { border-color: var(--border-hover); background: var(--surface3); }
207
+ .badge-icon {
208
+ width: 24px; height: 24px; border-radius: 5px;
209
+ display: flex; align-items: center; justify-content: center;
210
+ flex-shrink: 0;
211
+ }
212
+ .badge-icon.surgical-prompter { background: var(--accent-dim); color: var(--accent); }
213
+ .badge-icon.cache-whisperer { background: var(--green-dim); color: var(--green); }
214
+ .badge-icon.clean-slate { background: var(--yellow-dim); color: var(--yellow); }
215
+ .badge-icon.model-sniper { background: var(--purple-dim); color: var(--purple); }
216
+ .badge-icon.efficiency-diamond { background: var(--green-dim); color: var(--green); }
217
+ .badge.negative .badge-icon { background: var(--red-dim, rgba(239,68,68,0.15)); color: var(--red, #ef4444); }
218
+ .badge.negative .badge-name { color: var(--red, #ef4444); }
219
+ .badge-name { font-size: 12px; font-weight: 600; color: var(--text); white-space: nowrap; }
220
+ .badge-tip {
221
+ display: none; position: absolute; top: calc(100% + 6px); left: 0;
222
+ background: var(--surface3); border: 1px solid var(--border-hover);
223
+ border-radius: 8px; padding: 10px 14px; font-size: 12px; color: var(--text2);
224
+ width: 280px; line-height: 1.5; z-index: 20; pointer-events: none;
225
+ box-shadow: var(--shadow-tooltip); text-align: left;
226
+ }
227
+ .badge:hover .badge-tip { display: block; }
228
+
229
+ /* Stats grid */
230
+ .stats {
231
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
232
+ gap: 14px; margin-bottom: 32px;
233
+ }
234
+ .stat {
235
+ background: var(--surface); border: 1px solid var(--border);
236
+ border-radius: var(--radius); padding: 18px 20px; box-shadow: var(--shadow-card);
237
+ }
238
+ .stat .value {
239
+ font-family: var(--mono); font-size: 22px; font-weight: 600;
240
+ letter-spacing: -0.5px;
241
+ }
242
+ .stat .label { font-size: 11px; color: var(--text2); margin-top: 2px; font-weight: 500; }
243
+
244
+ /* Sections */
245
+ section { margin-bottom: 32px; }
246
+ section h2 {
247
+ font-size: 13px; font-weight: 600; margin-bottom: 14px;
248
+ text-transform: uppercase; letter-spacing: 0.6px; color: var(--text2);
249
+ text-align: left;
250
+ }
251
+
252
+ /* Chart */
253
+ .chart-container {
254
+ background: var(--surface); border: 1px solid var(--border);
255
+ border-radius: var(--radius); padding: 20px; overflow-x: auto; box-shadow: var(--shadow-card);
256
+ }
257
+ canvas { width: 100%; height: 200px; }
258
+
259
+ /* Tables */
260
+ .table-wrap { overflow-x: auto; }
261
+ table {
262
+ width: 100%; border-collapse: separate; border-spacing: 0;
263
+ font-size: 12px; table-layout: fixed;
264
+ }
265
+ th {
266
+ text-align: left; padding: 10px 10px; color: var(--text3); font-weight: 600;
267
+ font-size: 10px; text-transform: uppercase; letter-spacing: 0.6px;
268
+ border-bottom: 1px solid var(--border); cursor: pointer; white-space: nowrap;
269
+ user-select: none;
270
+ }
271
+ th:hover { color: var(--text2); }
272
+ th .sort-arrow { font-size: 9px; margin-left: 2px; }
273
+ td {
274
+ padding: 12px 10px; border-bottom: 1px solid var(--border); overflow: hidden;
275
+ text-overflow: ellipsis; white-space: nowrap; vertical-align: top;
276
+ font-size: 12px;
277
+ }
278
+ td.multi-line { white-space: normal; text-align: left; }
279
+ td.center { text-align: center; }
280
+ th.center { text-align: center; }
281
+ .td-sub { color: var(--text3); font-size: 10px; margin-top: 2px; font-family: var(--mono); }
282
+ tr.clickable { cursor: pointer; transition: background 0.1s; }
283
+ tr.clickable:hover td { background: var(--surface); }
284
+ .session-title {
285
+ color: var(--text2); font-size: 12px; padding-left: 2px;
286
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
287
+ overflow: hidden; white-space: normal; line-height: 1.5; text-align: left;
288
+ }
289
+
290
+ /* Column widths */
291
+ .col-date { width: 11%; } .col-prompt { width: 22%; }
292
+ .col-model { width: 8%; } .col-suggested { width: 8%; }
293
+ .col-score { width: 6%; } .col-msgs { width: 6%; }
294
+ .col-tokens { width: 9%; } .col-tools { width: 6%; } .col-cache { width: 7%; }
295
+ .col-cost { width: 8%; }
296
+
297
+ /* Model tags */
298
+ .model-tag {
299
+ display: inline-block; padding: 3px 9px; border-radius: 5px;
300
+ font-family: var(--mono); font-size: 10px; font-weight: 600;
301
+ white-space: nowrap; letter-spacing: -0.2px;
302
+ }
303
+ .model-opus { background: var(--purple-dim); color: var(--purple); }
304
+ .model-sonnet { background: var(--green-dim); color: var(--green); }
305
+ .model-haiku { background: var(--yellow-dim); color: var(--yellow); }
306
+
307
+ /* Score pills */
308
+ .score-pill {
309
+ display: inline-block; padding: 2px 10px; border-radius: 5px;
310
+ font-family: var(--mono); font-size: 11px; font-weight: 700;
311
+ }
312
+ .score-high { background: var(--green-dim); color: var(--green); }
313
+ .score-mid { background: var(--yellow-dim); color: var(--yellow); }
314
+ .score-low { background: var(--red-dim); color: var(--red); }
315
+
316
+ #app { text-align: left; }
317
+
318
+ /* Loading */
319
+ .loading { text-align: center; padding: 80px; color: var(--text3); font-size: 13px; }
320
+
321
+ /* Score breakdown */
322
+ .score-info-toggle {
323
+ background: none; border: none; color: var(--text3); cursor: pointer;
324
+ font-size: 11px; margin-top: 10px; padding: 0; transition: color 0.15s;
325
+ }
326
+ .score-info-toggle:hover { color: var(--accent); }
327
+ .score-breakdown-overlay {
328
+ display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.4);
329
+ z-index: 1000; align-items: center; justify-content: center;
330
+ backdrop-filter: blur(2px);
331
+ }
332
+ .score-breakdown-overlay.open { display: flex; }
333
+ .score-breakdown {
334
+ background: var(--surface); border: 1px solid var(--border);
335
+ border-radius: var(--radius); padding: 22px 26px; max-width: 540px; width: 90%;
336
+ box-shadow: 0 8px 32px rgba(0,0,0,0.18); position: relative;
337
+ }
338
+ .score-breakdown-close {
339
+ position: absolute; top: 12px; right: 14px; background: none; border: none;
340
+ color: var(--text3); cursor: pointer; font-size: 18px; line-height: 1; padding: 4px;
341
+ }
342
+ .score-breakdown-close:hover { color: var(--text); }
343
+ .score-breakdown h3 {
344
+ font-size: 12px; text-transform: uppercase; letter-spacing: 0.6px;
345
+ color: var(--text2); margin-bottom: 14px; font-weight: 600;
346
+ }
347
+ .score-breakdown p {
348
+ font-size: 13px; color: var(--text2); margin-bottom: 16px; line-height: 1.7;
349
+ }
350
+ .dim-explain { display: flex; flex-direction: column; gap: 10px; }
351
+ .dim-explain-row {
352
+ display: flex; align-items: baseline; gap: 14px; font-size: 13px;
353
+ }
354
+ .dim-explain-weight {
355
+ min-width: 36px; text-align: right; color: var(--accent);
356
+ font-family: var(--mono); font-weight: 600; font-size: 11px;
357
+ }
358
+ .dim-explain-name { font-weight: 600; color: var(--text); min-width: 140px; }
359
+ .dim-explain-desc { color: var(--text2); }
360
+
361
+ /* Summary cards */
362
+ .summary-card {
363
+ background: var(--surface); border: 1px solid var(--border);
364
+ border-radius: var(--radius); padding: 24px 28px; margin-bottom: 24px;
365
+ height: 100%; box-shadow: var(--shadow-card);
366
+ }
367
+ .summary-card.standalone { height: auto; }
368
+ .summary-card h3 {
369
+ font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px;
370
+ color: var(--text2); margin-bottom: 14px; font-weight: 600; text-align: left;
371
+ display: flex; align-items: center; gap: 7px;
372
+ }
373
+ .help-icon {
374
+ display: inline-flex; align-items: center; justify-content: center;
375
+ width: 15px; height: 15px; border-radius: 50%;
376
+ background: var(--surface3); border: 1px solid var(--border);
377
+ color: var(--text3); font-size: 9px; font-weight: 700;
378
+ cursor: default; text-transform: none; letter-spacing: 0;
379
+ position: relative; flex-shrink: 0;
380
+ }
381
+ .help-icon .help-tooltip {
382
+ display: none; position: absolute; top: calc(100% + 6px); left: 50%;
383
+ transform: translateX(-50%); width: 230px; padding: 10px 12px;
384
+ background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
385
+ box-shadow: var(--shadow-tooltip);
386
+ font-size: 11px; line-height: 1.6; color: var(--text2);
387
+ font-weight: 400; text-transform: none; letter-spacing: 0; z-index: 200;
388
+ white-space: normal; pointer-events: none;
389
+ }
390
+ .help-icon:hover .help-tooltip { display: block; }
391
+ .help-tooltip a { color: var(--purple); text-decoration: none; cursor: pointer; pointer-events: all; }
392
+ .help-tooltip a:hover { text-decoration: underline; }
393
+ .summary-paragraphs p {
394
+ font-size: 13px; line-height: 1.75; color: var(--text); margin-bottom: 10px;
395
+ text-align: left;
396
+ }
397
+ .summary-recommendations {
398
+ margin-top: 18px; padding-top: 18px; border-top: 1px solid var(--border);
399
+ }
400
+ .summary-recommendations h4 {
401
+ font-size: 10px; text-transform: uppercase; letter-spacing: 0.8px;
402
+ color: var(--accent); margin-bottom: 12px; font-weight: 600; text-align: left;
403
+ }
404
+ .summary-recommendations li {
405
+ font-size: 13px; color: var(--text); margin-bottom: 8px;
406
+ padding-left: 18px; position: relative; list-style: none; line-height: 1.6;
407
+ text-align: left;
408
+ }
409
+ .summary-recommendations li::before {
410
+ content: "->"; position: absolute; left: 0; color: var(--accent);
411
+ font-family: var(--mono); font-size: 11px;
412
+ }
413
+ .session-summary {
414
+ font-size: 13px; line-height: 1.75; color: var(--text); text-align: left;
415
+ padding: 18px 22px; background: var(--surface); border-radius: var(--radius);
416
+ margin-bottom: 24px;
417
+ border: 1px solid var(--border); border-left: 3px solid var(--accent);
418
+ }
419
+
420
+ /* Suggested model */
421
+ .suggested-model-card {
422
+ display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
423
+ background: var(--surface); border: 1px solid var(--purple-dim);
424
+ border-radius: var(--radius); padding: 14px 22px; margin-bottom: 24px;
425
+ }
426
+ .suggested-model-label {
427
+ font-size: 10px; text-transform: uppercase; letter-spacing: 0.8px;
428
+ color: var(--purple); font-weight: 700;
429
+ }
430
+ .suggested-model-value .model-tag { font-size: 12px; padding: 4px 12px; }
431
+ .suggested-model-hint { font-size: 12px; color: var(--text2); }
432
+
433
+ /* Session detail */
434
+ .back-btn {
435
+ background: none; border: none; color: var(--accent); cursor: pointer;
436
+ font-size: 13px; padding: 0; margin-bottom: 22px; display: inline-block;
437
+ font-weight: 500; transition: opacity 0.15s;
438
+ }
439
+ .back-btn:hover { opacity: 0.75; }
440
+ .session-header {
441
+ background: var(--surface); border: 1px solid var(--border);
442
+ border-radius: var(--radius); padding: 24px 28px; margin-bottom: 24px;
443
+ text-align: left; box-shadow: var(--shadow-card);
444
+ }
445
+ .session-header h2 {
446
+ margin-bottom: 10px; font-size: 17px; font-weight: 600; letter-spacing: -0.3px;
447
+ text-align: left;
448
+ }
449
+ .session-meta {
450
+ display: flex; gap: 16px; flex-wrap: wrap; color: var(--text2); font-size: 12px;
451
+ }
452
+ .session-meta span { display: flex; align-items: center; gap: 5px; }
453
+
454
+ /* Dimension grid */
455
+ .dim-grid {
456
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
457
+ gap: 12px; margin-bottom: 24px;
458
+ }
459
+ .dim-item {
460
+ background: var(--surface); border: 1px solid var(--border);
461
+ border-radius: var(--radius); padding: 16px 18px; box-shadow: var(--shadow-card);
462
+ }
463
+ .dim-item .dim-label { font-size: 11px; color: var(--text2); margin-bottom: 4px; font-weight: 500; }
464
+ .dim-item .dim-score { font-family: var(--mono); font-size: 20px; font-weight: 700; }
465
+ .dim-bar { height: 3px; border-radius: 2px; background: var(--surface3); margin-top: 8px; }
466
+ .dim-bar-fill { height: 100%; border-radius: 2px; transition: width 0.4s ease; }
467
+
468
+ /* Turn list */
469
+ .turn-list { display: flex; flex-direction: column; gap: 6px; }
470
+ .turn {
471
+ background: var(--surface); border: 1px solid var(--border);
472
+ border-radius: var(--radius); padding: 14px 18px;
473
+ }
474
+ .turn-header {
475
+ display: flex; justify-content: space-between; align-items: center;
476
+ margin-bottom: 6px; font-size: 12px;
477
+ }
478
+ .turn-role {
479
+ font-family: var(--mono); font-weight: 700; text-transform: uppercase;
480
+ font-size: 10px; letter-spacing: 0.6px; padding: 2px 8px; border-radius: 4px;
481
+ }
482
+ .role-user { background: var(--accent-dim); color: var(--accent); }
483
+ .role-assistant { background: var(--green-dim); color: var(--green); }
484
+ .turn-meta { color: var(--text3); display: flex; gap: 12px; align-items: center; font-family: var(--mono); font-size: 11px; }
485
+ .turn-text {
486
+ font-size: 13px; color: var(--text); line-height: 1.65;
487
+ white-space: pre-wrap; word-break: break-word;
488
+ }
489
+ .turn-tools { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }
490
+ .tool-tag {
491
+ font-family: var(--mono); font-size: 10px; padding: 2px 8px; border-radius: 4px;
492
+ background: var(--surface2); color: var(--text3); border: 1px solid var(--border);
493
+ }
494
+ .turn-tokens {
495
+ display: flex; gap: 14px; margin-top: 8px;
496
+ font-family: var(--mono); font-size: 10px; color: var(--text3);
497
+ }
498
+ .cost-chart-container {
499
+ background: var(--surface); border: 1px solid var(--border);
500
+ border-radius: var(--radius); padding: 20px; margin-bottom: 24px;
501
+ box-shadow: var(--shadow-card);
502
+ }
503
+
504
+ /* Tip styles (kept for compatibility) */
505
+ .tip {
506
+ background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
507
+ padding: 14px 18px; margin-bottom: 8px; font-size: 13px;
508
+ }
509
+ .tip .tip-type {
510
+ font-size: 10px; text-transform: uppercase; letter-spacing: 0.6px;
511
+ color: var(--yellow); margin-bottom: 4px; font-weight: 600;
512
+ }
513
+ .tip .tip-project { color: var(--text3); font-size: 11px; margin-top: 4px; }
514
+
515
+ /* AI Insights */
516
+ .ai-insights-card {
517
+ background: var(--surface); border: 1px solid var(--border);
518
+ border-radius: var(--radius); padding: 24px 28px; margin-bottom: 24px;
519
+ position: relative; overflow: visible; box-shadow: var(--shadow-card);
520
+ }
521
+ .ai-insights-card::before {
522
+ content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
523
+ background: linear-gradient(90deg, var(--purple), var(--accent));
524
+ opacity: 0.7; border-radius: var(--radius) var(--radius) 0 0;
525
+ }
526
+ .ai-insights-card h3 {
527
+ font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px;
528
+ color: var(--purple); margin-bottom: 14px; font-weight: 600;
529
+ display: flex; align-items: center; gap: 8px;
530
+ }
531
+ .ai-insights-content { font-size: 13px; line-height: 1.75; color: var(--text); }
532
+ .ai-insights-content h2 {
533
+ font-size: 13px; font-weight: 700; color: var(--text); margin: 16px 0 8px 0;
534
+ text-transform: none; letter-spacing: normal;
535
+ }
536
+ .ai-insights-content p { margin-bottom: 8px; }
537
+ .ai-insights-content ul { padding-left: 18px; margin-bottom: 8px; }
538
+ .ai-insights-content li { margin-bottom: 4px; list-style: disc; }
539
+ .ai-insights-content strong { color: var(--text); }
540
+ .ai-insights-content code {
541
+ font-family: var(--mono); font-size: 11px; background: var(--surface2);
542
+ padding: 1px 5px; border-radius: 3px;
543
+ }
544
+ .ai-generate-btn {
545
+ background: var(--purple-dim); border: 1px solid rgba(210,168,255,0.2);
546
+ color: var(--purple); padding: 10px 20px; border-radius: 7px;
547
+ cursor: pointer; font-size: 12px; font-weight: 600; transition: all 0.2s;
548
+ }
549
+ .ai-generate-btn:hover {
550
+ background: rgba(210,168,255,0.2); border-color: rgba(210,168,255,0.3);
551
+ }
552
+ .ai-generate-btn:disabled {
553
+ opacity: 0.5; cursor: not-allowed;
554
+ }
555
+ .ai-timestamp {
556
+ font-size: 10px; color: var(--text3); font-family: var(--mono);
557
+ margin-top: 14px; padding-top: 10px; border-top: 1px solid var(--border);
558
+ }
559
+ .ai-error {
560
+ color: var(--red); font-size: 13px; padding: 12px 0;
561
+ }
562
+ .ai-loading {
563
+ display: flex; align-items: center; gap: 10px;
564
+ color: var(--text2); font-size: 13px; padding: 12px 0;
565
+ }
566
+ .ai-spinner {
567
+ width: 16px; height: 16px; border: 2px solid var(--surface3);
568
+ border-top-color: var(--purple); border-radius: 50%;
569
+ animation: ai-spin 0.8s linear infinite;
570
+ }
571
+ @keyframes ai-spin { to { transform: rotate(360deg); } }
572
+ .ai-controls { display: flex; align-items: center; gap: 10px; }
573
+ .ai-model-picker { position: relative; display: inline-block; }
574
+ .ai-model-pill {
575
+ display: inline-flex; align-items: center; gap: 5px;
576
+ background: var(--surface2); border: 1px solid var(--border); color: var(--text3);
577
+ padding: 5px 10px; border-radius: 20px; font-size: 11px; font-family: var(--mono);
578
+ cursor: pointer; transition: all 0.2s; user-select: none; position: relative;
579
+ }
580
+ .ai-model-pill:hover { border-color: var(--border-hover); color: var(--text2); }
581
+ .ai-model-pill[data-tip]:hover::after {
582
+ content: attr(data-tip);
583
+ position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
584
+ background: var(--text); color: var(--bg); font-size: 10px; white-space: nowrap;
585
+ padding: 4px 8px; border-radius: 4px; pointer-events: none; z-index: 200;
586
+ }
587
+ .ai-model-pill .pill-chevron {
588
+ font-size: 8px; opacity: 0.4; transition: transform 0.2s;
589
+ }
590
+ .ai-model-picker.open .pill-chevron { transform: rotate(180deg); }
591
+ .ai-model-menu {
592
+ position: absolute; top: calc(100% + 4px); left: 0; z-index: 100;
593
+ background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
594
+ padding: 4px; min-width: 140px;
595
+ box-shadow: var(--shadow-tooltip);
596
+ opacity: 0; transform: translateY(-4px); pointer-events: none;
597
+ transition: opacity 0.15s, transform 0.15s;
598
+ }
599
+ .ai-model-picker.open .ai-model-menu {
600
+ opacity: 1; transform: translateY(0); pointer-events: auto;
601
+ }
602
+ .ai-model-option {
603
+ display: flex; align-items: center; gap: 8px;
604
+ padding: 6px 10px; border-radius: 5px; font-size: 11px; font-family: var(--mono);
605
+ color: var(--text2); cursor: pointer; transition: background 0.1s;
606
+ }
607
+ .ai-model-option:hover { background: var(--surface2); }
608
+ .ai-model-option.active { color: var(--text); font-weight: 600; }
609
+ .ai-model-option .check { width: 14px; color: var(--purple); font-size: 13px; }
610
+ .ai-token-warning {
611
+ margin-top: 10px; padding: 8px 12px; border-radius: 6px;
612
+ background: rgba(255,200,80,0.07); border: 1px solid rgba(255,200,80,0.2);
613
+ font-size: 11px; color: var(--text3); display: flex; align-items: flex-start; gap: 7px;
614
+ }
615
+ .ai-token-warning .warn-icon { color: #f5c842; flex-shrink: 0; margin-top: 1px; }
616
+ .ai-model-label {
617
+ font-size: 10px; color: var(--text3); font-family: var(--mono);
618
+ display: flex; align-items: center; gap: 6px;
619
+ }
620
+ .ai-model-label .model-tag { font-size: 10px; padding: 2px 8px; }
621
+ .ai-prompt-overlay {
622
+ display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.4);
623
+ z-index: 1000; align-items: center; justify-content: center;
624
+ backdrop-filter: blur(2px);
625
+ }
626
+ .ai-prompt-overlay.open { display: flex; }
627
+ .ai-prompt-box {
628
+ background: var(--surface); border: 1px solid var(--border);
629
+ border-radius: var(--radius); padding: 22px 26px; max-width: 680px; width: 90%;
630
+ max-height: 80vh; display: flex; flex-direction: column;
631
+ box-shadow: 0 8px 32px rgba(0,0,0,0.18); position: relative;
632
+ }
633
+ .ai-prompt-box h3 {
634
+ font-size: 12px; text-transform: uppercase; letter-spacing: 0.6px;
635
+ color: var(--text2); margin-bottom: 14px; font-weight: 600;
636
+ }
637
+ .ai-prompt-box pre {
638
+ font-family: var(--mono); font-size: 11px; line-height: 1.6;
639
+ color: var(--text); background: var(--surface2); border-radius: 6px;
640
+ padding: 14px; overflow: auto; flex: 1; white-space: pre-wrap; word-break: break-word;
641
+ }
642
+ .ai-prompt-close {
643
+ position: absolute; top: 12px; right: 14px; background: none; border: none;
644
+ color: var(--text3); cursor: pointer; font-size: 18px; line-height: 1; padding: 4px;
645
+ }
646
+ .ai-prompt-close:hover { color: var(--text); }
647
+ </style>
648
+ </head>
649
+ <body>
650
+ <div id="app" class="loading">Loading sessions...</div>
651
+
652
+ <script>
653
+ const $ = (s) => document.querySelector(s);
654
+
655
+ // --- Theme ---
656
+ function getPreferredTheme() {
657
+ const stored = localStorage.getItem('claude-session-insights-theme');
658
+ if (stored) return stored;
659
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
660
+ }
661
+
662
+ function applyTheme(theme) {
663
+ document.documentElement.setAttribute('data-theme', theme);
664
+ const btn = document.getElementById('theme-toggle-btn');
665
+ if (btn) btn.textContent = theme === 'dark' ? '\u2600' : '\u263E';
666
+ }
667
+
668
+ function toggleTheme() {
669
+ const current = document.documentElement.getAttribute('data-theme') || getPreferredTheme();
670
+ const next = current === 'dark' ? 'light' : 'dark';
671
+ localStorage.setItem('claude-session-insights-theme', next);
672
+ applyTheme(next);
673
+ // Redraw charts with new theme colors
674
+ if (currentView === 'dashboard' && currentData) {
675
+ const canvas = document.getElementById('chart');
676
+ if (canvas) drawChart(canvas, currentData.dailyScores);
677
+ }
678
+ }
679
+
680
+ applyTheme(getPreferredTheme());
681
+
682
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
683
+ if (!localStorage.getItem('claude-session-insights-theme')) {
684
+ applyTheme(getPreferredTheme());
685
+ }
686
+ });
687
+
688
+ async function fetchData(refresh = false) {
689
+ if (refresh) await fetch('/api/refresh');
690
+ const res = await fetch('/api/data');
691
+ return res.json();
692
+ }
693
+
694
+ async function fetchSession(id) {
695
+ const res = await fetch(`/api/session/${id}`);
696
+ return res.json();
697
+ }
698
+
699
+ const BADGE_ICONS = {
700
+ 'surgical-prompter': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v8M8 12h8"/></svg>`,
701
+ 'cache-whisperer': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>`,
702
+ 'clean-slate': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>`,
703
+ 'model-sniper': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>`,
704
+ 'efficiency-diamond': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 22 12 12 22 2 12"/></svg>`,
705
+ 'opus-addict': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a10 10 0 1 0 10 10"/><path d="M12 8v4l3 3"/></svg>`,
706
+ 'token-furnace': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12c2-2.96 0-7-1-8 0 3.038-1.773 4.741-3 6-1.226 1.26-2 3.24-2 5a6 6 0 1 0 12 0c0-1.532-1.056-3.94-2-5-1.786 3-2.791 3-4 2z"/></svg>`,
707
+ 'context-hoarder': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>`,
708
+ 'vague-commander': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
709
+ };
710
+
711
+ function renderBadge(b) {
712
+ const icon = BADGE_ICONS[b.id] || '';
713
+ const cls = b.negative ? 'badge negative' : 'badge';
714
+ return `<div class="${cls}">
715
+ <div class="badge-icon ${b.id}">${icon}</div>
716
+ <div class="badge-name">${b.name}</div>
717
+ <div class="badge-tip">${escHtml(b.description)}</div>
718
+ </div>`;
719
+ }
720
+
721
+ function formatModel(model) {
722
+ if (!model) return '<span class="model-tag">unknown</span>';
723
+ const family = model.includes('opus') ? 'opus' : model.includes('haiku') ? 'haiku' : 'sonnet';
724
+ const label = model.replace('claude-', '').replace(/-/g, ' ').replace(/(\d+) (\d+)/, '$1.$2');
725
+ return `<span class="model-tag model-${family}">${label}</span>`;
726
+ }
727
+
728
+ function formatTokens(n) {
729
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
730
+ if (n >= 1_000) return (n / 1_000).toFixed(0) + 'k';
731
+ return n.toString();
732
+ }
733
+
734
+ function formatCost(n) { return '$' + n.toFixed(2); }
735
+
736
+ function formatDate(ts) {
737
+ if (!ts) return '-';
738
+ const d = new Date(ts);
739
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ' ' +
740
+ d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
741
+ }
742
+
743
+ function formatTime(ts) {
744
+ if (!ts) return '';
745
+ return new Date(ts).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
746
+ }
747
+
748
+ function scorePill(score) {
749
+ const cls = score >= 80 ? 'score-high' : score >= 50 ? 'score-mid' : 'score-low';
750
+ return `<span class="score-pill ${cls}">${score}</span>`;
751
+ }
752
+
753
+ function dimColor(score) {
754
+ if (score >= 80) return 'var(--green)';
755
+ if (score >= 50) return 'var(--yellow)';
756
+ return 'var(--red)';
757
+ }
758
+
759
+ function escHtml(s) {
760
+ if (!s) return '';
761
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
762
+ }
763
+
764
+ function drawChart(canvas, dailyScores) {
765
+ const ctx = canvas.getContext('2d');
766
+ const dpr = window.devicePixelRatio || 1;
767
+ const rect = canvas.getBoundingClientRect();
768
+ canvas.width = rect.width * dpr;
769
+ canvas.height = rect.height * dpr;
770
+ ctx.scale(dpr, dpr);
771
+ const w = rect.width, h = rect.height;
772
+ const pad = { top: 20, right: 20, bottom: 40, left: 50 };
773
+ const plotW = w - pad.left - pad.right;
774
+ const plotH = h - pad.top - pad.bottom;
775
+
776
+ const styles = getComputedStyle(document.documentElement);
777
+ const chartAxis = styles.getPropertyValue('--chart-axis').trim();
778
+ const chartGrid = styles.getPropertyValue('--chart-grid').trim();
779
+ const chartScoreLabel = styles.getPropertyValue('--chart-score-label').trim();
780
+ const cGreen = styles.getPropertyValue('--green').trim();
781
+ const cYellow = styles.getPropertyValue('--yellow').trim();
782
+ const cRed = styles.getPropertyValue('--red').trim();
783
+
784
+ if (dailyScores.length === 0) {
785
+ ctx.fillStyle = chartAxis; ctx.font = '13px -apple-system, sans-serif';
786
+ ctx.textAlign = 'center'; ctx.fillText('No daily data yet', w / 2, h / 2);
787
+ return;
788
+ }
789
+
790
+ const maxTokens = Math.max(...dailyScores.map(d => d.tokens), 1);
791
+ const barWidth = Math.min(36, (plotW / dailyScores.length) * 0.6);
792
+ const gap = plotW / dailyScores.length;
793
+
794
+ // Grid lines
795
+ ctx.strokeStyle = chartGrid; ctx.lineWidth = 1;
796
+ for (let i = 0; i <= 4; i++) {
797
+ const y = pad.top + (plotH / 4) * i;
798
+ ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(w - pad.right, y); ctx.stroke();
799
+ ctx.fillStyle = chartAxis; ctx.font = '10px -apple-system, sans-serif'; ctx.textAlign = 'right';
800
+ const val = maxTokens - (maxTokens / 4) * i;
801
+ ctx.fillText(formatTokens(Math.round(val)), pad.left - 10, y + 3);
802
+ }
803
+
804
+ dailyScores.forEach((d, i) => {
805
+ const x = pad.left + gap * i + (gap - barWidth) / 2;
806
+ const barH = (d.tokens / maxTokens) * plotH;
807
+ const y = pad.top + plotH - barH;
808
+
809
+ // Bar with rounded top
810
+ const color = d.score >= 80 ? cGreen : d.score >= 50 ? cYellow : cRed;
811
+ ctx.globalAlpha = 0.2;
812
+ ctx.fillStyle = color;
813
+ ctx.beginPath(); ctx.roundRect(x, y, barWidth, barH, [4, 4, 0, 0]); ctx.fill();
814
+ ctx.globalAlpha = 0.85;
815
+ ctx.fillStyle = color;
816
+ ctx.beginPath(); ctx.roundRect(x, y, barWidth, barH, [4, 4, 0, 0]); ctx.fill();
817
+ ctx.globalAlpha = 1;
818
+
819
+ // Score label
820
+ if (barH > 22) {
821
+ ctx.fillStyle = chartScoreLabel; ctx.font = 'bold 10px -apple-system, sans-serif';
822
+ ctx.textAlign = 'center'; ctx.fillText(d.score, x + barWidth / 2, y + 14);
823
+ }
824
+ // Date label
825
+ ctx.fillStyle = chartAxis; ctx.font = '10px -apple-system, sans-serif';
826
+ ctx.textAlign = 'center'; ctx.fillText(d.date.slice(5), x + barWidth / 2, pad.top + plotH + 20);
827
+ });
828
+ }
829
+
830
+ function drawCostPerTurn(canvas, turns) {
831
+ const assistantTurns = turns.filter(t => t.role === 'assistant' && t.cost > 0);
832
+ const ctx = canvas.getContext('2d');
833
+ const dpr = window.devicePixelRatio || 1;
834
+ const rect = canvas.getBoundingClientRect();
835
+ canvas.width = rect.width * dpr;
836
+ canvas.height = rect.height * dpr;
837
+ ctx.scale(dpr, dpr);
838
+ const w = rect.width, h = rect.height;
839
+ const pad = { top: 20, right: 20, bottom: 30, left: 50 };
840
+ const plotW = w - pad.left - pad.right;
841
+ const plotH = h - pad.top - pad.bottom;
842
+
843
+ if (assistantTurns.length < 2) return;
844
+
845
+ const costs = assistantTurns.map(t => t.cost);
846
+ const maxCost = Math.max(...costs);
847
+ const step = plotW / (costs.length - 1);
848
+
849
+ const styles = getComputedStyle(document.documentElement);
850
+ const chartAxis = styles.getPropertyValue('--chart-axis').trim();
851
+ const chartGrid = styles.getPropertyValue('--chart-grid').trim();
852
+ const accentColor = styles.getPropertyValue('--accent').trim();
853
+
854
+ // Grid
855
+ ctx.strokeStyle = chartGrid; ctx.lineWidth = 1;
856
+ for (let i = 0; i <= 3; i++) {
857
+ const y = pad.top + (plotH / 3) * i;
858
+ ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(w - pad.right, y); ctx.stroke();
859
+ ctx.fillStyle = chartAxis; ctx.font = '10px -apple-system, sans-serif'; ctx.textAlign = 'right';
860
+ const val = maxCost - (maxCost / 3) * i;
861
+ ctx.fillText('$' + val.toFixed(3), pad.left - 10, y + 3);
862
+ }
863
+
864
+ // Area fill
865
+ ctx.beginPath();
866
+ costs.forEach((c, i) => {
867
+ const x = pad.left + step * i;
868
+ const y = pad.top + plotH - (c / maxCost) * plotH;
869
+ i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
870
+ });
871
+ ctx.lineTo(pad.left + step * (costs.length - 1), pad.top + plotH);
872
+ ctx.lineTo(pad.left, pad.top + plotH);
873
+ ctx.closePath();
874
+ const grad = ctx.createLinearGradient(0, pad.top, 0, pad.top + plotH);
875
+ grad.addColorStop(0, accentColor + '26');
876
+ grad.addColorStop(1, accentColor + '05');
877
+ ctx.fillStyle = grad; ctx.fill();
878
+
879
+ // Line
880
+ ctx.beginPath();
881
+ costs.forEach((c, i) => {
882
+ const x = pad.left + step * i;
883
+ const y = pad.top + plotH - (c / maxCost) * plotH;
884
+ i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
885
+ });
886
+ ctx.strokeStyle = accentColor; ctx.lineWidth = 2; ctx.stroke();
887
+
888
+ // Dots
889
+ costs.forEach((c, i) => {
890
+ const x = pad.left + step * i;
891
+ const y = pad.top + plotH - (c / maxCost) * plotH;
892
+ ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2);
893
+ ctx.fillStyle = accentColor; ctx.fill();
894
+ });
895
+
896
+ ctx.fillStyle = chartAxis; ctx.font = '10px -apple-system, sans-serif'; ctx.textAlign = 'center';
897
+ ctx.fillText('Turn', pad.left + plotW / 2, h - 4);
898
+ }
899
+
900
+ // --- Views ---
901
+
902
+ let sortCol = 'startTime';
903
+ let sortDir = -1;
904
+ let currentView = 'dashboard';
905
+
906
+ function renderDashboard(data) {
907
+ currentView = 'dashboard';
908
+ const totalTokens = data.sessions.reduce((s, x) => s + x.totals.totalTokens, 0);
909
+ const totalInput = data.sessions.reduce((s, x) => s + x.totals.inputTokens + (x.totals.cacheCreationTokens || 0) + (x.totals.cacheReadTokens || 0), 0);
910
+ const totalOutput = data.sessions.reduce((s, x) => s + x.totals.outputTokens, 0);
911
+ const totalCost = data.sessions.reduce((s, x) => s + x.totals.estimatedCost, 0);
912
+ const avgCacheHit = data.sessions.length > 0
913
+ ? data.sessions.reduce((s, x) => s + x.totals.cacheHitRate, 0) / data.sessions.length : 0;
914
+
915
+ const sorted = [...data.sessions].sort((a, b) => {
916
+ let av, bv;
917
+ if (sortCol === 'startTime') { av = a.startTime || ''; bv = b.startTime || ''; }
918
+ else if (sortCol === 'score') { av = a.score; bv = b.score; }
919
+ else if (sortCol === 'cost') { av = a.totals.estimatedCost; bv = b.totals.estimatedCost; }
920
+ else if (sortCol === 'tokens') { av = a.totals.totalTokens; bv = b.totals.totalTokens; }
921
+ else if (sortCol === 'project') { av = a.project; bv = b.project; }
922
+ if (av < bv) return -1 * sortDir;
923
+ if (av > bv) return 1 * sortDir;
924
+ return 0;
925
+ });
926
+
927
+ const arrow = (col) => sortCol === col ? `<span class="sort-arrow">${sortDir === 1 ? '\u25B2' : '\u25BC'}</span>` : '';
928
+
929
+ $('#app').innerHTML = `
930
+ <header>
931
+ <h1>claude-session-insights <span>v${data.version}</span></h1>
932
+ <div class="header-actions">
933
+ <label class="auto-refresh-toggle" title="Auto-refresh every 15s">
934
+ <input type="checkbox" id="auto-refresh-cb" onchange="toggleAutoRefresh(this.checked)" ${autoRefreshActive ? 'checked' : ''}>
935
+ <span class="auto-refresh-track"></span>
936
+ <span id="auto-refresh-label">${autoRefreshActive ? 'Auto-refresh (' + String(autoRefreshCountdown).padStart(2, '0') + 's)' : 'Auto-refresh (15s)'}</span>
937
+ </label>
938
+ <button class="refresh-btn" onclick="doRefresh()" title="Refresh" id="refresh-btn">
939
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6"/><path d="M21.34 13a10 10 0 1 1-2.84-8.84L21.5 8"/></svg>
940
+ </button>
941
+ <button class="theme-toggle" onclick="toggleTheme()" title="Toggle light/dark mode" id="theme-toggle-btn"></button>
942
+ </div>
943
+ </header>
944
+ <div class="hero">
945
+ <div class="hero-left">
946
+ <div class="score-card">
947
+ <div class="score">${data.overallScore}</div>
948
+ <div class="label">Efficiency Score (7-day)</div>
949
+ <button class="score-info-toggle" onclick="toggleScoreBreakdown()">How is this computed?</button>
950
+ </div>
951
+ <div class="badges-card">
952
+ <h3>Badges Earned</h3>
953
+ ${data.badges.length > 0
954
+ ? `<div class="badges-grid">${data.badges.map(b => renderBadge(b)).join('')}</div>`
955
+ : '<span style="color: var(--text3); font-size: 13px">Keep going -- badges unlock after 5+ sessions</span>'}
956
+ </div>
957
+ </div>
958
+ ${data.overallSummary ? `
959
+ <div class="hero-right">
960
+ <div class="summary-card">
961
+ <h3>How You're Using Claude Code
962
+ <span class="help-icon">?<span class="help-tooltip">Generated from static rules based on your session data — patterns, model choices, and token habits.<br><br><a onclick="document.getElementById('ai-insights-section').scrollIntoView({behavior:'smooth'})">Try AI Insights ↓</a> for a deeper, non-obvious analysis powered by Claude.</span></span>
963
+ </h3>
964
+ <div class="summary-paragraphs">
965
+ ${data.overallSummary.paragraphs.map(p => `<p>${escHtml(p)}</p>`).join('')}
966
+ </div>
967
+ ${data.overallSummary.recommendations.length > 0 ? `
968
+ <div class="summary-recommendations">
969
+ <h4>Recommendations</h4>
970
+ <ul>${data.overallSummary.recommendations.map(r => `<li>${escHtml(r)}</li>`).join('')}</ul>
971
+ </div>` : ''}
972
+ </div>
973
+ </div>` : ''}
974
+ </div>
975
+ <section id="ai-insights-section">
976
+ <div class="ai-insights-card">
977
+ <h3>AI Insights</h3>
978
+ <div id="ai-insights-body">
979
+ <div class="ai-controls">
980
+ <button class="ai-generate-btn" onclick="generateAIInsights()">Generate AI Insights</button>
981
+ <span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
982
+ </div>
983
+ <button class="score-info-toggle" onclick="togglePromptView()">What prompt is it sending to Claude?</button>
984
+ <div class="ai-token-warning">
985
+ <span class="warn-icon">⚠</span>
986
+ <span>This runs a real Claude session and will show up in your own usage stats. Each generation costs roughly the same as a short coding task.</span>
987
+ </div>
988
+ </div>
989
+ </div>
990
+ </section>
991
+ <div class="score-breakdown-overlay" id="score-breakdown-overlay" onclick="if(event.target===this)toggleScoreBreakdown()">
992
+ <div class="score-breakdown">
993
+ <button class="score-breakdown-close" onclick="toggleScoreBreakdown()">&times;</button>
994
+ <h3>How Efficiency Score Works</h3>
995
+ <p>Each session is scored 0-100 across five dimensions. The overall score is a weighted average of recent sessions (last 7 days).</p>
996
+ <div class="dim-explain">
997
+ <div class="dim-explain-row">
998
+ <span class="dim-explain-weight">30%</span>
999
+ <span class="dim-explain-name">Tool Call Ratio</span>
1000
+ <span class="dim-explain-desc">Fewer tool calls per message means your prompts are specific enough that Claude doesn't need to search around. Under 2x is ideal.</span>
1001
+ </div>
1002
+ <div class="dim-explain-row">
1003
+ <span class="dim-explain-weight">25%</span>
1004
+ <span class="dim-explain-name">Cache Hit Rate</span>
1005
+ <span class="dim-explain-desc">Higher cache hits mean Claude is reusing previously read context instead of re-processing it. Above 60% is good, 75%+ is great.</span>
1006
+ </div>
1007
+ <div class="dim-explain-row">
1008
+ <span class="dim-explain-weight">20%</span>
1009
+ <span class="dim-explain-name">Context Management</span>
1010
+ <span class="dim-explain-desc">Detects if cost-per-turn spirals up mid-session (context bloat) and whether you used /clear at the right time to reset.</span>
1011
+ </div>
1012
+ <div class="dim-explain-row">
1013
+ <span class="dim-explain-weight">15%</span>
1014
+ <span class="dim-explain-name">Model Fit</span>
1015
+ <span class="dim-explain-desc">Checks if Opus is justified based on the tool-to-message ratio. High tool ratio means mechanical work (edits, searches) that Sonnet handles fine. Low ratio with many messages means reasoning-heavy work where Opus shines.</span>
1016
+ </div>
1017
+ <div class="dim-explain-row">
1018
+ <span class="dim-explain-weight">10%</span>
1019
+ <span class="dim-explain-name">Prompt Specificity</span>
1020
+ <span class="dim-explain-desc">Flags vague prompts (under 30 chars) that lead to expensive responses. Specific prompts produce cheaper, more accurate results.</span>
1021
+ </div>
1022
+ </div>
1023
+ </div>
1024
+ </div>
1025
+ <div class="ai-prompt-overlay" id="ai-prompt-overlay" onclick="if(event.target===this)togglePromptView()">
1026
+ <div class="ai-prompt-box">
1027
+ <button class="ai-prompt-close" onclick="togglePromptView()">&times;</button>
1028
+ <h3>AI Insights Prompt</h3>
1029
+ <pre id="ai-prompt-content">Loading...</pre>
1030
+ </div>
1031
+ </div>
1032
+ <div class="stats">
1033
+ <div class="stat"><div class="value">${data.sessions.length}</div><div class="label">Sessions</div></div>
1034
+ <div class="stat"><div class="value">${formatTokens(totalTokens)}</div><div class="label">Total Tokens</div><div class="label" style="color:var(--text3); font-family:var(--mono); font-size:10px">${formatTokens(totalInput)} in / ${formatTokens(totalOutput)} out</div></div>
1035
+ <div class="stat"><div class="value">${formatCost(totalCost)}</div><div class="label">Est. API Cost</div></div>
1036
+ <div class="stat"><div class="value">${(avgCacheHit * 100).toFixed(0)}%</div><div class="label">Avg Cache Hit Rate</div></div>
1037
+ </div>
1038
+ <section>
1039
+ <h2>Daily Token Usage & Score</h2>
1040
+ <div class="chart-container"><canvas id="chart" style="height:200px"></canvas></div>
1041
+ </section>
1042
+ <section>
1043
+ <h2>Heaviest Sessions</h2>
1044
+ <div class="table-wrap">
1045
+ <table>
1046
+ <thead><tr>
1047
+ <th style="width:11%">Date</th>
1048
+ <th style="width:40%">Prompt</th>
1049
+ <th class="center" style="width:9%">Model</th>
1050
+ <th class="center" style="width:9%">Suggested</th>
1051
+ <th class="center" style="width:7%">Score</th>
1052
+ <th class="center" style="width:8%">Tokens</th>
1053
+ <th class="center" style="width:8%">Cost</th>
1054
+ </tr></thead>
1055
+ <tbody>
1056
+ ${[...data.sessions].sort((a, b) => b.totals.totalTokens - a.totals.totalTokens).slice(0, 5).map(s => `<tr class="clickable" data-id="${s.id}">
1057
+ <td class="multi-line">${formatDate(s.startTime)}<div class="td-sub">${s.project}</div></td>
1058
+ <td><div class="session-title">${escHtml(s.title)}</div></td>
1059
+ <td class="center">${formatModel(s.model)}</td>
1060
+ <td class="center">${s.suggestedModel ? formatModel('claude-' + s.suggestedModel) : '-'}</td>
1061
+ <td class="center">${scorePill(s.score)}</td>
1062
+ <td class="center" style="font-family:var(--mono)">${formatTokens(s.totals.totalTokens)}</td>
1063
+ <td class="center" style="font-family:var(--mono)">${formatCost(s.totals.estimatedCost)}</td>
1064
+ </tr>`).join('')}
1065
+ </tbody>
1066
+ </table>
1067
+ </div>
1068
+ </section>
1069
+ <section>
1070
+ <h2>All Sessions</h2>
1071
+ <div class="table-wrap">
1072
+ <table>
1073
+ <thead><tr>
1074
+ <th class="col-date" data-col="startTime">Date ${arrow('startTime')}</th>
1075
+ <th class="col-prompt">Prompt</th>
1076
+ <th class="col-model center">Model</th>
1077
+ <th class="col-suggested center">Suggested</th>
1078
+ <th class="col-score center" data-col="score">Score ${arrow('score')}</th>
1079
+ <th class="col-msgs center">Msgs</th>
1080
+ <th class="col-tokens center" data-col="tokens">Tokens ${arrow('tokens')}</th>
1081
+ <th class="col-tools center">Tools</th>
1082
+ <th class="col-cache center">Cache</th>
1083
+ <th class="col-cost center" data-col="cost">Cost ${arrow('cost')}</th>
1084
+ </tr></thead>
1085
+ <tbody>
1086
+ ${sorted.map(s => `<tr class="clickable" data-id="${s.id}">
1087
+ <td class="multi-line">${formatDate(s.startTime)}<div class="td-sub">${s.project}</div></td>
1088
+ <td><div class="session-title">${escHtml(s.title)}</div></td>
1089
+ <td class="center">${formatModel(s.model)}</td>
1090
+ <td class="center">${s.suggestedModel ? formatModel('claude-' + s.suggestedModel) : '-'}</td>
1091
+ <td class="center">${scorePill(s.score)}</td>
1092
+ <td class="center" style="font-family:var(--mono)">${s.totals.userMessages}</td>
1093
+ <td class="center" style="font-family:var(--mono)">${formatTokens(s.totals.totalTokens)}</td>
1094
+ <td class="center" style="font-family:var(--mono)">${s.totals.toolCalls}</td>
1095
+ <td class="center" style="font-family:var(--mono)">${(s.totals.cacheHitRate * 100).toFixed(0)}%</td>
1096
+ <td class="center" style="font-family:var(--mono)">${formatCost(s.totals.estimatedCost)}</td>
1097
+ </tr>`).join('')}
1098
+ </tbody>
1099
+ </table>
1100
+ </div>
1101
+ </section>
1102
+ `;
1103
+
1104
+ const canvas = document.getElementById('chart');
1105
+ if (canvas) drawChart(canvas, data.dailyScores);
1106
+
1107
+ // Update theme toggle icon after render
1108
+ const themeBtn = document.getElementById('theme-toggle-btn');
1109
+ const curTheme = document.documentElement.getAttribute('data-theme') || getPreferredTheme();
1110
+ if (themeBtn) themeBtn.textContent = curTheme === 'dark' ? '\u2600' : '\u263E';
1111
+
1112
+ // Sort handlers
1113
+ document.querySelectorAll('th[data-col]').forEach(th => {
1114
+ th.addEventListener('click', () => {
1115
+ const col = th.dataset.col;
1116
+ if (sortCol === col) sortDir *= -1;
1117
+ else { sortCol = col; sortDir = -1; }
1118
+ renderDashboard(data);
1119
+ });
1120
+ });
1121
+
1122
+ // Row click -> session detail
1123
+ document.querySelectorAll('tr.clickable').forEach(tr => {
1124
+ tr.addEventListener('click', () => openSession(tr.dataset.id));
1125
+ });
1126
+ }
1127
+
1128
+ async function openSession(id) {
1129
+ currentView = 'session';
1130
+ $('#app').innerHTML = '<div class="loading">Loading session...</div>';
1131
+ const s = await fetchSession(id);
1132
+ renderSession(s);
1133
+ }
1134
+
1135
+ function renderSession(s) {
1136
+ const dimLabels = {
1137
+ toolRatio: 'Tool Call Ratio',
1138
+ cacheHitRate: 'Cache Hit Rate',
1139
+ contextManagement: 'Context Mgmt',
1140
+ modelFit: 'Model Fit',
1141
+ promptSpecificity: 'Prompt Specificity',
1142
+ };
1143
+
1144
+ let cumCost = 0;
1145
+
1146
+ $('#app').innerHTML = `
1147
+ <button class="back-btn" onclick="goBack()">&larr; Back to dashboard</button>
1148
+
1149
+ <div class="session-header">
1150
+ <h2>${escHtml(s.title)}</h2>
1151
+ <div class="session-meta">
1152
+ <span>${s.project}</span>
1153
+ <span>${formatModel(s.model)}</span>
1154
+ <span>${scorePill(s.score)}</span>
1155
+ <span>${formatDate(s.startTime)} - ${formatTime(s.endTime)}</span>
1156
+ <span>${s.totals.userMessages} messages</span>
1157
+ <span>${formatTokens(s.totals.totalTokens)} tokens</span>
1158
+ <span>${formatCost(s.totals.estimatedCost)}</span>
1159
+ <span>Cache: ${(s.totals.cacheHitRate * 100).toFixed(0)}%</span>
1160
+ </div>
1161
+ </div>
1162
+
1163
+ ${s.suggestedModel ? `<div class="suggested-model-card">
1164
+ <span class="suggested-model-label">Suggested Model</span>
1165
+ <span class="suggested-model-value">${formatModel('claude-' + s.suggestedModel)}</span>
1166
+ <span class="suggested-model-hint">A different model may be more cost-efficient for this type of session</span>
1167
+ </div>` : ''}
1168
+
1169
+ ${s.summary ? `<div class="session-summary">${escHtml(s.summary)}</div>` : ''}
1170
+
1171
+ <div class="dim-grid">
1172
+ ${Object.entries(s.dimensions).map(([key, val]) => `
1173
+ <div class="dim-item">
1174
+ <div class="dim-label">${dimLabels[key] || key}</div>
1175
+ <div class="dim-score" style="color: ${dimColor(val)}">${val}</div>
1176
+ <div class="dim-bar"><div class="dim-bar-fill" style="width:${val}%; background:${dimColor(val)}"></div></div>
1177
+ </div>
1178
+ `).join('')}
1179
+ </div>
1180
+
1181
+ <section>
1182
+ <h2>Cost per Turn</h2>
1183
+ <div class="cost-chart-container"><canvas id="cost-chart" style="height:160px"></canvas></div>
1184
+ </section>
1185
+
1186
+ <section>
1187
+ <h2>Conversation</h2>
1188
+ <div class="turn-list">
1189
+ ${s.turns.map((t, i) => {
1190
+ if (t.role === 'assistant') cumCost += t.cost || 0;
1191
+ const totalTok = t.tokens.input + t.tokens.output + t.tokens.cacheCreation + t.tokens.cacheRead;
1192
+ return `
1193
+ <div class="turn">
1194
+ <div class="turn-header">
1195
+ <div>
1196
+ <span class="turn-role ${t.role === 'user' ? 'role-user' : 'role-assistant'}">${t.role}</span>
1197
+ <span style="color:var(--text3); font-family:var(--mono); font-size:10px; margin-left:8px">#${i + 1}</span>
1198
+ </div>
1199
+ <div class="turn-meta">
1200
+ ${t.role === 'assistant' ? `<span>${formatCost(t.cost || 0)} (cum: ${formatCost(cumCost)})</span>` : ''}
1201
+ ${totalTok > 0 ? `<span>${formatTokens(totalTok)} tok</span>` : ''}
1202
+ <span>${formatTime(t.timestamp)}</span>
1203
+ </div>
1204
+ </div>
1205
+ ${t.role === 'user' && t.promptPreview ? `<div class="turn-text">${escHtml(t.promptPreview)}</div>` : ''}
1206
+ ${t.role === 'assistant' && t.textPreview ? `<div class="turn-text">${escHtml(t.textPreview)}</div>` : ''}
1207
+ ${t.toolCalls && t.toolCalls.length > 0 ? `
1208
+ <div class="turn-tools">${t.toolCalls.map(tc => `<span class="tool-tag">${tc}</span>`).join('')}</div>
1209
+ ` : ''}
1210
+ ${t.role === 'assistant' && totalTok > 0 ? `
1211
+ <div class="turn-tokens">
1212
+ <span>in: ${formatTokens(t.tokens.input)}</span>
1213
+ <span>out: ${formatTokens(t.tokens.output)}</span>
1214
+ ${t.tokens.cacheCreation > 0 ? `<span>cache-write: ${formatTokens(t.tokens.cacheCreation)}</span>` : ''}
1215
+ ${t.tokens.cacheRead > 0 ? `<span>cache-read: ${formatTokens(t.tokens.cacheRead)}</span>` : ''}
1216
+ </div>
1217
+ ` : ''}
1218
+ </div>`;
1219
+ }).join('')}
1220
+ </div>
1221
+ </section>
1222
+ `;
1223
+
1224
+ const costCanvas = document.getElementById('cost-chart');
1225
+ if (costCanvas) drawCostPerTurn(costCanvas, s.turns);
1226
+ }
1227
+
1228
+ function toggleScoreBreakdown() {
1229
+ const el = document.getElementById('score-breakdown-overlay');
1230
+ if (el) el.classList.toggle('open');
1231
+ }
1232
+
1233
+ function goBack() {
1234
+ if (currentData) { renderDashboard(currentData); loadCachedAIInsights(); }
1235
+ }
1236
+
1237
+ // --- AI Insights ---
1238
+
1239
+ let aiModels = [];
1240
+ let aiDefaultModel = null;
1241
+ let aiDefaultModelLabel = null;
1242
+
1243
+ function renderMarkdown(text) {
1244
+ return text
1245
+ .replace(/^## (.+)$/gm, '<h2>$1</h2>')
1246
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
1247
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
1248
+ .replace(/^\- (.+)$/gm, '<li>$1</li>')
1249
+ .replace(/(<li>.*<\/li>\n?)+/gs, m => '<ul>' + m + '</ul>')
1250
+ .replace(/\n{2,}/g, '</p><p>')
1251
+ .replace(/^(?!<[hul])/gm, s => s ? '<p>' + s : '')
1252
+ .replace(/<p><\/p>/g, '')
1253
+ .replace(/<p>(<[hul])/g, '$1');
1254
+ }
1255
+
1256
+ function modelLabel(modelId) {
1257
+ if (!modelId || modelId === 'default') return 'default';
1258
+ const m = aiModels.find(m => m.id === modelId);
1259
+ if (m) return m.label;
1260
+ const family = modelId.includes('opus') ? 'opus' : modelId.includes('haiku') ? 'haiku' : 'sonnet';
1261
+ return `<span class="model-tag model-${family}">${modelId.replace('claude-', '')}</span>`;
1262
+ }
1263
+
1264
+ let selectedModelId = '';
1265
+
1266
+ function renderModelPicker() {
1267
+ const pickers = document.querySelectorAll('.ai-model-picker');
1268
+ if (aiModels.length === 0) return;
1269
+ const current = aiModels.find(m => m.id === selectedModelId) ||
1270
+ aiModels.find(m => m.id === aiDefaultModel) || aiModels[0];
1271
+ selectedModelId = current.id;
1272
+ for (const picker of pickers) {
1273
+ const pill = picker.querySelector('.ai-model-pill');
1274
+ const menu = picker.querySelector('.ai-model-menu');
1275
+ if (pill) pill.innerHTML = `${current.label} <span class="pill-chevron">&#x25BE;</span>`;
1276
+ if (menu) menu.innerHTML = aiModels.map(m =>
1277
+ `<div class="ai-model-option${m.id === selectedModelId ? ' active' : ''}" data-model="${m.id}" onclick="selectModel('${m.id}')">` +
1278
+ `<span class="check">${m.id === selectedModelId ? '●' : ''}</span>${m.label}</div>`
1279
+ ).join('');
1280
+ }
1281
+ }
1282
+
1283
+ function toggleModelMenu(e) {
1284
+ e.stopPropagation();
1285
+ const picker = e.target.closest('.ai-model-picker');
1286
+ if (!picker) return;
1287
+ const wasOpen = picker.classList.contains('open');
1288
+ document.querySelectorAll('.ai-model-picker.open').forEach(p => p.classList.remove('open'));
1289
+ if (!wasOpen) picker.classList.add('open');
1290
+ }
1291
+
1292
+ function selectModel(modelId) {
1293
+ selectedModelId = modelId;
1294
+ document.querySelectorAll('.ai-model-picker.open').forEach(p => p.classList.remove('open'));
1295
+ renderModelPicker();
1296
+ }
1297
+
1298
+ document.addEventListener('click', () => {
1299
+ document.querySelectorAll('.ai-model-picker.open').forEach(p => p.classList.remove('open'));
1300
+ });
1301
+
1302
+ function renderAIComplete(content, generatedAt, model) {
1303
+ const body = document.getElementById('ai-insights-body');
1304
+ if (!body) return;
1305
+ body.innerHTML = `
1306
+ <div class="ai-insights-content">${renderMarkdown(escHtml(content))}</div>
1307
+ <div class="ai-timestamp">
1308
+ Generated ${new Date(generatedAt).toLocaleString()}
1309
+ ${model ? ` · ${modelLabel(model)}` : ''}
1310
+ <div style="margin-top: 10px;" class="ai-controls">
1311
+ <button class="ai-generate-btn" style="padding: 5px 14px; font-size: 11px;" onclick="generateAIInsights()">Regenerate</button>
1312
+ <button class="ai-generate-btn" style="padding: 5px 14px; font-size: 11px; background: var(--surface2); color: var(--text2);" onclick="clearAIInsights()">Clear</button>
1313
+ <span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
1314
+ </div>
1315
+ <button class="score-info-toggle" onclick="togglePromptView()">What prompt is it sending to Claude?</button>
1316
+ </div>`;
1317
+ renderModelPicker();
1318
+ }
1319
+
1320
+ function clearAIInsights() {
1321
+ fetch('/api/ai-analyze', { method: 'DELETE' }).catch(() => {});
1322
+ const body = document.getElementById('ai-insights-body');
1323
+ if (!body) return;
1324
+ body.innerHTML = `
1325
+ <div class="ai-controls">
1326
+ <button class="ai-generate-btn" onclick="generateAIInsights()">Generate AI Insights</button>
1327
+ <span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
1328
+ </div>
1329
+ <button class="score-info-toggle" onclick="togglePromptView()">What prompt is it sending to Claude?</button>
1330
+ <div class="ai-token-warning">
1331
+ <span class="warn-icon">⚠</span>
1332
+ <span>This runs a real Claude session and will show up in your own usage stats. Each generation costs roughly the same as a short coding task.</span>
1333
+ </div>`;
1334
+ renderModelPicker();
1335
+ }
1336
+
1337
+ async function togglePromptView() {
1338
+ const overlay = document.getElementById('ai-prompt-overlay');
1339
+ if (overlay.classList.contains('open')) {
1340
+ overlay.classList.remove('open');
1341
+ return;
1342
+ }
1343
+ const pre = document.getElementById('ai-prompt-content');
1344
+ pre.textContent = 'Loading...';
1345
+ overlay.classList.add('open');
1346
+ try {
1347
+ const res = await fetch('/api/ai-prompt');
1348
+ const { prompt } = await res.json();
1349
+ pre.textContent = prompt;
1350
+ } catch (e) {
1351
+ pre.textContent = 'Failed to load prompt: ' + e.message;
1352
+ }
1353
+ }
1354
+
1355
+ function generateAIInsights() {
1356
+ const body = document.getElementById('ai-insights-body');
1357
+ if (!body) return;
1358
+
1359
+ const wasActive = autoRefreshActive;
1360
+ if (autoRefreshActive) pauseAutoRefresh();
1361
+
1362
+ const modelId = selectedModelId;
1363
+ const modelParam = modelId ? `?model=${encodeURIComponent(modelId)}` : '';
1364
+
1365
+ body.innerHTML = `
1366
+ <div class="ai-loading">
1367
+ <div class="ai-spinner"></div>
1368
+ <div>
1369
+ <div>Generating with ${modelId ? modelLabel(modelId) : 'default model'}, please wait...</div>
1370
+ ${wasActive ? '<div style="font-size:11px; color:var(--yellow); margin-top:4px">Auto-refresh paused while generating</div>' : ''}
1371
+ </div>
1372
+ </div>
1373
+ <div class="ai-insights-content" id="ai-stream-content"></div>`;
1374
+
1375
+ streamAIFetch(modelParam, body);
1376
+ }
1377
+
1378
+ async function streamAIFetch(modelParam, body) {
1379
+ let fullContent = '';
1380
+ let usedModel = '';
1381
+
1382
+ try {
1383
+ const res = await fetch('/api/ai-analyze' + modelParam, { method: 'POST' });
1384
+ const reader = res.body.getReader();
1385
+ const decoder = new TextDecoder();
1386
+ let buffer = '';
1387
+
1388
+ while (true) {
1389
+ const { done, value } = await reader.read();
1390
+ if (done) break;
1391
+
1392
+ buffer += decoder.decode(value, { stream: true });
1393
+ const lines = buffer.split('\n');
1394
+ buffer = lines.pop();
1395
+
1396
+ let currentEvent = '';
1397
+ for (const line of lines) {
1398
+ if (line.startsWith('event: ')) {
1399
+ currentEvent = line.slice(7);
1400
+ } else if (line.startsWith('data: ')) {
1401
+ const data = line.slice(6);
1402
+ if (currentEvent === 'model') {
1403
+ usedModel = JSON.parse(data);
1404
+ const loading = body.querySelector('.ai-loading div div:first-child');
1405
+ if (loading) loading.textContent = 'Generating with ' + (modelLabel(usedModel)) + ', please wait...';
1406
+ } else if (currentEvent === 'chunk') {
1407
+ fullContent += JSON.parse(data);
1408
+ const el = document.getElementById('ai-stream-content');
1409
+ if (el) el.innerHTML = renderMarkdown(escHtml(fullContent));
1410
+ } else if (currentEvent === 'done') {
1411
+ const { generatedAt, model } = JSON.parse(data);
1412
+ if (model) usedModel = model;
1413
+ const loading = body.querySelector('.ai-loading');
1414
+ if (loading) loading.remove();
1415
+ renderAIComplete(fullContent, generatedAt, usedModel);
1416
+ } else if (currentEvent === 'error') {
1417
+ const { message } = JSON.parse(data);
1418
+ body.innerHTML = `<div class="ai-error">${escHtml(message)}</div>
1419
+ <div class="ai-controls" style="margin-top: 8px;">
1420
+ <button class="ai-generate-btn" onclick="generateAIInsights()">Retry</button>
1421
+ <span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
1422
+ </div>`;
1423
+ renderModelPicker();
1424
+ resumeAutoRefreshIfPaused();
1425
+ }
1426
+ currentEvent = '';
1427
+ }
1428
+ }
1429
+ }
1430
+ resumeAutoRefreshIfPaused();
1431
+ } catch (err) {
1432
+ body.innerHTML = `<div class="ai-error">Failed to connect to server</div>
1433
+ <div class="ai-controls" style="margin-top: 8px;">
1434
+ <button class="ai-generate-btn" onclick="generateAIInsights()">Retry</button>
1435
+ <span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
1436
+ </div>`;
1437
+ renderModelPicker();
1438
+ resumeAutoRefreshIfPaused();
1439
+ }
1440
+ }
1441
+
1442
+ async function loadCachedAIInsights() {
1443
+ try {
1444
+ const res = await fetch('/api/ai-analyze');
1445
+ const data = await res.json();
1446
+ console.log('[ai] Cached insights:', { hasContent: !!data.content, models: data.models?.length, defaultModel: data.defaultModel });
1447
+ if (data.models) aiModels = data.models;
1448
+ if (data.defaultModel) {
1449
+ aiDefaultModel = data.defaultModel;
1450
+ aiDefaultModelLabel = data.defaultModelLabel;
1451
+ }
1452
+ renderModelPicker();
1453
+ if (data && data.content) {
1454
+ renderAIComplete(data.content, data.generatedAt, data.model);
1455
+ }
1456
+ } catch {}
1457
+ }
1458
+
1459
+ // --- Init ---
1460
+
1461
+ let currentData;
1462
+ let autoRefreshTimer = null;
1463
+ let autoRefreshTick = null;
1464
+ let autoRefreshActive = localStorage.getItem('claude-session-insights-auto-refresh') === 'true';
1465
+ let autoRefreshCountdown = 15;
1466
+ let autoRefreshPaused = false;
1467
+
1468
+ async function doRefresh() {
1469
+ const btn = $('#refresh-btn');
1470
+ btn.classList.add('spinning');
1471
+ currentData = await fetchData(true);
1472
+ renderDashboard(currentData);
1473
+ btn.classList.remove('spinning');
1474
+ }
1475
+
1476
+ function updateCountdownLabel() {
1477
+ const label = document.getElementById('auto-refresh-label');
1478
+ if (label && autoRefreshActive) {
1479
+ label.textContent = 'Auto-refresh (' + String(autoRefreshCountdown).padStart(2, '0') + 's)';
1480
+ }
1481
+ }
1482
+
1483
+ function startAutoRefresh() {
1484
+ stopAutoRefresh();
1485
+ autoRefreshActive = true;
1486
+ autoRefreshCountdown = 15;
1487
+ localStorage.setItem('claude-session-insights-auto-refresh', 'true');
1488
+ updateCountdownLabel();
1489
+ autoRefreshTick = setInterval(() => {
1490
+ autoRefreshCountdown--;
1491
+ updateCountdownLabel();
1492
+ if (autoRefreshCountdown <= 0) {
1493
+ autoRefreshCountdown = 15;
1494
+ (async () => {
1495
+ const btn = $('#refresh-btn');
1496
+ if (btn) btn.classList.add('spinning');
1497
+ currentData = await fetchData(true);
1498
+ renderDashboard(currentData);
1499
+ if (btn) btn.classList.remove('spinning');
1500
+ updateCountdownLabel();
1501
+ })();
1502
+ }
1503
+ }, 1000);
1504
+ }
1505
+
1506
+ function stopAutoRefresh() {
1507
+ clearInterval(autoRefreshTick);
1508
+ autoRefreshTick = null;
1509
+ autoRefreshTimer = null;
1510
+ autoRefreshActive = false;
1511
+ autoRefreshCountdown = 15;
1512
+ localStorage.setItem('claude-session-insights-auto-refresh', 'false');
1513
+ const label = document.getElementById('auto-refresh-label');
1514
+ if (label) label.textContent = 'Auto-refresh (15s)';
1515
+ }
1516
+
1517
+ function toggleAutoRefresh(on) {
1518
+ if (on) startAutoRefresh();
1519
+ else stopAutoRefresh();
1520
+ }
1521
+
1522
+ function pauseAutoRefresh() {
1523
+ clearInterval(autoRefreshTick);
1524
+ autoRefreshTick = null;
1525
+ autoRefreshPaused = true;
1526
+ const label = document.getElementById('auto-refresh-label');
1527
+ if (label) label.textContent = 'Auto-refresh (paused)';
1528
+ }
1529
+
1530
+ function resumeAutoRefreshIfPaused() {
1531
+ if (autoRefreshPaused) {
1532
+ autoRefreshPaused = false;
1533
+ startAutoRefresh();
1534
+ }
1535
+ }
1536
+
1537
+ // Clear AI insights on fresh page load (not on in-app refresh)
1538
+ fetch('/api/ai-analyze', { method: 'DELETE' }).catch(() => {});
1539
+
1540
+ fetchData().then(data => {
1541
+ currentData = data;
1542
+ renderDashboard(data);
1543
+ loadCachedAIInsights(); // loads model list (cache already cleared above)
1544
+ if (autoRefreshActive) startAutoRefresh();
1545
+ });
1546
+
1547
+ // Live reload via SSE
1548
+ const sse = new EventSource('/api/reload');
1549
+ sse.onmessage = async (e) => {
1550
+ console.log('[reload]', e.data);
1551
+ if (e.data.startsWith('public/')) {
1552
+ location.reload();
1553
+ } else {
1554
+ currentData = await fetchData();
1555
+ if (currentView === 'dashboard') renderDashboard(currentData);
1556
+ }
1557
+ };
1558
+ </script>
1559
+ </body>
1560
+ </html>