@mikulgohil/ai-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -0
- package/commands/accessibility-audit.md +143 -0
- package/commands/api-route.md +203 -0
- package/commands/commit-msg.md +127 -0
- package/commands/dep-check.md +148 -0
- package/commands/design-tokens.md +146 -0
- package/commands/document.md +175 -0
- package/commands/env-setup.md +165 -0
- package/commands/error-boundary.md +254 -0
- package/commands/extract-hook.md +237 -0
- package/commands/figma-to-code.md +152 -0
- package/commands/fix-bug.md +112 -0
- package/commands/migrate.md +174 -0
- package/commands/new-component.md +121 -0
- package/commands/new-page.md +113 -0
- package/commands/optimize.md +120 -0
- package/commands/pre-pr.md +159 -0
- package/commands/prompt-help.md +175 -0
- package/commands/refactor.md +219 -0
- package/commands/responsive-check.md +164 -0
- package/commands/review.md +120 -0
- package/commands/security-check.md +175 -0
- package/commands/sitecore-debug.md +216 -0
- package/commands/test.md +154 -0
- package/commands/token-tips.md +72 -0
- package/commands/type-fix.md +224 -0
- package/commands/understand.md +84 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1425 -0
- package/dist/index.js.map +1 -0
- package/docs-scaffolds/component-doc.md +35 -0
- package/docs-scaffolds/decisions-log.md +15 -0
- package/docs-scaffolds/mistakes-log.md +15 -0
- package/docs-scaffolds/time-log.md +14 -0
- package/guides/figma-workflow.md +135 -0
- package/guides/getting-started.md +61 -0
- package/guides/prompt-playbook.md +64 -0
- package/guides/token-saving-tips.md +50 -0
- package/guides/when-to-use-ai.md +44 -0
- package/package.json +58 -0
- package/templates/claude-md/base.md +173 -0
- package/templates/claude-md/figma.md +62 -0
- package/templates/claude-md/monorepo.md +17 -0
- package/templates/claude-md/nextjs-app-router.md +29 -0
- package/templates/claude-md/nextjs-pages-router.md +28 -0
- package/templates/claude-md/sitecore-xmc.md +46 -0
- package/templates/claude-md/tailwind.md +18 -0
- package/templates/claude-md/typescript.md +19 -0
- package/templates/cursorrules/base.md +84 -0
- package/templates/cursorrules/figma.md +32 -0
- package/templates/cursorrules/monorepo.md +7 -0
- package/templates/cursorrules/nextjs-app-router.md +8 -0
- package/templates/cursorrules/nextjs-pages-router.md +7 -0
- package/templates/cursorrules/sitecore-xmc.md +9 -0
- package/templates/cursorrules/tailwind.md +8 -0
- package/templates/cursorrules/typescript.md +8 -0
- package/templates/header.md +4 -0
- package/templates/token-dashboard.html +732 -0
|
@@ -0,0 +1,732 @@
|
|
|
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>AI Kit — Token Usage Dashboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #ffffff;
|
|
10
|
+
--bg-card: #f8f9fa;
|
|
11
|
+
--bg-hover: #e9ecef;
|
|
12
|
+
--text: #1a1a2e;
|
|
13
|
+
--text-muted: #6c757d;
|
|
14
|
+
--accent: #4361ee;
|
|
15
|
+
--accent-light: #e8ecff;
|
|
16
|
+
--green: #2dc653;
|
|
17
|
+
--yellow: #ffc107;
|
|
18
|
+
--red: #e63946;
|
|
19
|
+
--orange: #fd7e14;
|
|
20
|
+
--border: #dee2e6;
|
|
21
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
22
|
+
--radius: 8px;
|
|
23
|
+
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
24
|
+
--font-mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@media (prefers-color-scheme: dark) {
|
|
28
|
+
:root {
|
|
29
|
+
--bg: #0d1117;
|
|
30
|
+
--bg-card: #161b22;
|
|
31
|
+
--bg-hover: #21262d;
|
|
32
|
+
--text: #e6edf3;
|
|
33
|
+
--text-muted: #8b949e;
|
|
34
|
+
--accent: #58a6ff;
|
|
35
|
+
--accent-light: #1a2332;
|
|
36
|
+
--green: #3fb950;
|
|
37
|
+
--yellow: #d29922;
|
|
38
|
+
--red: #f85149;
|
|
39
|
+
--orange: #d18616;
|
|
40
|
+
--border: #30363d;
|
|
41
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.3);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
46
|
+
|
|
47
|
+
body {
|
|
48
|
+
font-family: var(--font);
|
|
49
|
+
background: var(--bg);
|
|
50
|
+
color: var(--text);
|
|
51
|
+
line-height: 1.6;
|
|
52
|
+
padding: 2rem;
|
|
53
|
+
max-width: 1200px;
|
|
54
|
+
margin: 0 auto;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
h1 { font-size: 1.75rem; margin-bottom: 0.25rem; }
|
|
58
|
+
h2 { font-size: 1.25rem; margin-bottom: 1rem; color: var(--text); }
|
|
59
|
+
h3 { font-size: 1rem; margin-bottom: 0.5rem; color: var(--text-muted); font-weight: 500; }
|
|
60
|
+
|
|
61
|
+
.subtitle { color: var(--text-muted); margin-bottom: 2rem; font-size: 0.9rem; }
|
|
62
|
+
.generated { color: var(--text-muted); font-size: 0.8rem; margin-bottom: 2rem; }
|
|
63
|
+
|
|
64
|
+
.grid {
|
|
65
|
+
display: grid;
|
|
66
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
67
|
+
gap: 1rem;
|
|
68
|
+
margin-bottom: 2rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.card {
|
|
72
|
+
background: var(--bg-card);
|
|
73
|
+
border: 1px solid var(--border);
|
|
74
|
+
border-radius: var(--radius);
|
|
75
|
+
padding: 1.25rem;
|
|
76
|
+
box-shadow: var(--shadow);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.stat-value {
|
|
80
|
+
font-size: 1.75rem;
|
|
81
|
+
font-weight: 700;
|
|
82
|
+
font-family: var(--font-mono);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.stat-label {
|
|
86
|
+
font-size: 0.8rem;
|
|
87
|
+
color: var(--text-muted);
|
|
88
|
+
text-transform: uppercase;
|
|
89
|
+
letter-spacing: 0.05em;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.cost { color: var(--yellow); }
|
|
93
|
+
.sessions-count { color: var(--accent); }
|
|
94
|
+
.tokens-count { color: var(--green); }
|
|
95
|
+
|
|
96
|
+
/* Budget ring */
|
|
97
|
+
.budget-ring-container {
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: 1.5rem;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.budget-ring {
|
|
104
|
+
position: relative;
|
|
105
|
+
width: 120px;
|
|
106
|
+
height: 120px;
|
|
107
|
+
flex-shrink: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.budget-ring svg {
|
|
111
|
+
transform: rotate(-90deg);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.budget-ring .ring-bg {
|
|
115
|
+
fill: none;
|
|
116
|
+
stroke: var(--border);
|
|
117
|
+
stroke-width: 8;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.budget-ring .ring-fill {
|
|
121
|
+
fill: none;
|
|
122
|
+
stroke-width: 8;
|
|
123
|
+
stroke-linecap: round;
|
|
124
|
+
transition: stroke-dashoffset 0.5s ease;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.budget-ring .ring-text {
|
|
128
|
+
position: absolute;
|
|
129
|
+
top: 50%;
|
|
130
|
+
left: 50%;
|
|
131
|
+
transform: translate(-50%, -50%);
|
|
132
|
+
text-align: center;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.budget-ring .ring-percent {
|
|
136
|
+
font-size: 1.5rem;
|
|
137
|
+
font-weight: 700;
|
|
138
|
+
font-family: var(--font-mono);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.budget-ring .ring-label {
|
|
142
|
+
font-size: 0.7rem;
|
|
143
|
+
color: var(--text-muted);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.budget-details p {
|
|
147
|
+
margin-bottom: 0.25rem;
|
|
148
|
+
font-size: 0.9rem;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.budget-details .amount {
|
|
152
|
+
font-family: var(--font-mono);
|
|
153
|
+
font-weight: 600;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Bar chart */
|
|
157
|
+
.chart-container {
|
|
158
|
+
margin-bottom: 2rem;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.bar-chart {
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: flex-end;
|
|
164
|
+
gap: 4px;
|
|
165
|
+
height: 200px;
|
|
166
|
+
padding: 0 0 30px 0;
|
|
167
|
+
position: relative;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.bar-group {
|
|
171
|
+
flex: 1;
|
|
172
|
+
display: flex;
|
|
173
|
+
flex-direction: column;
|
|
174
|
+
align-items: center;
|
|
175
|
+
height: 100%;
|
|
176
|
+
justify-content: flex-end;
|
|
177
|
+
position: relative;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.bar {
|
|
181
|
+
width: 100%;
|
|
182
|
+
max-width: 48px;
|
|
183
|
+
border-radius: 3px 3px 0 0;
|
|
184
|
+
position: relative;
|
|
185
|
+
cursor: pointer;
|
|
186
|
+
transition: opacity 0.2s;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.bar:hover { opacity: 0.8; }
|
|
190
|
+
|
|
191
|
+
.bar-input { background: var(--accent); }
|
|
192
|
+
.bar-output { background: var(--green); }
|
|
193
|
+
.bar-cache { background: var(--yellow); }
|
|
194
|
+
|
|
195
|
+
.bar-label {
|
|
196
|
+
position: absolute;
|
|
197
|
+
bottom: -25px;
|
|
198
|
+
font-size: 0.65rem;
|
|
199
|
+
color: var(--text-muted);
|
|
200
|
+
white-space: nowrap;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.bar-tooltip {
|
|
204
|
+
display: none;
|
|
205
|
+
position: absolute;
|
|
206
|
+
bottom: 100%;
|
|
207
|
+
left: 50%;
|
|
208
|
+
transform: translateX(-50%);
|
|
209
|
+
background: var(--text);
|
|
210
|
+
color: var(--bg);
|
|
211
|
+
padding: 4px 8px;
|
|
212
|
+
border-radius: 4px;
|
|
213
|
+
font-size: 0.75rem;
|
|
214
|
+
white-space: nowrap;
|
|
215
|
+
z-index: 10;
|
|
216
|
+
font-family: var(--font-mono);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.bar-group:hover .bar-tooltip { display: block; }
|
|
220
|
+
|
|
221
|
+
.chart-legend {
|
|
222
|
+
display: flex;
|
|
223
|
+
gap: 1rem;
|
|
224
|
+
margin-top: 0.75rem;
|
|
225
|
+
font-size: 0.8rem;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.legend-item {
|
|
229
|
+
display: flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
gap: 0.35rem;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.legend-dot {
|
|
235
|
+
width: 10px;
|
|
236
|
+
height: 10px;
|
|
237
|
+
border-radius: 2px;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* Pie chart */
|
|
241
|
+
.pie-container {
|
|
242
|
+
display: flex;
|
|
243
|
+
align-items: center;
|
|
244
|
+
gap: 1.5rem;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.pie-chart {
|
|
248
|
+
width: 140px;
|
|
249
|
+
height: 140px;
|
|
250
|
+
flex-shrink: 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.pie-legend {
|
|
254
|
+
font-size: 0.85rem;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.pie-legend-item {
|
|
258
|
+
display: flex;
|
|
259
|
+
align-items: center;
|
|
260
|
+
gap: 0.5rem;
|
|
261
|
+
margin-bottom: 0.5rem;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.pie-legend-dot {
|
|
265
|
+
width: 12px;
|
|
266
|
+
height: 12px;
|
|
267
|
+
border-radius: 2px;
|
|
268
|
+
flex-shrink: 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.pie-legend-value {
|
|
272
|
+
font-family: var(--font-mono);
|
|
273
|
+
font-weight: 600;
|
|
274
|
+
margin-left: auto;
|
|
275
|
+
padding-left: 1rem;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* Table */
|
|
279
|
+
.table-container {
|
|
280
|
+
overflow-x: auto;
|
|
281
|
+
margin-bottom: 2rem;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
table {
|
|
285
|
+
width: 100%;
|
|
286
|
+
border-collapse: collapse;
|
|
287
|
+
font-size: 0.85rem;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
th {
|
|
291
|
+
text-align: left;
|
|
292
|
+
padding: 0.75rem;
|
|
293
|
+
border-bottom: 2px solid var(--border);
|
|
294
|
+
color: var(--text-muted);
|
|
295
|
+
font-weight: 600;
|
|
296
|
+
font-size: 0.75rem;
|
|
297
|
+
text-transform: uppercase;
|
|
298
|
+
letter-spacing: 0.05em;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
td {
|
|
302
|
+
padding: 0.6rem 0.75rem;
|
|
303
|
+
border-bottom: 1px solid var(--border);
|
|
304
|
+
font-family: var(--font-mono);
|
|
305
|
+
font-size: 0.8rem;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
tr:hover td { background: var(--bg-hover); }
|
|
309
|
+
|
|
310
|
+
.td-date { color: var(--text-muted); font-family: var(--font); }
|
|
311
|
+
.td-cost { color: var(--yellow); font-weight: 600; }
|
|
312
|
+
.td-model { font-family: var(--font); }
|
|
313
|
+
|
|
314
|
+
.model-badge {
|
|
315
|
+
display: inline-block;
|
|
316
|
+
padding: 2px 8px;
|
|
317
|
+
border-radius: 10px;
|
|
318
|
+
font-size: 0.7rem;
|
|
319
|
+
font-weight: 600;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.model-sonnet { background: var(--accent-light); color: var(--accent); }
|
|
323
|
+
.model-opus { background: #2d1b4e; color: #c084fc; }
|
|
324
|
+
|
|
325
|
+
@media (prefers-color-scheme: light) {
|
|
326
|
+
.model-opus { background: #f3e8ff; color: #7c3aed; }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* Tips */
|
|
330
|
+
.tips {
|
|
331
|
+
display: grid;
|
|
332
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
333
|
+
gap: 0.75rem;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.tip {
|
|
337
|
+
padding: 0.75rem 1rem;
|
|
338
|
+
background: var(--bg-card);
|
|
339
|
+
border: 1px solid var(--border);
|
|
340
|
+
border-radius: var(--radius);
|
|
341
|
+
font-size: 0.85rem;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.tip-impact {
|
|
345
|
+
font-size: 0.7rem;
|
|
346
|
+
font-weight: 600;
|
|
347
|
+
text-transform: uppercase;
|
|
348
|
+
letter-spacing: 0.05em;
|
|
349
|
+
margin-bottom: 0.25rem;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.tip-high { color: var(--red); }
|
|
353
|
+
.tip-medium { color: var(--orange); }
|
|
354
|
+
.tip-low { color: var(--text-muted); }
|
|
355
|
+
|
|
356
|
+
/* Expensive sessions */
|
|
357
|
+
.expensive-list { list-style: none; }
|
|
358
|
+
.expensive-list li {
|
|
359
|
+
display: flex;
|
|
360
|
+
justify-content: space-between;
|
|
361
|
+
align-items: center;
|
|
362
|
+
padding: 0.5rem 0;
|
|
363
|
+
border-bottom: 1px solid var(--border);
|
|
364
|
+
font-size: 0.85rem;
|
|
365
|
+
}
|
|
366
|
+
.expensive-list li:last-child { border-bottom: none; }
|
|
367
|
+
.expensive-id {
|
|
368
|
+
font-family: var(--font-mono);
|
|
369
|
+
font-size: 0.8rem;
|
|
370
|
+
color: var(--text-muted);
|
|
371
|
+
max-width: 200px;
|
|
372
|
+
overflow: hidden;
|
|
373
|
+
text-overflow: ellipsis;
|
|
374
|
+
white-space: nowrap;
|
|
375
|
+
}
|
|
376
|
+
.expensive-cost {
|
|
377
|
+
font-family: var(--font-mono);
|
|
378
|
+
font-weight: 600;
|
|
379
|
+
color: var(--yellow);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.error-state {
|
|
383
|
+
text-align: center;
|
|
384
|
+
padding: 3rem;
|
|
385
|
+
color: var(--text-muted);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.error-state code {
|
|
389
|
+
display: block;
|
|
390
|
+
margin-top: 0.5rem;
|
|
391
|
+
font-family: var(--font-mono);
|
|
392
|
+
font-size: 0.85rem;
|
|
393
|
+
color: var(--accent);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
footer {
|
|
397
|
+
margin-top: 3rem;
|
|
398
|
+
padding-top: 1rem;
|
|
399
|
+
border-top: 1px solid var(--border);
|
|
400
|
+
color: var(--text-muted);
|
|
401
|
+
font-size: 0.75rem;
|
|
402
|
+
text-align: center;
|
|
403
|
+
}
|
|
404
|
+
</style>
|
|
405
|
+
</head>
|
|
406
|
+
<body>
|
|
407
|
+
<h1>AI Kit — Token Usage Dashboard</h1>
|
|
408
|
+
<p class="subtitle">Claude Code token consumption and cost analysis</p>
|
|
409
|
+
<p class="generated" id="generated-at"></p>
|
|
410
|
+
|
|
411
|
+
<div id="app">
|
|
412
|
+
<div class="error-state" id="loading">Loading token data...<code>token-data.json</code></div>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<footer>
|
|
416
|
+
Generated by <strong>AI Kit</strong> — run <code>ai-kit tokens --export</code> to refresh
|
|
417
|
+
</footer>
|
|
418
|
+
|
|
419
|
+
<script>
|
|
420
|
+
(async function() {
|
|
421
|
+
const app = document.getElementById('app');
|
|
422
|
+
const genEl = document.getElementById('generated-at');
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const res = await fetch('./token-data.json');
|
|
426
|
+
if (!res.ok) throw new Error('Could not load token-data.json');
|
|
427
|
+
const data = await res.json();
|
|
428
|
+
render(data);
|
|
429
|
+
} catch (e) {
|
|
430
|
+
app.innerHTML = `
|
|
431
|
+
<div class="error-state">
|
|
432
|
+
<p>Could not load token data.</p>
|
|
433
|
+
<p>Make sure <code>token-data.json</code> is in the same directory as this HTML file.</p>
|
|
434
|
+
<p style="margin-top:1rem">Run: <code>ai-kit tokens --export</code></p>
|
|
435
|
+
</div>`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function fmt(n) {
|
|
439
|
+
return (n || 0).toLocaleString('en-US');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function fmtCost(n) {
|
|
443
|
+
return '$' + (n || 0).toFixed(2);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function render(data) {
|
|
447
|
+
genEl.textContent = 'Generated: ' + new Date(data.generatedAt).toLocaleString();
|
|
448
|
+
|
|
449
|
+
const t = data.totals;
|
|
450
|
+
const budget = data.planBudget || 20;
|
|
451
|
+
const monthCost = t.thisMonth.cost;
|
|
452
|
+
const budgetPct = Math.min((monthCost / budget) * 100, 100);
|
|
453
|
+
|
|
454
|
+
app.innerHTML = `
|
|
455
|
+
<!-- Summary cards -->
|
|
456
|
+
<div class="grid">
|
|
457
|
+
<div class="card">
|
|
458
|
+
<h3>Today</h3>
|
|
459
|
+
<div class="stat-value cost">${fmtCost(t.today.cost)}</div>
|
|
460
|
+
<div class="stat-label">${t.today.sessions} sessions · ${fmt(t.today.usage.inputTokens + t.today.usage.outputTokens)} tokens</div>
|
|
461
|
+
</div>
|
|
462
|
+
<div class="card">
|
|
463
|
+
<h3>This Week</h3>
|
|
464
|
+
<div class="stat-value cost">${fmtCost(t.thisWeek.cost)}</div>
|
|
465
|
+
<div class="stat-label">${t.thisWeek.sessions} sessions · ${fmt(t.thisWeek.usage.inputTokens + t.thisWeek.usage.outputTokens)} tokens</div>
|
|
466
|
+
</div>
|
|
467
|
+
<div class="card">
|
|
468
|
+
<h3>This Month</h3>
|
|
469
|
+
<div class="stat-value cost">${fmtCost(t.thisMonth.cost)}</div>
|
|
470
|
+
<div class="stat-label">${t.thisMonth.sessions} sessions · ${fmt(t.thisMonth.usage.inputTokens + t.thisMonth.usage.outputTokens)} tokens</div>
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
<!-- Budget -->
|
|
475
|
+
<div class="card" style="margin-bottom:2rem">
|
|
476
|
+
<h2>${fmtCost(budget)} Plan Budget</h2>
|
|
477
|
+
<div class="budget-ring-container">
|
|
478
|
+
<div class="budget-ring">
|
|
479
|
+
<svg width="120" height="120" viewBox="0 0 120 120">
|
|
480
|
+
<circle class="ring-bg" cx="60" cy="60" r="50"/>
|
|
481
|
+
<circle class="ring-fill" cx="60" cy="60" r="50"
|
|
482
|
+
stroke="${budgetPct > 80 ? 'var(--red)' : budgetPct > 50 ? 'var(--yellow)' : 'var(--green)'}"
|
|
483
|
+
stroke-dasharray="${2 * Math.PI * 50}"
|
|
484
|
+
stroke-dashoffset="${2 * Math.PI * 50 * (1 - budgetPct / 100)}"
|
|
485
|
+
/>
|
|
486
|
+
</svg>
|
|
487
|
+
<div class="ring-text">
|
|
488
|
+
<div class="ring-percent">${Math.round(budgetPct)}%</div>
|
|
489
|
+
<div class="ring-label">used</div>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
<div class="budget-details">
|
|
493
|
+
<p>Spent: <span class="amount">${fmtCost(monthCost)}</span> of <span class="amount">${fmtCost(budget)}</span></p>
|
|
494
|
+
<p>Remaining: <span class="amount">${fmtCost(Math.max(budget - monthCost, 0))}</span></p>
|
|
495
|
+
<p>Daily average: <span class="amount">${fmtCost(getDailyAvg(monthCost))}</span></p>
|
|
496
|
+
<p>Est. days left: <span class="amount">${getEstDaysLeft(monthCost, budget)}</span></p>
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
|
|
501
|
+
<!-- Daily bar chart -->
|
|
502
|
+
<div class="card chart-container">
|
|
503
|
+
<h2>Daily Usage (Last 14 Days)</h2>
|
|
504
|
+
${renderBarChart(data.daily)}
|
|
505
|
+
<div class="chart-legend">
|
|
506
|
+
<span class="legend-item"><span class="legend-dot" style="background:var(--accent)"></span> Input</span>
|
|
507
|
+
<span class="legend-item"><span class="legend-dot" style="background:var(--green)"></span> Output</span>
|
|
508
|
+
<span class="legend-item"><span class="legend-dot" style="background:var(--yellow)"></span> Cache</span>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
<!-- Cost breakdown pie -->
|
|
513
|
+
<div class="grid">
|
|
514
|
+
<div class="card">
|
|
515
|
+
<h2>Cost Breakdown (Month)</h2>
|
|
516
|
+
${renderPieChart(t.thisMonth.usage)}
|
|
517
|
+
</div>
|
|
518
|
+
<div class="card">
|
|
519
|
+
<h2>Most Expensive Sessions</h2>
|
|
520
|
+
${renderExpensiveSessions(data.sessions)}
|
|
521
|
+
</div>
|
|
522
|
+
</div>
|
|
523
|
+
|
|
524
|
+
<!-- Session table -->
|
|
525
|
+
<div class="card" style="margin-top:2rem">
|
|
526
|
+
<h2>Recent Sessions</h2>
|
|
527
|
+
<div class="table-container">
|
|
528
|
+
${renderSessionTable(data.sessions)}
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
|
|
532
|
+
<!-- Tips -->
|
|
533
|
+
<div class="card" style="margin-top:2rem">
|
|
534
|
+
<h2>Token-Saving Tips</h2>
|
|
535
|
+
<div class="tips" style="margin-top:1rem">
|
|
536
|
+
${renderTips()}
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function getDailyAvg(monthCost) {
|
|
543
|
+
const day = new Date().getDate();
|
|
544
|
+
return day > 0 ? monthCost / day : 0;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function getEstDaysLeft(monthCost, budget) {
|
|
548
|
+
const day = new Date().getDate();
|
|
549
|
+
const dailyAvg = day > 0 ? monthCost / day : 0;
|
|
550
|
+
if (dailyAvg <= 0) return '--';
|
|
551
|
+
const remaining = budget - monthCost;
|
|
552
|
+
return Math.max(Math.floor(remaining / dailyAvg), 0);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function renderBarChart(daily) {
|
|
556
|
+
const last14 = daily.slice(0, 14).reverse();
|
|
557
|
+
if (last14.length === 0) return '<p style="color:var(--text-muted);padding:2rem 0">No daily data available</p>';
|
|
558
|
+
|
|
559
|
+
const maxTokens = Math.max(...last14.map(d =>
|
|
560
|
+
d.usage.inputTokens + d.usage.outputTokens + d.usage.cacheReadTokens
|
|
561
|
+
), 1);
|
|
562
|
+
|
|
563
|
+
const chartHeight = 170;
|
|
564
|
+
|
|
565
|
+
const bars = last14.map(d => {
|
|
566
|
+
const total = d.usage.inputTokens + d.usage.outputTokens + d.usage.cacheReadTokens;
|
|
567
|
+
const inputH = (d.usage.inputTokens / maxTokens) * chartHeight;
|
|
568
|
+
const outputH = (d.usage.outputTokens / maxTokens) * chartHeight;
|
|
569
|
+
const cacheH = (d.usage.cacheReadTokens / maxTokens) * chartHeight;
|
|
570
|
+
const dateLabel = d.date.slice(5); // MM-DD
|
|
571
|
+
const cost = fmtCost(d.cost);
|
|
572
|
+
|
|
573
|
+
return `
|
|
574
|
+
<div class="bar-group">
|
|
575
|
+
<div class="bar-tooltip">${d.date}: ${fmt(total)} tokens (${cost})</div>
|
|
576
|
+
<div class="bar bar-cache" style="height:${cacheH}px"></div>
|
|
577
|
+
<div class="bar bar-output" style="height:${outputH}px"></div>
|
|
578
|
+
<div class="bar bar-input" style="height:${inputH}px"></div>
|
|
579
|
+
<span class="bar-label">${dateLabel}</span>
|
|
580
|
+
</div>`;
|
|
581
|
+
}).join('');
|
|
582
|
+
|
|
583
|
+
return `<div class="bar-chart">${bars}</div>`;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function renderPieChart(usage) {
|
|
587
|
+
const input = usage.inputTokens || 0;
|
|
588
|
+
const output = usage.outputTokens || 0;
|
|
589
|
+
const cache = usage.cacheReadTokens || 0;
|
|
590
|
+
const total = input + output + cache;
|
|
591
|
+
|
|
592
|
+
if (total === 0) {
|
|
593
|
+
return '<p style="color:var(--text-muted)">No data</p>';
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const inputPct = input / total;
|
|
597
|
+
const outputPct = output / total;
|
|
598
|
+
const cachePct = cache / total;
|
|
599
|
+
|
|
600
|
+
// SVG pie using stroke-dasharray on circles
|
|
601
|
+
const r = 50;
|
|
602
|
+
const circ = 2 * Math.PI * r;
|
|
603
|
+
|
|
604
|
+
const inputLen = inputPct * circ;
|
|
605
|
+
const outputLen = outputPct * circ;
|
|
606
|
+
const cacheLen = cachePct * circ;
|
|
607
|
+
|
|
608
|
+
const inputOffset = 0;
|
|
609
|
+
const outputOffset = inputLen;
|
|
610
|
+
const cacheOffset = inputLen + outputLen;
|
|
611
|
+
|
|
612
|
+
return `
|
|
613
|
+
<div class="pie-container">
|
|
614
|
+
<svg class="pie-chart" viewBox="0 0 140 140">
|
|
615
|
+
<circle cx="70" cy="70" r="${r}" fill="none" stroke="var(--accent)" stroke-width="20"
|
|
616
|
+
stroke-dasharray="${inputLen} ${circ - inputLen}"
|
|
617
|
+
stroke-dashoffset="0" transform="rotate(-90 70 70)"/>
|
|
618
|
+
<circle cx="70" cy="70" r="${r}" fill="none" stroke="var(--green)" stroke-width="20"
|
|
619
|
+
stroke-dasharray="${outputLen} ${circ - outputLen}"
|
|
620
|
+
stroke-dashoffset="${-inputLen}" transform="rotate(-90 70 70)"/>
|
|
621
|
+
<circle cx="70" cy="70" r="${r}" fill="none" stroke="var(--yellow)" stroke-width="20"
|
|
622
|
+
stroke-dasharray="${cacheLen} ${circ - cacheLen}"
|
|
623
|
+
stroke-dashoffset="${-(inputLen + outputLen)}" transform="rotate(-90 70 70)"/>
|
|
624
|
+
</svg>
|
|
625
|
+
<div class="pie-legend">
|
|
626
|
+
<div class="pie-legend-item">
|
|
627
|
+
<span class="pie-legend-dot" style="background:var(--accent)"></span>
|
|
628
|
+
Input <span class="pie-legend-value">${fmt(input)} (${Math.round(inputPct * 100)}%)</span>
|
|
629
|
+
</div>
|
|
630
|
+
<div class="pie-legend-item">
|
|
631
|
+
<span class="pie-legend-dot" style="background:var(--green)"></span>
|
|
632
|
+
Output <span class="pie-legend-value">${fmt(output)} (${Math.round(outputPct * 100)}%)</span>
|
|
633
|
+
</div>
|
|
634
|
+
<div class="pie-legend-item">
|
|
635
|
+
<span class="pie-legend-dot" style="background:var(--yellow)"></span>
|
|
636
|
+
Cache <span class="pie-legend-value">${fmt(cache)} (${Math.round(cachePct * 100)}%)</span>
|
|
637
|
+
</div>
|
|
638
|
+
</div>
|
|
639
|
+
</div>`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function renderExpensiveSessions(sessions) {
|
|
643
|
+
const sorted = [...sessions]
|
|
644
|
+
.map(s => ({ ...s, cost: calcCost(s) }))
|
|
645
|
+
.sort((a, b) => b.cost - a.cost)
|
|
646
|
+
.slice(0, 8);
|
|
647
|
+
|
|
648
|
+
if (sorted.length === 0) return '<p style="color:var(--text-muted)">No sessions</p>';
|
|
649
|
+
|
|
650
|
+
const items = sorted.map(s =>
|
|
651
|
+
`<li>
|
|
652
|
+
<span>
|
|
653
|
+
<span class="expensive-id">${s.sessionId.slice(0, 16)}...</span>
|
|
654
|
+
<span style="color:var(--text-muted);font-size:0.75rem;margin-left:0.5rem">${s.date}</span>
|
|
655
|
+
</span>
|
|
656
|
+
<span class="expensive-cost">${fmtCost(s.cost)}</span>
|
|
657
|
+
</li>`
|
|
658
|
+
).join('');
|
|
659
|
+
|
|
660
|
+
return `<ul class="expensive-list">${items}</ul>`;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function calcCost(s) {
|
|
664
|
+
const model = s.model || 'sonnet';
|
|
665
|
+
const rates = model === 'opus'
|
|
666
|
+
? { input: 15, output: 75, cache: 0.3 }
|
|
667
|
+
: { input: 3, output: 15, cache: 0.3 };
|
|
668
|
+
return (
|
|
669
|
+
((s.usage.inputTokens || 0) / 1e6) * rates.input +
|
|
670
|
+
((s.usage.outputTokens || 0) / 1e6) * rates.output +
|
|
671
|
+
((s.usage.cacheReadTokens || 0) / 1e6) * rates.cache
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function renderSessionTable(sessions) {
|
|
676
|
+
const rows = sessions.slice(0, 20).map(s => {
|
|
677
|
+
const cost = calcCost(s);
|
|
678
|
+
const modelClass = (s.model || 'sonnet') === 'opus' ? 'model-opus' : 'model-sonnet';
|
|
679
|
+
const modelName = (s.model || 'sonnet') === 'opus' ? 'Opus' : 'Sonnet';
|
|
680
|
+
return `
|
|
681
|
+
<tr>
|
|
682
|
+
<td class="td-date">${s.date}</td>
|
|
683
|
+
<td><span class="model-badge ${modelClass}">${modelName}</span></td>
|
|
684
|
+
<td>${fmt(s.usage.inputTokens)}</td>
|
|
685
|
+
<td>${fmt(s.usage.outputTokens)}</td>
|
|
686
|
+
<td>${fmt(s.usage.cacheReadTokens)}</td>
|
|
687
|
+
<td>${s.messageCount || '--'}</td>
|
|
688
|
+
<td class="td-cost">${fmtCost(cost)}</td>
|
|
689
|
+
</tr>`;
|
|
690
|
+
}).join('');
|
|
691
|
+
|
|
692
|
+
return `
|
|
693
|
+
<table>
|
|
694
|
+
<thead>
|
|
695
|
+
<tr>
|
|
696
|
+
<th>Date</th>
|
|
697
|
+
<th>Model</th>
|
|
698
|
+
<th>Input</th>
|
|
699
|
+
<th>Output</th>
|
|
700
|
+
<th>Cache</th>
|
|
701
|
+
<th>Messages</th>
|
|
702
|
+
<th>Cost</th>
|
|
703
|
+
</tr>
|
|
704
|
+
</thead>
|
|
705
|
+
<tbody>${rows}</tbody>
|
|
706
|
+
</table>`;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function renderTips() {
|
|
710
|
+
const tips = [
|
|
711
|
+
{ impact: 'high', text: 'Be specific about files \u2014 "fix auth in src/lib/auth.ts:45" not "fix the auth"' },
|
|
712
|
+
{ impact: 'high', text: 'Use /understand BEFORE modifying unfamiliar code \u2014 cheaper than a failed attempt' },
|
|
713
|
+
{ impact: 'high', text: 'Break large tasks into focused prompts \u2014 5 small prompts < 1 vague prompt with 3 follow-ups' },
|
|
714
|
+
{ impact: 'high', text: 'Use slash commands \u2014 they provide structured context efficiently' },
|
|
715
|
+
{ impact: 'medium', text: 'Close and reopen sessions when switching tasks to prevent context bloat' },
|
|
716
|
+
{ impact: 'medium', text: 'Use git diff to show AI only what changed, not entire files' },
|
|
717
|
+
{ impact: 'medium', text: 'Don\'t ask AI to read the entire codebase \u2014 point to specific files' },
|
|
718
|
+
{ impact: 'low', text: 'Keep files under 200 lines \u2014 smaller reads per file' },
|
|
719
|
+
{ impact: 'low', text: 'Use barrel exports so AI reads index files, not every module' },
|
|
720
|
+
];
|
|
721
|
+
|
|
722
|
+
return tips.map(t =>
|
|
723
|
+
`<div class="tip">
|
|
724
|
+
<div class="tip-impact tip-${t.impact}">${t.impact} impact</div>
|
|
725
|
+
<div>${t.text}</div>
|
|
726
|
+
</div>`
|
|
727
|
+
).join('');
|
|
728
|
+
}
|
|
729
|
+
})();
|
|
730
|
+
</script>
|
|
731
|
+
</body>
|
|
732
|
+
</html>
|