agentfit 0.1.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/.claude/settings.local.json +26 -0
- package/.prettierignore +7 -0
- package/.prettierrc +11 -0
- package/CONTRIBUTING.md +209 -0
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/app/(dashboard)/coach/page.tsx +11 -0
- package/app/(dashboard)/commands/page.tsx +7 -0
- package/app/(dashboard)/community/[slug]/page.tsx +23 -0
- package/app/(dashboard)/community/page.tsx +71 -0
- package/app/(dashboard)/daily/page.tsx +19 -0
- package/app/(dashboard)/images/page.tsx +5 -0
- package/app/(dashboard)/layout.tsx +12 -0
- package/app/(dashboard)/page.tsx +23 -0
- package/app/(dashboard)/personality/page.tsx +11 -0
- package/app/(dashboard)/projects/page.tsx +11 -0
- package/app/(dashboard)/sessions/page.tsx +11 -0
- package/app/(dashboard)/tokens/page.tsx +11 -0
- package/app/(dashboard)/tools/page.tsx +11 -0
- package/app/api/check/route.ts +13 -0
- package/app/api/commands/route.ts +16 -0
- package/app/api/images/[...path]/route.ts +33 -0
- package/app/api/images-analysis/route.ts +177 -0
- package/app/api/sync/route.ts +14 -0
- package/app/api/usage/route.ts +117 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +144 -0
- package/app/icon.svg +3 -0
- package/app/layout.tsx +35 -0
- package/bin/agentfit.mjs +69 -0
- package/components/.gitkeep +0 -0
- package/components/agent-coach.tsx +248 -0
- package/components/app-sidebar.tsx +161 -0
- package/components/command-usage.tsx +294 -0
- package/components/daily-chart.tsx +118 -0
- package/components/daily-table.tsx +115 -0
- package/components/dashboard-shell.tsx +149 -0
- package/components/data-provider.tsx +213 -0
- package/components/fitness-score.tsx +95 -0
- package/components/overview-cards.tsx +198 -0
- package/components/pagination-controls.tsx +104 -0
- package/components/personality-fit.tsx +446 -0
- package/components/projects-table.tsx +70 -0
- package/components/screenshots-analysis.tsx +359 -0
- package/components/sessions-table.tsx +97 -0
- package/components/theme-provider.tsx +71 -0
- package/components/token-breakdown.tsx +179 -0
- package/components/tool-usage-chart.tsx +63 -0
- package/components/ui/badge.tsx +52 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/card.tsx +103 -0
- package/components/ui/chart.tsx +373 -0
- package/components/ui/dialog.tsx +160 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/scroll-area.tsx +55 -0
- package/components/ui/select.tsx +201 -0
- package/components/ui/separator.tsx +25 -0
- package/components/ui/sheet.tsx +138 -0
- package/components/ui/sidebar.tsx +723 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +82 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components.json +25 -0
- package/generated/prisma/browser.ts +34 -0
- package/generated/prisma/client.ts +58 -0
- package/generated/prisma/commonInputTypes.ts +237 -0
- package/generated/prisma/enums.ts +15 -0
- package/generated/prisma/internal/class.ts +224 -0
- package/generated/prisma/internal/prismaNamespace.ts +920 -0
- package/generated/prisma/internal/prismaNamespaceBrowser.ts +130 -0
- package/generated/prisma/models/Image.ts +1310 -0
- package/generated/prisma/models/Session.ts +1695 -0
- package/generated/prisma/models/SyncLog.ts +1203 -0
- package/generated/prisma/models.ts +14 -0
- package/hooks/.gitkeep +0 -0
- package/hooks/use-mobile.ts +19 -0
- package/hooks/use-pagination.ts +60 -0
- package/lib/.gitkeep +0 -0
- package/lib/coach.ts +425 -0
- package/lib/commands.ts +239 -0
- package/lib/db.ts +15 -0
- package/lib/format.ts +26 -0
- package/lib/parse-codex.ts +201 -0
- package/lib/parse-logs.ts +369 -0
- package/lib/personality.ts +481 -0
- package/lib/plugins.ts +107 -0
- package/lib/pricing.ts +112 -0
- package/lib/queries-codex.ts +130 -0
- package/lib/queries.ts +154 -0
- package/lib/resolve-icon.ts +12 -0
- package/lib/sync.ts +335 -0
- package/lib/utils.ts +6 -0
- package/next.config.mjs +4 -0
- package/package.json +73 -0
- package/plugins/cost-heatmap/component.test.tsx +52 -0
- package/plugins/cost-heatmap/component.tsx +227 -0
- package/plugins/cost-heatmap/manifest.ts +13 -0
- package/plugins/index.ts +18 -0
- package/prisma/migrations/20260328152517_init/migration.sql +41 -0
- package/prisma/migrations/20260328153801_add_image_model/migration.sql +18 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +57 -0
- package/prisma.config.ts +14 -0
- package/public/.gitkeep +0 -0
- package/public/logo.svg +3 -0
- package/setup.sh +73 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
|
5
|
+
import { Badge } from '@/components/ui/badge'
|
|
6
|
+
import {
|
|
7
|
+
Table,
|
|
8
|
+
TableBody,
|
|
9
|
+
TableCell,
|
|
10
|
+
TableHead,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableRow,
|
|
13
|
+
} from '@/components/ui/table'
|
|
14
|
+
import { Check, Circle, Terminal, Lightbulb, Sparkles, TrendingUp } from 'lucide-react'
|
|
15
|
+
import type { CommandAnalysis } from '@/lib/commands'
|
|
16
|
+
|
|
17
|
+
function CategoryBar({ name, used, total, invocations }: {
|
|
18
|
+
name: string
|
|
19
|
+
used: number
|
|
20
|
+
total: number
|
|
21
|
+
invocations: number
|
|
22
|
+
}) {
|
|
23
|
+
const pct = total > 0 ? Math.round((used / total) * 100) : 0
|
|
24
|
+
const getColor = (p: number) => {
|
|
25
|
+
if (p >= 75) return 'bg-chart-2'
|
|
26
|
+
if (p >= 50) return 'bg-chart-1'
|
|
27
|
+
if (p >= 25) return 'bg-chart-3'
|
|
28
|
+
return 'bg-chart-5'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-1.5">
|
|
33
|
+
<div className="flex items-center justify-between text-sm">
|
|
34
|
+
<span className="font-medium">{name}</span>
|
|
35
|
+
<span className="text-muted-foreground">
|
|
36
|
+
{used}/{total} commands ({invocations} uses)
|
|
37
|
+
</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="h-2.5 w-full overflow-hidden rounded-full bg-muted">
|
|
40
|
+
<div
|
|
41
|
+
className={`h-full rounded-full transition-all duration-500 ${getColor(pct)}`}
|
|
42
|
+
style={{ width: `${Math.max(2, pct)}%` }}
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function CommandUsage() {
|
|
50
|
+
const [analysis, setAnalysis] = useState<CommandAnalysis | null>(null)
|
|
51
|
+
const [loading, setLoading] = useState(true)
|
|
52
|
+
const [filter, setFilter] = useState<'all' | 'used' | 'unused'>('all')
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
async function load() {
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch('/api/commands')
|
|
58
|
+
const data = await res.json()
|
|
59
|
+
setAnalysis(data)
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error('Failed to load command analysis:', e)
|
|
62
|
+
} finally {
|
|
63
|
+
setLoading(false)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
load()
|
|
67
|
+
}, [])
|
|
68
|
+
|
|
69
|
+
if (loading) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
|
72
|
+
Loading command usage...
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!analysis) {
|
|
78
|
+
return (
|
|
79
|
+
<div className="text-destructive py-12 text-center">
|
|
80
|
+
Failed to load command analysis
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const filteredCommands = analysis.commands.filter(cmd => {
|
|
86
|
+
if (filter === 'used') return cmd.used
|
|
87
|
+
if (filter === 'unused') return !cmd.used
|
|
88
|
+
return true
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Pick top unused recommendations
|
|
92
|
+
const recommendations = analysis.commands
|
|
93
|
+
.filter(c => !c.used)
|
|
94
|
+
.filter(c => [
|
|
95
|
+
'/compact', '/diff', '/context', '/btw', '/branch', '/export',
|
|
96
|
+
'/plan', '/stats', '/insights', '/security-review', '/doctor',
|
|
97
|
+
'/cost', '/memory', '/hooks', '/keybindings',
|
|
98
|
+
].includes(c.command))
|
|
99
|
+
.slice(0, 8)
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className="space-y-6">
|
|
103
|
+
{/* Overview cards */}
|
|
104
|
+
<div className="grid gap-4 sm:grid-cols-4">
|
|
105
|
+
<Card>
|
|
106
|
+
<CardContent className="pt-6">
|
|
107
|
+
<div className="flex items-center gap-2">
|
|
108
|
+
<Terminal className="h-4 w-4 text-muted-foreground" />
|
|
109
|
+
<span className="text-sm text-muted-foreground">Built-in Commands</span>
|
|
110
|
+
</div>
|
|
111
|
+
<div className="text-3xl font-bold mt-1">{analysis.totalCommands}</div>
|
|
112
|
+
</CardContent>
|
|
113
|
+
</Card>
|
|
114
|
+
<Card>
|
|
115
|
+
<CardContent className="pt-6">
|
|
116
|
+
<div className="flex items-center gap-2">
|
|
117
|
+
<Check className="h-4 w-4 text-chart-2" />
|
|
118
|
+
<span className="text-sm text-muted-foreground">Commands Used</span>
|
|
119
|
+
</div>
|
|
120
|
+
<div className="text-3xl font-bold mt-1">
|
|
121
|
+
{analysis.usedCommands}
|
|
122
|
+
</div>
|
|
123
|
+
</CardContent>
|
|
124
|
+
</Card>
|
|
125
|
+
<Card>
|
|
126
|
+
<CardContent className="pt-6">
|
|
127
|
+
<div className="flex items-center gap-2">
|
|
128
|
+
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
|
129
|
+
<span className="text-sm text-muted-foreground">Usage Rate</span>
|
|
130
|
+
</div>
|
|
131
|
+
<div className="text-3xl font-bold mt-1">{analysis.usagePercentage}%</div>
|
|
132
|
+
</CardContent>
|
|
133
|
+
</Card>
|
|
134
|
+
<Card>
|
|
135
|
+
<CardContent className="pt-6">
|
|
136
|
+
<div className="flex items-center gap-2">
|
|
137
|
+
<Sparkles className="h-4 w-4 text-muted-foreground" />
|
|
138
|
+
<span className="text-sm text-muted-foreground">Total Invocations</span>
|
|
139
|
+
</div>
|
|
140
|
+
<div className="text-3xl font-bold mt-1">{analysis.totalInvocations}</div>
|
|
141
|
+
</CardContent>
|
|
142
|
+
</Card>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{/* Category breakdown */}
|
|
146
|
+
<Card>
|
|
147
|
+
<CardHeader>
|
|
148
|
+
<CardTitle>Coverage by Category</CardTitle>
|
|
149
|
+
<CardDescription>How many commands you've used in each category</CardDescription>
|
|
150
|
+
</CardHeader>
|
|
151
|
+
<CardContent className="space-y-4">
|
|
152
|
+
{Object.entries(analysis.categories)
|
|
153
|
+
.sort(([, a], [, b]) => b.invocations - a.invocations)
|
|
154
|
+
.map(([name, stats]) => (
|
|
155
|
+
<CategoryBar
|
|
156
|
+
key={name}
|
|
157
|
+
name={name}
|
|
158
|
+
used={stats.used}
|
|
159
|
+
total={stats.total}
|
|
160
|
+
invocations={stats.invocations}
|
|
161
|
+
/>
|
|
162
|
+
))}
|
|
163
|
+
</CardContent>
|
|
164
|
+
</Card>
|
|
165
|
+
|
|
166
|
+
{/* Recommendations */}
|
|
167
|
+
{recommendations.length > 0 && (
|
|
168
|
+
<Card>
|
|
169
|
+
<CardHeader>
|
|
170
|
+
<div className="flex items-center gap-2">
|
|
171
|
+
<Lightbulb className="h-5 w-5 text-chart-3" />
|
|
172
|
+
<div>
|
|
173
|
+
<CardTitle>Recommended Commands to Try</CardTitle>
|
|
174
|
+
<CardDescription>
|
|
175
|
+
Useful commands you haven't used yet
|
|
176
|
+
</CardDescription>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</CardHeader>
|
|
180
|
+
<CardContent>
|
|
181
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
182
|
+
{recommendations.map(cmd => (
|
|
183
|
+
<div key={cmd.command} className="flex items-start gap-3 rounded-lg border p-3">
|
|
184
|
+
<code className="shrink-0 rounded bg-muted px-2 py-0.5 text-sm font-semibold text-primary">
|
|
185
|
+
{cmd.command}
|
|
186
|
+
</code>
|
|
187
|
+
<span className="text-sm text-muted-foreground">{cmd.description}</span>
|
|
188
|
+
</div>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
</CardContent>
|
|
192
|
+
</Card>
|
|
193
|
+
)}
|
|
194
|
+
|
|
195
|
+
{/* Custom commands (skills/plugins) */}
|
|
196
|
+
{analysis.customCommands.length > 0 && (
|
|
197
|
+
<Card>
|
|
198
|
+
<CardHeader>
|
|
199
|
+
<CardTitle>Custom Commands (Skills & Plugins)</CardTitle>
|
|
200
|
+
<CardDescription>
|
|
201
|
+
Non-built-in commands you've used — these come from installed skills and plugins
|
|
202
|
+
</CardDescription>
|
|
203
|
+
</CardHeader>
|
|
204
|
+
<CardContent>
|
|
205
|
+
<div className="flex flex-wrap gap-2">
|
|
206
|
+
{analysis.customCommands.map(({ command, count }) => (
|
|
207
|
+
<Badge key={command} variant="secondary" className="gap-1.5 text-sm">
|
|
208
|
+
<code>{command}</code>
|
|
209
|
+
<span className="text-muted-foreground">{count}x</span>
|
|
210
|
+
</Badge>
|
|
211
|
+
))}
|
|
212
|
+
</div>
|
|
213
|
+
</CardContent>
|
|
214
|
+
</Card>
|
|
215
|
+
)}
|
|
216
|
+
|
|
217
|
+
{/* Full command table */}
|
|
218
|
+
<Card>
|
|
219
|
+
<CardHeader>
|
|
220
|
+
<div className="flex items-center justify-between">
|
|
221
|
+
<div>
|
|
222
|
+
<CardTitle>All Commands</CardTitle>
|
|
223
|
+
<CardDescription>
|
|
224
|
+
Complete list of built-in Claude Code commands
|
|
225
|
+
</CardDescription>
|
|
226
|
+
</div>
|
|
227
|
+
<div className="flex gap-1.5">
|
|
228
|
+
{(['all', 'used', 'unused'] as const).map(f => (
|
|
229
|
+
<button
|
|
230
|
+
key={f}
|
|
231
|
+
onClick={() => setFilter(f)}
|
|
232
|
+
className={`rounded-md px-3 py-1 text-xs font-medium transition-colors ${
|
|
233
|
+
filter === f
|
|
234
|
+
? 'bg-primary text-primary-foreground'
|
|
235
|
+
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
|
236
|
+
}`}
|
|
237
|
+
>
|
|
238
|
+
{f === 'all' ? `All (${analysis.totalCommands})` :
|
|
239
|
+
f === 'used' ? `Used (${analysis.usedCommands})` :
|
|
240
|
+
`Unused (${analysis.unusedCommands})`}
|
|
241
|
+
</button>
|
|
242
|
+
))}
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</CardHeader>
|
|
246
|
+
<CardContent>
|
|
247
|
+
<Table>
|
|
248
|
+
<TableHeader>
|
|
249
|
+
<TableRow>
|
|
250
|
+
<TableHead className="w-8"></TableHead>
|
|
251
|
+
<TableHead>Command</TableHead>
|
|
252
|
+
<TableHead>Description</TableHead>
|
|
253
|
+
<TableHead>Category</TableHead>
|
|
254
|
+
<TableHead className="text-right">Uses</TableHead>
|
|
255
|
+
</TableRow>
|
|
256
|
+
</TableHeader>
|
|
257
|
+
<TableBody>
|
|
258
|
+
{filteredCommands.map(cmd => (
|
|
259
|
+
<TableRow key={cmd.command} className={cmd.used ? '' : 'opacity-60'}>
|
|
260
|
+
<TableCell>
|
|
261
|
+
{cmd.used ? (
|
|
262
|
+
<Check className="h-4 w-4 text-chart-2" />
|
|
263
|
+
) : (
|
|
264
|
+
<Circle className="h-4 w-4 text-muted-foreground/40" />
|
|
265
|
+
)}
|
|
266
|
+
</TableCell>
|
|
267
|
+
<TableCell>
|
|
268
|
+
<div className="flex items-center gap-2">
|
|
269
|
+
<code className="text-sm font-semibold">{cmd.command}</code>
|
|
270
|
+
{cmd.aliases && cmd.aliases.length > 0 && (
|
|
271
|
+
<span className="text-xs text-muted-foreground">
|
|
272
|
+
({cmd.aliases.join(', ')})
|
|
273
|
+
</span>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
</TableCell>
|
|
277
|
+
<TableCell className="text-sm text-muted-foreground">
|
|
278
|
+
{cmd.description}
|
|
279
|
+
</TableCell>
|
|
280
|
+
<TableCell>
|
|
281
|
+
<Badge variant="outline" className="text-xs">{cmd.category}</Badge>
|
|
282
|
+
</TableCell>
|
|
283
|
+
<TableCell className="text-right font-mono">
|
|
284
|
+
{cmd.count > 0 ? cmd.count : '–'}
|
|
285
|
+
</TableCell>
|
|
286
|
+
</TableRow>
|
|
287
|
+
))}
|
|
288
|
+
</TableBody>
|
|
289
|
+
</Table>
|
|
290
|
+
</CardContent>
|
|
291
|
+
</Card>
|
|
292
|
+
</div>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Line, LineChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
5
|
+
import {
|
|
6
|
+
ChartContainer,
|
|
7
|
+
ChartTooltip,
|
|
8
|
+
ChartTooltipContent,
|
|
9
|
+
type ChartConfig,
|
|
10
|
+
} from '@/components/ui/chart'
|
|
11
|
+
import type { DailyUsage } from '@/lib/parse-logs'
|
|
12
|
+
import { formatCost } from '@/lib/format'
|
|
13
|
+
|
|
14
|
+
const costConfig = {
|
|
15
|
+
cost: { label: 'Cost', color: 'var(--chart-5)' },
|
|
16
|
+
} satisfies ChartConfig
|
|
17
|
+
|
|
18
|
+
const sessionsConfig = {
|
|
19
|
+
sessions: { label: 'Sessions', color: 'var(--chart-2)' },
|
|
20
|
+
} satisfies ChartConfig
|
|
21
|
+
|
|
22
|
+
const messagesConfig = {
|
|
23
|
+
messages: { label: 'Messages', color: 'var(--chart-1)' },
|
|
24
|
+
} satisfies ChartConfig
|
|
25
|
+
|
|
26
|
+
export function DailyChart({ daily }: { daily: DailyUsage[] }) {
|
|
27
|
+
const data = daily.map((d) => ({
|
|
28
|
+
date: d.date.slice(5),
|
|
29
|
+
cost: Number(d.costUSD.toFixed(2)),
|
|
30
|
+
sessions: d.sessions,
|
|
31
|
+
messages: d.messages,
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
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>
|
|
66
|
+
|
|
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>
|
|
91
|
+
|
|
92
|
+
<Card>
|
|
93
|
+
<CardHeader>
|
|
94
|
+
<CardTitle>Daily Messages</CardTitle>
|
|
95
|
+
<CardDescription>Messages exchanged per day</CardDescription>
|
|
96
|
+
</CardHeader>
|
|
97
|
+
<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>
|
|
114
|
+
</CardContent>
|
|
115
|
+
</Card>
|
|
116
|
+
</>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
4
|
+
import {
|
|
5
|
+
Table,
|
|
6
|
+
TableBody,
|
|
7
|
+
TableCell,
|
|
8
|
+
TableHead,
|
|
9
|
+
TableHeader,
|
|
10
|
+
TableRow,
|
|
11
|
+
} from '@/components/ui/table'
|
|
12
|
+
import { PaginationControls } from '@/components/pagination-controls'
|
|
13
|
+
import { usePagination } from '@/hooks/use-pagination'
|
|
14
|
+
import type { DailyUsage, SessionSummary } from '@/lib/parse-logs'
|
|
15
|
+
import { formatCost, formatTokens, formatNumber } from '@/lib/format'
|
|
16
|
+
|
|
17
|
+
interface DailyTableProps {
|
|
18
|
+
daily: DailyUsage[]
|
|
19
|
+
sessions: SessionSummary[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function DailyTable({ daily, sessions }: DailyTableProps) {
|
|
23
|
+
// Build models-per-day from sessions
|
|
24
|
+
const modelsByDate = new Map<string, Set<string>>()
|
|
25
|
+
for (const s of sessions) {
|
|
26
|
+
const date = s.startTime.slice(0, 10)
|
|
27
|
+
if (!modelsByDate.has(date)) modelsByDate.set(date, new Set())
|
|
28
|
+
modelsByDate.get(date)!.add(s.model)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Totals
|
|
32
|
+
const totals = daily.reduce(
|
|
33
|
+
(acc, d) => ({
|
|
34
|
+
sessions: acc.sessions + d.sessions,
|
|
35
|
+
messages: acc.messages + d.messages,
|
|
36
|
+
inputTokens: acc.inputTokens + d.inputTokens,
|
|
37
|
+
outputTokens: acc.outputTokens + d.outputTokens,
|
|
38
|
+
cacheCreationTokens: acc.cacheCreationTokens + d.cacheCreationTokens,
|
|
39
|
+
cacheReadTokens: acc.cacheReadTokens + d.cacheReadTokens,
|
|
40
|
+
totalTokens: acc.totalTokens + d.totalTokens,
|
|
41
|
+
costUSD: acc.costUSD + d.costUSD,
|
|
42
|
+
}),
|
|
43
|
+
{
|
|
44
|
+
sessions: 0, messages: 0, inputTokens: 0, outputTokens: 0,
|
|
45
|
+
cacheCreationTokens: 0, cacheReadTokens: 0, totalTokens: 0, costUSD: 0,
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
// Sort daily by date descending (most recent first)
|
|
50
|
+
const sorted = [...daily].sort((a, b) => b.date.localeCompare(a.date))
|
|
51
|
+
const pagination = usePagination(sorted, 20)
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Card>
|
|
55
|
+
<CardHeader>
|
|
56
|
+
<CardTitle>Daily Breakdown</CardTitle>
|
|
57
|
+
<CardDescription>{daily.length} days of activity</CardDescription>
|
|
58
|
+
</CardHeader>
|
|
59
|
+
<CardContent>
|
|
60
|
+
<Table>
|
|
61
|
+
<TableHeader>
|
|
62
|
+
<TableRow>
|
|
63
|
+
<TableHead>Date</TableHead>
|
|
64
|
+
<TableHead>Models</TableHead>
|
|
65
|
+
<TableHead className="text-right">Sessions</TableHead>
|
|
66
|
+
<TableHead className="text-right">Input</TableHead>
|
|
67
|
+
<TableHead className="text-right">Output</TableHead>
|
|
68
|
+
<TableHead className="text-right">Cache Create</TableHead>
|
|
69
|
+
<TableHead className="text-right">Cache Read</TableHead>
|
|
70
|
+
<TableHead className="text-right">Total Tokens</TableHead>
|
|
71
|
+
<TableHead className="text-right">Cost</TableHead>
|
|
72
|
+
</TableRow>
|
|
73
|
+
</TableHeader>
|
|
74
|
+
<TableBody>
|
|
75
|
+
{pagination.pageItems.map((d) => {
|
|
76
|
+
const models = modelsByDate.get(d.date)
|
|
77
|
+
const modelList = models ? Array.from(models).sort() : []
|
|
78
|
+
return (
|
|
79
|
+
<TableRow key={d.date}>
|
|
80
|
+
<TableCell className="font-medium whitespace-nowrap">{d.date}</TableCell>
|
|
81
|
+
<TableCell className="text-xs text-muted-foreground max-w-[180px]">
|
|
82
|
+
{modelList.map((m) => (
|
|
83
|
+
<div key={m}>{m}</div>
|
|
84
|
+
))}
|
|
85
|
+
</TableCell>
|
|
86
|
+
<TableCell className="text-right">{d.sessions}</TableCell>
|
|
87
|
+
<TableCell className="text-right font-mono text-xs">{formatNumber(d.inputTokens)}</TableCell>
|
|
88
|
+
<TableCell className="text-right font-mono text-xs">{formatNumber(d.outputTokens)}</TableCell>
|
|
89
|
+
<TableCell className="text-right font-mono text-xs">{formatNumber(d.cacheCreationTokens)}</TableCell>
|
|
90
|
+
<TableCell className="text-right font-mono text-xs">{formatNumber(d.cacheReadTokens)}</TableCell>
|
|
91
|
+
<TableCell className="text-right font-mono text-xs">{formatNumber(d.totalTokens)}</TableCell>
|
|
92
|
+
<TableCell className="text-right font-medium">{formatCost(d.costUSD)}</TableCell>
|
|
93
|
+
</TableRow>
|
|
94
|
+
)
|
|
95
|
+
})}
|
|
96
|
+
</TableBody>
|
|
97
|
+
<TableBody>
|
|
98
|
+
<TableRow className="bg-muted/50 font-bold">
|
|
99
|
+
<TableCell>Total</TableCell>
|
|
100
|
+
<TableCell />
|
|
101
|
+
<TableCell className="text-right">{totals.sessions}</TableCell>
|
|
102
|
+
<TableCell className="text-right font-mono text-xs">{formatTokens(totals.inputTokens)}</TableCell>
|
|
103
|
+
<TableCell className="text-right font-mono text-xs">{formatTokens(totals.outputTokens)}</TableCell>
|
|
104
|
+
<TableCell className="text-right font-mono text-xs">{formatTokens(totals.cacheCreationTokens)}</TableCell>
|
|
105
|
+
<TableCell className="text-right font-mono text-xs">{formatTokens(totals.cacheReadTokens)}</TableCell>
|
|
106
|
+
<TableCell className="text-right font-mono text-xs">{formatTokens(totals.totalTokens)}</TableCell>
|
|
107
|
+
<TableCell className="text-right">{formatCost(totals.costUSD)}</TableCell>
|
|
108
|
+
</TableRow>
|
|
109
|
+
</TableBody>
|
|
110
|
+
</Table>
|
|
111
|
+
<PaginationControls pagination={pagination} noun="days" />
|
|
112
|
+
</CardContent>
|
|
113
|
+
</Card>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { usePathname } from 'next/navigation'
|
|
4
|
+
import { RefreshCw } from 'lucide-react'
|
|
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
|
+
import {
|
|
11
|
+
Select,
|
|
12
|
+
SelectContent,
|
|
13
|
+
SelectItem,
|
|
14
|
+
SelectTrigger,
|
|
15
|
+
SelectValue,
|
|
16
|
+
} from '@/components/ui/select'
|
|
17
|
+
import { AppSidebar } from './app-sidebar'
|
|
18
|
+
import { useData, type TimeRange } from './data-provider'
|
|
19
|
+
import { getPlugin } from '@/lib/plugins'
|
|
20
|
+
import '@/plugins' // ensure plugins are registered
|
|
21
|
+
import type { ReactNode } from 'react'
|
|
22
|
+
|
|
23
|
+
const VIEW_TITLES: Record<string, string> = {
|
|
24
|
+
'/': 'Dashboard',
|
|
25
|
+
'/daily': 'Daily Usage',
|
|
26
|
+
'/tokens': 'Token Breakdown',
|
|
27
|
+
'/tools': 'Tool Usage',
|
|
28
|
+
'/projects': 'Projects',
|
|
29
|
+
'/sessions': 'Sessions',
|
|
30
|
+
'/personality': 'Personality Fit',
|
|
31
|
+
'/commands': 'Command Usage',
|
|
32
|
+
'/images': 'Images',
|
|
33
|
+
'/coach': 'Agent Coach',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveTitle(pathname: string): string {
|
|
37
|
+
if (VIEW_TITLES[pathname]) return VIEW_TITLES[pathname]
|
|
38
|
+
if (pathname === '/community') return 'Community'
|
|
39
|
+
// Community plugin routes: /community/<slug>
|
|
40
|
+
const match = pathname.match(/^\/community\/([a-z0-9-]+)$/)
|
|
41
|
+
if (match) {
|
|
42
|
+
const plugin = getPlugin(match[1])
|
|
43
|
+
if (plugin) return plugin.manifest.name
|
|
44
|
+
}
|
|
45
|
+
return 'Dashboard'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const AGENT_LABELS: Record<string, string> = {
|
|
49
|
+
claude: 'Claude Code',
|
|
50
|
+
codex: 'Codex',
|
|
51
|
+
combined: 'Combined',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const TIME_RANGE_LABELS: Record<TimeRange, string> = {
|
|
55
|
+
'7d': 'Last 7 days',
|
|
56
|
+
'30d': 'Last 30 days',
|
|
57
|
+
'90d': 'Last 90 days',
|
|
58
|
+
'all': 'All time',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Pages where time range filter doesn't apply
|
|
62
|
+
const NO_TIME_FILTER = new Set(['/commands'])
|
|
63
|
+
|
|
64
|
+
export function DashboardShell({ children }: { children: ReactNode }) {
|
|
65
|
+
const pathname = usePathname()
|
|
66
|
+
const {
|
|
67
|
+
agent, setAgent, timeRange, setTimeRange,
|
|
68
|
+
syncing, lastSyncResult, lastSyncTime, handleSync, loading,
|
|
69
|
+
newSessionsAvailable,
|
|
70
|
+
} = useData()
|
|
71
|
+
|
|
72
|
+
const title = resolveTitle(pathname)
|
|
73
|
+
const communityMatch = pathname.match(/^\/community\/([a-z0-9-]+)$/)
|
|
74
|
+
const communityPlugin = communityMatch ? getPlugin(communityMatch[1]) : undefined
|
|
75
|
+
const showTimeFilter =
|
|
76
|
+
!NO_TIME_FILTER.has(pathname) && !communityPlugin?.manifest.customDataSource
|
|
77
|
+
|
|
78
|
+
if (loading) {
|
|
79
|
+
return (
|
|
80
|
+
<div className="flex min-h-screen items-center justify-center">
|
|
81
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
82
|
+
<RefreshCw className="h-4 w-4 animate-spin" />
|
|
83
|
+
Syncing logs...
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<SidebarProvider>
|
|
91
|
+
<AppSidebar />
|
|
92
|
+
<SidebarInset>
|
|
93
|
+
<header className="flex h-14 shrink-0 items-center justify-between border-b px-4">
|
|
94
|
+
<div className="flex items-center gap-2">
|
|
95
|
+
<SidebarTrigger className="-ml-1" />
|
|
96
|
+
<h1 className="ml-2 text-sm font-semibold">{title}</h1>
|
|
97
|
+
</div>
|
|
98
|
+
<div className="flex items-center gap-2">
|
|
99
|
+
{showTimeFilter && (
|
|
100
|
+
<div className="flex rounded-lg border p-0.5">
|
|
101
|
+
{(Object.entries(TIME_RANGE_LABELS) as [TimeRange, string][]).map(([key, label]) => (
|
|
102
|
+
<button
|
|
103
|
+
key={key}
|
|
104
|
+
onClick={() => setTimeRange(key)}
|
|
105
|
+
className={`rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${
|
|
106
|
+
timeRange === key
|
|
107
|
+
? 'bg-primary text-primary-foreground'
|
|
108
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
109
|
+
}`}
|
|
110
|
+
>
|
|
111
|
+
{key === 'all' ? 'All' : key}
|
|
112
|
+
</button>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
<Select value={agent} onValueChange={(v) => v && setAgent(v as 'claude' | 'codex' | 'combined')}>
|
|
117
|
+
<SelectTrigger className="h-8 w-[130px] text-xs">
|
|
118
|
+
<SelectValue>{AGENT_LABELS[agent]}</SelectValue>
|
|
119
|
+
</SelectTrigger>
|
|
120
|
+
<SelectContent>
|
|
121
|
+
<SelectItem value="claude">Claude Code</SelectItem>
|
|
122
|
+
<SelectItem value="codex">Codex</SelectItem>
|
|
123
|
+
<SelectItem value="combined">Combined</SelectItem>
|
|
124
|
+
</SelectContent>
|
|
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
|
+
</div>
|
|
142
|
+
</header>
|
|
143
|
+
<main className="flex-1 space-y-6 p-6">
|
|
144
|
+
{children}
|
|
145
|
+
</main>
|
|
146
|
+
</SidebarInset>
|
|
147
|
+
</SidebarProvider>
|
|
148
|
+
)
|
|
149
|
+
}
|