claude-code-templates 1.8.0 → 1.8.1
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/README.md +246 -0
- package/package.json +26 -12
- package/src/analytics/core/ConversationAnalyzer.js +754 -0
- package/src/analytics/core/FileWatcher.js +285 -0
- package/src/analytics/core/ProcessDetector.js +242 -0
- package/src/analytics/core/SessionAnalyzer.js +597 -0
- package/src/analytics/core/StateCalculator.js +190 -0
- package/src/analytics/data/DataCache.js +550 -0
- package/src/analytics/notifications/NotificationManager.js +448 -0
- package/src/analytics/notifications/WebSocketServer.js +526 -0
- package/src/analytics/utils/PerformanceMonitor.js +455 -0
- package/src/analytics-web/assets/js/main.js +312 -0
- package/src/analytics-web/components/Charts.js +114 -0
- package/src/analytics-web/components/ConversationTable.js +437 -0
- package/src/analytics-web/components/Dashboard.js +573 -0
- package/src/analytics-web/components/SessionTimer.js +596 -0
- package/src/analytics-web/index.html +882 -49
- package/src/analytics-web/index.html.original +1939 -0
- package/src/analytics-web/services/DataService.js +357 -0
- package/src/analytics-web/services/StateService.js +276 -0
- package/src/analytics-web/services/WebSocketService.js +523 -0
- package/src/analytics.js +626 -2317
- package/src/analytics.log +0 -0
|
@@ -0,0 +1,1939 @@
|
|
|
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 Code Analytics - Terminal</title>
|
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
* {
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
17
|
+
background: #0d1117;
|
|
18
|
+
color: #c9d1d9;
|
|
19
|
+
min-height: 100vh;
|
|
20
|
+
line-height: 1.4;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.terminal {
|
|
24
|
+
max-width: 1400px;
|
|
25
|
+
margin: 0 auto;
|
|
26
|
+
padding: 20px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.terminal-header {
|
|
30
|
+
border-bottom: 1px solid #30363d;
|
|
31
|
+
padding-bottom: 20px;
|
|
32
|
+
margin-bottom: 20px;
|
|
33
|
+
position: relative;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.terminal-title {
|
|
37
|
+
color: #d57455;
|
|
38
|
+
font-size: 1.25rem;
|
|
39
|
+
font-weight: normal;
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 8px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.status-dot {
|
|
46
|
+
width: 8px;
|
|
47
|
+
height: 8px;
|
|
48
|
+
border-radius: 50%;
|
|
49
|
+
background: #3fb950;
|
|
50
|
+
animation: pulse 2s infinite;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@keyframes pulse {
|
|
54
|
+
0%, 100% { opacity: 1; }
|
|
55
|
+
50% { opacity: 0.6; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.terminal-subtitle {
|
|
59
|
+
color: #7d8590;
|
|
60
|
+
font-size: 0.875rem;
|
|
61
|
+
margin-top: 4px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.github-star-btn {
|
|
65
|
+
position: absolute;
|
|
66
|
+
top: 0;
|
|
67
|
+
right: 0;
|
|
68
|
+
background: #21262d;
|
|
69
|
+
border: 1px solid #30363d;
|
|
70
|
+
color: #c9d1d9;
|
|
71
|
+
padding: 8px 12px;
|
|
72
|
+
border-radius: 6px;
|
|
73
|
+
text-decoration: none;
|
|
74
|
+
font-family: inherit;
|
|
75
|
+
font-size: 0.875rem;
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
gap: 6px;
|
|
79
|
+
transition: all 0.2s ease;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.github-star-btn:hover {
|
|
84
|
+
border-color: #d57455;
|
|
85
|
+
background: #30363d;
|
|
86
|
+
color: #d57455;
|
|
87
|
+
text-decoration: none;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.github-star-btn .star-icon {
|
|
91
|
+
font-size: 0.75rem;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.stats-bar {
|
|
95
|
+
display: flex;
|
|
96
|
+
gap: 40px;
|
|
97
|
+
margin: 20px 0;
|
|
98
|
+
flex-wrap: wrap;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.stat {
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
gap: 8px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.stat-label {
|
|
108
|
+
color: #7d8590;
|
|
109
|
+
font-size: 0.875rem;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.stat-value {
|
|
113
|
+
color: #d57455;
|
|
114
|
+
font-weight: bold;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.stat-sublabel {
|
|
118
|
+
color: #7d8590;
|
|
119
|
+
font-size: 0.75rem;
|
|
120
|
+
display: block;
|
|
121
|
+
margin-top: 2px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.chart-controls {
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
justify-content: space-between;
|
|
128
|
+
gap: 16px;
|
|
129
|
+
margin: 20px 0;
|
|
130
|
+
padding: 12px 0;
|
|
131
|
+
border-top: 1px solid #21262d;
|
|
132
|
+
border-bottom: 1px solid #21262d;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.chart-controls-left {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
gap: 16px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.chart-controls-right {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 12px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.date-control {
|
|
148
|
+
display: flex;
|
|
149
|
+
align-items: center;
|
|
150
|
+
gap: 8px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.date-label {
|
|
154
|
+
color: #7d8590;
|
|
155
|
+
font-size: 0.875rem;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.date-input {
|
|
159
|
+
background: #21262d;
|
|
160
|
+
border: 1px solid #30363d;
|
|
161
|
+
color: #c9d1d9;
|
|
162
|
+
padding: 6px 12px;
|
|
163
|
+
border-radius: 4px;
|
|
164
|
+
font-family: inherit;
|
|
165
|
+
font-size: 0.875rem;
|
|
166
|
+
cursor: pointer;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.date-input:focus {
|
|
170
|
+
outline: none;
|
|
171
|
+
border-color: #d57455;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.refresh-btn {
|
|
175
|
+
background: none;
|
|
176
|
+
border: 1px solid #30363d;
|
|
177
|
+
color: #7d8590;
|
|
178
|
+
padding: 6px 12px;
|
|
179
|
+
border-radius: 4px;
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
font-family: inherit;
|
|
182
|
+
font-size: 0.875rem;
|
|
183
|
+
transition: all 0.2s ease;
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: center;
|
|
186
|
+
gap: 6px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.refresh-btn:hover {
|
|
190
|
+
border-color: #d57455;
|
|
191
|
+
color: #d57455;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.refresh-btn.loading {
|
|
195
|
+
opacity: 0.6;
|
|
196
|
+
cursor: not-allowed;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.charts-container {
|
|
200
|
+
display: grid;
|
|
201
|
+
grid-template-columns: 2fr 1fr;
|
|
202
|
+
gap: 30px;
|
|
203
|
+
margin: 20px 0 30px 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.chart-card {
|
|
207
|
+
background: #161b22;
|
|
208
|
+
border: 1px solid #30363d;
|
|
209
|
+
border-radius: 6px;
|
|
210
|
+
padding: 20px;
|
|
211
|
+
position: relative;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.chart-title {
|
|
215
|
+
color: #d57455;
|
|
216
|
+
font-size: 0.875rem;
|
|
217
|
+
text-transform: uppercase;
|
|
218
|
+
margin-bottom: 16px;
|
|
219
|
+
display: flex;
|
|
220
|
+
align-items: center;
|
|
221
|
+
gap: 8px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.chart-canvas {
|
|
225
|
+
width: 100% !important;
|
|
226
|
+
height: 200px !important;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.filter-bar {
|
|
230
|
+
display: flex;
|
|
231
|
+
align-items: center;
|
|
232
|
+
gap: 16px;
|
|
233
|
+
margin: 20px 0;
|
|
234
|
+
padding: 12px 0;
|
|
235
|
+
border-top: 1px solid #21262d;
|
|
236
|
+
border-bottom: 1px solid #21262d;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.filter-label {
|
|
240
|
+
color: #7d8590;
|
|
241
|
+
font-size: 0.875rem;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.filter-buttons {
|
|
245
|
+
display: flex;
|
|
246
|
+
gap: 8px;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.filter-btn {
|
|
250
|
+
background: none;
|
|
251
|
+
border: 1px solid #30363d;
|
|
252
|
+
color: #7d8590;
|
|
253
|
+
padding: 4px 12px;
|
|
254
|
+
border-radius: 4px;
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
font-family: inherit;
|
|
257
|
+
font-size: 0.875rem;
|
|
258
|
+
transition: all 0.2s ease;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.filter-btn:hover {
|
|
262
|
+
border-color: #d57455;
|
|
263
|
+
color: #d57455;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.filter-btn.active {
|
|
267
|
+
background: #d57455;
|
|
268
|
+
border-color: #d57455;
|
|
269
|
+
color: #0d1117;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.sessions-table {
|
|
273
|
+
width: 100%;
|
|
274
|
+
border-collapse: collapse;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.sessions-table th {
|
|
278
|
+
text-align: left;
|
|
279
|
+
padding: 8px 12px;
|
|
280
|
+
color: #7d8590;
|
|
281
|
+
font-size: 0.875rem;
|
|
282
|
+
font-weight: normal;
|
|
283
|
+
border-bottom: 1px solid #30363d;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.sessions-table td {
|
|
287
|
+
padding: 8px 12px;
|
|
288
|
+
font-size: 0.875rem;
|
|
289
|
+
border-bottom: 1px solid #21262d;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.sessions-table tr:hover {
|
|
293
|
+
background: #161b22;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.session-id {
|
|
297
|
+
color: #d57455;
|
|
298
|
+
font-family: monospace;
|
|
299
|
+
display: flex;
|
|
300
|
+
align-items: center;
|
|
301
|
+
gap: 6px;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.process-indicator {
|
|
305
|
+
display: inline-block;
|
|
306
|
+
width: 6px;
|
|
307
|
+
height: 6px;
|
|
308
|
+
background: #3fb950;
|
|
309
|
+
border-radius: 50%;
|
|
310
|
+
animation: pulse 2s infinite;
|
|
311
|
+
cursor: help;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.process-indicator.orphan {
|
|
315
|
+
background: #f85149;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.session-id-container {
|
|
319
|
+
display: flex;
|
|
320
|
+
flex-direction: column;
|
|
321
|
+
gap: 4px;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.session-project {
|
|
325
|
+
color: #c9d1d9;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.session-model {
|
|
329
|
+
color: #a5d6ff;
|
|
330
|
+
font-size: 0.8rem;
|
|
331
|
+
max-width: 150px;
|
|
332
|
+
white-space: nowrap;
|
|
333
|
+
overflow: hidden;
|
|
334
|
+
text-overflow: ellipsis;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.session-messages {
|
|
338
|
+
color: #7d8590;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.session-tokens {
|
|
342
|
+
color: #f85149;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.session-time {
|
|
346
|
+
color: #7d8590;
|
|
347
|
+
font-size: 0.8rem;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.status-active {
|
|
351
|
+
color: #3fb950;
|
|
352
|
+
font-weight: bold;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.status-recent {
|
|
356
|
+
color: #d29922;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.status-inactive {
|
|
360
|
+
color: #7d8590;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.conversation-state {
|
|
364
|
+
color: #d57455;
|
|
365
|
+
font-style: italic;
|
|
366
|
+
font-size: 0.8rem;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.conversation-state.working {
|
|
370
|
+
animation: working-pulse 1.5s infinite;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.conversation-state.typing {
|
|
374
|
+
animation: typing-pulse 1.5s infinite;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
@keyframes working-pulse {
|
|
378
|
+
0%, 100% { opacity: 1; }
|
|
379
|
+
50% { opacity: 0.7; }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
@keyframes typing-pulse {
|
|
383
|
+
0%, 100% { opacity: 1; }
|
|
384
|
+
50% { opacity: 0.6; }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.status-squares {
|
|
388
|
+
display: flex;
|
|
389
|
+
gap: 2px;
|
|
390
|
+
align-items: center;
|
|
391
|
+
flex-wrap: wrap;
|
|
392
|
+
margin: 0;
|
|
393
|
+
padding: 0;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.status-square {
|
|
397
|
+
width: 10px !important;
|
|
398
|
+
height: 10px !important;
|
|
399
|
+
min-width: 10px !important;
|
|
400
|
+
min-height: 10px !important;
|
|
401
|
+
max-width: 10px !important;
|
|
402
|
+
max-height: 10px !important;
|
|
403
|
+
border-radius: 2px;
|
|
404
|
+
cursor: help;
|
|
405
|
+
position: relative;
|
|
406
|
+
flex-shrink: 0;
|
|
407
|
+
box-sizing: border-box;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.status-square.success {
|
|
411
|
+
background: #d57455;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.status-square.tool {
|
|
415
|
+
background: #f97316;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.status-square.error {
|
|
419
|
+
background: #dc2626;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.status-square.pending {
|
|
423
|
+
background: #6b7280;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/* Additional specificity to override any table styling */
|
|
427
|
+
.sessions-table .status-squares .status-square {
|
|
428
|
+
width: 10px !important;
|
|
429
|
+
height: 10px !important;
|
|
430
|
+
min-width: 10px !important;
|
|
431
|
+
min-height: 10px !important;
|
|
432
|
+
max-width: 10px !important;
|
|
433
|
+
max-height: 10px !important;
|
|
434
|
+
display: inline-block !important;
|
|
435
|
+
font-size: 0 !important;
|
|
436
|
+
line-height: 0 !important;
|
|
437
|
+
border: none !important;
|
|
438
|
+
outline: none !important;
|
|
439
|
+
vertical-align: top !important;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.status-square:hover::after {
|
|
443
|
+
content: attr(data-tooltip);
|
|
444
|
+
position: absolute;
|
|
445
|
+
bottom: 100%;
|
|
446
|
+
left: 50%;
|
|
447
|
+
transform: translateX(-50%);
|
|
448
|
+
background: #1c1c1c;
|
|
449
|
+
color: #fff;
|
|
450
|
+
padding: 4px 8px;
|
|
451
|
+
border-radius: 4px;
|
|
452
|
+
font-size: 0.75rem;
|
|
453
|
+
white-space: nowrap;
|
|
454
|
+
z-index: 1000;
|
|
455
|
+
margin-bottom: 4px;
|
|
456
|
+
border: 1px solid #30363d;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.status-square:hover::before {
|
|
460
|
+
content: '';
|
|
461
|
+
position: absolute;
|
|
462
|
+
bottom: 100%;
|
|
463
|
+
left: 50%;
|
|
464
|
+
transform: translateX(-50%);
|
|
465
|
+
border: 4px solid transparent;
|
|
466
|
+
border-top-color: #30363d;
|
|
467
|
+
z-index: 1000;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.loading, #error {
|
|
471
|
+
text-align: center;
|
|
472
|
+
padding: 40px;
|
|
473
|
+
color: #7d8590;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
#error {
|
|
477
|
+
color: #f85149;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.no-sessions {
|
|
481
|
+
text-align: center;
|
|
482
|
+
padding: 40px;
|
|
483
|
+
color: #7d8590;
|
|
484
|
+
font-style: italic;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.session-detail {
|
|
488
|
+
display: none;
|
|
489
|
+
margin-top: 20px;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.session-detail.active {
|
|
493
|
+
display: block;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.detail-header {
|
|
497
|
+
display: flex;
|
|
498
|
+
justify-content: space-between;
|
|
499
|
+
align-items: center;
|
|
500
|
+
padding: 16px 0;
|
|
501
|
+
border-bottom: 1px solid #30363d;
|
|
502
|
+
margin-bottom: 20px;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.detail-title {
|
|
506
|
+
color: #d57455;
|
|
507
|
+
font-size: 1.1rem;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.detail-actions {
|
|
511
|
+
display: flex;
|
|
512
|
+
gap: 12px;
|
|
513
|
+
align-items: center;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.export-format-select {
|
|
517
|
+
background: #21262d;
|
|
518
|
+
border: 1px solid #30363d;
|
|
519
|
+
color: #c9d1d9;
|
|
520
|
+
padding: 6px 12px;
|
|
521
|
+
border-radius: 4px;
|
|
522
|
+
font-family: inherit;
|
|
523
|
+
font-size: 0.875rem;
|
|
524
|
+
cursor: pointer;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.export-format-select:focus {
|
|
528
|
+
outline: none;
|
|
529
|
+
border-color: #d57455;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.export-format-select option {
|
|
533
|
+
background: #21262d;
|
|
534
|
+
color: #c9d1d9;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.btn {
|
|
538
|
+
background: none;
|
|
539
|
+
border: 1px solid #30363d;
|
|
540
|
+
color: #7d8590;
|
|
541
|
+
padding: 6px 12px;
|
|
542
|
+
border-radius: 4px;
|
|
543
|
+
cursor: pointer;
|
|
544
|
+
font-family: inherit;
|
|
545
|
+
font-size: 0.875rem;
|
|
546
|
+
transition: all 0.2s ease;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.btn:hover {
|
|
550
|
+
border-color: #d57455;
|
|
551
|
+
color: #d57455;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.btn-primary {
|
|
555
|
+
background: #d57455;
|
|
556
|
+
border-color: #d57455;
|
|
557
|
+
color: #0d1117;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.btn-primary:hover {
|
|
561
|
+
background: #e8956f;
|
|
562
|
+
border-color: #e8956f;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.session-info {
|
|
566
|
+
display: grid;
|
|
567
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
568
|
+
gap: 20px;
|
|
569
|
+
margin-bottom: 30px;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.info-item {
|
|
573
|
+
display: flex;
|
|
574
|
+
flex-direction: column;
|
|
575
|
+
gap: 4px;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.info-label {
|
|
579
|
+
color: #7d8590;
|
|
580
|
+
font-size: 0.75rem;
|
|
581
|
+
text-transform: uppercase;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.info-value {
|
|
585
|
+
color: #c9d1d9;
|
|
586
|
+
font-size: 0.875rem;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.info-value.model {
|
|
590
|
+
color: #a5d6ff;
|
|
591
|
+
font-weight: bold;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.search-input {
|
|
595
|
+
width: 100%;
|
|
596
|
+
background: #21262d;
|
|
597
|
+
border: 1px solid #30363d;
|
|
598
|
+
color: #c9d1d9;
|
|
599
|
+
padding: 8px 12px;
|
|
600
|
+
border-radius: 4px;
|
|
601
|
+
font-family: inherit;
|
|
602
|
+
font-size: 0.875rem;
|
|
603
|
+
margin-bottom: 16px;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.search-input:focus {
|
|
607
|
+
outline: none;
|
|
608
|
+
border-color: #d57455;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.conversation-history {
|
|
612
|
+
border: 1px solid #30363d;
|
|
613
|
+
border-radius: 6px;
|
|
614
|
+
max-height: 600px;
|
|
615
|
+
overflow-y: auto;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.message {
|
|
619
|
+
padding: 16px;
|
|
620
|
+
border-bottom: 1px solid #21262d;
|
|
621
|
+
position: relative;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.message:last-child {
|
|
625
|
+
border-bottom: none;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.message-header {
|
|
629
|
+
display: flex;
|
|
630
|
+
justify-content: space-between;
|
|
631
|
+
align-items: center;
|
|
632
|
+
margin-bottom: 8px;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.message-role {
|
|
636
|
+
color: #58a6ff;
|
|
637
|
+
font-size: 0.875rem;
|
|
638
|
+
font-weight: bold;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.message-role.user {
|
|
642
|
+
color: #3fb950;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.message-role.assistant {
|
|
646
|
+
color: #d57455;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.message-time {
|
|
650
|
+
color: #7d8590;
|
|
651
|
+
font-size: 0.75rem;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.message-content {
|
|
655
|
+
color: #c9d1d9;
|
|
656
|
+
font-size: 0.875rem;
|
|
657
|
+
line-height: 1.5;
|
|
658
|
+
white-space: pre-wrap;
|
|
659
|
+
word-wrap: break-word;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.message-type-indicator {
|
|
663
|
+
position: absolute;
|
|
664
|
+
top: 8px;
|
|
665
|
+
right: 8px;
|
|
666
|
+
width: 8px;
|
|
667
|
+
height: 8px;
|
|
668
|
+
border-radius: 2px;
|
|
669
|
+
cursor: help;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.message-type-indicator.success {
|
|
673
|
+
background: #d57455;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.message-type-indicator.tool {
|
|
677
|
+
background: #f97316;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.message-type-indicator.error {
|
|
681
|
+
background: #dc2626;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.message-type-indicator.pending {
|
|
685
|
+
background: #6b7280;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.message-type-indicator:hover::after {
|
|
689
|
+
content: attr(data-tooltip);
|
|
690
|
+
position: absolute;
|
|
691
|
+
top: 100%;
|
|
692
|
+
right: 0;
|
|
693
|
+
background: #1c1c1c;
|
|
694
|
+
color: #fff;
|
|
695
|
+
padding: 4px 8px;
|
|
696
|
+
border-radius: 4px;
|
|
697
|
+
font-size: 0.75rem;
|
|
698
|
+
white-space: nowrap;
|
|
699
|
+
z-index: 1000;
|
|
700
|
+
margin-top: 4px;
|
|
701
|
+
border: 1px solid #30363d;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.back-btn {
|
|
705
|
+
margin-bottom: 20px;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
@media (max-width: 768px) {
|
|
709
|
+
.stats-bar {
|
|
710
|
+
gap: 20px;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.chart-controls {
|
|
714
|
+
flex-direction: column;
|
|
715
|
+
gap: 12px;
|
|
716
|
+
align-items: stretch;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.chart-controls-left {
|
|
720
|
+
flex-direction: column;
|
|
721
|
+
gap: 12px;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.chart-controls-right {
|
|
725
|
+
justify-content: center;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.charts-container {
|
|
729
|
+
grid-template-columns: 1fr;
|
|
730
|
+
gap: 20px;
|
|
731
|
+
margin: 20px 0;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.chart-card {
|
|
735
|
+
padding: 16px;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.chart-canvas {
|
|
739
|
+
height: 180px !important;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.filter-bar {
|
|
743
|
+
flex-direction: column;
|
|
744
|
+
align-items: flex-start;
|
|
745
|
+
gap: 8px;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.sessions-table {
|
|
749
|
+
font-size: 0.8rem;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.sessions-table th,
|
|
753
|
+
.sessions-table td {
|
|
754
|
+
padding: 6px 8px;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.github-star-btn {
|
|
758
|
+
position: relative;
|
|
759
|
+
margin-top: 12px;
|
|
760
|
+
align-self: flex-start;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.terminal-header {
|
|
764
|
+
display: flex;
|
|
765
|
+
flex-direction: column;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
</style>
|
|
769
|
+
</head>
|
|
770
|
+
<body>
|
|
771
|
+
<div class="terminal">
|
|
772
|
+
<div class="terminal-header">
|
|
773
|
+
<div class="terminal-title">
|
|
774
|
+
<span class="status-dot"></span>
|
|
775
|
+
claude-code-analytics
|
|
776
|
+
</div>
|
|
777
|
+
<div class="terminal-subtitle">real-time monitoring dashboard</div>
|
|
778
|
+
<div class="terminal-subtitle" id="lastUpdate"></div>
|
|
779
|
+
|
|
780
|
+
<a href="https://github.com/davila7/claude-code-templates" target="_blank" class="github-star-btn" title="Give us a star on GitHub to support the project!">
|
|
781
|
+
<span class="star-icon">⭐</span>
|
|
782
|
+
<span>Star on GitHub</span>
|
|
783
|
+
</a>
|
|
784
|
+
</div>
|
|
785
|
+
|
|
786
|
+
<div id="loading" class="loading">
|
|
787
|
+
loading claude code data...
|
|
788
|
+
</div>
|
|
789
|
+
|
|
790
|
+
<div id="error" class="error" style="display: none;">
|
|
791
|
+
error: failed to load claude code data
|
|
792
|
+
</div>
|
|
793
|
+
|
|
794
|
+
<div id="dashboard" style="display: none;">
|
|
795
|
+
<div class="stats-bar">
|
|
796
|
+
<div class="stat">
|
|
797
|
+
<span class="stat-label">conversations:</span>
|
|
798
|
+
<span class="stat-value" id="totalConversations">0</span>
|
|
799
|
+
</div>
|
|
800
|
+
<div class="stat">
|
|
801
|
+
<span class="stat-label">claude sessions:</span>
|
|
802
|
+
<span class="stat-value" id="claudeSessions">0</span>
|
|
803
|
+
<span class="stat-sublabel" id="claudeSessionsDetail"></span>
|
|
804
|
+
</div>
|
|
805
|
+
<div class="stat">
|
|
806
|
+
<span class="stat-label">tokens:</span>
|
|
807
|
+
<span class="stat-value" id="totalTokens">0</span>
|
|
808
|
+
</div>
|
|
809
|
+
<div class="stat">
|
|
810
|
+
<span class="stat-label">projects:</span>
|
|
811
|
+
<span class="stat-value" id="activeProjects">0</span>
|
|
812
|
+
</div>
|
|
813
|
+
<div class="stat">
|
|
814
|
+
<span class="stat-label">storage:</span>
|
|
815
|
+
<span class="stat-value" id="dataSize">0</span>
|
|
816
|
+
</div>
|
|
817
|
+
</div>
|
|
818
|
+
|
|
819
|
+
<div class="chart-controls">
|
|
820
|
+
<div class="chart-controls-left">
|
|
821
|
+
<div class="date-control">
|
|
822
|
+
<span class="date-label">from:</span>
|
|
823
|
+
<input type="date" id="dateFrom" class="date-input">
|
|
824
|
+
</div>
|
|
825
|
+
<div class="date-control">
|
|
826
|
+
<span class="date-label">to:</span>
|
|
827
|
+
<input type="date" id="dateTo" class="date-input">
|
|
828
|
+
</div>
|
|
829
|
+
</div>
|
|
830
|
+
<div class="chart-controls-right">
|
|
831
|
+
<button class="refresh-btn" onclick="toggleNotifications()" id="notificationBtn">
|
|
832
|
+
enable notifications
|
|
833
|
+
</button>
|
|
834
|
+
<button class="refresh-btn" onclick="refreshCharts()" id="refreshBtn">
|
|
835
|
+
refresh charts
|
|
836
|
+
</button>
|
|
837
|
+
</div>
|
|
838
|
+
</div>
|
|
839
|
+
|
|
840
|
+
<div class="charts-container">
|
|
841
|
+
<div class="chart-card">
|
|
842
|
+
<div class="chart-title">
|
|
843
|
+
📊 token usage over time
|
|
844
|
+
</div>
|
|
845
|
+
<canvas id="tokenChart" class="chart-canvas"></canvas>
|
|
846
|
+
</div>
|
|
847
|
+
|
|
848
|
+
<div class="chart-card">
|
|
849
|
+
<div class="chart-title">
|
|
850
|
+
🎯 project activity distribution
|
|
851
|
+
</div>
|
|
852
|
+
<canvas id="projectChart" class="chart-canvas"></canvas>
|
|
853
|
+
</div>
|
|
854
|
+
</div>
|
|
855
|
+
|
|
856
|
+
<div class="filter-bar">
|
|
857
|
+
<span class="filter-label">filter conversations:</span>
|
|
858
|
+
<div class="filter-buttons">
|
|
859
|
+
<button class="filter-btn active" data-filter="active">active</button>
|
|
860
|
+
<button class="filter-btn" data-filter="recent">recent</button>
|
|
861
|
+
<button class="filter-btn" data-filter="inactive">inactive</button>
|
|
862
|
+
<button class="filter-btn" data-filter="all">all</button>
|
|
863
|
+
</div>
|
|
864
|
+
</div>
|
|
865
|
+
|
|
866
|
+
<table class="sessions-table">
|
|
867
|
+
<thead>
|
|
868
|
+
<tr>
|
|
869
|
+
<th>conversation id</th>
|
|
870
|
+
<th>project</th>
|
|
871
|
+
<th>model</th>
|
|
872
|
+
<th>messages</th>
|
|
873
|
+
<th>tokens</th>
|
|
874
|
+
<th>last activity</th>
|
|
875
|
+
<th>conversation state</th>
|
|
876
|
+
<th>status</th>
|
|
877
|
+
</tr>
|
|
878
|
+
</thead>
|
|
879
|
+
<tbody id="sessionsTable">
|
|
880
|
+
<!-- Sessions will be loaded here -->
|
|
881
|
+
</tbody>
|
|
882
|
+
</table>
|
|
883
|
+
|
|
884
|
+
<div id="noSessions" class="no-sessions" style="display: none;">
|
|
885
|
+
no conversations found for current filter
|
|
886
|
+
</div>
|
|
887
|
+
|
|
888
|
+
<div id="sessionDetail" class="session-detail">
|
|
889
|
+
<button class="btn back-btn" onclick="showSessionsList()">← back to conversations</button>
|
|
890
|
+
|
|
891
|
+
<div class="detail-header">
|
|
892
|
+
<div class="detail-title" id="detailTitle">conversation details</div>
|
|
893
|
+
<div class="detail-actions">
|
|
894
|
+
<select id="exportFormat" class="export-format-select">
|
|
895
|
+
<option value="csv">CSV</option>
|
|
896
|
+
<option value="json">JSON</option>
|
|
897
|
+
</select>
|
|
898
|
+
<button class="btn" onclick="exportSession()">export</button>
|
|
899
|
+
<button class="btn btn-primary" onclick="refreshSessionDetail()">refresh</button>
|
|
900
|
+
</div>
|
|
901
|
+
</div>
|
|
902
|
+
|
|
903
|
+
<div class="session-info" id="sessionInfo">
|
|
904
|
+
<!-- Session info will be loaded here -->
|
|
905
|
+
</div>
|
|
906
|
+
|
|
907
|
+
<div>
|
|
908
|
+
<h3 style="color: #7d8590; margin-bottom: 16px; font-size: 0.875rem; text-transform: uppercase;">conversation history</h3>
|
|
909
|
+
<input type="text" id="conversationSearch" class="search-input" placeholder="Search messages...">
|
|
910
|
+
<div class="conversation-history" id="conversationHistory">
|
|
911
|
+
<!-- Conversation history will be loaded here -->
|
|
912
|
+
</div>
|
|
913
|
+
</div>
|
|
914
|
+
</div>
|
|
915
|
+
</div>
|
|
916
|
+
</div>
|
|
917
|
+
|
|
918
|
+
<script>
|
|
919
|
+
let allConversations = [];
|
|
920
|
+
let currentFilter = 'active';
|
|
921
|
+
let currentSession = null;
|
|
922
|
+
let tokenChart = null;
|
|
923
|
+
let projectChart = null;
|
|
924
|
+
let allData = null;
|
|
925
|
+
let notificationsEnabled = false;
|
|
926
|
+
let previousConversationStates = new Map();
|
|
927
|
+
|
|
928
|
+
async function loadData() {
|
|
929
|
+
try {
|
|
930
|
+
const response = await fetch('/api/data');
|
|
931
|
+
const data = await response.json();
|
|
932
|
+
|
|
933
|
+
console.log('Data loaded:', data.timestamp);
|
|
934
|
+
|
|
935
|
+
document.getElementById('loading').style.display = 'none';
|
|
936
|
+
document.getElementById('dashboard').style.display = 'block';
|
|
937
|
+
|
|
938
|
+
// Update timestamp
|
|
939
|
+
document.getElementById('lastUpdate').textContent = `last update: ${data.lastUpdate}`;
|
|
940
|
+
|
|
941
|
+
updateStats(data.summary);
|
|
942
|
+
allConversations = data.conversations;
|
|
943
|
+
allData = data; // Store data globally for access
|
|
944
|
+
window.allData = data; // Keep for backward compatibility
|
|
945
|
+
|
|
946
|
+
// Initialize date inputs on first load
|
|
947
|
+
if (!document.getElementById('dateFrom').value) {
|
|
948
|
+
initializeDateInputs();
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
updateCharts(data);
|
|
952
|
+
updateSessionsTable();
|
|
953
|
+
|
|
954
|
+
// Check for conversation state changes and send notifications
|
|
955
|
+
checkForNotifications(data.conversations);
|
|
956
|
+
|
|
957
|
+
} catch (error) {
|
|
958
|
+
document.getElementById('loading').style.display = 'none';
|
|
959
|
+
document.getElementById('error').style.display = 'block';
|
|
960
|
+
console.error('Failed to load data:', error);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Function to only update conversation data without refreshing charts
|
|
965
|
+
async function loadConversationData() {
|
|
966
|
+
try {
|
|
967
|
+
const response = await fetch('/api/fast-update');
|
|
968
|
+
const data = await response.json();
|
|
969
|
+
|
|
970
|
+
// Only log state changes, not every refresh
|
|
971
|
+
let hasStateChanges = false;
|
|
972
|
+
|
|
973
|
+
// Log conversation state changes
|
|
974
|
+
if (data.conversations && allConversations) {
|
|
975
|
+
data.conversations.forEach(conv => {
|
|
976
|
+
const prevConv = allConversations.find(c => c.id === conv.id);
|
|
977
|
+
if (prevConv && prevConv.conversationState !== conv.conversationState) {
|
|
978
|
+
console.log('🔄 State change: ' + conv.project + ' from "' + prevConv.conversationState + '" to "' + conv.conversationState + '"');
|
|
979
|
+
hasStateChanges = true;
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Only log refresh timestamp if there were actual changes
|
|
985
|
+
if (hasStateChanges) {
|
|
986
|
+
console.log('⚡ Update completed at:', new Date().toLocaleTimeString());
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Update timestamp
|
|
990
|
+
document.getElementById('lastUpdate').textContent = `last update: ${data.lastUpdate}`;
|
|
991
|
+
|
|
992
|
+
updateStats(data.summary);
|
|
993
|
+
allConversations = data.conversations;
|
|
994
|
+
allData = data; // Store data globally for access
|
|
995
|
+
window.allData = data; // Keep for backward compatibility
|
|
996
|
+
|
|
997
|
+
// Only update sessions table, not charts
|
|
998
|
+
updateSessionsTable();
|
|
999
|
+
|
|
1000
|
+
// Check for conversation state changes and send notifications
|
|
1001
|
+
checkForNotifications(data.conversations);
|
|
1002
|
+
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
console.error('Failed to refresh conversation data:', error);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// NEW: Function to update ONLY conversation states (ultra-fast)
|
|
1009
|
+
async function updateConversationStatesOnly() {
|
|
1010
|
+
try {
|
|
1011
|
+
const response = await fetch('/api/conversation-state');
|
|
1012
|
+
const data = await response.json();
|
|
1013
|
+
|
|
1014
|
+
// Update only the conversation state fields in the UI
|
|
1015
|
+
data.activeStates.forEach(stateInfo => {
|
|
1016
|
+
// Update in sessions table
|
|
1017
|
+
const sessionRow = document.querySelector('tr[data-session-id="' + stateInfo.id + '"]');
|
|
1018
|
+
if (sessionRow) {
|
|
1019
|
+
const stateCell = sessionRow.querySelector('.conversation-state');
|
|
1020
|
+
if (stateCell && stateCell.textContent !== stateInfo.state) {
|
|
1021
|
+
console.log('⚡ INSTANT State Update: ' + stateInfo.project + ' → "' + stateInfo.state + '"');
|
|
1022
|
+
stateCell.textContent = stateInfo.state;
|
|
1023
|
+
stateCell.className = 'conversation-state ' + getStateClass(stateInfo.state);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Update in session detail if visible
|
|
1028
|
+
if (currentSession && currentSession.id === stateInfo.id) {
|
|
1029
|
+
const detailStateElement = document.querySelector('#sessionDetail .conversation-state');
|
|
1030
|
+
if (detailStateElement && detailStateElement.textContent !== stateInfo.state) {
|
|
1031
|
+
detailStateElement.textContent = stateInfo.state;
|
|
1032
|
+
detailStateElement.className = 'conversation-state ' + getStateClass(stateInfo.state);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
// Silently fail - don't interfere with main data flow
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Notification functions
|
|
1043
|
+
async function requestNotificationPermission() {
|
|
1044
|
+
if (!('Notification' in window)) {
|
|
1045
|
+
console.log('This browser does not support notifications');
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (Notification.permission === 'granted') {
|
|
1050
|
+
notificationsEnabled = true;
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (Notification.permission !== 'denied') {
|
|
1055
|
+
const permission = await Notification.requestPermission();
|
|
1056
|
+
notificationsEnabled = permission === 'granted';
|
|
1057
|
+
return notificationsEnabled;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
return false;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function sendNotification(title, body, conversationId) {
|
|
1064
|
+
if (!notificationsEnabled) return;
|
|
1065
|
+
|
|
1066
|
+
const notification = new Notification(title, {
|
|
1067
|
+
body: body,
|
|
1068
|
+
icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iNCIgZmlsbD0iIzIxMjYyZCIvPgo8cGF0aCBkPSJNOCA4aDE2djE2SDh6IiBmaWxsPSIjZDU3NDU1Ii8+CjwvZGJnPgo=',
|
|
1069
|
+
badge: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iOCIgY3k9IjgiIHI9IjgiIGZpbGw9IiNkNTc0NTUiLz4KPC9zdmc+',
|
|
1070
|
+
tag: conversationId,
|
|
1071
|
+
requireInteraction: true
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
notification.onclick = function() {
|
|
1075
|
+
window.focus();
|
|
1076
|
+
this.close();
|
|
1077
|
+
// Focus on the conversation if possible
|
|
1078
|
+
if (conversationId) {
|
|
1079
|
+
showSessionDetail(conversationId);
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
// Auto close after 10 seconds
|
|
1084
|
+
setTimeout(() => {
|
|
1085
|
+
notification.close();
|
|
1086
|
+
}, 10000);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function checkForNotifications(conversations) {
|
|
1090
|
+
if (!notificationsEnabled) return;
|
|
1091
|
+
|
|
1092
|
+
conversations.forEach(conv => {
|
|
1093
|
+
const currentState = conv.conversationState;
|
|
1094
|
+
const prevState = previousConversationStates.get(conv.id);
|
|
1095
|
+
|
|
1096
|
+
// Check if conversation state changed to "Awaiting user input..."
|
|
1097
|
+
if (prevState && prevState !== currentState) {
|
|
1098
|
+
if (currentState === 'Awaiting user input...' ||
|
|
1099
|
+
currentState === 'User may be typing...' ||
|
|
1100
|
+
currentState === 'Awaiting response...') {
|
|
1101
|
+
|
|
1102
|
+
const title = 'Claude is waiting for you!';
|
|
1103
|
+
const body = `Project: ${conv.project} - Claude needs your input`;
|
|
1104
|
+
|
|
1105
|
+
sendNotification(title, body, conv.id);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Update previous state
|
|
1110
|
+
previousConversationStates.set(conv.id, currentState);
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
async function toggleNotifications() {
|
|
1115
|
+
const btn = document.getElementById('notificationBtn');
|
|
1116
|
+
|
|
1117
|
+
if (!notificationsEnabled) {
|
|
1118
|
+
const granted = await requestNotificationPermission();
|
|
1119
|
+
if (granted) {
|
|
1120
|
+
btn.textContent = 'notifications on';
|
|
1121
|
+
btn.style.borderColor = '#3fb950';
|
|
1122
|
+
btn.style.color = '#3fb950';
|
|
1123
|
+
|
|
1124
|
+
// Send a test notification
|
|
1125
|
+
sendNotification(
|
|
1126
|
+
'Notifications enabled!',
|
|
1127
|
+
'You will now receive alerts when Claude is waiting for your input.',
|
|
1128
|
+
null
|
|
1129
|
+
);
|
|
1130
|
+
} else {
|
|
1131
|
+
btn.textContent = 'notifications denied';
|
|
1132
|
+
btn.style.borderColor = '#f85149';
|
|
1133
|
+
btn.style.color = '#f85149';
|
|
1134
|
+
}
|
|
1135
|
+
} else {
|
|
1136
|
+
// Disable notifications
|
|
1137
|
+
notificationsEnabled = false;
|
|
1138
|
+
btn.textContent = 'enable notifications';
|
|
1139
|
+
btn.style.borderColor = '#30363d';
|
|
1140
|
+
btn.style.color = '#7d8590';
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function updateStats(summary) {
|
|
1145
|
+
document.getElementById('totalConversations').textContent = summary.totalConversations.toLocaleString();
|
|
1146
|
+
document.getElementById('totalTokens').textContent = summary.totalTokens.toLocaleString();
|
|
1147
|
+
document.getElementById('activeProjects').textContent = summary.activeProjects;
|
|
1148
|
+
document.getElementById('dataSize').textContent = summary.totalFileSize;
|
|
1149
|
+
|
|
1150
|
+
// Update Claude sessions
|
|
1151
|
+
if (summary.claudeSessions) {
|
|
1152
|
+
document.getElementById('claudeSessions').textContent = summary.claudeSessions.total.toLocaleString();
|
|
1153
|
+
document.getElementById('claudeSessionsDetail').textContent =
|
|
1154
|
+
`this month: ${summary.claudeSessions.currentMonth} • this week: ${summary.claudeSessions.thisWeek}`;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function initializeDateInputs() {
|
|
1159
|
+
const today = new Date();
|
|
1160
|
+
const sevenDaysAgo = new Date(today);
|
|
1161
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
1162
|
+
|
|
1163
|
+
document.getElementById('dateFrom').value = sevenDaysAgo.toISOString().split('T')[0];
|
|
1164
|
+
document.getElementById('dateTo').value = today.toISOString().split('T')[0];
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function getDateRange() {
|
|
1168
|
+
const fromDate = new Date(document.getElementById('dateFrom').value);
|
|
1169
|
+
const toDate = new Date(document.getElementById('dateTo').value);
|
|
1170
|
+
toDate.setHours(23, 59, 59, 999); // Include the entire end date
|
|
1171
|
+
|
|
1172
|
+
return { fromDate, toDate };
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function filterConversationsByDate(conversations) {
|
|
1176
|
+
const { fromDate, toDate } = getDateRange();
|
|
1177
|
+
|
|
1178
|
+
return conversations.filter(conv => {
|
|
1179
|
+
const convDate = new Date(conv.lastModified);
|
|
1180
|
+
return convDate >= fromDate && convDate <= toDate;
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function updateCharts(data) {
|
|
1185
|
+
// Wait for Chart.js to load before creating charts
|
|
1186
|
+
if (typeof Chart === 'undefined') {
|
|
1187
|
+
console.log('Chart.js not loaded yet, retrying in 100ms...');
|
|
1188
|
+
setTimeout(() => updateCharts(data), 100);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Use ALL conversations but filter chart display by date range
|
|
1193
|
+
// This maintains the original behavior
|
|
1194
|
+
|
|
1195
|
+
// Update Token Usage Over Time Chart
|
|
1196
|
+
updateTokenChart(data.conversations);
|
|
1197
|
+
|
|
1198
|
+
// Update Project Activity Distribution Chart
|
|
1199
|
+
updateProjectChart(data.conversations);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
async function refreshCharts() {
|
|
1203
|
+
const refreshBtn = document.getElementById('refreshBtn');
|
|
1204
|
+
refreshBtn.classList.add('loading');
|
|
1205
|
+
refreshBtn.textContent = '🔄 refreshing...';
|
|
1206
|
+
|
|
1207
|
+
try {
|
|
1208
|
+
// Use existing data but re-filter and update charts
|
|
1209
|
+
if (allData) {
|
|
1210
|
+
updateCharts(allData);
|
|
1211
|
+
}
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
console.error('Error refreshing charts:', error);
|
|
1214
|
+
} finally {
|
|
1215
|
+
refreshBtn.classList.remove('loading');
|
|
1216
|
+
refreshBtn.textContent = 'refresh charts';
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function updateTokenChart(conversations) {
|
|
1221
|
+
// Check if Chart.js is available
|
|
1222
|
+
if (typeof Chart === 'undefined') {
|
|
1223
|
+
console.warn('Chart.js not available for updateTokenChart');
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Prepare data for selected date range
|
|
1228
|
+
const { fromDate, toDate } = getDateRange();
|
|
1229
|
+
const dateRange = [];
|
|
1230
|
+
|
|
1231
|
+
const currentDate = new Date(fromDate);
|
|
1232
|
+
while (currentDate <= toDate) {
|
|
1233
|
+
dateRange.push({
|
|
1234
|
+
date: currentDate.toISOString().split('T')[0],
|
|
1235
|
+
label: currentDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
|
1236
|
+
tokens: 0
|
|
1237
|
+
});
|
|
1238
|
+
currentDate.setDate(currentDate.getDate() + 1);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// Aggregate tokens by day
|
|
1242
|
+
conversations.forEach(conv => {
|
|
1243
|
+
const convDate = new Date(conv.lastModified).toISOString().split('T')[0];
|
|
1244
|
+
const dayData = dateRange.find(day => day.date === convDate);
|
|
1245
|
+
if (dayData) {
|
|
1246
|
+
dayData.tokens += conv.tokens;
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
const ctx = document.getElementById('tokenChart').getContext('2d');
|
|
1251
|
+
|
|
1252
|
+
if (tokenChart) {
|
|
1253
|
+
tokenChart.destroy();
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
tokenChart = new Chart(ctx, {
|
|
1257
|
+
type: 'line',
|
|
1258
|
+
data: {
|
|
1259
|
+
labels: dateRange.map(day => day.label),
|
|
1260
|
+
datasets: [{
|
|
1261
|
+
label: 'Tokens',
|
|
1262
|
+
data: dateRange.map(day => day.tokens),
|
|
1263
|
+
borderColor: '#d57455',
|
|
1264
|
+
backgroundColor: 'rgba(213, 116, 85, 0.1)',
|
|
1265
|
+
borderWidth: 2,
|
|
1266
|
+
pointBackgroundColor: '#d57455',
|
|
1267
|
+
pointBorderColor: '#d57455',
|
|
1268
|
+
pointRadius: 4,
|
|
1269
|
+
pointHoverRadius: 6,
|
|
1270
|
+
fill: true,
|
|
1271
|
+
tension: 0.3
|
|
1272
|
+
}]
|
|
1273
|
+
},
|
|
1274
|
+
options: {
|
|
1275
|
+
responsive: true,
|
|
1276
|
+
maintainAspectRatio: false,
|
|
1277
|
+
plugins: {
|
|
1278
|
+
legend: {
|
|
1279
|
+
display: false
|
|
1280
|
+
},
|
|
1281
|
+
tooltip: {
|
|
1282
|
+
backgroundColor: '#161b22',
|
|
1283
|
+
titleColor: '#d57455',
|
|
1284
|
+
bodyColor: '#c9d1d9',
|
|
1285
|
+
borderColor: '#30363d',
|
|
1286
|
+
borderWidth: 1,
|
|
1287
|
+
titleFont: {
|
|
1288
|
+
family: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
|
1289
|
+
size: 12
|
|
1290
|
+
},
|
|
1291
|
+
bodyFont: {
|
|
1292
|
+
family: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
|
1293
|
+
size: 11
|
|
1294
|
+
},
|
|
1295
|
+
callbacks: {
|
|
1296
|
+
title: function(context) {
|
|
1297
|
+
return context[0].label;
|
|
1298
|
+
},
|
|
1299
|
+
label: function(context) {
|
|
1300
|
+
return `Tokens: ${context.parsed.y.toLocaleString()}`;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
},
|
|
1305
|
+
interaction: {
|
|
1306
|
+
intersect: false,
|
|
1307
|
+
mode: 'index'
|
|
1308
|
+
},
|
|
1309
|
+
hover: {
|
|
1310
|
+
animationDuration: 200
|
|
1311
|
+
},
|
|
1312
|
+
scales: {
|
|
1313
|
+
x: {
|
|
1314
|
+
grid: {
|
|
1315
|
+
color: '#30363d',
|
|
1316
|
+
borderColor: '#30363d'
|
|
1317
|
+
},
|
|
1318
|
+
ticks: {
|
|
1319
|
+
color: '#7d8590',
|
|
1320
|
+
font: {
|
|
1321
|
+
family: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
|
1322
|
+
size: 11
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
},
|
|
1326
|
+
y: {
|
|
1327
|
+
grid: {
|
|
1328
|
+
color: '#30363d',
|
|
1329
|
+
borderColor: '#30363d'
|
|
1330
|
+
},
|
|
1331
|
+
ticks: {
|
|
1332
|
+
color: '#7d8590',
|
|
1333
|
+
font: {
|
|
1334
|
+
family: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
|
1335
|
+
size: 11
|
|
1336
|
+
},
|
|
1337
|
+
callback: function(value) {
|
|
1338
|
+
return value.toLocaleString();
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function updateProjectChart(conversations) {
|
|
1348
|
+
// Check if Chart.js is available
|
|
1349
|
+
if (typeof Chart === 'undefined') {
|
|
1350
|
+
console.warn('Chart.js not available for updateProjectChart');
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// Aggregate data by project
|
|
1355
|
+
const projectData = {};
|
|
1356
|
+
|
|
1357
|
+
conversations.forEach(conv => {
|
|
1358
|
+
if (!projectData[conv.project]) {
|
|
1359
|
+
projectData[conv.project] = 0;
|
|
1360
|
+
}
|
|
1361
|
+
projectData[conv.project] += conv.tokens;
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
// Get top 5 projects and group others
|
|
1365
|
+
const sortedProjects = Object.entries(projectData)
|
|
1366
|
+
.sort(([,a], [,b]) => b - a)
|
|
1367
|
+
.slice(0, 5);
|
|
1368
|
+
|
|
1369
|
+
const othersTotal = Object.entries(projectData)
|
|
1370
|
+
.slice(5)
|
|
1371
|
+
.reduce((sum, [,tokens]) => sum + tokens, 0);
|
|
1372
|
+
|
|
1373
|
+
if (othersTotal > 0) {
|
|
1374
|
+
sortedProjects.push(['others', othersTotal]);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Terminal-style colors
|
|
1378
|
+
const colors = [
|
|
1379
|
+
'#d57455', // Orange
|
|
1380
|
+
'#3fb950', // Green
|
|
1381
|
+
'#a5d6ff', // Blue
|
|
1382
|
+
'#f97316', // Orange variant
|
|
1383
|
+
'#c9d1d9', // Light gray
|
|
1384
|
+
'#7d8590' // Gray
|
|
1385
|
+
];
|
|
1386
|
+
|
|
1387
|
+
const ctx = document.getElementById('projectChart').getContext('2d');
|
|
1388
|
+
|
|
1389
|
+
if (projectChart) {
|
|
1390
|
+
projectChart.destroy();
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
projectChart = new Chart(ctx, {
|
|
1394
|
+
type: 'doughnut',
|
|
1395
|
+
data: {
|
|
1396
|
+
labels: sortedProjects.map(([project]) => project),
|
|
1397
|
+
datasets: [{
|
|
1398
|
+
data: sortedProjects.map(([,tokens]) => tokens),
|
|
1399
|
+
backgroundColor: colors.slice(0, sortedProjects.length),
|
|
1400
|
+
borderColor: '#161b22',
|
|
1401
|
+
borderWidth: 2,
|
|
1402
|
+
hoverBorderWidth: 3
|
|
1403
|
+
}]
|
|
1404
|
+
},
|
|
1405
|
+
options: {
|
|
1406
|
+
responsive: true,
|
|
1407
|
+
maintainAspectRatio: false,
|
|
1408
|
+
plugins: {
|
|
1409
|
+
legend: {
|
|
1410
|
+
position: 'bottom',
|
|
1411
|
+
labels: {
|
|
1412
|
+
color: '#7d8590',
|
|
1413
|
+
font: {
|
|
1414
|
+
family: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
|
1415
|
+
size: 10
|
|
1416
|
+
},
|
|
1417
|
+
padding: 15,
|
|
1418
|
+
usePointStyle: true,
|
|
1419
|
+
pointStyle: 'circle'
|
|
1420
|
+
}
|
|
1421
|
+
},
|
|
1422
|
+
tooltip: {
|
|
1423
|
+
backgroundColor: '#161b22',
|
|
1424
|
+
titleColor: '#d57455',
|
|
1425
|
+
bodyColor: '#c9d1d9',
|
|
1426
|
+
borderColor: '#30363d',
|
|
1427
|
+
borderWidth: 1,
|
|
1428
|
+
titleFont: {
|
|
1429
|
+
family: 'Monaco, Menlo, Ubuntu Mono, monospace'
|
|
1430
|
+
},
|
|
1431
|
+
bodyFont: {
|
|
1432
|
+
family: 'Monaco, Menlo, Ubuntu Mono, monospace'
|
|
1433
|
+
},
|
|
1434
|
+
callbacks: {
|
|
1435
|
+
label: function(context) {
|
|
1436
|
+
const total = context.dataset.data.reduce((sum, value) => sum + value, 0);
|
|
1437
|
+
const percentage = ((context.parsed / total) * 100).toFixed(1);
|
|
1438
|
+
return `${context.label}: ${context.parsed.toLocaleString()} tokens (${percentage}%)`;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
},
|
|
1443
|
+
cutout: '60%'
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function updateSessionsTable() {
|
|
1449
|
+
const tableBody = document.getElementById('sessionsTable');
|
|
1450
|
+
const noSessionsDiv = document.getElementById('noSessions');
|
|
1451
|
+
|
|
1452
|
+
// Filter conversations based on current filter
|
|
1453
|
+
let filteredConversations = allConversations;
|
|
1454
|
+
if (currentFilter !== 'all') {
|
|
1455
|
+
filteredConversations = allConversations.filter(conv => conv.status === currentFilter);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
if (filteredConversations.length === 0) {
|
|
1459
|
+
tableBody.innerHTML = '';
|
|
1460
|
+
noSessionsDiv.style.display = 'block';
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
noSessionsDiv.style.display = 'none';
|
|
1465
|
+
|
|
1466
|
+
tableBody.innerHTML = filteredConversations.map(conv => `
|
|
1467
|
+
<tr onclick="showSessionDetail('${conv.id}')" style="cursor: pointer;">
|
|
1468
|
+
<td>
|
|
1469
|
+
<div class="session-id-container">
|
|
1470
|
+
<div class="session-id">
|
|
1471
|
+
${conv.id.substring(0, 8)}...
|
|
1472
|
+
${conv.runningProcess ? `<span class="process-indicator" title="Active claude process (PID: ${conv.runningProcess.pid})"></span>` : ''}
|
|
1473
|
+
</div>
|
|
1474
|
+
<div class="status-squares">
|
|
1475
|
+
${generateStatusSquaresHTML(conv.statusSquares || [])}
|
|
1476
|
+
</div>
|
|
1477
|
+
</div>
|
|
1478
|
+
</td>
|
|
1479
|
+
<td class="session-project">${conv.project}</td>
|
|
1480
|
+
<td class="session-model" title="${conv.modelInfo ? conv.modelInfo.primaryModel + ' (' + conv.modelInfo.currentServiceTier + ')' : 'N/A'}">${conv.modelInfo ? conv.modelInfo.primaryModel : 'N/A'}</td>
|
|
1481
|
+
<td class="session-messages">${conv.messageCount}</td>
|
|
1482
|
+
<td class="session-tokens">${conv.tokens.toLocaleString()}</td>
|
|
1483
|
+
<td class="session-time">${formatTime(conv.lastModified)}</td>
|
|
1484
|
+
<td class="conversation-state ${getStateClass(conv.conversationState)}">${conv.conversationState}</td>
|
|
1485
|
+
<td class="status-${conv.status}">${conv.status}</td>
|
|
1486
|
+
</tr>
|
|
1487
|
+
`).join('');
|
|
1488
|
+
|
|
1489
|
+
// NEW: Add orphan processes (active claude commands without conversation)
|
|
1490
|
+
if (window.allData && window.allData.orphanProcesses && window.allData.orphanProcesses.length > 0) {
|
|
1491
|
+
const orphanRows = window.allData.orphanProcesses.map(process => `
|
|
1492
|
+
<tr style="background: rgba(248, 81, 73, 0.1); cursor: default;">
|
|
1493
|
+
<td>
|
|
1494
|
+
<div class="session-id-container">
|
|
1495
|
+
<div class="session-id">
|
|
1496
|
+
orphan-${process.pid}
|
|
1497
|
+
<span class="process-indicator orphan" title="Orphan claude process (PID: ${process.pid})"></span>
|
|
1498
|
+
</div>
|
|
1499
|
+
</div>
|
|
1500
|
+
</td>
|
|
1501
|
+
<td class="session-project">${process.workingDir}</td>
|
|
1502
|
+
<td class="session-model">Unknown</td>
|
|
1503
|
+
<td class="session-messages">-</td>
|
|
1504
|
+
<td class="session-tokens">-</td>
|
|
1505
|
+
<td class="session-time">Running</td>
|
|
1506
|
+
<td class="conversation-state">Active process</td>
|
|
1507
|
+
<td class="status-active">orphan</td>
|
|
1508
|
+
</tr>
|
|
1509
|
+
`).join('');
|
|
1510
|
+
|
|
1511
|
+
if (currentFilter === 'active' || currentFilter === 'all') {
|
|
1512
|
+
tableBody.innerHTML += orphanRows;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function formatTime(date) {
|
|
1518
|
+
const now = new Date();
|
|
1519
|
+
const diff = now - new Date(date);
|
|
1520
|
+
const minutes = Math.floor(diff / (1000 * 60));
|
|
1521
|
+
const hours = Math.floor(minutes / 60);
|
|
1522
|
+
const days = Math.floor(hours / 24);
|
|
1523
|
+
|
|
1524
|
+
if (minutes < 1) return 'now';
|
|
1525
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
1526
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1527
|
+
return `${days}d ago`;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function formatMessageTime(timestamp) {
|
|
1531
|
+
const date = new Date(timestamp);
|
|
1532
|
+
return date.toLocaleTimeString('en-US', {
|
|
1533
|
+
hour12: false,
|
|
1534
|
+
hour: '2-digit',
|
|
1535
|
+
minute: '2-digit',
|
|
1536
|
+
second: '2-digit'
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function getStateClass(conversationState) {
|
|
1541
|
+
if (conversationState.includes('working') || conversationState.includes('Working')) {
|
|
1542
|
+
return 'working';
|
|
1543
|
+
}
|
|
1544
|
+
if (conversationState.includes('typing') || conversationState.includes('Typing')) {
|
|
1545
|
+
return 'typing';
|
|
1546
|
+
}
|
|
1547
|
+
return '';
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function generateStatusSquaresHTML(statusSquares) {
|
|
1551
|
+
if (!statusSquares || statusSquares.length === 0) {
|
|
1552
|
+
return '';
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
return statusSquares.map(square =>
|
|
1556
|
+
`<div class="status-square ${square.type}" data-tooltip="${square.tooltip}"></div>`
|
|
1557
|
+
).join('');
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
function getMessageType(message) {
|
|
1561
|
+
if (message.role === 'user') {
|
|
1562
|
+
return {
|
|
1563
|
+
type: 'pending',
|
|
1564
|
+
tooltip: 'User input'
|
|
1565
|
+
};
|
|
1566
|
+
} else if (message.role === 'assistant') {
|
|
1567
|
+
const content = message.content || '';
|
|
1568
|
+
|
|
1569
|
+
if (typeof content === 'string') {
|
|
1570
|
+
if (content.includes('[Tool:') || content.includes('tool_use')) {
|
|
1571
|
+
return {
|
|
1572
|
+
type: 'tool',
|
|
1573
|
+
tooltip: 'Tool execution'
|
|
1574
|
+
};
|
|
1575
|
+
} else if (content.includes('error') || content.includes('Error') || content.includes('failed')) {
|
|
1576
|
+
return {
|
|
1577
|
+
type: 'error',
|
|
1578
|
+
tooltip: 'Error in response'
|
|
1579
|
+
};
|
|
1580
|
+
} else {
|
|
1581
|
+
return {
|
|
1582
|
+
type: 'success',
|
|
1583
|
+
tooltip: 'Successful response'
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
return {
|
|
1590
|
+
type: 'success',
|
|
1591
|
+
tooltip: 'Message'
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// Filter button handlers
|
|
1596
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1597
|
+
const filterButtons = document.querySelectorAll('.filter-btn');
|
|
1598
|
+
|
|
1599
|
+
filterButtons.forEach(button => {
|
|
1600
|
+
button.addEventListener('click', function() {
|
|
1601
|
+
// Remove active class from all buttons
|
|
1602
|
+
filterButtons.forEach(btn => btn.classList.remove('active'));
|
|
1603
|
+
|
|
1604
|
+
// Add active class to clicked button
|
|
1605
|
+
this.classList.add('active');
|
|
1606
|
+
|
|
1607
|
+
// Update current filter
|
|
1608
|
+
currentFilter = this.dataset.filter;
|
|
1609
|
+
|
|
1610
|
+
// Update table
|
|
1611
|
+
updateSessionsTable();
|
|
1612
|
+
});
|
|
1613
|
+
});
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
// Session detail functions
|
|
1617
|
+
async function showSessionDetail(sessionId) {
|
|
1618
|
+
currentSession = allConversations.find(conv => conv.id === sessionId);
|
|
1619
|
+
if (!currentSession) return;
|
|
1620
|
+
|
|
1621
|
+
// Hide sessions list and show detail
|
|
1622
|
+
document.querySelector('.filter-bar').style.display = 'none';
|
|
1623
|
+
document.querySelector('.sessions-table').style.display = 'none';
|
|
1624
|
+
document.getElementById('noSessions').style.display = 'none';
|
|
1625
|
+
document.getElementById('sessionDetail').classList.add('active');
|
|
1626
|
+
|
|
1627
|
+
// Update title
|
|
1628
|
+
document.getElementById('detailTitle').textContent = `conversation: ${sessionId.substring(0, 8)}...`;
|
|
1629
|
+
|
|
1630
|
+
// Load session info
|
|
1631
|
+
updateSessionInfo(currentSession);
|
|
1632
|
+
|
|
1633
|
+
// Load conversation history
|
|
1634
|
+
await loadConversationHistory(currentSession);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function showSessionsList() {
|
|
1638
|
+
document.getElementById('sessionDetail').classList.remove('active');
|
|
1639
|
+
document.querySelector('.filter-bar').style.display = 'flex';
|
|
1640
|
+
document.querySelector('.sessions-table').style.display = 'table';
|
|
1641
|
+
updateSessionsTable();
|
|
1642
|
+
currentSession = null;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function updateSessionInfo(session) {
|
|
1646
|
+
const container = document.getElementById('sessionInfo');
|
|
1647
|
+
|
|
1648
|
+
container.innerHTML = `
|
|
1649
|
+
<div class="info-item">
|
|
1650
|
+
<div class="info-label">conversation id</div>
|
|
1651
|
+
<div class="info-value">${session.id}</div>
|
|
1652
|
+
</div>
|
|
1653
|
+
<div class="info-item">
|
|
1654
|
+
<div class="info-label">project</div>
|
|
1655
|
+
<div class="info-value">${session.project}</div>
|
|
1656
|
+
</div>
|
|
1657
|
+
<div class="info-item">
|
|
1658
|
+
<div class="info-label">messages</div>
|
|
1659
|
+
<div class="info-value">${session.messageCount}</div>
|
|
1660
|
+
</div>
|
|
1661
|
+
<div class="info-item">
|
|
1662
|
+
<div class="info-label">total tokens</div>
|
|
1663
|
+
<div class="info-value">${session.tokens.toLocaleString()}</div>
|
|
1664
|
+
</div>
|
|
1665
|
+
<div class="info-item">
|
|
1666
|
+
<div class="info-label">model</div>
|
|
1667
|
+
<div class="info-value model">${session.modelInfo ? session.modelInfo.primaryModel : 'N/A'}</div>
|
|
1668
|
+
</div>
|
|
1669
|
+
<div class="info-item">
|
|
1670
|
+
<div class="info-label">service tier</div>
|
|
1671
|
+
<div class="info-value">${session.modelInfo ? session.modelInfo.currentServiceTier : 'N/A'}</div>
|
|
1672
|
+
</div>
|
|
1673
|
+
<div class="info-item">
|
|
1674
|
+
<div class="info-label">token details</div>
|
|
1675
|
+
<div class="info-value">${session.tokenUsage ? session.tokenUsage.inputTokens.toLocaleString() + ' in / ' + session.tokenUsage.outputTokens.toLocaleString() + ' out' : 'N/A'}</div>
|
|
1676
|
+
</div>
|
|
1677
|
+
${session.tokenUsage && (session.tokenUsage.cacheCreationTokens > 0 || session.tokenUsage.cacheReadTokens > 0) ?
|
|
1678
|
+
`<div class="info-item">
|
|
1679
|
+
<div class="info-label">cache tokens</div>
|
|
1680
|
+
<div class="info-value">${session.tokenUsage.cacheCreationTokens.toLocaleString()} created / ${session.tokenUsage.cacheReadTokens.toLocaleString()} read</div>
|
|
1681
|
+
</div>` : ''
|
|
1682
|
+
}
|
|
1683
|
+
<div class="info-item">
|
|
1684
|
+
<div class="info-label">file size</div>
|
|
1685
|
+
<div class="info-value">${formatBytes(session.fileSize)}</div>
|
|
1686
|
+
</div>
|
|
1687
|
+
<div class="info-item">
|
|
1688
|
+
<div class="info-label">created</div>
|
|
1689
|
+
<div class="info-value">${new Date(session.created).toLocaleString()}</div>
|
|
1690
|
+
</div>
|
|
1691
|
+
<div class="info-item">
|
|
1692
|
+
<div class="info-label">last modified</div>
|
|
1693
|
+
<div class="info-value">${new Date(session.lastModified).toLocaleString()}</div>
|
|
1694
|
+
</div>
|
|
1695
|
+
<div class="info-item">
|
|
1696
|
+
<div class="info-label">conversation state</div>
|
|
1697
|
+
<div class="info-value conversation-state ${getStateClass(session.conversationState)}">${session.conversationState}</div>
|
|
1698
|
+
</div>
|
|
1699
|
+
<div class="info-item">
|
|
1700
|
+
<div class="info-label">status</div>
|
|
1701
|
+
<div class="info-value status-${session.status}">${session.status}</div>
|
|
1702
|
+
</div>
|
|
1703
|
+
`;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
async function loadConversationHistory(session) {
|
|
1707
|
+
try {
|
|
1708
|
+
const response = await fetch(`/api/session/${session.id}`);
|
|
1709
|
+
const sessionData = await response.json();
|
|
1710
|
+
|
|
1711
|
+
const container = document.getElementById('conversationHistory');
|
|
1712
|
+
const searchInput = document.getElementById('conversationSearch');
|
|
1713
|
+
|
|
1714
|
+
if (!sessionData.messages || sessionData.messages.length === 0) {
|
|
1715
|
+
container.innerHTML = '<div style="padding: 20px; text-align: center; color: #7d8590;">no messages found</div>';
|
|
1716
|
+
searchInput.style.display = 'none';
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
searchInput.style.display = 'block';
|
|
1720
|
+
|
|
1721
|
+
const renderMessages = (filter = '') => {
|
|
1722
|
+
const lowerCaseFilter = filter.toLowerCase();
|
|
1723
|
+
const filteredMessages = sessionData.messages.filter(m =>
|
|
1724
|
+
(m.content || '').toLowerCase().includes(lowerCaseFilter)
|
|
1725
|
+
);
|
|
1726
|
+
|
|
1727
|
+
if (filteredMessages.length === 0) {
|
|
1728
|
+
container.innerHTML = '<div style="padding: 20px; text-align: center; color: #7d8590;">No messages match your search.</div>';
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
const reversedMessages = filteredMessages.slice().reverse();
|
|
1733
|
+
|
|
1734
|
+
container.innerHTML = reversedMessages.map((message, index) => {
|
|
1735
|
+
const messageType = getMessageType(message);
|
|
1736
|
+
const messageNum = filteredMessages.length - index;
|
|
1737
|
+
return `
|
|
1738
|
+
<div class="message">
|
|
1739
|
+
<div class="message-type-indicator ${messageType.type}" data-tooltip="${messageType.tooltip}"></div>
|
|
1740
|
+
<div class="message-header">
|
|
1741
|
+
<div class="message-role ${message.role}">${message.role}</div>
|
|
1742
|
+
<div class="message-time">
|
|
1743
|
+
#${messageNum} • ${message.timestamp ? formatMessageTime(message.timestamp) : 'unknown time'}
|
|
1744
|
+
</div>
|
|
1745
|
+
</div>
|
|
1746
|
+
<div class="message-content">${truncateContent(message.content || 'no content')}</div>
|
|
1747
|
+
</div>
|
|
1748
|
+
`;
|
|
1749
|
+
}).join('');
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
renderMessages();
|
|
1753
|
+
|
|
1754
|
+
searchInput.addEventListener('input', () => {
|
|
1755
|
+
renderMessages(searchInput.value);
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
document.getElementById('conversationHistory').innerHTML =
|
|
1760
|
+
'<div style="padding: 20px; text-align: center; color: #f85149;">error loading conversation history</div>';
|
|
1761
|
+
console.error('Failed to load conversation history:', error);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
function truncateContent(content, maxLength = 1000) {
|
|
1766
|
+
if (typeof content !== 'string') return 'no content';
|
|
1767
|
+
if (!content.trim()) return 'empty message';
|
|
1768
|
+
if (content.length <= maxLength) return content;
|
|
1769
|
+
return content.substring(0, maxLength) + '\n\n[... message truncated ...]';
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function formatBytes(bytes) {
|
|
1773
|
+
if (bytes === 0) return '0 B';
|
|
1774
|
+
const k = 1024;
|
|
1775
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
1776
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1777
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
function exportSession() {
|
|
1781
|
+
if (!currentSession) return;
|
|
1782
|
+
|
|
1783
|
+
const format = document.getElementById('exportFormat').value;
|
|
1784
|
+
|
|
1785
|
+
// Fetch conversation history and export
|
|
1786
|
+
fetch(`/api/session/${currentSession.id}`)
|
|
1787
|
+
.then(response => response.json())
|
|
1788
|
+
.then(sessionData => {
|
|
1789
|
+
if (format === 'csv') {
|
|
1790
|
+
exportSessionAsCSV(sessionData);
|
|
1791
|
+
} else if (format === 'json') {
|
|
1792
|
+
exportSessionAsJSON(sessionData);
|
|
1793
|
+
}
|
|
1794
|
+
})
|
|
1795
|
+
.catch(error => {
|
|
1796
|
+
console.error(`Failed to export ${format.toUpperCase()}:`, error);
|
|
1797
|
+
alert(`Failed to export ${format.toUpperCase()}. Please try again.`);
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function exportSessionAsCSV(sessionData) {
|
|
1802
|
+
// Create CSV content
|
|
1803
|
+
let csvContent = 'Conversation ID,Project,Message Count,Tokens,File Size,Created,Last Modified,Conversation State,Status\n';
|
|
1804
|
+
csvContent += `"${currentSession.id}","${currentSession.project}",${currentSession.messageCount},${currentSession.tokens},${currentSession.fileSize},"${new Date(currentSession.created).toISOString()}","${new Date(currentSession.lastModified).toISOString()}","${currentSession.conversationState}","${currentSession.status}"\n\n`;
|
|
1805
|
+
|
|
1806
|
+
csvContent += 'Message #,Role,Timestamp,Content\n';
|
|
1807
|
+
|
|
1808
|
+
// Add conversation history
|
|
1809
|
+
if (sessionData.messages) {
|
|
1810
|
+
sessionData.messages.forEach((message, index) => {
|
|
1811
|
+
const content = (message.content || 'no content').replace(/"/g, '""');
|
|
1812
|
+
const timestamp = message.timestamp ? new Date(message.timestamp).toISOString() : 'unknown';
|
|
1813
|
+
csvContent += `${index + 1},"${message.role}","${timestamp}","${content}"\n`;
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// Download CSV
|
|
1818
|
+
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
1819
|
+
downloadFile(blob, `claude-conversation-${currentSession.id.substring(0, 8)}.csv`);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
function exportSessionAsJSON(sessionData) {
|
|
1823
|
+
// Create comprehensive JSON export
|
|
1824
|
+
const exportData = {
|
|
1825
|
+
conversation: {
|
|
1826
|
+
id: currentSession.id,
|
|
1827
|
+
filename: currentSession.filename,
|
|
1828
|
+
project: currentSession.project,
|
|
1829
|
+
messageCount: currentSession.messageCount,
|
|
1830
|
+
tokens: currentSession.tokens,
|
|
1831
|
+
fileSize: currentSession.fileSize,
|
|
1832
|
+
created: currentSession.created,
|
|
1833
|
+
lastModified: currentSession.lastModified,
|
|
1834
|
+
conversationState: currentSession.conversationState,
|
|
1835
|
+
status: currentSession.status
|
|
1836
|
+
},
|
|
1837
|
+
messages: sessionData.messages || [],
|
|
1838
|
+
metadata: {
|
|
1839
|
+
exportedAt: new Date().toISOString(),
|
|
1840
|
+
exportFormat: 'json',
|
|
1841
|
+
toolVersion: '1.5.7'
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
|
|
1845
|
+
// Download JSON
|
|
1846
|
+
const jsonString = JSON.stringify(exportData, null, 2);
|
|
1847
|
+
const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8;' });
|
|
1848
|
+
downloadFile(blob, `claude-conversation-${currentSession.id.substring(0, 8)}.json`);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function downloadFile(blob, filename) {
|
|
1852
|
+
const link = document.createElement('a');
|
|
1853
|
+
const url = URL.createObjectURL(blob);
|
|
1854
|
+
link.setAttribute('href', url);
|
|
1855
|
+
link.setAttribute('download', filename);
|
|
1856
|
+
link.style.visibility = 'hidden';
|
|
1857
|
+
document.body.appendChild(link);
|
|
1858
|
+
link.click();
|
|
1859
|
+
document.body.removeChild(link);
|
|
1860
|
+
URL.revokeObjectURL(url);
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
function refreshSessionDetail() {
|
|
1864
|
+
if (currentSession) {
|
|
1865
|
+
loadConversationHistory(currentSession);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// Manual refresh function
|
|
1870
|
+
async function forceRefresh() {
|
|
1871
|
+
try {
|
|
1872
|
+
const response = await fetch('/api/refresh');
|
|
1873
|
+
const result = await response.json();
|
|
1874
|
+
console.log('Manual refresh:', result);
|
|
1875
|
+
await loadData();
|
|
1876
|
+
} catch (error) {
|
|
1877
|
+
console.error('Failed to refresh:', error);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// Wait for DOM and Chart.js to load
|
|
1882
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1883
|
+
// Check if Chart.js is loaded
|
|
1884
|
+
function initWhenReady() {
|
|
1885
|
+
if (typeof Chart !== 'undefined') {
|
|
1886
|
+
console.log('Chart.js loaded successfully');
|
|
1887
|
+
loadData();
|
|
1888
|
+
|
|
1889
|
+
// Regular refresh for conversation data every 1 second (slower)
|
|
1890
|
+
setInterval(() => {
|
|
1891
|
+
loadConversationData();
|
|
1892
|
+
}, 1000);
|
|
1893
|
+
|
|
1894
|
+
// NEW: Ultra-fast refresh ONLY for conversation states (every 100ms)
|
|
1895
|
+
setInterval(() => {
|
|
1896
|
+
updateConversationStatesOnly();
|
|
1897
|
+
}, 100);
|
|
1898
|
+
} else {
|
|
1899
|
+
console.log('Waiting for Chart.js to load...');
|
|
1900
|
+
setTimeout(initWhenReady, 100);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
initWhenReady();
|
|
1905
|
+
|
|
1906
|
+
// Add event listeners for date inputs
|
|
1907
|
+
document.getElementById('dateFrom').addEventListener('change', refreshCharts);
|
|
1908
|
+
document.getElementById('dateTo').addEventListener('change', refreshCharts);
|
|
1909
|
+
|
|
1910
|
+
// Initialize notification button state
|
|
1911
|
+
updateNotificationButtonState();
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
function updateNotificationButtonState() {
|
|
1915
|
+
const btn = document.getElementById('notificationBtn');
|
|
1916
|
+
if (!btn) return;
|
|
1917
|
+
|
|
1918
|
+
if (Notification.permission === 'granted') {
|
|
1919
|
+
notificationsEnabled = true;
|
|
1920
|
+
btn.textContent = 'notifications on';
|
|
1921
|
+
btn.style.borderColor = '#3fb950';
|
|
1922
|
+
btn.style.color = '#3fb950';
|
|
1923
|
+
} else if (Notification.permission === 'denied') {
|
|
1924
|
+
btn.textContent = 'notifications denied';
|
|
1925
|
+
btn.style.borderColor = '#f85149';
|
|
1926
|
+
btn.style.color = '#f85149';
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
// Add keyboard shortcut for refresh (F5 or Ctrl+R)
|
|
1931
|
+
document.addEventListener('keydown', function(e) {
|
|
1932
|
+
if (e.key === 'F5' || (e.ctrlKey && e.key === 'r')) {
|
|
1933
|
+
e.preventDefault();
|
|
1934
|
+
forceRefresh();
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
</script>
|
|
1938
|
+
</body>
|
|
1939
|
+
</html>
|