agentfit 0.1.0 → 0.1.2

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.
Files changed (74) hide show
  1. package/.github/workflows/release.yml +111 -0
  2. package/README.md +41 -38
  3. package/app/(dashboard)/daily/page.tsx +1 -1
  4. package/app/(dashboard)/data-management/page.tsx +180 -0
  5. package/app/(dashboard)/flow/page.tsx +17 -0
  6. package/app/(dashboard)/layout.tsx +2 -0
  7. package/app/(dashboard)/page.tsx +24 -5
  8. package/app/(dashboard)/reports/[id]/page.tsx +72 -0
  9. package/app/(dashboard)/reports/page.tsx +132 -0
  10. package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
  11. package/app/api/backup/route.ts +215 -0
  12. package/app/api/check/route.ts +11 -1
  13. package/app/api/command-insights/route.ts +13 -0
  14. package/app/api/commands/route.ts +55 -1
  15. package/app/api/images-analysis/route.ts +3 -4
  16. package/app/api/reports/[id]/route.ts +23 -0
  17. package/app/api/reports/route.ts +50 -0
  18. package/app/api/reset/route.ts +21 -0
  19. package/app/api/session/route.ts +40 -0
  20. package/app/api/usage/route.ts +26 -1
  21. package/app/layout.tsx +1 -1
  22. package/bin/agentfit.mjs +2 -2
  23. package/components/agent-coach.tsx +256 -129
  24. package/components/app-sidebar.tsx +45 -10
  25. package/components/backup-section.tsx +236 -0
  26. package/components/daily-chart.tsx +447 -83
  27. package/components/dashboard-shell.tsx +29 -31
  28. package/components/data-provider.tsx +88 -8
  29. package/components/fitness-score.tsx +95 -54
  30. package/components/overview-cards.tsx +148 -41
  31. package/components/report-view.tsx +307 -0
  32. package/components/screenshots-analysis.tsx +51 -46
  33. package/components/session-chatlog.tsx +124 -0
  34. package/components/session-timeline.tsx +184 -0
  35. package/components/session-workflow.tsx +183 -0
  36. package/components/sessions-table.tsx +9 -1
  37. package/components/tool-flow-graph.tsx +144 -0
  38. package/components/ui/carousel.tsx +242 -0
  39. package/components/ui/sidebar.tsx +1 -1
  40. package/components/ui/sonner.tsx +51 -0
  41. package/electron/entitlements.mac.plist +16 -0
  42. package/electron/init-db.mjs +37 -0
  43. package/electron/main.mjs +203 -0
  44. package/generated/prisma/browser.ts +5 -0
  45. package/generated/prisma/client.ts +5 -0
  46. package/generated/prisma/internal/class.ts +14 -4
  47. package/generated/prisma/internal/prismaNamespace.ts +97 -2
  48. package/generated/prisma/internal/prismaNamespaceBrowser.ts +21 -1
  49. package/generated/prisma/models/Report.ts +1219 -0
  50. package/generated/prisma/models/Session.ts +221 -1
  51. package/generated/prisma/models.ts +1 -0
  52. package/lib/coach.ts +571 -211
  53. package/lib/command-insights.ts +231 -0
  54. package/lib/db.ts +2 -2
  55. package/lib/parse-codex.ts +6 -0
  56. package/lib/parse-logs.ts +80 -1
  57. package/lib/queries-codex.ts +24 -0
  58. package/lib/queries.ts +45 -0
  59. package/lib/report.ts +156 -0
  60. package/lib/session-detail.ts +382 -0
  61. package/lib/sync.ts +87 -0
  62. package/lib/tool-flow.ts +71 -0
  63. package/next.config.mjs +6 -1
  64. package/package.json +17 -2
  65. package/plugins/cost-heatmap/component.tsx +72 -50
  66. package/prisma/migrations/20260401144555_add_system_prompt_edits/migration.sql +80 -0
  67. package/prisma/schema.prisma +18 -0
  68. package/prisma/schema.sql +81 -0
  69. package/.claude/settings.local.json +0 -26
  70. package/CONTRIBUTING.md +0 -209
  71. package/prisma/migrations/20260328152517_init/migration.sql +0 -41
  72. package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
  73. package/prisma.config.ts +0 -14
  74. package/setup.sh +0 -73
@@ -1,6 +1,20 @@
1
1
  'use client'
2
2
 
3
- import { Line, LineChart, CartesianGrid, XAxis, YAxis } from 'recharts'
3
+ import { useMemo, useState, useEffect } from 'react'
4
+ import {
5
+ Line,
6
+ LineChart,
7
+ Bar,
8
+ BarChart,
9
+ Area,
10
+ AreaChart,
11
+ CartesianGrid,
12
+ XAxis,
13
+ YAxis,
14
+ Legend,
15
+ Cell,
16
+ Tooltip as RechartsTooltip,
17
+ } from 'recharts'
4
18
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
5
19
  import {
6
20
  ChartContainer,
@@ -8,111 +22,461 @@ import {
8
22
  ChartTooltipContent,
9
23
  type ChartConfig,
10
24
  } from '@/components/ui/chart'
11
- import type { DailyUsage } from '@/lib/parse-logs'
25
+ import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
26
+ import type { DailyUsage, SessionSummary } from '@/lib/parse-logs'
12
27
  import { formatCost } from '@/lib/format'
13
28
 
29
+ // ─── 1. Daily Cost ──────────────────────────────────────────────────
30
+
14
31
  const costConfig = {
15
32
  cost: { label: 'Cost', color: 'var(--chart-5)' },
16
33
  } satisfies ChartConfig
17
34
 
18
- const sessionsConfig = {
19
- sessions: { label: 'Sessions', color: 'var(--chart-2)' },
35
+ export function DailyCostChart({ daily }: { daily: DailyUsage[] }) {
36
+ const data = daily.map((d) => ({
37
+ date: d.date.slice(5),
38
+ cost: Number(d.costUSD.toFixed(2)),
39
+ }))
40
+
41
+ return (
42
+ <Card>
43
+ <CardHeader>
44
+ <CardTitle>Daily Cost</CardTitle>
45
+ <CardDescription>USD spent per day</CardDescription>
46
+ </CardHeader>
47
+ <CardContent>
48
+ <ChartContainer config={costConfig} className="min-h-[250px] w-full">
49
+ <LineChart data={data} accessibilityLayer>
50
+ <CartesianGrid vertical={false} />
51
+ <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
52
+ <YAxis tickLine={false} axisLine={false} tickFormatter={(v) => `$${v}`} />
53
+ <ChartTooltip
54
+ content={<ChartTooltipContent formatter={(value) => formatCost(Number(value))} />}
55
+ />
56
+ <Line dataKey="cost" type="monotone" stroke="var(--color-cost)" strokeWidth={2} dot={{ r: 3, fill: 'var(--color-cost)' }} activeDot={{ r: 5 }} />
57
+ </LineChart>
58
+ </ChartContainer>
59
+ </CardContent>
60
+ </Card>
61
+ )
62
+ }
63
+
64
+ // ─── 2. Tool Mix Over Time ──────────────────────────────────────────
65
+
66
+ const TOP_TOOLS = ['Read', 'Edit', 'Write', 'Bash', 'Grep', 'Agent'] as const
67
+ const toolMixConfig = {
68
+ Read: { label: 'Read', color: 'var(--chart-1)' },
69
+ Edit: { label: 'Edit', color: 'var(--chart-2)' },
70
+ Write: { label: 'Write', color: 'var(--chart-3)' },
71
+ Bash: { label: 'Bash', color: 'var(--chart-4)' },
72
+ Grep: { label: 'Grep', color: 'var(--chart-6)' },
73
+ Agent: { label: 'Agent', color: 'var(--chart-7)' },
20
74
  } satisfies ChartConfig
21
75
 
22
- const messagesConfig = {
23
- messages: { label: 'Messages', color: 'var(--chart-1)' },
76
+ export function ToolMixChart({ daily }: { daily: DailyUsage[] }) {
77
+ const data = daily.map((d) => {
78
+ const row: Record<string, string | number> = { date: d.date.slice(5) }
79
+ for (const tool of TOP_TOOLS) {
80
+ row[tool] = d.toolCallsDetail[tool] || 0
81
+ }
82
+ return row
83
+ })
84
+
85
+ return (
86
+ <Card>
87
+ <CardHeader>
88
+ <CardTitle>Tool Mix Over Time</CardTitle>
89
+ <CardDescription>Daily tool calls — explore (Read/Grep) vs build (Edit/Write)</CardDescription>
90
+ </CardHeader>
91
+ <CardContent>
92
+ <ChartContainer config={toolMixConfig} className="min-h-[250px] w-full">
93
+ <AreaChart data={data} accessibilityLayer>
94
+ <CartesianGrid vertical={false} />
95
+ <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
96
+ <YAxis tickLine={false} axisLine={false} />
97
+ <ChartTooltip content={<ChartTooltipContent />} />
98
+ {TOP_TOOLS.map((tool) => (
99
+ <Area
100
+ key={tool}
101
+ dataKey={tool}
102
+ type="monotone"
103
+ stackId="1"
104
+ stroke={`var(--color-${tool})`}
105
+ fill={`var(--color-${tool})`}
106
+ fillOpacity={0.4}
107
+ />
108
+ ))}
109
+ </AreaChart>
110
+ </ChartContainer>
111
+ </CardContent>
112
+ </Card>
113
+ )
114
+ }
115
+
116
+ // ─── 3. Interruption Rate Trend ─────────────────────────────────────
117
+
118
+ const interruptionConfig = {
119
+ rate: { label: 'Interruption Rate', color: 'var(--chart-5)' },
24
120
  } satisfies ChartConfig
25
121
 
26
- export function DailyChart({ daily }: { daily: DailyUsage[] }) {
122
+ export function InterruptionRateChart({ daily }: { daily: DailyUsage[] }) {
27
123
  const data = daily.map((d) => ({
28
124
  date: d.date.slice(5),
29
- cost: Number(d.costUSD.toFixed(2)),
30
- sessions: d.sessions,
31
- messages: d.messages,
125
+ rate: d.messages > 0 ? Number(((d.interruptions / d.messages) * 100).toFixed(1)) : 0,
126
+ interruptions: d.interruptions,
32
127
  }))
33
128
 
34
129
  return (
35
- <>
36
- <Card>
37
- <CardHeader>
38
- <CardTitle>Daily Cost</CardTitle>
39
- <CardDescription>USD spent per day</CardDescription>
40
- </CardHeader>
41
- <CardContent>
42
- <ChartContainer config={costConfig} className="min-h-[250px] w-full">
43
- <LineChart data={data} accessibilityLayer>
44
- <CartesianGrid vertical={false} />
45
- <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
46
- <YAxis tickLine={false} axisLine={false} tickFormatter={(v) => `$${v}`} />
47
- <ChartTooltip
48
- content={
49
- <ChartTooltipContent
50
- formatter={(value) => formatCost(Number(value))}
51
- />
52
- }
53
- />
54
- <Line
55
- dataKey="cost"
56
- type="monotone"
57
- stroke="var(--color-cost)"
58
- strokeWidth={2}
59
- dot={{ r: 3, fill: 'var(--color-cost)' }}
60
- activeDot={{ r: 5 }}
61
- />
62
- </LineChart>
63
- </ChartContainer>
64
- </CardContent>
65
- </Card>
130
+ <Card>
131
+ <CardHeader>
132
+ <CardTitle>Interruption Rate</CardTitle>
133
+ <CardDescription>Daily interruptions as % of messages — lower is better</CardDescription>
134
+ </CardHeader>
135
+ <CardContent>
136
+ <ChartContainer config={interruptionConfig} className="min-h-[250px] w-full">
137
+ <LineChart data={data} accessibilityLayer>
138
+ <CartesianGrid vertical={false} />
139
+ <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
140
+ <YAxis tickLine={false} axisLine={false} tickFormatter={(v) => `${v}%`} />
141
+ <ChartTooltip
142
+ content={
143
+ <ChartTooltipContent
144
+ formatter={(value, name) =>
145
+ name === 'rate' ? `${value}%` : String(value)
146
+ }
147
+ />
148
+ }
149
+ />
150
+ <Line dataKey="rate" type="monotone" stroke="var(--color-rate)" strokeWidth={2} dot={{ r: 3, fill: 'var(--color-rate)' }} activeDot={{ r: 5 }} />
151
+ </LineChart>
152
+ </ChartContainer>
153
+ </CardContent>
154
+ </Card>
155
+ )
156
+ }
66
157
 
67
- <Card>
68
- <CardHeader>
69
- <CardTitle>Daily Sessions</CardTitle>
70
- <CardDescription>Number of sessions per day</CardDescription>
71
- </CardHeader>
72
- <CardContent>
73
- <ChartContainer config={sessionsConfig} className="min-h-[250px] w-full">
74
- <LineChart data={data} accessibilityLayer>
75
- <CartesianGrid vertical={false} />
76
- <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
77
- <YAxis tickLine={false} axisLine={false} />
78
- <ChartTooltip content={<ChartTooltipContent />} />
79
- <Line
80
- dataKey="sessions"
81
- type="monotone"
82
- stroke="var(--color-sessions)"
83
- strokeWidth={2}
84
- dot={{ r: 3, fill: 'var(--color-sessions)' }}
85
- activeDot={{ r: 5 }}
86
- />
87
- </LineChart>
88
- </ChartContainer>
89
- </CardContent>
90
- </Card>
158
+ // ─── 4. Top Skills Used ─────────────────────────────────────────────
159
+
160
+ const commandConfig = {
161
+ count: { label: 'Uses', color: 'var(--chart-1)' },
162
+ } satisfies ChartConfig
91
163
 
164
+ interface CommandBarEntry {
165
+ name: string
166
+ count: number
167
+ fill: string
168
+ historyCount: number
169
+ sessionCount: number
170
+ }
171
+
172
+ export function TopCommandsChart() {
173
+ const [data, setData] = useState<CommandBarEntry[]>([])
174
+
175
+ useEffect(() => {
176
+ fetch('/api/commands')
177
+ .then(r => r.json())
178
+ .then(analysis => {
179
+ const dbSkills: Record<string, number> = analysis.dbSkillCounts || {}
180
+ const bi = analysis.commands
181
+ .filter((c: { used: boolean; count: number }) => c.used && c.count > 0)
182
+ .map((c: { command: string; count: number }) => {
183
+ const sk = dbSkills[c.command] || 0
184
+ return { name: c.command, count: c.count, fill: 'var(--chart-1)', historyCount: c.count - sk, sessionCount: sk }
185
+ })
186
+ .sort((a: { count: number }, b: { count: number }) => b.count - a.count)
187
+ .slice(0, 5)
188
+ const cu = analysis.customCommands
189
+ .sort((a: { count: number }, b: { count: number }) => b.count - a.count)
190
+ .slice(0, 5)
191
+ .map((c: { command: string; count: number; historyCount?: number; sessionCount?: number }) => ({
192
+ name: c.command,
193
+ count: c.count,
194
+ fill: 'var(--chart-4)',
195
+ historyCount: c.historyCount || 0,
196
+ sessionCount: c.sessionCount || 0,
197
+ }))
198
+ const combined = [...bi, ...cu].sort((a, b) => b.count - a.count)
199
+ setData(combined)
200
+ })
201
+ .catch(() => {})
202
+ }, [])
203
+
204
+ if (data.length === 0) {
205
+ return (
92
206
  <Card>
93
207
  <CardHeader>
94
- <CardTitle>Daily Messages</CardTitle>
95
- <CardDescription>Messages exchanged per day</CardDescription>
208
+ <CardTitle>Top Commands & Skills</CardTitle>
209
+ <CardDescription>Most used built-in commands and custom skills</CardDescription>
96
210
  </CardHeader>
97
211
  <CardContent>
98
- <ChartContainer config={messagesConfig} className="min-h-[250px] w-full">
99
- <LineChart data={data} accessibilityLayer>
100
- <CartesianGrid vertical={false} />
101
- <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
102
- <YAxis tickLine={false} axisLine={false} />
103
- <ChartTooltip content={<ChartTooltipContent />} />
104
- <Line
105
- dataKey="messages"
106
- type="monotone"
107
- stroke="var(--color-messages)"
108
- strokeWidth={2}
109
- dot={{ r: 3, fill: 'var(--color-messages)' }}
110
- activeDot={{ r: 5 }}
111
- />
112
- </LineChart>
113
- </ChartContainer>
212
+ <div className="flex min-h-[250px] items-center justify-center text-sm text-muted-foreground">
213
+ No commands used yet
214
+ </div>
114
215
  </CardContent>
115
216
  </Card>
217
+ )
218
+ }
219
+
220
+ return (
221
+ <Card>
222
+ <CardHeader>
223
+ <CardTitle>Top Commands & Skills</CardTitle>
224
+ <CardDescription>
225
+ <span className="inline-flex items-center gap-1.5"><span className="inline-block h-2.5 w-2.5 rounded-sm" style={{ backgroundColor: 'var(--chart-1)' }} /> Built-in</span>
226
+ <span className="inline-flex items-center gap-1.5 ml-3"><span className="inline-block h-2.5 w-2.5 rounded-sm" style={{ backgroundColor: 'var(--chart-4)' }} /> Custom skill</span>
227
+ </CardDescription>
228
+ </CardHeader>
229
+ <CardContent>
230
+ <ChartContainer config={commandConfig} className="w-full" style={{ minHeight: Math.max(200, data.length * 36) }}>
231
+ <BarChart data={data} layout="vertical" margin={{ left: 8 }} accessibilityLayer>
232
+ <XAxis type="number" tickLine={false} axisLine={false} />
233
+ <YAxis type="category" dataKey="name" tickLine={false} axisLine={false} width={140} />
234
+ <RechartsTooltip
235
+ cursor={{ fill: 'var(--muted)', opacity: 0.3 }}
236
+ content={({ active, payload }) => {
237
+ if (!active || !payload?.length) return null
238
+ const d = payload[0].payload as CommandBarEntry
239
+ return (
240
+ <div className="rounded-lg border bg-background px-3 py-2 text-sm shadow-md">
241
+ <div className="font-semibold">{d.name}</div>
242
+ <div className="text-muted-foreground mt-1 space-y-0.5">
243
+ <div className="flex justify-between gap-4">
244
+ <span>Slash command</span>
245
+ <span className="font-mono">{d.historyCount ?? 0}</span>
246
+ </div>
247
+ <div className="flex justify-between gap-4">
248
+ <span>Skill invocation</span>
249
+ <span className="font-mono">{d.sessionCount ?? 0}</span>
250
+ </div>
251
+ <div className="flex justify-between gap-4 border-t pt-0.5 text-foreground font-medium">
252
+ <span>Total</span>
253
+ <span className="font-mono">{d.count ?? 0}</span>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ )
258
+ }}
259
+ />
260
+ <Bar dataKey="count" radius={[0, 4, 4, 0]}>
261
+ {data.map((entry, i) => (
262
+ <Cell key={i} fill={entry.fill} />
263
+ ))}
264
+ </Bar>
265
+ </BarChart>
266
+ </ChartContainer>
267
+ </CardContent>
268
+ </Card>
269
+ )
270
+ }
271
+
272
+ // ─── 5. User vs Assistant Messages ──────────────────────────────────
273
+
274
+ const msgRatioConfig = {
275
+ userMessages: { label: 'User', color: 'var(--chart-1)' },
276
+ assistantMessages: { label: 'Assistant', color: 'var(--chart-3)' },
277
+ } satisfies ChartConfig
278
+
279
+ export function UserVsAssistantChart({ daily }: { daily: DailyUsage[] }) {
280
+ const data = daily.map((d) => ({
281
+ date: d.date.slice(5),
282
+ userMessages: d.userMessages,
283
+ assistantMessages: d.assistantMessages,
284
+ }))
285
+
286
+ return (
287
+ <Card>
288
+ <CardHeader>
289
+ <CardTitle>User vs Assistant Messages</CardTitle>
290
+ <CardDescription>Low user ratio = Claude doing more autonomous turns</CardDescription>
291
+ </CardHeader>
292
+ <CardContent>
293
+ <ChartContainer config={msgRatioConfig} className="min-h-[250px] w-full">
294
+ <BarChart data={data} accessibilityLayer>
295
+ <CartesianGrid vertical={false} />
296
+ <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
297
+ <YAxis tickLine={false} axisLine={false} />
298
+ <ChartTooltip content={<ChartTooltipContent />} />
299
+ <Bar dataKey="userMessages" stackId="1" fill="var(--color-userMessages)" radius={[0, 0, 0, 0]} />
300
+ <Bar dataKey="assistantMessages" stackId="1" fill="var(--color-assistantMessages)" radius={[4, 4, 0, 0]} />
301
+ </BarChart>
302
+ </ChartContainer>
303
+ </CardContent>
304
+ </Card>
305
+ )
306
+ }
307
+
308
+ // ─── 6. Token Usage Heatmap ─────────────────────────────────────────
309
+
310
+ const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
311
+
312
+ function formatTokensShort(n: number): string {
313
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
314
+ if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`
315
+ return String(n)
316
+ }
317
+
318
+ function formatDateLabel(dateStr: string): string {
319
+ const d = new Date(dateStr + 'T00:00:00')
320
+ const month = MONTH_NAMES[d.getMonth()]
321
+ const day = d.getDate()
322
+ const suffix = day === 1 || day === 21 || day === 31 ? 'st' : day === 2 || day === 22 ? 'nd' : day === 3 || day === 23 ? 'rd' : 'th'
323
+ return `${month} ${day}${suffix}`
324
+ }
325
+
326
+ export function TokenUsageHeatmap({ daily }: { daily: DailyUsage[] }) {
327
+ const { tokensByDate, rateLimitByDate, maxTokens, totalTokens } = useMemo(() => {
328
+ const tMap = new Map<string, number>()
329
+ const rMap = new Map<string, number>()
330
+ let max = 0
331
+ let total = 0
332
+ for (const d of daily) {
333
+ tMap.set(d.date, d.totalTokens)
334
+ total += d.totalTokens
335
+ if (d.totalTokens > max) max = d.totalTokens
336
+ if (d.rateLimitErrors > 0) rMap.set(d.date, d.rateLimitErrors)
337
+ }
338
+ return { tokensByDate: tMap, rateLimitByDate: rMap, maxTokens: max, totalTokens: total }
339
+ }, [daily])
340
+
341
+ const { weeks, monthLabels } = useMemo(() => {
342
+ if (daily.length === 0) return { weeks: [], monthLabels: [] }
343
+ const lastDate = new Date(daily[daily.length - 1].date)
344
+ const result: { date: string; tokens: number; rateLimits: number; dayOfWeek: number }[][] = []
345
+ const startDate = new Date(lastDate)
346
+ startDate.setDate(startDate.getDate() - 83)
347
+ // Align to Sunday
348
+ startDate.setDate(startDate.getDate() - startDate.getDay())
349
+
350
+ let currentWeek: { date: string; tokens: number; rateLimits: number; dayOfWeek: number }[] = []
351
+ const d = new Date(startDate)
352
+ while (d <= lastDate) {
353
+ const dateStr = d.toLocaleDateString('en-CA')
354
+ currentWeek.push({
355
+ date: dateStr,
356
+ tokens: tokensByDate.get(dateStr) || 0,
357
+ rateLimits: rateLimitByDate.get(dateStr) || 0,
358
+ dayOfWeek: d.getDay(),
359
+ })
360
+ if (d.getDay() === 6) {
361
+ result.push(currentWeek)
362
+ currentWeek = []
363
+ }
364
+ d.setDate(d.getDate() + 1)
365
+ }
366
+ if (currentWeek.length > 0) result.push(currentWeek)
367
+
368
+ // Compute month labels with column positions
369
+ const labels: { label: string; col: number }[] = []
370
+ let lastMonth = -1
371
+ for (let wi = 0; wi < result.length; wi++) {
372
+ const firstDay = result[wi][0]
373
+ if (!firstDay) continue
374
+ const month = new Date(firstDay.date).getMonth()
375
+ if (month !== lastMonth) {
376
+ labels.push({ label: MONTH_NAMES[month], col: wi })
377
+ lastMonth = month
378
+ }
379
+ }
380
+
381
+ return { weeks: result, monthLabels: labels }
382
+ }, [daily, tokensByDate, rateLimitByDate])
383
+
384
+ const totalRateLimitDays = rateLimitByDate.size
385
+
386
+ function getIntensity(tokens: number): string {
387
+ if (tokens === 0) return 'bg-muted'
388
+ const ratio = tokens / maxTokens
389
+ if (ratio <= 0.25) return 'bg-blue-200 dark:bg-blue-900/50'
390
+ if (ratio <= 0.5) return 'bg-blue-300 dark:bg-blue-700/60'
391
+ if (ratio <= 0.75) return 'bg-blue-400 dark:bg-blue-600/70'
392
+ return 'bg-blue-500 dark:bg-blue-500'
393
+ }
394
+
395
+ return (
396
+ <Card>
397
+ <CardHeader>
398
+ <CardTitle>
399
+ {formatTokensShort(totalTokens)} tokens in the last {daily.length} days
400
+ </CardTitle>
401
+ <CardDescription>
402
+ Daily token volume
403
+ {totalRateLimitDays > 0 && (
404
+ <span className="ml-1">
405
+ · <span className="inline-block h-2.5 w-2.5 rounded-sm ring-2 ring-rose-400 ring-inset bg-blue-300 align-middle" /> = rate limited ({totalRateLimitDays}d)
406
+ </span>
407
+ )}
408
+ </CardDescription>
409
+ </CardHeader>
410
+ <CardContent>
411
+ <TooltipProvider>
412
+ <div className="overflow-x-auto">
413
+ <div className="flex w-full gap-[3px]">
414
+ {/* Day labels column — rendered as a matching grid so heights stay in sync */}
415
+ <div className="grid shrink-0 grid-rows-[16px_repeat(7,1fr)] gap-[3px] pr-1 text-[10px] text-muted-foreground">
416
+ <div />
417
+ <div className="flex items-center">Sun</div>
418
+ <div className="flex items-center">Mon</div>
419
+ <div className="flex items-center">Tue</div>
420
+ <div className="flex items-center">Wed</div>
421
+ <div className="flex items-center">Thu</div>
422
+ <div className="flex items-center">Fri</div>
423
+ <div className="flex items-center">Sat</div>
424
+ </div>
425
+ {/* Week columns */}
426
+ {weeks.map((week, wi) => {
427
+ const monthLabel = monthLabels.find((m) => m.col === wi)
428
+ return (
429
+ <div key={wi} className="grid flex-1 grid-rows-[16px_repeat(7,1fr)] gap-[3px]">
430
+ <div className="text-[10px] text-muted-foreground leading-4 truncate">
431
+ {monthLabel?.label ?? ''}
432
+ </div>
433
+ {Array.from({ length: 7 }, (_, dayIdx) => {
434
+ const cell = week.find((c) => c.dayOfWeek === dayIdx)
435
+ if (!cell) return <div key={dayIdx} className="aspect-square w-full" />
436
+ const hasRateLimit = cell.rateLimits > 0
437
+ const label = cell.tokens > 0
438
+ ? `${formatTokensShort(cell.tokens)} tokens on ${formatDateLabel(cell.date)}.${hasRateLimit ? ` Rate limited ${cell.rateLimits}×.` : ''}`
439
+ : `No tokens on ${formatDateLabel(cell.date)}.`
440
+ return (
441
+ <Tooltip key={dayIdx}>
442
+ <TooltipTrigger
443
+ render={<div />}
444
+ className={`aspect-square w-full rounded-sm ${getIntensity(cell.tokens)} ${hasRateLimit ? 'ring-2 ring-rose-400 ring-inset' : ''}`}
445
+ />
446
+ <TooltipContent>{label}</TooltipContent>
447
+ </Tooltip>
448
+ )
449
+ })}
450
+ </div>
451
+ )
452
+ })}
453
+ </div>
454
+ {/* Legend */}
455
+ <div className="mt-2 flex items-center justify-end gap-1 text-[10px] text-muted-foreground">
456
+ <span>Less</span>
457
+ <div className="h-[10px] w-[10px] rounded-sm bg-muted" />
458
+ <div className="h-[10px] w-[10px] rounded-sm bg-blue-200 dark:bg-blue-900/50" />
459
+ <div className="h-[10px] w-[10px] rounded-sm bg-blue-300 dark:bg-blue-700/60" />
460
+ <div className="h-[10px] w-[10px] rounded-sm bg-blue-400 dark:bg-blue-600/70" />
461
+ <div className="h-[10px] w-[10px] rounded-sm bg-blue-500 dark:bg-blue-500" />
462
+ <span>More</span>
463
+ </div>
464
+ </div>
465
+ </TooltipProvider>
466
+ </CardContent>
467
+ </Card>
468
+ )
469
+ }
470
+
471
+ // ─── Export ──────────────────────────────────────────────────────────
472
+
473
+ export function DailyChart({ daily, sessions }: { daily: DailyUsage[]; sessions: SessionSummary[] }) {
474
+ return (
475
+ <>
476
+ <DailyCostChart daily={daily} />
477
+ <TopCommandsChart />
478
+ <InterruptionRateChart daily={daily} />
479
+ <UserVsAssistantChart daily={daily} />
116
480
  </>
117
481
  )
118
482
  }