agentfit 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -34
- package/app/(dashboard)/daily/page.tsx +1 -1
- 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/(dashboard)/settings/page.tsx +180 -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/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 +25 -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 +258 -8
- package/components/backup-section.tsx +236 -0
- package/components/daily-chart.tsx +404 -83
- package/components/dashboard-shell.tsx +9 -24
- package/components/data-provider.tsx +66 -2
- 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/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 +96 -2
- package/generated/prisma/internal/prismaNamespaceBrowser.ts +20 -1
- package/generated/prisma/models/Report.ts +1219 -0
- package/generated/prisma/models/Session.ts +187 -1
- package/generated/prisma/models.ts +1 -0
- package/lib/coach.ts +530 -211
- package/lib/command-insights.ts +231 -0
- package/lib/db.ts +1 -1
- package/lib/parse-codex.ts +5 -0
- package/lib/parse-logs.ts +65 -0
- package/lib/queries-codex.ts +22 -0
- package/lib/queries.ts +42 -0
- package/lib/report.ts +156 -0
- package/lib/session-detail.ts +382 -0
- package/lib/sync.ts +77 -0
- package/lib/tool-flow.ts +71 -0
- package/next.config.mjs +6 -1
- package/package.json +16 -2
- package/plugins/cost-heatmap/component.tsx +72 -50
- package/prisma/schema.prisma +17 -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/migrations/migration_lock.toml +0 -3
- 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,418 @@ 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
|
-
<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 ─────────────────────────────────────────────
|
|
91
159
|
|
|
160
|
+
const commandConfig = {
|
|
161
|
+
count: { label: 'Uses', color: 'var(--chart-1)' },
|
|
162
|
+
} satisfies ChartConfig
|
|
163
|
+
|
|
164
|
+
export function TopCommandsChart() {
|
|
165
|
+
const [data, setData] = useState<{ name: string; count: number; fill: string }[]>([])
|
|
166
|
+
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
fetch('/api/commands')
|
|
169
|
+
.then(r => r.json())
|
|
170
|
+
.then(analysis => {
|
|
171
|
+
const bi = analysis.commands
|
|
172
|
+
.filter((c: { used: boolean; count: number }) => c.used && c.count > 0)
|
|
173
|
+
.map((c: { command: string; count: number }) => ({ name: c.command, count: c.count, fill: 'var(--chart-1)' }))
|
|
174
|
+
.sort((a: { count: number }, b: { count: number }) => b.count - a.count)
|
|
175
|
+
.slice(0, 5)
|
|
176
|
+
const cu = analysis.customCommands
|
|
177
|
+
.sort((a: { count: number }, b: { count: number }) => b.count - a.count)
|
|
178
|
+
.slice(0, 5)
|
|
179
|
+
.map((c: { command: string; count: number }) => ({ name: c.command, count: c.count, fill: 'var(--chart-4)' }))
|
|
180
|
+
const combined = [...bi, ...cu].sort((a, b) => b.count - a.count)
|
|
181
|
+
setData(combined)
|
|
182
|
+
})
|
|
183
|
+
.catch(() => {})
|
|
184
|
+
}, [])
|
|
185
|
+
|
|
186
|
+
if (data.length === 0) {
|
|
187
|
+
return (
|
|
92
188
|
<Card>
|
|
93
189
|
<CardHeader>
|
|
94
|
-
<CardTitle>
|
|
95
|
-
<CardDescription>
|
|
190
|
+
<CardTitle>Top Commands & Skills</CardTitle>
|
|
191
|
+
<CardDescription>Most used built-in commands and custom skills</CardDescription>
|
|
96
192
|
</CardHeader>
|
|
97
193
|
<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>
|
|
194
|
+
<div className="flex min-h-[250px] items-center justify-center text-sm text-muted-foreground">
|
|
195
|
+
No commands used yet
|
|
196
|
+
</div>
|
|
114
197
|
</CardContent>
|
|
115
198
|
</Card>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<Card>
|
|
204
|
+
<CardHeader>
|
|
205
|
+
<CardTitle>Top Commands & Skills</CardTitle>
|
|
206
|
+
<CardDescription>
|
|
207
|
+
<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>
|
|
208
|
+
<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>
|
|
209
|
+
</CardDescription>
|
|
210
|
+
</CardHeader>
|
|
211
|
+
<CardContent>
|
|
212
|
+
<ChartContainer config={commandConfig} className="w-full" style={{ minHeight: Math.max(200, data.length * 36) }}>
|
|
213
|
+
<BarChart data={data} layout="vertical" margin={{ left: 8 }} accessibilityLayer>
|
|
214
|
+
<XAxis type="number" tickLine={false} axisLine={false} />
|
|
215
|
+
<YAxis type="category" dataKey="name" tickLine={false} axisLine={false} width={140} />
|
|
216
|
+
<ChartTooltip content={<ChartTooltipContent />} />
|
|
217
|
+
<Bar dataKey="count" radius={[0, 4, 4, 0]}>
|
|
218
|
+
{data.map((entry, i) => (
|
|
219
|
+
<Cell key={i} fill={entry.fill} />
|
|
220
|
+
))}
|
|
221
|
+
</Bar>
|
|
222
|
+
</BarChart>
|
|
223
|
+
</ChartContainer>
|
|
224
|
+
</CardContent>
|
|
225
|
+
</Card>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ─── 5. User vs Assistant Messages ──────────────────────────────────
|
|
230
|
+
|
|
231
|
+
const msgRatioConfig = {
|
|
232
|
+
userMessages: { label: 'User', color: 'var(--chart-1)' },
|
|
233
|
+
assistantMessages: { label: 'Assistant', color: 'var(--chart-3)' },
|
|
234
|
+
} satisfies ChartConfig
|
|
235
|
+
|
|
236
|
+
export function UserVsAssistantChart({ daily }: { daily: DailyUsage[] }) {
|
|
237
|
+
const data = daily.map((d) => ({
|
|
238
|
+
date: d.date.slice(5),
|
|
239
|
+
userMessages: d.userMessages,
|
|
240
|
+
assistantMessages: d.assistantMessages,
|
|
241
|
+
}))
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<Card>
|
|
245
|
+
<CardHeader>
|
|
246
|
+
<CardTitle>User vs Assistant Messages</CardTitle>
|
|
247
|
+
<CardDescription>Low user ratio = Claude doing more autonomous turns</CardDescription>
|
|
248
|
+
</CardHeader>
|
|
249
|
+
<CardContent>
|
|
250
|
+
<ChartContainer config={msgRatioConfig} className="min-h-[250px] w-full">
|
|
251
|
+
<BarChart data={data} accessibilityLayer>
|
|
252
|
+
<CartesianGrid vertical={false} />
|
|
253
|
+
<XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
|
|
254
|
+
<YAxis tickLine={false} axisLine={false} />
|
|
255
|
+
<ChartTooltip content={<ChartTooltipContent />} />
|
|
256
|
+
<Bar dataKey="userMessages" stackId="1" fill="var(--color-userMessages)" radius={[0, 0, 0, 0]} />
|
|
257
|
+
<Bar dataKey="assistantMessages" stackId="1" fill="var(--color-assistantMessages)" radius={[4, 4, 0, 0]} />
|
|
258
|
+
</BarChart>
|
|
259
|
+
</ChartContainer>
|
|
260
|
+
</CardContent>
|
|
261
|
+
</Card>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ─── 6. Token Usage Heatmap ─────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
|
268
|
+
|
|
269
|
+
function formatTokensShort(n: number): string {
|
|
270
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
|
|
271
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`
|
|
272
|
+
return String(n)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function formatDateLabel(dateStr: string): string {
|
|
276
|
+
const d = new Date(dateStr + 'T00:00:00')
|
|
277
|
+
const month = MONTH_NAMES[d.getMonth()]
|
|
278
|
+
const day = d.getDate()
|
|
279
|
+
const suffix = day === 1 || day === 21 || day === 31 ? 'st' : day === 2 || day === 22 ? 'nd' : day === 3 || day === 23 ? 'rd' : 'th'
|
|
280
|
+
return `${month} ${day}${suffix}`
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function TokenUsageHeatmap({ daily }: { daily: DailyUsage[] }) {
|
|
284
|
+
const { tokensByDate, rateLimitByDate, maxTokens, totalTokens } = useMemo(() => {
|
|
285
|
+
const tMap = new Map<string, number>()
|
|
286
|
+
const rMap = new Map<string, number>()
|
|
287
|
+
let max = 0
|
|
288
|
+
let total = 0
|
|
289
|
+
for (const d of daily) {
|
|
290
|
+
tMap.set(d.date, d.totalTokens)
|
|
291
|
+
total += d.totalTokens
|
|
292
|
+
if (d.totalTokens > max) max = d.totalTokens
|
|
293
|
+
if (d.rateLimitErrors > 0) rMap.set(d.date, d.rateLimitErrors)
|
|
294
|
+
}
|
|
295
|
+
return { tokensByDate: tMap, rateLimitByDate: rMap, maxTokens: max, totalTokens: total }
|
|
296
|
+
}, [daily])
|
|
297
|
+
|
|
298
|
+
const { weeks, monthLabels } = useMemo(() => {
|
|
299
|
+
if (daily.length === 0) return { weeks: [], monthLabels: [] }
|
|
300
|
+
const lastDate = new Date(daily[daily.length - 1].date)
|
|
301
|
+
const result: { date: string; tokens: number; rateLimits: number; dayOfWeek: number }[][] = []
|
|
302
|
+
const startDate = new Date(lastDate)
|
|
303
|
+
startDate.setDate(startDate.getDate() - 83)
|
|
304
|
+
// Align to Sunday
|
|
305
|
+
startDate.setDate(startDate.getDate() - startDate.getDay())
|
|
306
|
+
|
|
307
|
+
let currentWeek: { date: string; tokens: number; rateLimits: number; dayOfWeek: number }[] = []
|
|
308
|
+
const d = new Date(startDate)
|
|
309
|
+
while (d <= lastDate) {
|
|
310
|
+
const dateStr = d.toLocaleDateString('en-CA')
|
|
311
|
+
currentWeek.push({
|
|
312
|
+
date: dateStr,
|
|
313
|
+
tokens: tokensByDate.get(dateStr) || 0,
|
|
314
|
+
rateLimits: rateLimitByDate.get(dateStr) || 0,
|
|
315
|
+
dayOfWeek: d.getDay(),
|
|
316
|
+
})
|
|
317
|
+
if (d.getDay() === 6) {
|
|
318
|
+
result.push(currentWeek)
|
|
319
|
+
currentWeek = []
|
|
320
|
+
}
|
|
321
|
+
d.setDate(d.getDate() + 1)
|
|
322
|
+
}
|
|
323
|
+
if (currentWeek.length > 0) result.push(currentWeek)
|
|
324
|
+
|
|
325
|
+
// Compute month labels with column positions
|
|
326
|
+
const labels: { label: string; col: number }[] = []
|
|
327
|
+
let lastMonth = -1
|
|
328
|
+
for (let wi = 0; wi < result.length; wi++) {
|
|
329
|
+
const firstDay = result[wi][0]
|
|
330
|
+
if (!firstDay) continue
|
|
331
|
+
const month = new Date(firstDay.date).getMonth()
|
|
332
|
+
if (month !== lastMonth) {
|
|
333
|
+
labels.push({ label: MONTH_NAMES[month], col: wi })
|
|
334
|
+
lastMonth = month
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return { weeks: result, monthLabels: labels }
|
|
339
|
+
}, [daily, tokensByDate, rateLimitByDate])
|
|
340
|
+
|
|
341
|
+
const totalRateLimitDays = rateLimitByDate.size
|
|
342
|
+
|
|
343
|
+
function getIntensity(tokens: number): string {
|
|
344
|
+
if (tokens === 0) return 'bg-muted'
|
|
345
|
+
const ratio = tokens / maxTokens
|
|
346
|
+
if (ratio <= 0.25) return 'bg-blue-200 dark:bg-blue-900/50'
|
|
347
|
+
if (ratio <= 0.5) return 'bg-blue-300 dark:bg-blue-700/60'
|
|
348
|
+
if (ratio <= 0.75) return 'bg-blue-400 dark:bg-blue-600/70'
|
|
349
|
+
return 'bg-blue-500 dark:bg-blue-500'
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<Card>
|
|
354
|
+
<CardHeader>
|
|
355
|
+
<CardTitle>
|
|
356
|
+
{formatTokensShort(totalTokens)} tokens in the last {daily.length} days
|
|
357
|
+
</CardTitle>
|
|
358
|
+
<CardDescription>
|
|
359
|
+
Daily token volume
|
|
360
|
+
{totalRateLimitDays > 0 && (
|
|
361
|
+
<span className="ml-1">
|
|
362
|
+
· <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)
|
|
363
|
+
</span>
|
|
364
|
+
)}
|
|
365
|
+
</CardDescription>
|
|
366
|
+
</CardHeader>
|
|
367
|
+
<CardContent>
|
|
368
|
+
<TooltipProvider>
|
|
369
|
+
<div className="overflow-x-auto">
|
|
370
|
+
<div className="flex w-full gap-[3px]">
|
|
371
|
+
{/* Day labels column — rendered as a matching grid so heights stay in sync */}
|
|
372
|
+
<div className="grid shrink-0 grid-rows-[16px_repeat(7,1fr)] gap-[3px] pr-1 text-[10px] text-muted-foreground">
|
|
373
|
+
<div />
|
|
374
|
+
<div className="flex items-center">Sun</div>
|
|
375
|
+
<div className="flex items-center">Mon</div>
|
|
376
|
+
<div className="flex items-center">Tue</div>
|
|
377
|
+
<div className="flex items-center">Wed</div>
|
|
378
|
+
<div className="flex items-center">Thu</div>
|
|
379
|
+
<div className="flex items-center">Fri</div>
|
|
380
|
+
<div className="flex items-center">Sat</div>
|
|
381
|
+
</div>
|
|
382
|
+
{/* Week columns */}
|
|
383
|
+
{weeks.map((week, wi) => {
|
|
384
|
+
const monthLabel = monthLabels.find((m) => m.col === wi)
|
|
385
|
+
return (
|
|
386
|
+
<div key={wi} className="grid flex-1 grid-rows-[16px_repeat(7,1fr)] gap-[3px]">
|
|
387
|
+
<div className="text-[10px] text-muted-foreground leading-4 truncate">
|
|
388
|
+
{monthLabel?.label ?? ''}
|
|
389
|
+
</div>
|
|
390
|
+
{Array.from({ length: 7 }, (_, dayIdx) => {
|
|
391
|
+
const cell = week.find((c) => c.dayOfWeek === dayIdx)
|
|
392
|
+
if (!cell) return <div key={dayIdx} className="aspect-square w-full" />
|
|
393
|
+
const hasRateLimit = cell.rateLimits > 0
|
|
394
|
+
const label = cell.tokens > 0
|
|
395
|
+
? `${formatTokensShort(cell.tokens)} tokens on ${formatDateLabel(cell.date)}.${hasRateLimit ? ` Rate limited ${cell.rateLimits}×.` : ''}`
|
|
396
|
+
: `No tokens on ${formatDateLabel(cell.date)}.`
|
|
397
|
+
return (
|
|
398
|
+
<Tooltip key={dayIdx}>
|
|
399
|
+
<TooltipTrigger
|
|
400
|
+
render={<div />}
|
|
401
|
+
className={`aspect-square w-full rounded-sm ${getIntensity(cell.tokens)} ${hasRateLimit ? 'ring-2 ring-rose-400 ring-inset' : ''}`}
|
|
402
|
+
/>
|
|
403
|
+
<TooltipContent>{label}</TooltipContent>
|
|
404
|
+
</Tooltip>
|
|
405
|
+
)
|
|
406
|
+
})}
|
|
407
|
+
</div>
|
|
408
|
+
)
|
|
409
|
+
})}
|
|
410
|
+
</div>
|
|
411
|
+
{/* Legend */}
|
|
412
|
+
<div className="mt-2 flex items-center justify-end gap-1 text-[10px] text-muted-foreground">
|
|
413
|
+
<span>Less</span>
|
|
414
|
+
<div className="h-[10px] w-[10px] rounded-sm bg-muted" />
|
|
415
|
+
<div className="h-[10px] w-[10px] rounded-sm bg-blue-200 dark:bg-blue-900/50" />
|
|
416
|
+
<div className="h-[10px] w-[10px] rounded-sm bg-blue-300 dark:bg-blue-700/60" />
|
|
417
|
+
<div className="h-[10px] w-[10px] rounded-sm bg-blue-400 dark:bg-blue-600/70" />
|
|
418
|
+
<div className="h-[10px] w-[10px] rounded-sm bg-blue-500 dark:bg-blue-500" />
|
|
419
|
+
<span>More</span>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
</TooltipProvider>
|
|
423
|
+
</CardContent>
|
|
424
|
+
</Card>
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ─── Export ──────────────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
export function DailyChart({ daily, sessions }: { daily: DailyUsage[]; sessions: SessionSummary[] }) {
|
|
431
|
+
return (
|
|
432
|
+
<>
|
|
433
|
+
<DailyCostChart daily={daily} />
|
|
434
|
+
<TopCommandsChart />
|
|
435
|
+
<InterruptionRateChart daily={daily} />
|
|
436
|
+
<UserVsAssistantChart daily={daily} />
|
|
116
437
|
</>
|
|
117
438
|
)
|
|
118
439
|
}
|
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
import { usePathname } from 'next/navigation'
|
|
4
4
|
import { RefreshCw } from 'lucide-react'
|
|
5
5
|
import { SidebarProvider, SidebarInset, SidebarTrigger } from '@/components/ui/sidebar'
|
|
6
|
-
import { Separator } from '@/components/ui/separator'
|
|
7
|
-
import { Button } from '@/components/ui/button'
|
|
8
|
-
import { Badge } from '@/components/ui/badge'
|
|
9
|
-
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
|
10
6
|
import {
|
|
11
7
|
Select,
|
|
12
8
|
SelectContent,
|
|
@@ -20,7 +16,7 @@ import { getPlugin } from '@/lib/plugins'
|
|
|
20
16
|
import '@/plugins' // ensure plugins are registered
|
|
21
17
|
import type { ReactNode } from 'react'
|
|
22
18
|
|
|
23
|
-
const VIEW_TITLES: Record<string,
|
|
19
|
+
const VIEW_TITLES: Record<string, ReactNode> = {
|
|
24
20
|
'/': 'Dashboard',
|
|
25
21
|
'/daily': 'Daily Usage',
|
|
26
22
|
'/tokens': 'Token Breakdown',
|
|
@@ -30,10 +26,11 @@ const VIEW_TITLES: Record<string, string> = {
|
|
|
30
26
|
'/personality': 'Personality Fit',
|
|
31
27
|
'/commands': 'Command Usage',
|
|
32
28
|
'/images': 'Images',
|
|
33
|
-
'/coach': '
|
|
29
|
+
'/coach': 'CRAFT Coach',
|
|
30
|
+
'/flow': 'Session Flow',
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
function resolveTitle(pathname: string):
|
|
33
|
+
function resolveTitle(pathname: string): ReactNode {
|
|
37
34
|
if (VIEW_TITLES[pathname]) return VIEW_TITLES[pathname]
|
|
38
35
|
if (pathname === '/community') return 'Community'
|
|
39
36
|
// Community plugin routes: /community/<slug>
|
|
@@ -42,6 +39,10 @@ function resolveTitle(pathname: string): string {
|
|
|
42
39
|
const plugin = getPlugin(match[1])
|
|
43
40
|
if (plugin) return plugin.manifest.name
|
|
44
41
|
}
|
|
42
|
+
if (pathname === '/reports') return 'Reports'
|
|
43
|
+
if (pathname.startsWith('/reports/')) return 'Report Detail'
|
|
44
|
+
if (pathname === '/settings') return 'Settings'
|
|
45
|
+
if (pathname.startsWith('/sessions/')) return 'Session Detail'
|
|
45
46
|
return 'Dashboard'
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -65,8 +66,7 @@ export function DashboardShell({ children }: { children: ReactNode }) {
|
|
|
65
66
|
const pathname = usePathname()
|
|
66
67
|
const {
|
|
67
68
|
agent, setAgent, timeRange, setTimeRange,
|
|
68
|
-
|
|
69
|
-
newSessionsAvailable,
|
|
69
|
+
loading,
|
|
70
70
|
} = useData()
|
|
71
71
|
|
|
72
72
|
const title = resolveTitle(pathname)
|
|
@@ -123,21 +123,6 @@ export function DashboardShell({ children }: { children: ReactNode }) {
|
|
|
123
123
|
<SelectItem value="combined">Combined</SelectItem>
|
|
124
124
|
</SelectContent>
|
|
125
125
|
</Select>
|
|
126
|
-
{lastSyncTime && lastSyncResult && lastSyncResult.sessionsAdded > 0 && (
|
|
127
|
-
<Badge variant="secondary" className="hidden text-xs sm:inline-flex">
|
|
128
|
-
+{lastSyncResult.sessionsAdded} new
|
|
129
|
-
</Badge>
|
|
130
|
-
)}
|
|
131
|
-
<Button
|
|
132
|
-
variant="outline"
|
|
133
|
-
size="sm"
|
|
134
|
-
onClick={handleSync}
|
|
135
|
-
disabled={syncing}
|
|
136
|
-
className="gap-2"
|
|
137
|
-
>
|
|
138
|
-
<RefreshCw className={`h-3.5 w-3.5 ${syncing ? 'animate-spin' : ''}`} />
|
|
139
|
-
{syncing ? 'Syncing...' : 'Sync'}
|
|
140
|
-
</Button>
|
|
141
126
|
</div>
|
|
142
127
|
</header>
|
|
143
128
|
<main className="flex-1 space-y-6 p-6">
|