@towles/tool 0.0.41 → 0.0.49
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 +67 -109
- package/package.json +51 -41
- package/src/commands/base.ts +3 -18
- package/src/commands/config.ts +9 -8
- package/src/commands/doctor.ts +4 -1
- package/src/commands/gh/branch-clean.ts +10 -4
- package/src/commands/gh/branch.ts +6 -3
- package/src/commands/gh/pr.ts +10 -3
- package/src/commands/graph-template.html +1214 -0
- package/src/commands/graph.test.ts +176 -0
- package/src/commands/graph.ts +970 -0
- package/src/commands/install.ts +8 -2
- package/src/commands/journal/daily-notes.ts +9 -5
- package/src/commands/journal/meeting.ts +12 -6
- package/src/commands/journal/note.ts +12 -6
- package/src/commands/ralph/plan/add.ts +75 -0
- package/src/commands/ralph/plan/done.ts +82 -0
- package/src/commands/ralph/{task → plan}/list.test.ts +5 -5
- package/src/commands/ralph/{task → plan}/list.ts +28 -39
- package/src/commands/ralph/plan/remove.ts +71 -0
- package/src/commands/ralph/run.test.ts +521 -0
- package/src/commands/ralph/run.ts +126 -189
- package/src/commands/ralph/show.ts +88 -0
- package/src/config/settings.ts +8 -27
- package/src/{commands/ralph/lib → lib/ralph}/execution.ts +4 -14
- package/src/lib/ralph/formatter.ts +238 -0
- package/src/{commands/ralph/lib → lib/ralph}/state.ts +17 -42
- package/src/utils/date-utils.test.ts +2 -1
- package/src/utils/date-utils.ts +2 -2
- package/LICENSE.md +0 -20
- package/src/commands/index.ts +0 -55
- package/src/commands/observe/graph.test.ts +0 -89
- package/src/commands/observe/graph.ts +0 -1640
- package/src/commands/observe/report.ts +0 -166
- package/src/commands/observe/session.ts +0 -385
- package/src/commands/observe/setup.ts +0 -180
- package/src/commands/observe/status.ts +0 -146
- package/src/commands/ralph/lib/formatter.ts +0 -298
- package/src/commands/ralph/lib/marker.ts +0 -108
- package/src/commands/ralph/marker/create.ts +0 -23
- package/src/commands/ralph/plan.ts +0 -73
- package/src/commands/ralph/progress.ts +0 -44
- package/src/commands/ralph/ralph.test.ts +0 -673
- package/src/commands/ralph/task/add.ts +0 -105
- package/src/commands/ralph/task/done.ts +0 -73
- package/src/commands/ralph/task/remove.ts +0 -62
- package/src/config/context.ts +0 -7
- package/src/constants.ts +0 -3
- package/src/utils/anthropic/types.ts +0 -158
- package/src/utils/exec.ts +0 -8
- package/src/utils/git/git.ts +0 -25
- /package/src/{commands → lib}/journal/utils.ts +0 -0
- /package/src/{commands/ralph/lib → lib/ralph}/index.ts +0 -0
|
@@ -0,0 +1,1214 @@
|
|
|
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 Usage</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
11
|
+
background: #1a1a2e;
|
|
12
|
+
color: #eee;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
padding: 20px;
|
|
15
|
+
}
|
|
16
|
+
h1 {
|
|
17
|
+
font-size: 1.5rem;
|
|
18
|
+
margin-bottom: 15px;
|
|
19
|
+
color: #fff;
|
|
20
|
+
}
|
|
21
|
+
.container {
|
|
22
|
+
max-width: 1540px;
|
|
23
|
+
margin: 0 auto;
|
|
24
|
+
}
|
|
25
|
+
.main-content {
|
|
26
|
+
display: flex;
|
|
27
|
+
gap: 20px;
|
|
28
|
+
}
|
|
29
|
+
.treemap-wrapper {
|
|
30
|
+
flex: 1;
|
|
31
|
+
}
|
|
32
|
+
.detail-panel {
|
|
33
|
+
width: 280px;
|
|
34
|
+
flex-shrink: 0;
|
|
35
|
+
background: #16213e;
|
|
36
|
+
border-radius: 8px;
|
|
37
|
+
padding: 16px;
|
|
38
|
+
height: fit-content;
|
|
39
|
+
max-height: 800px;
|
|
40
|
+
overflow-y: auto;
|
|
41
|
+
}
|
|
42
|
+
.detail-panel.empty {
|
|
43
|
+
color: #666;
|
|
44
|
+
font-size: 0.9rem;
|
|
45
|
+
text-align: center;
|
|
46
|
+
padding: 40px 16px;
|
|
47
|
+
}
|
|
48
|
+
.detail-title {
|
|
49
|
+
font-weight: 600;
|
|
50
|
+
font-size: 1rem;
|
|
51
|
+
margin-bottom: 12px;
|
|
52
|
+
color: #fff;
|
|
53
|
+
word-break: break-word;
|
|
54
|
+
}
|
|
55
|
+
.detail-row {
|
|
56
|
+
display: flex;
|
|
57
|
+
justify-content: space-between;
|
|
58
|
+
gap: 12px;
|
|
59
|
+
margin: 6px 0;
|
|
60
|
+
font-size: 0.85rem;
|
|
61
|
+
}
|
|
62
|
+
.detail-label { color: #888; }
|
|
63
|
+
.detail-value { font-weight: 500; color: #ccc; }
|
|
64
|
+
.detail-actions {
|
|
65
|
+
margin-top: 16px;
|
|
66
|
+
padding-top: 12px;
|
|
67
|
+
border-top: 1px solid #333;
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-direction: column;
|
|
70
|
+
gap: 8px;
|
|
71
|
+
}
|
|
72
|
+
.detail-btn {
|
|
73
|
+
background: #2a2a4a;
|
|
74
|
+
border: 1px solid #444;
|
|
75
|
+
color: #6b9fff;
|
|
76
|
+
padding: 8px 12px;
|
|
77
|
+
border-radius: 4px;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
font-size: 0.85rem;
|
|
80
|
+
text-align: left;
|
|
81
|
+
}
|
|
82
|
+
.detail-btn:hover {
|
|
83
|
+
background: #3a3a5a;
|
|
84
|
+
border-color: #666;
|
|
85
|
+
}
|
|
86
|
+
.legend {
|
|
87
|
+
display: flex;
|
|
88
|
+
gap: 20px;
|
|
89
|
+
margin-bottom: 15px;
|
|
90
|
+
font-size: 0.85rem;
|
|
91
|
+
}
|
|
92
|
+
.legend-item {
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
gap: 6px;
|
|
96
|
+
}
|
|
97
|
+
.legend-color {
|
|
98
|
+
width: 20px;
|
|
99
|
+
height: 14px;
|
|
100
|
+
border-radius: 3px;
|
|
101
|
+
}
|
|
102
|
+
#treemap {
|
|
103
|
+
position: relative;
|
|
104
|
+
width: {{WIDTH}}px;
|
|
105
|
+
height: {{HEIGHT}}px;
|
|
106
|
+
background: #16213e;
|
|
107
|
+
border-radius: 8px;
|
|
108
|
+
overflow: hidden;
|
|
109
|
+
}
|
|
110
|
+
#barchart {
|
|
111
|
+
display: none;
|
|
112
|
+
width: {{WIDTH}}px;
|
|
113
|
+
height: {{HEIGHT}}px;
|
|
114
|
+
background: #16213e;
|
|
115
|
+
border-radius: 8px;
|
|
116
|
+
overflow: hidden;
|
|
117
|
+
}
|
|
118
|
+
#barchart svg {
|
|
119
|
+
display: block;
|
|
120
|
+
}
|
|
121
|
+
.bar-segment {
|
|
122
|
+
cursor: pointer;
|
|
123
|
+
transition: opacity 0.15s;
|
|
124
|
+
}
|
|
125
|
+
.bar-segment:hover {
|
|
126
|
+
opacity: 0.85;
|
|
127
|
+
}
|
|
128
|
+
.bar-segment.selected {
|
|
129
|
+
stroke: #fff;
|
|
130
|
+
stroke-width: 2;
|
|
131
|
+
}
|
|
132
|
+
.axis text {
|
|
133
|
+
fill: #888;
|
|
134
|
+
font-size: 11px;
|
|
135
|
+
}
|
|
136
|
+
.axis line, .axis path {
|
|
137
|
+
stroke: #444;
|
|
138
|
+
}
|
|
139
|
+
.axis-label {
|
|
140
|
+
fill: #888;
|
|
141
|
+
font-size: 12px;
|
|
142
|
+
}
|
|
143
|
+
.node {
|
|
144
|
+
position: absolute;
|
|
145
|
+
overflow: hidden;
|
|
146
|
+
border-radius: 3px;
|
|
147
|
+
transition: opacity 0.15s;
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
}
|
|
150
|
+
.node:hover {
|
|
151
|
+
opacity: 0.85;
|
|
152
|
+
}
|
|
153
|
+
.node-label {
|
|
154
|
+
font-size: 11px;
|
|
155
|
+
padding: 2px 4px;
|
|
156
|
+
white-space: nowrap;
|
|
157
|
+
overflow: hidden;
|
|
158
|
+
text-overflow: ellipsis;
|
|
159
|
+
color: #fff;
|
|
160
|
+
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
|
|
161
|
+
}
|
|
162
|
+
.node-group {
|
|
163
|
+
background: rgba(255,255,255,0.05);
|
|
164
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
165
|
+
}
|
|
166
|
+
.node-group .node-label {
|
|
167
|
+
font-weight: 600;
|
|
168
|
+
font-size: 12px;
|
|
169
|
+
color: rgba(255,255,255,0.9);
|
|
170
|
+
}
|
|
171
|
+
.tooltip {
|
|
172
|
+
position: fixed;
|
|
173
|
+
background: #2a2a4a;
|
|
174
|
+
border: 1px solid #444;
|
|
175
|
+
border-radius: 6px;
|
|
176
|
+
padding: 12px;
|
|
177
|
+
font-size: 0.85rem;
|
|
178
|
+
pointer-events: none;
|
|
179
|
+
z-index: 1000;
|
|
180
|
+
max-width: 300px;
|
|
181
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
|
182
|
+
display: none;
|
|
183
|
+
}
|
|
184
|
+
.tooltip-title {
|
|
185
|
+
font-weight: 600;
|
|
186
|
+
margin-bottom: 8px;
|
|
187
|
+
color: #fff;
|
|
188
|
+
}
|
|
189
|
+
.tooltip-row {
|
|
190
|
+
display: flex;
|
|
191
|
+
justify-content: space-between;
|
|
192
|
+
gap: 20px;
|
|
193
|
+
margin: 4px 0;
|
|
194
|
+
color: #ccc;
|
|
195
|
+
}
|
|
196
|
+
.tooltip-label { color: #888; }
|
|
197
|
+
.tooltip-value { font-weight: 500; }
|
|
198
|
+
.ratio-good { color: #4ade80; }
|
|
199
|
+
.ratio-moderate { color: #fbbf24; }
|
|
200
|
+
.ratio-high { color: #f87171; }
|
|
201
|
+
.tooltip-link {
|
|
202
|
+
color: #6b9fff;
|
|
203
|
+
cursor: pointer;
|
|
204
|
+
text-decoration: none;
|
|
205
|
+
}
|
|
206
|
+
.tooltip-link:hover {
|
|
207
|
+
text-decoration: underline;
|
|
208
|
+
}
|
|
209
|
+
.tooltip-actions {
|
|
210
|
+
margin-top: 10px;
|
|
211
|
+
padding-top: 8px;
|
|
212
|
+
border-top: 1px solid #444;
|
|
213
|
+
display: flex;
|
|
214
|
+
gap: 12px;
|
|
215
|
+
font-size: 0.8rem;
|
|
216
|
+
}
|
|
217
|
+
.tool-table {
|
|
218
|
+
margin-top: 8px;
|
|
219
|
+
width: 100%;
|
|
220
|
+
border-collapse: collapse;
|
|
221
|
+
font-size: 0.8rem;
|
|
222
|
+
}
|
|
223
|
+
.tool-table th {
|
|
224
|
+
text-align: left;
|
|
225
|
+
color: #888;
|
|
226
|
+
font-weight: 500;
|
|
227
|
+
padding: 3px 6px 3px 0;
|
|
228
|
+
border-bottom: 1px solid #444;
|
|
229
|
+
}
|
|
230
|
+
.tool-table td {
|
|
231
|
+
padding: 3px 6px 3px 0;
|
|
232
|
+
color: #ccc;
|
|
233
|
+
}
|
|
234
|
+
.tool-table td:last-child,
|
|
235
|
+
.tool-table th:last-child {
|
|
236
|
+
text-align: right;
|
|
237
|
+
padding-right: 0;
|
|
238
|
+
}
|
|
239
|
+
.tool-table-header {
|
|
240
|
+
color: #888;
|
|
241
|
+
font-size: 0.75rem;
|
|
242
|
+
margin-top: 10px;
|
|
243
|
+
margin-bottom: 4px;
|
|
244
|
+
}
|
|
245
|
+
.stats {
|
|
246
|
+
margin-top: 15px;
|
|
247
|
+
font-size: 0.85rem;
|
|
248
|
+
color: #888;
|
|
249
|
+
}
|
|
250
|
+
.breadcrumb {
|
|
251
|
+
margin-bottom: 10px;
|
|
252
|
+
font-size: 0.9rem;
|
|
253
|
+
color: #aaa;
|
|
254
|
+
}
|
|
255
|
+
.crumb {
|
|
256
|
+
color: #6b9fff;
|
|
257
|
+
}
|
|
258
|
+
.crumb:hover:not(.current) {
|
|
259
|
+
text-decoration: underline;
|
|
260
|
+
}
|
|
261
|
+
.crumb.current {
|
|
262
|
+
color: #fff;
|
|
263
|
+
cursor: default;
|
|
264
|
+
}
|
|
265
|
+
.crumb-sep {
|
|
266
|
+
color: #666;
|
|
267
|
+
margin: 0 4px;
|
|
268
|
+
}
|
|
269
|
+
.controls {
|
|
270
|
+
display: flex;
|
|
271
|
+
align-items: center;
|
|
272
|
+
gap: 30px;
|
|
273
|
+
margin-bottom: 15px;
|
|
274
|
+
flex-wrap: wrap;
|
|
275
|
+
}
|
|
276
|
+
.tile-selector, .min-tokens {
|
|
277
|
+
display: flex;
|
|
278
|
+
align-items: center;
|
|
279
|
+
gap: 8px;
|
|
280
|
+
}
|
|
281
|
+
.tile-selector label, .min-tokens label {
|
|
282
|
+
color: #888;
|
|
283
|
+
font-size: 0.85rem;
|
|
284
|
+
}
|
|
285
|
+
.tile-selector select, .min-tokens select {
|
|
286
|
+
background: #2a2a4a;
|
|
287
|
+
color: #fff;
|
|
288
|
+
border: 1px solid #444;
|
|
289
|
+
border-radius: 4px;
|
|
290
|
+
padding: 4px 8px;
|
|
291
|
+
font-size: 0.85rem;
|
|
292
|
+
cursor: pointer;
|
|
293
|
+
}
|
|
294
|
+
.tile-selector select:hover, .min-tokens select:hover {
|
|
295
|
+
border-color: #666;
|
|
296
|
+
}
|
|
297
|
+
.view-toggle {
|
|
298
|
+
display: flex;
|
|
299
|
+
align-items: center;
|
|
300
|
+
gap: 8px;
|
|
301
|
+
}
|
|
302
|
+
.view-toggle label {
|
|
303
|
+
color: #888;
|
|
304
|
+
font-size: 0.85rem;
|
|
305
|
+
}
|
|
306
|
+
.view-toggle-btn {
|
|
307
|
+
background: #2a2a4a;
|
|
308
|
+
color: #fff;
|
|
309
|
+
border: 1px solid #444;
|
|
310
|
+
border-radius: 4px;
|
|
311
|
+
padding: 4px 12px;
|
|
312
|
+
font-size: 0.85rem;
|
|
313
|
+
cursor: pointer;
|
|
314
|
+
display: flex;
|
|
315
|
+
align-items: center;
|
|
316
|
+
gap: 6px;
|
|
317
|
+
}
|
|
318
|
+
.view-toggle-btn:hover {
|
|
319
|
+
border-color: #666;
|
|
320
|
+
background: #3a3a5a;
|
|
321
|
+
}
|
|
322
|
+
.view-toggle-btn.active {
|
|
323
|
+
background: #4a4a6a;
|
|
324
|
+
border-color: #6b9fff;
|
|
325
|
+
}
|
|
326
|
+
</style>
|
|
327
|
+
<script src="https://cdn.jsdelivr.net/npm/d3-hierarchy@3"></script>
|
|
328
|
+
</head>
|
|
329
|
+
<body>
|
|
330
|
+
<div class="container">
|
|
331
|
+
<h1>Claude Code Usage</h1>
|
|
332
|
+
|
|
333
|
+
<div class="controls">
|
|
334
|
+
<div class="legend" id="legend"></div>
|
|
335
|
+
<div class="tile-selector">
|
|
336
|
+
<label for="tileMethod">Layout:</label>
|
|
337
|
+
<select id="tileMethod">
|
|
338
|
+
<option value="squarify" selected>Squarify (readable)</option>
|
|
339
|
+
<option value="binary">Binary (balanced)</option>
|
|
340
|
+
<option value="sliceDice">Slice & Dice (alternating)</option>
|
|
341
|
+
</select>
|
|
342
|
+
</div>
|
|
343
|
+
<div class="min-tokens">
|
|
344
|
+
<label for="minTokens">Min tokens:</label>
|
|
345
|
+
<select id="minTokens">
|
|
346
|
+
<option value="0">All</option>
|
|
347
|
+
<option value="100">100+</option>
|
|
348
|
+
<option value="500">500+</option>
|
|
349
|
+
<option value="1000" selected>1K+</option>
|
|
350
|
+
<option value="5000">5K+</option>
|
|
351
|
+
<option value="10000">10K+</option>
|
|
352
|
+
</select>
|
|
353
|
+
</div>
|
|
354
|
+
<div class="view-toggle">
|
|
355
|
+
<label>View:</label>
|
|
356
|
+
<button id="viewTreemap" class="view-toggle-btn">Treemap</button>
|
|
357
|
+
<button id="viewBarChart" class="view-toggle-btn active">Bar Chart</button>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<div class="main-content">
|
|
362
|
+
<div class="treemap-wrapper">
|
|
363
|
+
<div id="treemap"></div>
|
|
364
|
+
<div id="barchart"></div>
|
|
365
|
+
<div class="tooltip" id="tooltip"></div>
|
|
366
|
+
<div class="breadcrumb" id="breadcrumb"></div>
|
|
367
|
+
<div class="stats" id="stats"></div>
|
|
368
|
+
</div>
|
|
369
|
+
<div class="detail-panel empty" id="detailPanel">Click a tile to see details</div>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<script>
|
|
374
|
+
const treeData = {{DATA}};
|
|
375
|
+
const barChartData = {{BAR_CHART_DATA}};
|
|
376
|
+
const container = document.getElementById('treemap');
|
|
377
|
+
const barChartContainer = document.getElementById('barchart');
|
|
378
|
+
const tooltip = document.getElementById('tooltip');
|
|
379
|
+
const stats = document.getElementById('stats');
|
|
380
|
+
const breadcrumb = document.getElementById('breadcrumb');
|
|
381
|
+
const detailPanel = document.getElementById('detailPanel');
|
|
382
|
+
const WIDTH = {{WIDTH}};
|
|
383
|
+
const HEIGHT = {{HEIGHT}};
|
|
384
|
+
|
|
385
|
+
// Controls
|
|
386
|
+
const tileMethodSelect = document.getElementById('tileMethod');
|
|
387
|
+
const minTokensSelect = document.getElementById('minTokens');
|
|
388
|
+
const viewTreemapBtn = document.getElementById('viewTreemap');
|
|
389
|
+
const viewBarChartBtn = document.getElementById('viewBarChart');
|
|
390
|
+
const legendContainer = document.getElementById('legend');
|
|
391
|
+
|
|
392
|
+
// Tool colors for treemap legend
|
|
393
|
+
const toolLegendItems = [
|
|
394
|
+
{ color: '#4ade80', label: 'Read' },
|
|
395
|
+
{ color: '#f87171', label: 'Write' },
|
|
396
|
+
{ color: '#fb923c', label: 'Edit' },
|
|
397
|
+
{ color: '#a78bfa', label: 'Bash' },
|
|
398
|
+
{ color: '#38bdf8', label: 'Glob' },
|
|
399
|
+
{ color: '#22d3ee', label: 'Grep' },
|
|
400
|
+
{ color: '#facc15', label: 'Task' },
|
|
401
|
+
{ color: '#60a5fa', label: 'MCP' },
|
|
402
|
+
];
|
|
403
|
+
|
|
404
|
+
function renderLegend(items) {
|
|
405
|
+
legendContainer.replaceChildren();
|
|
406
|
+
for (const item of items) {
|
|
407
|
+
const div = document.createElement('div');
|
|
408
|
+
div.className = 'legend-item';
|
|
409
|
+
const colorBox = document.createElement('div');
|
|
410
|
+
colorBox.className = 'legend-color';
|
|
411
|
+
colorBox.style.background = item.color;
|
|
412
|
+
const label = document.createElement('span');
|
|
413
|
+
label.textContent = item.label;
|
|
414
|
+
div.appendChild(colorBox);
|
|
415
|
+
div.appendChild(label);
|
|
416
|
+
legendContainer.appendChild(div);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// View state: 'treemap' | 'bar'
|
|
421
|
+
let currentView = 'bar';
|
|
422
|
+
|
|
423
|
+
function showTreemap() {
|
|
424
|
+
currentView = 'treemap';
|
|
425
|
+
container.style.display = 'block';
|
|
426
|
+
barChartContainer.style.display = 'none';
|
|
427
|
+
viewTreemapBtn.classList.add('active');
|
|
428
|
+
viewBarChartBtn.classList.remove('active');
|
|
429
|
+
// Show treemap-specific controls
|
|
430
|
+
tileMethodSelect.parentElement.style.display = 'flex';
|
|
431
|
+
renderLegend(toolLegendItems);
|
|
432
|
+
render();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function showBarChart() {
|
|
436
|
+
currentView = 'bar';
|
|
437
|
+
container.style.display = 'none';
|
|
438
|
+
barChartContainer.style.display = 'block';
|
|
439
|
+
viewTreemapBtn.classList.remove('active');
|
|
440
|
+
viewBarChartBtn.classList.add('active');
|
|
441
|
+
// Hide treemap-specific controls (layout doesn't apply to bar chart)
|
|
442
|
+
tileMethodSelect.parentElement.style.display = 'none';
|
|
443
|
+
// Legend will be populated after rendering with actual projects
|
|
444
|
+
renderBarChart(barChartData);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// View toggle handlers
|
|
448
|
+
viewTreemapBtn.addEventListener('click', showTreemap);
|
|
449
|
+
viewBarChartBtn.addEventListener('click', showBarChart);
|
|
450
|
+
|
|
451
|
+
// Navigation stack for zoom
|
|
452
|
+
let navStack = [treeData];
|
|
453
|
+
|
|
454
|
+
function getCurrentNode() {
|
|
455
|
+
return navStack[navStack.length - 1];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function zoomTo(node) {
|
|
459
|
+
if (node.children && node.children.length > 0) {
|
|
460
|
+
navStack.push(node);
|
|
461
|
+
render();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function zoomOut(index) {
|
|
466
|
+
navStack = navStack.slice(0, index + 1);
|
|
467
|
+
render();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Filter nodes below minTokens threshold
|
|
471
|
+
function filterByMinTokens(node, minTokens) {
|
|
472
|
+
if (minTokens <= 0) return node;
|
|
473
|
+
|
|
474
|
+
function sumValue(n) {
|
|
475
|
+
if (n.value !== undefined && n.value > 0) return n.value;
|
|
476
|
+
if (!n.children) return 0;
|
|
477
|
+
return n.children.reduce((sum, c) => sum + sumValue(c), 0);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function filterNode(n) {
|
|
481
|
+
const val = sumValue(n);
|
|
482
|
+
if (val < minTokens && !n.children) return null;
|
|
483
|
+
|
|
484
|
+
if (!n.children) return n;
|
|
485
|
+
|
|
486
|
+
const filteredChildren = n.children
|
|
487
|
+
.map(filterNode)
|
|
488
|
+
.filter(c => c !== null);
|
|
489
|
+
|
|
490
|
+
if (filteredChildren.length === 0 && val < minTokens) return null;
|
|
491
|
+
|
|
492
|
+
return { ...n, children: filteredChildren.length > 0 ? filteredChildren : undefined };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return filterNode(node) || node;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Get d3 tile method
|
|
499
|
+
function getTileMethod() {
|
|
500
|
+
const method = tileMethodSelect.value;
|
|
501
|
+
switch (method) {
|
|
502
|
+
case 'binary': return d3.treemapBinary;
|
|
503
|
+
case 'sliceDice': return d3.treemapSliceDice;
|
|
504
|
+
default: return d3.treemapSquarify;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function computeLayout(data) {
|
|
509
|
+
const minTokens = parseInt(minTokensSelect.value) || 0;
|
|
510
|
+
const filteredData = filterByMinTokens(data, minTokens);
|
|
511
|
+
|
|
512
|
+
// Use d3-hierarchy for layout
|
|
513
|
+
const root = d3.hierarchy(filteredData)
|
|
514
|
+
.sum(d => d.value || 0)
|
|
515
|
+
.sort((a, b) => (b.value || 0) - (a.value || 0));
|
|
516
|
+
|
|
517
|
+
const layout = d3.treemap()
|
|
518
|
+
.size([WIDTH, HEIGHT])
|
|
519
|
+
.paddingOuter(3)
|
|
520
|
+
.paddingTop(19)
|
|
521
|
+
.paddingInner(1)
|
|
522
|
+
.tile(getTileMethod());
|
|
523
|
+
|
|
524
|
+
layout(root);
|
|
525
|
+
|
|
526
|
+
// Convert to rect array
|
|
527
|
+
return root.descendants().map(d => ({
|
|
528
|
+
x: d.x0,
|
|
529
|
+
y: d.y0,
|
|
530
|
+
width: d.x1 - d.x0,
|
|
531
|
+
height: d.y1 - d.y0,
|
|
532
|
+
depth: d.depth,
|
|
533
|
+
name: d.data.name,
|
|
534
|
+
value: d.value || 0,
|
|
535
|
+
hasChildren: !!d.children?.length,
|
|
536
|
+
sessionId: d.data.sessionId,
|
|
537
|
+
fullSessionId: d.data.fullSessionId,
|
|
538
|
+
filePath: d.data.filePath,
|
|
539
|
+
startTime: d.data.startTime,
|
|
540
|
+
model: d.data.model,
|
|
541
|
+
inputTokens: d.data.inputTokens,
|
|
542
|
+
outputTokens: d.data.outputTokens,
|
|
543
|
+
ratio: d.data.ratio,
|
|
544
|
+
date: d.data.date,
|
|
545
|
+
project: d.data.project,
|
|
546
|
+
repeatedReads: d.data.repeatedReads,
|
|
547
|
+
modelEfficiency: d.data.modelEfficiency,
|
|
548
|
+
tools: d.data.tools,
|
|
549
|
+
toolName: d.data.toolName,
|
|
550
|
+
nodeRef: d.data
|
|
551
|
+
}));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Re-render on control changes
|
|
555
|
+
tileMethodSelect.addEventListener('change', render);
|
|
556
|
+
minTokensSelect.addEventListener('change', render);
|
|
557
|
+
|
|
558
|
+
function render() {
|
|
559
|
+
// Clear container using replaceChildren (safe)
|
|
560
|
+
container.replaceChildren();
|
|
561
|
+
const currentNode = getCurrentNode();
|
|
562
|
+
const rects = computeLayout(currentNode);
|
|
563
|
+
|
|
564
|
+
// Update breadcrumb using DOM methods (safe)
|
|
565
|
+
breadcrumb.replaceChildren();
|
|
566
|
+
navStack.forEach((node, i) => {
|
|
567
|
+
const crumb = document.createElement('span');
|
|
568
|
+
crumb.className = 'crumb' + (i === navStack.length - 1 ? ' current' : '');
|
|
569
|
+
crumb.textContent = node.name;
|
|
570
|
+
if (i < navStack.length - 1) {
|
|
571
|
+
crumb.style.cursor = 'pointer';
|
|
572
|
+
crumb.onclick = () => zoomOut(i);
|
|
573
|
+
}
|
|
574
|
+
breadcrumb.appendChild(crumb);
|
|
575
|
+
if (i < navStack.length - 1) {
|
|
576
|
+
const sep = document.createElement('span');
|
|
577
|
+
sep.className = 'crumb-sep';
|
|
578
|
+
sep.textContent = ' > ';
|
|
579
|
+
breadcrumb.appendChild(sep);
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Calculate totals for current view
|
|
584
|
+
let totalTokens = 0, totalInput = 0, totalOutput = 0;
|
|
585
|
+
rects.forEach(r => {
|
|
586
|
+
if (!r.hasChildren && r.depth > 0) {
|
|
587
|
+
totalTokens += r.value || 0;
|
|
588
|
+
totalInput += r.inputTokens || 0;
|
|
589
|
+
totalOutput += r.outputTokens || 0;
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
const overallRatio = totalOutput > 0 ? (totalInput / totalOutput).toFixed(1) : 'N/A';
|
|
594
|
+
stats.textContent = 'Total: ' + formatTokens(totalTokens) + ' tokens | Input: ' + formatTokens(totalInput) + ' | Output: ' + formatTokens(totalOutput) + ' | Ratio: ' + overallRatio + ':1';
|
|
595
|
+
|
|
596
|
+
// Render nodes
|
|
597
|
+
rects.forEach((r) => {
|
|
598
|
+
if (r.width < 1 || r.height < 1) return;
|
|
599
|
+
|
|
600
|
+
const node = document.createElement('div');
|
|
601
|
+
node.className = 'node' + (r.hasChildren ? ' node-group' : '');
|
|
602
|
+
node.style.left = r.x + 'px';
|
|
603
|
+
node.style.top = r.y + 'px';
|
|
604
|
+
node.style.width = r.width + 'px';
|
|
605
|
+
node.style.height = r.height + 'px';
|
|
606
|
+
|
|
607
|
+
if (r.toolName && r.depth > 0) {
|
|
608
|
+
node.style.background = getColor(r.toolName);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (r.width > 30 && r.height > 15) {
|
|
612
|
+
const label = document.createElement('div');
|
|
613
|
+
label.className = 'node-label';
|
|
614
|
+
label.textContent = r.name + (r.value > 0 && !r.hasChildren ? ' (' + formatTokens(r.value) + ')' : '');
|
|
615
|
+
node.appendChild(label);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Click to zoom and/or show detail
|
|
619
|
+
node.addEventListener('click', (e) => {
|
|
620
|
+
e.stopPropagation();
|
|
621
|
+
showDetail(r);
|
|
622
|
+
if (r.hasChildren && r.nodeRef) {
|
|
623
|
+
zoomTo(r.nodeRef);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
node.addEventListener('mouseenter', (e) => showTooltip(e, r));
|
|
628
|
+
node.addEventListener('mousemove', (e) => moveTooltip(e));
|
|
629
|
+
node.addEventListener('mouseleave', hideTooltip);
|
|
630
|
+
|
|
631
|
+
container.appendChild(node);
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const toolColors = {
|
|
636
|
+
Read: '#4ade80', // green
|
|
637
|
+
Write: '#f87171', // red
|
|
638
|
+
Edit: '#fb923c', // orange
|
|
639
|
+
MultiEdit: '#f97316', // darker orange
|
|
640
|
+
Bash: '#a78bfa', // purple
|
|
641
|
+
Glob: '#38bdf8', // sky blue
|
|
642
|
+
Grep: '#22d3ee', // cyan
|
|
643
|
+
Task: '#facc15', // yellow
|
|
644
|
+
WebFetch: '#2dd4bf', // teal
|
|
645
|
+
WebSearch: '#14b8a6', // darker teal
|
|
646
|
+
TodoWrite: '#e879f9', // pink
|
|
647
|
+
LSP: '#818cf8', // indigo
|
|
648
|
+
Response: '#cbd5e1', // light gray for text-only responses
|
|
649
|
+
AskUserQuestion: '#f472b6', // pink
|
|
650
|
+
default: '#94a3b8' // gray
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
function getColor(toolName) {
|
|
654
|
+
if (!toolName) return '#4a5568';
|
|
655
|
+
// Check for MCP tools (mcp__*)
|
|
656
|
+
if (toolName.startsWith('mcp__')) return '#60a5fa'; // blue for MCP
|
|
657
|
+
return toolColors[toolName] || toolColors.default;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function getRatioClass(ratio) {
|
|
661
|
+
if (ratio === undefined || ratio === null) return '';
|
|
662
|
+
if (ratio < 2) return 'ratio-good';
|
|
663
|
+
if (ratio < 5) return 'ratio-moderate';
|
|
664
|
+
return 'ratio-high';
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function formatTokens(n) {
|
|
668
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
669
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
670
|
+
return n.toString();
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function showTooltip(e, r) {
|
|
674
|
+
// Build tooltip using DOM methods (safe from XSS)
|
|
675
|
+
tooltip.replaceChildren();
|
|
676
|
+
|
|
677
|
+
const title = document.createElement('div');
|
|
678
|
+
title.className = 'tooltip-title';
|
|
679
|
+
title.textContent = r.name;
|
|
680
|
+
tooltip.appendChild(title);
|
|
681
|
+
|
|
682
|
+
function addRow(label, value, extraClass) {
|
|
683
|
+
const row = document.createElement('div');
|
|
684
|
+
row.className = 'tooltip-row';
|
|
685
|
+
const labelEl = document.createElement('span');
|
|
686
|
+
labelEl.className = 'tooltip-label';
|
|
687
|
+
labelEl.textContent = label;
|
|
688
|
+
const valueEl = document.createElement('span');
|
|
689
|
+
valueEl.className = 'tooltip-value' + (extraClass ? ' ' + extraClass : '');
|
|
690
|
+
valueEl.textContent = value;
|
|
691
|
+
row.appendChild(labelEl);
|
|
692
|
+
row.appendChild(valueEl);
|
|
693
|
+
tooltip.appendChild(row);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function addLinkRow(label, text, onClick, title) {
|
|
697
|
+
const row = document.createElement('div');
|
|
698
|
+
row.className = 'tooltip-row';
|
|
699
|
+
const labelEl = document.createElement('span');
|
|
700
|
+
labelEl.className = 'tooltip-label';
|
|
701
|
+
labelEl.textContent = label;
|
|
702
|
+
const link = document.createElement('span');
|
|
703
|
+
link.className = 'tooltip-link';
|
|
704
|
+
link.textContent = text;
|
|
705
|
+
if (title) link.title = title;
|
|
706
|
+
link.onclick = (e) => { e.stopPropagation(); onClick(); };
|
|
707
|
+
row.appendChild(labelEl);
|
|
708
|
+
row.appendChild(link);
|
|
709
|
+
tooltip.appendChild(row);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (r.sessionId) {
|
|
713
|
+
addLinkRow('Session:', r.sessionId, () => {
|
|
714
|
+
navigator.clipboard.writeText(r.fullSessionId || r.sessionId);
|
|
715
|
+
}, 'Click to copy full session ID');
|
|
716
|
+
}
|
|
717
|
+
if (r.date) addRow('Date:', r.date);
|
|
718
|
+
if (r.startTime) addRow('Started:', r.startTime);
|
|
719
|
+
if (r.model) addRow('Model:', r.model);
|
|
720
|
+
if (r.value > 0) addRow('Total tokens:', formatTokens(r.value));
|
|
721
|
+
if (r.inputTokens !== undefined) addRow('Input:', formatTokens(r.inputTokens));
|
|
722
|
+
if (r.outputTokens !== undefined) addRow('Output:', formatTokens(r.outputTokens));
|
|
723
|
+
if (r.ratio !== undefined && r.ratio !== null) {
|
|
724
|
+
addRow('Ratio (in:out):', r.ratio.toFixed(1) + ':1', getRatioClass(r.ratio));
|
|
725
|
+
}
|
|
726
|
+
if (r.repeatedReads !== undefined && r.repeatedReads > 0) {
|
|
727
|
+
addRow('Repeated reads:', r.repeatedReads.toString());
|
|
728
|
+
}
|
|
729
|
+
if (r.modelEfficiency !== undefined && r.modelEfficiency > 0) {
|
|
730
|
+
addRow('Opus usage:', (r.modelEfficiency * 100).toFixed(0) + '%');
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Tool breakdown table
|
|
734
|
+
if (r.tools && r.tools.length > 0) {
|
|
735
|
+
const header = document.createElement('div');
|
|
736
|
+
header.className = 'tool-table-header';
|
|
737
|
+
header.textContent = 'Tool Usage';
|
|
738
|
+
tooltip.appendChild(header);
|
|
739
|
+
|
|
740
|
+
const table = document.createElement('table');
|
|
741
|
+
table.className = 'tool-table';
|
|
742
|
+
|
|
743
|
+
const thead = document.createElement('thead');
|
|
744
|
+
const headerRow = document.createElement('tr');
|
|
745
|
+
['Tool', 'Detail', 'Tokens'].forEach(text => {
|
|
746
|
+
const th = document.createElement('th');
|
|
747
|
+
th.textContent = text;
|
|
748
|
+
headerRow.appendChild(th);
|
|
749
|
+
});
|
|
750
|
+
thead.appendChild(headerRow);
|
|
751
|
+
table.appendChild(thead);
|
|
752
|
+
|
|
753
|
+
const tbody = document.createElement('tbody');
|
|
754
|
+
r.tools.forEach(tool => {
|
|
755
|
+
const tr = document.createElement('tr');
|
|
756
|
+
const tdName = document.createElement('td');
|
|
757
|
+
tdName.textContent = tool.name;
|
|
758
|
+
const tdDetail = document.createElement('td');
|
|
759
|
+
tdDetail.textContent = tool.detail || '';
|
|
760
|
+
const tdTokens = document.createElement('td');
|
|
761
|
+
tdTokens.textContent = formatTokens(tool.inputTokens + tool.outputTokens);
|
|
762
|
+
tr.appendChild(tdName);
|
|
763
|
+
tr.appendChild(tdDetail);
|
|
764
|
+
tr.appendChild(tdTokens);
|
|
765
|
+
tbody.appendChild(tr);
|
|
766
|
+
});
|
|
767
|
+
table.appendChild(tbody);
|
|
768
|
+
tooltip.appendChild(table);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Actions section with links
|
|
772
|
+
if (r.fullSessionId || r.filePath) {
|
|
773
|
+
const actions = document.createElement('div');
|
|
774
|
+
actions.className = 'tooltip-actions';
|
|
775
|
+
|
|
776
|
+
if (r.filePath) {
|
|
777
|
+
const fileLink = document.createElement('span');
|
|
778
|
+
fileLink.className = 'tooltip-link';
|
|
779
|
+
fileLink.textContent = '📄 Copy path';
|
|
780
|
+
fileLink.title = r.filePath;
|
|
781
|
+
fileLink.onclick = (e) => {
|
|
782
|
+
e.stopPropagation();
|
|
783
|
+
navigator.clipboard.writeText(r.filePath);
|
|
784
|
+
};
|
|
785
|
+
actions.appendChild(fileLink);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (r.fullSessionId) {
|
|
789
|
+
const transcriptLink = document.createElement('span');
|
|
790
|
+
transcriptLink.className = 'tooltip-link';
|
|
791
|
+
transcriptLink.textContent = '📜 View transcript';
|
|
792
|
+
transcriptLink.title = 'Copy command to view with claude-code-transcripts';
|
|
793
|
+
transcriptLink.onclick = (e) => {
|
|
794
|
+
e.stopPropagation();
|
|
795
|
+
navigator.clipboard.writeText('uvx claude-code-transcripts ' + r.fullSessionId);
|
|
796
|
+
};
|
|
797
|
+
actions.appendChild(transcriptLink);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
tooltip.appendChild(actions);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
tooltip.style.display = 'block';
|
|
804
|
+
moveTooltip(e);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function moveTooltip(e) {
|
|
808
|
+
const x = e.clientX + 15;
|
|
809
|
+
const y = e.clientY + 15;
|
|
810
|
+
const rect = tooltip.getBoundingClientRect();
|
|
811
|
+
|
|
812
|
+
tooltip.style.left = (x + rect.width > window.innerWidth ? e.clientX - rect.width - 15 : x) + 'px';
|
|
813
|
+
tooltip.style.top = (y + rect.height > window.innerHeight ? e.clientY - rect.height - 15 : y) + 'px';
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function hideTooltip() {
|
|
817
|
+
tooltip.style.display = 'none';
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function showDetail(r) {
|
|
821
|
+
detailPanel.className = 'detail-panel';
|
|
822
|
+
detailPanel.replaceChildren();
|
|
823
|
+
|
|
824
|
+
const title = document.createElement('div');
|
|
825
|
+
title.className = 'detail-title';
|
|
826
|
+
title.textContent = r.name;
|
|
827
|
+
detailPanel.appendChild(title);
|
|
828
|
+
|
|
829
|
+
function addDetailRow(label, value, extraClass) {
|
|
830
|
+
const row = document.createElement('div');
|
|
831
|
+
row.className = 'detail-row';
|
|
832
|
+
const labelEl = document.createElement('span');
|
|
833
|
+
labelEl.className = 'detail-label';
|
|
834
|
+
labelEl.textContent = label;
|
|
835
|
+
const valueEl = document.createElement('span');
|
|
836
|
+
valueEl.className = 'detail-value' + (extraClass ? ' ' + extraClass : '');
|
|
837
|
+
valueEl.textContent = value;
|
|
838
|
+
row.appendChild(labelEl);
|
|
839
|
+
row.appendChild(valueEl);
|
|
840
|
+
detailPanel.appendChild(row);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (r.sessionId) addDetailRow('Session:', r.fullSessionId || r.sessionId);
|
|
844
|
+
if (r.date) addDetailRow('Date:', r.date);
|
|
845
|
+
if (r.startTime) addDetailRow('Started:', r.startTime);
|
|
846
|
+
if (r.model) addDetailRow('Model:', r.model);
|
|
847
|
+
if (r.value > 0) addDetailRow('Total tokens:', formatTokens(r.value));
|
|
848
|
+
if (r.inputTokens !== undefined) addDetailRow('Input:', formatTokens(r.inputTokens));
|
|
849
|
+
if (r.outputTokens !== undefined) addDetailRow('Output:', formatTokens(r.outputTokens));
|
|
850
|
+
if (r.ratio !== undefined && r.ratio !== null) {
|
|
851
|
+
addDetailRow('Ratio (in:out):', r.ratio.toFixed(1) + ':1', getRatioClass(r.ratio));
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Action buttons
|
|
855
|
+
if (r.fullSessionId || r.filePath) {
|
|
856
|
+
const actions = document.createElement('div');
|
|
857
|
+
actions.className = 'detail-actions';
|
|
858
|
+
|
|
859
|
+
if (r.filePath) {
|
|
860
|
+
const fileBtn = document.createElement('button');
|
|
861
|
+
fileBtn.className = 'detail-btn';
|
|
862
|
+
fileBtn.textContent = '📄 Copy file path';
|
|
863
|
+
fileBtn.onclick = () => {
|
|
864
|
+
navigator.clipboard.writeText(r.filePath);
|
|
865
|
+
fileBtn.textContent = '✓ Copied!';
|
|
866
|
+
setTimeout(() => fileBtn.textContent = '📄 Copy file path', 1500);
|
|
867
|
+
};
|
|
868
|
+
actions.appendChild(fileBtn);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (r.fullSessionId) {
|
|
872
|
+
const copyIdBtn = document.createElement('button');
|
|
873
|
+
copyIdBtn.className = 'detail-btn';
|
|
874
|
+
copyIdBtn.textContent = '🔗 Copy session ID';
|
|
875
|
+
copyIdBtn.onclick = () => {
|
|
876
|
+
navigator.clipboard.writeText(r.fullSessionId);
|
|
877
|
+
copyIdBtn.textContent = '✓ Copied!';
|
|
878
|
+
setTimeout(() => copyIdBtn.textContent = '🔗 Copy session ID', 1500);
|
|
879
|
+
};
|
|
880
|
+
actions.appendChild(copyIdBtn);
|
|
881
|
+
|
|
882
|
+
const transcriptBtn = document.createElement('button');
|
|
883
|
+
transcriptBtn.className = 'detail-btn';
|
|
884
|
+
transcriptBtn.textContent = '📜 View transcript';
|
|
885
|
+
transcriptBtn.title = 'Copy command - use start time above when prompted';
|
|
886
|
+
transcriptBtn.onclick = () => {
|
|
887
|
+
navigator.clipboard.writeText('uvx claude-code-transcripts ' + r.fullSessionId);
|
|
888
|
+
transcriptBtn.textContent = '✓ Copied!';
|
|
889
|
+
setTimeout(() => transcriptBtn.textContent = '📜 View transcript', 1500);
|
|
890
|
+
};
|
|
891
|
+
actions.appendChild(transcriptBtn);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
detailPanel.appendChild(actions);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
render();
|
|
899
|
+
|
|
900
|
+
// Initialize bar chart view after defining renderBarChart below
|
|
901
|
+
setTimeout(() => showBarChart(), 0);
|
|
902
|
+
|
|
903
|
+
// Project colors for bar chart (distinct colors for different projects)
|
|
904
|
+
const projectColorPalette = [
|
|
905
|
+
'#60a5fa', // blue
|
|
906
|
+
'#4ade80', // green
|
|
907
|
+
'#a78bfa', // purple
|
|
908
|
+
'#f87171', // red
|
|
909
|
+
'#fbbf24', // amber
|
|
910
|
+
'#22d3ee', // cyan
|
|
911
|
+
'#f472b6', // pink
|
|
912
|
+
'#fb923c', // orange
|
|
913
|
+
'#2dd4bf', // teal
|
|
914
|
+
'#818cf8', // indigo
|
|
915
|
+
];
|
|
916
|
+
const projectColorMap = new Map();
|
|
917
|
+
let nextColorIndex = 0;
|
|
918
|
+
|
|
919
|
+
function getProjectColor(project) {
|
|
920
|
+
if (!projectColorMap.has(project)) {
|
|
921
|
+
projectColorMap.set(project, projectColorPalette[nextColorIndex % projectColorPalette.length]);
|
|
922
|
+
nextColorIndex++;
|
|
923
|
+
}
|
|
924
|
+
return projectColorMap.get(project);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Selected bar segment state
|
|
928
|
+
let selectedBarSegment = null;
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Render stacked bar chart visualization.
|
|
932
|
+
* Each day shows stacked bars by project folder.
|
|
933
|
+
*/
|
|
934
|
+
function renderBarChart(data) {
|
|
935
|
+
barChartContainer.replaceChildren();
|
|
936
|
+
projectColorMap.clear();
|
|
937
|
+
nextColorIndex = 0;
|
|
938
|
+
|
|
939
|
+
if (!data || !data.days || data.days.length === 0) {
|
|
940
|
+
const msg = document.createElement('div');
|
|
941
|
+
msg.style.cssText = 'color: #666; text-align: center; padding: 40px;';
|
|
942
|
+
msg.textContent = 'No bar chart data available';
|
|
943
|
+
barChartContainer.appendChild(msg);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const margin = { top: 30, right: 30, bottom: 60, left: 70 };
|
|
948
|
+
const width = WIDTH - margin.left - margin.right;
|
|
949
|
+
const height = HEIGHT - margin.top - margin.bottom;
|
|
950
|
+
|
|
951
|
+
// Create SVG
|
|
952
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
953
|
+
svg.setAttribute('width', WIDTH);
|
|
954
|
+
svg.setAttribute('height', HEIGHT);
|
|
955
|
+
barChartContainer.appendChild(svg);
|
|
956
|
+
|
|
957
|
+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
958
|
+
g.setAttribute('transform', `translate(${margin.left},${margin.top})`);
|
|
959
|
+
svg.appendChild(g);
|
|
960
|
+
|
|
961
|
+
// Calculate max tokens per day for y-axis scale
|
|
962
|
+
let maxDayTokens = 0;
|
|
963
|
+
for (const day of data.days) {
|
|
964
|
+
let dayTotal = 0;
|
|
965
|
+
for (const proj of day.projects) {
|
|
966
|
+
dayTotal += proj.totalTokens;
|
|
967
|
+
}
|
|
968
|
+
if (dayTotal > maxDayTokens) maxDayTokens = dayTotal;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// X scale: dates
|
|
972
|
+
const xDomain = data.days.map(d => d.date);
|
|
973
|
+
const xBandWidth = width / xDomain.length;
|
|
974
|
+
const xScale = (date) => xDomain.indexOf(date) * xBandWidth + xBandWidth / 2;
|
|
975
|
+
|
|
976
|
+
// Y scale: tokens
|
|
977
|
+
const yScale = (tokens) => height - (tokens / maxDayTokens) * height;
|
|
978
|
+
const yHeight = (tokens) => (tokens / maxDayTokens) * height;
|
|
979
|
+
|
|
980
|
+
// Draw axes
|
|
981
|
+
// X axis
|
|
982
|
+
const xAxisG = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
983
|
+
xAxisG.setAttribute('class', 'axis');
|
|
984
|
+
xAxisG.setAttribute('transform', `translate(0,${height})`);
|
|
985
|
+
g.appendChild(xAxisG);
|
|
986
|
+
|
|
987
|
+
// X axis line
|
|
988
|
+
const xLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
989
|
+
xLine.setAttribute('x1', 0);
|
|
990
|
+
xLine.setAttribute('x2', width);
|
|
991
|
+
xLine.setAttribute('stroke', '#444');
|
|
992
|
+
xAxisG.appendChild(xLine);
|
|
993
|
+
|
|
994
|
+
// X axis labels
|
|
995
|
+
for (const date of xDomain) {
|
|
996
|
+
const x = xScale(date);
|
|
997
|
+
const tick = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
998
|
+
tick.setAttribute('x1', x);
|
|
999
|
+
tick.setAttribute('x2', x);
|
|
1000
|
+
tick.setAttribute('y1', 0);
|
|
1001
|
+
tick.setAttribute('y2', 6);
|
|
1002
|
+
tick.setAttribute('stroke', '#444');
|
|
1003
|
+
xAxisG.appendChild(tick);
|
|
1004
|
+
|
|
1005
|
+
const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
1006
|
+
label.setAttribute('x', x);
|
|
1007
|
+
label.setAttribute('y', 20);
|
|
1008
|
+
label.setAttribute('text-anchor', 'middle');
|
|
1009
|
+
label.setAttribute('fill', '#888');
|
|
1010
|
+
label.setAttribute('font-size', '11');
|
|
1011
|
+
// Format date as MM/DD
|
|
1012
|
+
const parts = date.split('-');
|
|
1013
|
+
label.textContent = parts[1] + '/' + parts[2];
|
|
1014
|
+
xAxisG.appendChild(label);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Y axis
|
|
1018
|
+
const yAxisG = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
1019
|
+
yAxisG.setAttribute('class', 'axis');
|
|
1020
|
+
g.appendChild(yAxisG);
|
|
1021
|
+
|
|
1022
|
+
// Y axis line
|
|
1023
|
+
const yLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
1024
|
+
yLine.setAttribute('y1', 0);
|
|
1025
|
+
yLine.setAttribute('y2', height);
|
|
1026
|
+
yLine.setAttribute('stroke', '#444');
|
|
1027
|
+
yAxisG.appendChild(yLine);
|
|
1028
|
+
|
|
1029
|
+
// Y axis ticks (5 ticks)
|
|
1030
|
+
const yTicks = 5;
|
|
1031
|
+
for (let i = 0; i <= yTicks; i++) {
|
|
1032
|
+
const tokens = (maxDayTokens / yTicks) * i;
|
|
1033
|
+
const y = yScale(tokens);
|
|
1034
|
+
|
|
1035
|
+
const tick = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
1036
|
+
tick.setAttribute('x1', -6);
|
|
1037
|
+
tick.setAttribute('x2', 0);
|
|
1038
|
+
tick.setAttribute('y1', y);
|
|
1039
|
+
tick.setAttribute('y2', y);
|
|
1040
|
+
tick.setAttribute('stroke', '#444');
|
|
1041
|
+
yAxisG.appendChild(tick);
|
|
1042
|
+
|
|
1043
|
+
// Grid line
|
|
1044
|
+
const grid = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
1045
|
+
grid.setAttribute('x1', 0);
|
|
1046
|
+
grid.setAttribute('x2', width);
|
|
1047
|
+
grid.setAttribute('y1', y);
|
|
1048
|
+
grid.setAttribute('y2', y);
|
|
1049
|
+
grid.setAttribute('stroke', '#333');
|
|
1050
|
+
grid.setAttribute('stroke-dasharray', '2,2');
|
|
1051
|
+
yAxisG.appendChild(grid);
|
|
1052
|
+
|
|
1053
|
+
const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
1054
|
+
label.setAttribute('x', -10);
|
|
1055
|
+
label.setAttribute('y', y + 4);
|
|
1056
|
+
label.setAttribute('text-anchor', 'end');
|
|
1057
|
+
label.setAttribute('fill', '#888');
|
|
1058
|
+
label.setAttribute('font-size', '11');
|
|
1059
|
+
label.textContent = formatTokens(Math.round(tokens));
|
|
1060
|
+
yAxisG.appendChild(label);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Y axis label
|
|
1064
|
+
const yLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
1065
|
+
yLabel.setAttribute('transform', `rotate(-90)`);
|
|
1066
|
+
yLabel.setAttribute('x', -height / 2);
|
|
1067
|
+
yLabel.setAttribute('y', -50);
|
|
1068
|
+
yLabel.setAttribute('text-anchor', 'middle');
|
|
1069
|
+
yLabel.setAttribute('fill', '#888');
|
|
1070
|
+
yLabel.setAttribute('font-size', '12');
|
|
1071
|
+
yLabel.textContent = 'Tokens';
|
|
1072
|
+
yAxisG.appendChild(yLabel);
|
|
1073
|
+
|
|
1074
|
+
// Bar width (leave gaps between days)
|
|
1075
|
+
const barWidth = Math.min(xBandWidth * 0.7, 60);
|
|
1076
|
+
|
|
1077
|
+
// Draw stacked bars for each day (by project)
|
|
1078
|
+
for (const day of data.days) {
|
|
1079
|
+
const x = xScale(day.date) - barWidth / 2;
|
|
1080
|
+
let yOffset = 0; // Stack from bottom
|
|
1081
|
+
|
|
1082
|
+
for (const proj of day.projects) {
|
|
1083
|
+
const projHeight = yHeight(proj.totalTokens);
|
|
1084
|
+
if (projHeight < 1) continue;
|
|
1085
|
+
|
|
1086
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
1087
|
+
rect.setAttribute('class', 'bar-segment');
|
|
1088
|
+
rect.setAttribute('x', x);
|
|
1089
|
+
rect.setAttribute('y', height - yOffset - projHeight);
|
|
1090
|
+
rect.setAttribute('width', barWidth);
|
|
1091
|
+
rect.setAttribute('height', projHeight);
|
|
1092
|
+
rect.setAttribute('fill', getProjectColor(proj.project));
|
|
1093
|
+
rect.setAttribute('data-project', proj.project);
|
|
1094
|
+
|
|
1095
|
+
// Store project data for click/hover
|
|
1096
|
+
rect._projectData = {
|
|
1097
|
+
project: proj.project,
|
|
1098
|
+
totalTokens: proj.totalTokens,
|
|
1099
|
+
date: day.date
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
// Hover events
|
|
1103
|
+
rect.addEventListener('mouseenter', (e) => showBarTooltip(e, rect._projectData));
|
|
1104
|
+
rect.addEventListener('mousemove', (e) => moveTooltip(e));
|
|
1105
|
+
rect.addEventListener('mouseleave', hideTooltip);
|
|
1106
|
+
|
|
1107
|
+
// Click to show detail
|
|
1108
|
+
rect.addEventListener('click', (e) => {
|
|
1109
|
+
e.stopPropagation();
|
|
1110
|
+
// Clear previous selection
|
|
1111
|
+
if (selectedBarSegment) {
|
|
1112
|
+
selectedBarSegment.classList.remove('selected');
|
|
1113
|
+
}
|
|
1114
|
+
selectedBarSegment = rect;
|
|
1115
|
+
rect.classList.add('selected');
|
|
1116
|
+
showBarDetail(rect._projectData);
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
g.appendChild(rect);
|
|
1120
|
+
yOffset += projHeight;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Click elsewhere to clear selection
|
|
1125
|
+
svg.addEventListener('click', () => {
|
|
1126
|
+
if (selectedBarSegment) {
|
|
1127
|
+
selectedBarSegment.classList.remove('selected');
|
|
1128
|
+
selectedBarSegment = null;
|
|
1129
|
+
detailPanel.className = 'detail-panel empty';
|
|
1130
|
+
detailPanel.textContent = 'Click a bar segment to see details';
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
// Update stats for bar chart view
|
|
1135
|
+
let totalTokens = 0;
|
|
1136
|
+
for (const day of data.days) {
|
|
1137
|
+
for (const proj of day.projects) {
|
|
1138
|
+
totalTokens += proj.totalTokens;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
stats.textContent = 'Total: ' + formatTokens(totalTokens) + ' tokens';
|
|
1142
|
+
|
|
1143
|
+
// Update legend with project colors
|
|
1144
|
+
const legendItems = [...projectColorMap.entries()].map(([project, color]) => ({
|
|
1145
|
+
color,
|
|
1146
|
+
label: project
|
|
1147
|
+
}));
|
|
1148
|
+
renderLegend(legendItems);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Show tooltip for bar chart segment.
|
|
1153
|
+
*/
|
|
1154
|
+
function showBarTooltip(e, data) {
|
|
1155
|
+
tooltip.replaceChildren();
|
|
1156
|
+
|
|
1157
|
+
const title = document.createElement('div');
|
|
1158
|
+
title.className = 'tooltip-title';
|
|
1159
|
+
title.textContent = data.project;
|
|
1160
|
+
tooltip.appendChild(title);
|
|
1161
|
+
|
|
1162
|
+
function addRow(label, value) {
|
|
1163
|
+
const row = document.createElement('div');
|
|
1164
|
+
row.className = 'tooltip-row';
|
|
1165
|
+
const labelEl = document.createElement('span');
|
|
1166
|
+
labelEl.className = 'tooltip-label';
|
|
1167
|
+
labelEl.textContent = label;
|
|
1168
|
+
const valueEl = document.createElement('span');
|
|
1169
|
+
valueEl.className = 'tooltip-value';
|
|
1170
|
+
valueEl.textContent = value;
|
|
1171
|
+
row.appendChild(labelEl);
|
|
1172
|
+
row.appendChild(valueEl);
|
|
1173
|
+
tooltip.appendChild(row);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
addRow('Date:', data.date);
|
|
1177
|
+
addRow('Tokens:', formatTokens(data.totalTokens));
|
|
1178
|
+
|
|
1179
|
+
tooltip.style.display = 'block';
|
|
1180
|
+
moveTooltip(e);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Show detail panel for bar chart segment.
|
|
1185
|
+
*/
|
|
1186
|
+
function showBarDetail(data) {
|
|
1187
|
+
detailPanel.className = 'detail-panel';
|
|
1188
|
+
detailPanel.replaceChildren();
|
|
1189
|
+
|
|
1190
|
+
const title = document.createElement('div');
|
|
1191
|
+
title.className = 'detail-title';
|
|
1192
|
+
title.textContent = data.project;
|
|
1193
|
+
detailPanel.appendChild(title);
|
|
1194
|
+
|
|
1195
|
+
function addDetailRow(label, value) {
|
|
1196
|
+
const row = document.createElement('div');
|
|
1197
|
+
row.className = 'detail-row';
|
|
1198
|
+
const labelEl = document.createElement('span');
|
|
1199
|
+
labelEl.className = 'detail-label';
|
|
1200
|
+
labelEl.textContent = label;
|
|
1201
|
+
const valueEl = document.createElement('span');
|
|
1202
|
+
valueEl.className = 'detail-value';
|
|
1203
|
+
valueEl.textContent = value;
|
|
1204
|
+
row.appendChild(labelEl);
|
|
1205
|
+
row.appendChild(valueEl);
|
|
1206
|
+
detailPanel.appendChild(row);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
addDetailRow('Date:', data.date);
|
|
1210
|
+
addDetailRow('Total tokens:', formatTokens(data.totalTokens));
|
|
1211
|
+
}
|
|
1212
|
+
</script>
|
|
1213
|
+
</body>
|
|
1214
|
+
</html>
|