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.
- package/.github/workflows/release.yml +111 -0
- package/README.md +41 -38
- package/app/(dashboard)/daily/page.tsx +1 -1
- package/app/(dashboard)/data-management/page.tsx +180 -0
- package/app/(dashboard)/flow/page.tsx +17 -0
- package/app/(dashboard)/layout.tsx +2 -0
- package/app/(dashboard)/page.tsx +24 -5
- package/app/(dashboard)/reports/[id]/page.tsx +72 -0
- package/app/(dashboard)/reports/page.tsx +132 -0
- package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
- package/app/api/backup/route.ts +215 -0
- package/app/api/check/route.ts +11 -1
- package/app/api/command-insights/route.ts +13 -0
- package/app/api/commands/route.ts +55 -1
- package/app/api/images-analysis/route.ts +3 -4
- package/app/api/reports/[id]/route.ts +23 -0
- package/app/api/reports/route.ts +50 -0
- package/app/api/reset/route.ts +21 -0
- package/app/api/session/route.ts +40 -0
- package/app/api/usage/route.ts +26 -1
- package/app/layout.tsx +1 -1
- package/bin/agentfit.mjs +2 -2
- package/components/agent-coach.tsx +256 -129
- package/components/app-sidebar.tsx +45 -10
- package/components/backup-section.tsx +236 -0
- package/components/daily-chart.tsx +447 -83
- package/components/dashboard-shell.tsx +29 -31
- package/components/data-provider.tsx +88 -8
- package/components/fitness-score.tsx +95 -54
- package/components/overview-cards.tsx +148 -41
- package/components/report-view.tsx +307 -0
- package/components/screenshots-analysis.tsx +51 -46
- package/components/session-chatlog.tsx +124 -0
- package/components/session-timeline.tsx +184 -0
- package/components/session-workflow.tsx +183 -0
- package/components/sessions-table.tsx +9 -1
- package/components/tool-flow-graph.tsx +144 -0
- package/components/ui/carousel.tsx +242 -0
- package/components/ui/sidebar.tsx +1 -1
- package/components/ui/sonner.tsx +51 -0
- package/electron/entitlements.mac.plist +16 -0
- package/electron/init-db.mjs +37 -0
- package/electron/main.mjs +203 -0
- package/generated/prisma/browser.ts +5 -0
- package/generated/prisma/client.ts +5 -0
- package/generated/prisma/internal/class.ts +14 -4
- package/generated/prisma/internal/prismaNamespace.ts +97 -2
- package/generated/prisma/internal/prismaNamespaceBrowser.ts +21 -1
- package/generated/prisma/models/Report.ts +1219 -0
- package/generated/prisma/models/Session.ts +221 -1
- package/generated/prisma/models.ts +1 -0
- package/lib/coach.ts +571 -211
- package/lib/command-insights.ts +231 -0
- package/lib/db.ts +2 -2
- package/lib/parse-codex.ts +6 -0
- package/lib/parse-logs.ts +80 -1
- package/lib/queries-codex.ts +24 -0
- package/lib/queries.ts +45 -0
- package/lib/report.ts +156 -0
- package/lib/session-detail.ts +382 -0
- package/lib/sync.ts +87 -0
- package/lib/tool-flow.ts +71 -0
- package/next.config.mjs +6 -1
- package/package.json +17 -2
- package/plugins/cost-heatmap/component.tsx +72 -50
- package/prisma/migrations/20260401144555_add_system_prompt_edits/migration.sql +80 -0
- package/prisma/schema.prisma +18 -0
- package/prisma/schema.sql +81 -0
- package/.claude/settings.local.json +0 -26
- package/CONTRIBUTING.md +0 -209
- package/prisma/migrations/20260328152517_init/migration.sql +0 -41
- package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
- package/prisma.config.ts +0 -14
- package/setup.sh +0 -73
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
122
|
+
export function InterruptionRateChart({ daily }: { daily: DailyUsage[] }) {
|
|
27
123
|
const data = daily.map((d) => ({
|
|
28
124
|
date: d.date.slice(5),
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
<
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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>
|
|
95
|
-
<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
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
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
|
}
|