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.
Files changed (107) hide show
  1. package/.claude/settings.local.json +26 -0
  2. package/.prettierignore +7 -0
  3. package/.prettierrc +11 -0
  4. package/CONTRIBUTING.md +209 -0
  5. package/LICENSE +21 -0
  6. package/README.md +109 -0
  7. package/app/(dashboard)/coach/page.tsx +11 -0
  8. package/app/(dashboard)/commands/page.tsx +7 -0
  9. package/app/(dashboard)/community/[slug]/page.tsx +23 -0
  10. package/app/(dashboard)/community/page.tsx +71 -0
  11. package/app/(dashboard)/daily/page.tsx +19 -0
  12. package/app/(dashboard)/images/page.tsx +5 -0
  13. package/app/(dashboard)/layout.tsx +12 -0
  14. package/app/(dashboard)/page.tsx +23 -0
  15. package/app/(dashboard)/personality/page.tsx +11 -0
  16. package/app/(dashboard)/projects/page.tsx +11 -0
  17. package/app/(dashboard)/sessions/page.tsx +11 -0
  18. package/app/(dashboard)/tokens/page.tsx +11 -0
  19. package/app/(dashboard)/tools/page.tsx +11 -0
  20. package/app/api/check/route.ts +13 -0
  21. package/app/api/commands/route.ts +16 -0
  22. package/app/api/images/[...path]/route.ts +33 -0
  23. package/app/api/images-analysis/route.ts +177 -0
  24. package/app/api/sync/route.ts +14 -0
  25. package/app/api/usage/route.ts +117 -0
  26. package/app/favicon.ico +0 -0
  27. package/app/globals.css +144 -0
  28. package/app/icon.svg +3 -0
  29. package/app/layout.tsx +35 -0
  30. package/bin/agentfit.mjs +69 -0
  31. package/components/.gitkeep +0 -0
  32. package/components/agent-coach.tsx +248 -0
  33. package/components/app-sidebar.tsx +161 -0
  34. package/components/command-usage.tsx +294 -0
  35. package/components/daily-chart.tsx +118 -0
  36. package/components/daily-table.tsx +115 -0
  37. package/components/dashboard-shell.tsx +149 -0
  38. package/components/data-provider.tsx +213 -0
  39. package/components/fitness-score.tsx +95 -0
  40. package/components/overview-cards.tsx +198 -0
  41. package/components/pagination-controls.tsx +104 -0
  42. package/components/personality-fit.tsx +446 -0
  43. package/components/projects-table.tsx +70 -0
  44. package/components/screenshots-analysis.tsx +359 -0
  45. package/components/sessions-table.tsx +97 -0
  46. package/components/theme-provider.tsx +71 -0
  47. package/components/token-breakdown.tsx +179 -0
  48. package/components/tool-usage-chart.tsx +63 -0
  49. package/components/ui/badge.tsx +52 -0
  50. package/components/ui/button.tsx +60 -0
  51. package/components/ui/card.tsx +103 -0
  52. package/components/ui/chart.tsx +373 -0
  53. package/components/ui/dialog.tsx +160 -0
  54. package/components/ui/input.tsx +20 -0
  55. package/components/ui/scroll-area.tsx +55 -0
  56. package/components/ui/select.tsx +201 -0
  57. package/components/ui/separator.tsx +25 -0
  58. package/components/ui/sheet.tsx +138 -0
  59. package/components/ui/sidebar.tsx +723 -0
  60. package/components/ui/skeleton.tsx +13 -0
  61. package/components/ui/table.tsx +116 -0
  62. package/components/ui/tabs.tsx +82 -0
  63. package/components/ui/tooltip.tsx +66 -0
  64. package/components.json +25 -0
  65. package/generated/prisma/browser.ts +34 -0
  66. package/generated/prisma/client.ts +58 -0
  67. package/generated/prisma/commonInputTypes.ts +237 -0
  68. package/generated/prisma/enums.ts +15 -0
  69. package/generated/prisma/internal/class.ts +224 -0
  70. package/generated/prisma/internal/prismaNamespace.ts +920 -0
  71. package/generated/prisma/internal/prismaNamespaceBrowser.ts +130 -0
  72. package/generated/prisma/models/Image.ts +1310 -0
  73. package/generated/prisma/models/Session.ts +1695 -0
  74. package/generated/prisma/models/SyncLog.ts +1203 -0
  75. package/generated/prisma/models.ts +14 -0
  76. package/hooks/.gitkeep +0 -0
  77. package/hooks/use-mobile.ts +19 -0
  78. package/hooks/use-pagination.ts +60 -0
  79. package/lib/.gitkeep +0 -0
  80. package/lib/coach.ts +425 -0
  81. package/lib/commands.ts +239 -0
  82. package/lib/db.ts +15 -0
  83. package/lib/format.ts +26 -0
  84. package/lib/parse-codex.ts +201 -0
  85. package/lib/parse-logs.ts +369 -0
  86. package/lib/personality.ts +481 -0
  87. package/lib/plugins.ts +107 -0
  88. package/lib/pricing.ts +112 -0
  89. package/lib/queries-codex.ts +130 -0
  90. package/lib/queries.ts +154 -0
  91. package/lib/resolve-icon.ts +12 -0
  92. package/lib/sync.ts +335 -0
  93. package/lib/utils.ts +6 -0
  94. package/next.config.mjs +4 -0
  95. package/package.json +73 -0
  96. package/plugins/cost-heatmap/component.test.tsx +52 -0
  97. package/plugins/cost-heatmap/component.tsx +227 -0
  98. package/plugins/cost-heatmap/manifest.ts +13 -0
  99. package/plugins/index.ts +18 -0
  100. package/prisma/migrations/20260328152517_init/migration.sql +41 -0
  101. package/prisma/migrations/20260328153801_add_image_model/migration.sql +18 -0
  102. package/prisma/migrations/migration_lock.toml +3 -0
  103. package/prisma/schema.prisma +57 -0
  104. package/prisma.config.ts +14 -0
  105. package/public/.gitkeep +0 -0
  106. package/public/logo.svg +3 -0
  107. package/setup.sh +73 -0
@@ -0,0 +1,359 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
5
+ import {
6
+ ChartContainer,
7
+ ChartTooltip,
8
+ ChartTooltipContent,
9
+ ChartLegend,
10
+ ChartLegendContent,
11
+ type ChartConfig,
12
+ } from '@/components/ui/chart'
13
+ import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from 'recharts'
14
+ import { Badge } from '@/components/ui/badge'
15
+ import { formatCost, formatNumber } from '@/lib/format'
16
+ import {
17
+ Camera,
18
+ Zap,
19
+ Clock,
20
+ TrendingUp,
21
+ Image as ImageIcon,
22
+ } from 'lucide-react'
23
+
24
+ interface ImageAnalysis {
25
+ overview: {
26
+ totalImages: number
27
+ sessionsWithImages: number
28
+ totalSessions: number
29
+ percentWithImages: number
30
+ totalSizeMB: number
31
+ avgSizeKB: number
32
+ byMediaType: Record<string, number>
33
+ }
34
+ byProject: { name: string; count: number }[]
35
+ byHour: { hour: number; count: number }[]
36
+ byDate: { date: string; count: number }[]
37
+ comparison: {
38
+ withImages: { sessions: number; avgMessages: number; avgCost: number; avgDuration: number; avgTools: number }
39
+ withoutImages: { sessions: number; avgMessages: number; avgCost: number; avgDuration: number; avgTools: number }
40
+ }
41
+ screenshotFrequency: {
42
+ totalGaps: number
43
+ medianMinutes: number
44
+ meanMinutes: number
45
+ rapidFireCount: number
46
+ rapidFirePercent: number
47
+ under5Count: number
48
+ under5Percent: number
49
+ }
50
+ topSessions: {
51
+ sessionId: string
52
+ imageCount: number
53
+ project: string
54
+ messages: number
55
+ cost: number
56
+ date: string
57
+ }[]
58
+ recentImages: {
59
+ filename: string
60
+ sessionId: string
61
+ timestamp: string
62
+ sizeBytes: number
63
+ project: string
64
+ }[]
65
+ }
66
+
67
+ const projectConfig = {
68
+ count: { label: 'Images', color: 'var(--chart-3)' },
69
+ } satisfies ChartConfig
70
+
71
+ const hourConfig = {
72
+ count: { label: 'Images', color: 'var(--chart-1)' },
73
+ } satisfies ChartConfig
74
+
75
+ const dateConfig = {
76
+ count: { label: 'Images', color: 'var(--chart-8)' },
77
+ } satisfies ChartConfig
78
+
79
+ const comparisonConfig = {
80
+ withImages: { label: 'With Images', color: 'var(--chart-1)' },
81
+ withoutImages: { label: 'Text Only', color: 'var(--chart-4)' },
82
+ } satisfies ChartConfig
83
+
84
+ export function ScreenshotsAnalysis() {
85
+ const [data, setData] = useState<ImageAnalysis | null>(null)
86
+ const [loading, setLoading] = useState(true)
87
+
88
+ useEffect(() => {
89
+ fetch('/api/images-analysis')
90
+ .then((r) => r.json())
91
+ .then((d) => { setData(d); setLoading(false) })
92
+ .catch(() => setLoading(false))
93
+ }, [])
94
+
95
+ if (loading) return <div className="text-muted-foreground">Loading image analysis...</div>
96
+ if (!data) return <div className="text-muted-foreground">No image data available</div>
97
+
98
+ const { overview, comparison, screenshotFrequency } = data
99
+
100
+ const costMultiplier = comparison.withoutImages.avgCost > 0
101
+ ? (comparison.withImages.avgCost / comparison.withoutImages.avgCost).toFixed(1)
102
+ : '?'
103
+ const msgMultiplier = comparison.withoutImages.avgMessages > 0
104
+ ? (comparison.withImages.avgMessages / comparison.withoutImages.avgMessages).toFixed(1)
105
+ : '?'
106
+
107
+ const comparisonData = [
108
+ {
109
+ metric: 'Avg Cost',
110
+ withImages: comparison.withImages.avgCost,
111
+ withoutImages: comparison.withoutImages.avgCost,
112
+ },
113
+ {
114
+ metric: 'Avg Messages',
115
+ withImages: comparison.withImages.avgMessages,
116
+ withoutImages: comparison.withoutImages.avgMessages,
117
+ },
118
+ {
119
+ metric: 'Avg Tools',
120
+ withImages: comparison.withImages.avgTools,
121
+ withoutImages: comparison.withoutImages.avgTools,
122
+ },
123
+ ]
124
+
125
+ return (
126
+ <div className="space-y-6">
127
+ {/* Hero insight */}
128
+ <Card className="border-chart-1/30 bg-chart-1/5">
129
+ <CardContent className="pt-6">
130
+ <div className="flex items-start gap-4">
131
+ <Camera className="h-8 w-8 text-chart-1 mt-1 shrink-0" />
132
+ <div>
133
+ <h2 className="text-lg font-semibold mb-1">Sessions with Images Cost {costMultiplier}x More</h2>
134
+ <p className="text-sm text-muted-foreground">
135
+ Sessions where you shared images averaged <strong>{formatCost(comparison.withImages.avgCost)}</strong> vs{' '}
136
+ <strong>{formatCost(comparison.withoutImages.avgCost)}</strong> for text-only sessions.
137
+ They also had <strong>{msgMultiplier}x</strong> more messages and{' '}
138
+ <strong>{Math.round(comparison.withImages.avgTools / Math.max(1, comparison.withoutImages.avgTools))}x</strong> more tool calls.
139
+ Images signal complex visual work — UI debugging, design review, and iterative refinement.
140
+ </p>
141
+ </div>
142
+ </div>
143
+ </CardContent>
144
+ </Card>
145
+
146
+ {/* Stat cards */}
147
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
148
+ <Card>
149
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
150
+ <CardTitle className="text-sm font-medium text-muted-foreground">Total Images</CardTitle>
151
+ <ImageIcon className="h-4 w-4 text-muted-foreground" />
152
+ </CardHeader>
153
+ <CardContent>
154
+ <div className="text-2xl font-bold">{formatNumber(overview.totalImages)}</div>
155
+ <p className="text-xs text-muted-foreground">{overview.totalSizeMB} MB on disk</p>
156
+ </CardContent>
157
+ </Card>
158
+ <Card>
159
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
160
+ <CardTitle className="text-sm font-medium text-muted-foreground">Sessions with Images</CardTitle>
161
+ <Camera className="h-4 w-4 text-muted-foreground" />
162
+ </CardHeader>
163
+ <CardContent>
164
+ <div className="text-2xl font-bold">{overview.percentWithImages}%</div>
165
+ <p className="text-xs text-muted-foreground">{overview.sessionsWithImages} of {overview.totalSessions}</p>
166
+ </CardContent>
167
+ </Card>
168
+ <Card>
169
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
170
+ <CardTitle className="text-sm font-medium text-muted-foreground">Avg Size</CardTitle>
171
+ <TrendingUp className="h-4 w-4 text-muted-foreground" />
172
+ </CardHeader>
173
+ <CardContent>
174
+ <div className="text-2xl font-bold">{overview.avgSizeKB} KB</div>
175
+ <p className="text-xs text-muted-foreground">{overview.byMediaType['image/png'] || 0} PNG, {overview.byMediaType['image/jpeg'] || 0} JPEG</p>
176
+ </CardContent>
177
+ </Card>
178
+ <Card>
179
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
180
+ <CardTitle className="text-sm font-medium text-muted-foreground">Rapid Fire</CardTitle>
181
+ <Zap className="h-4 w-4 text-muted-foreground" />
182
+ </CardHeader>
183
+ <CardContent>
184
+ <div className="text-2xl font-bold">{screenshotFrequency.rapidFirePercent}%</div>
185
+ <p className="text-xs text-muted-foreground">{screenshotFrequency.rapidFireCount} sent &lt;2 min apart</p>
186
+ </CardContent>
187
+ </Card>
188
+ <Card>
189
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
190
+ <CardTitle className="text-sm font-medium text-muted-foreground">Median Gap</CardTitle>
191
+ <Clock className="h-4 w-4 text-muted-foreground" />
192
+ </CardHeader>
193
+ <CardContent>
194
+ <div className="text-2xl font-bold">{screenshotFrequency.medianMinutes} min</div>
195
+ <p className="text-xs text-muted-foreground">Between consecutive images</p>
196
+ </CardContent>
197
+ </Card>
198
+ </div>
199
+
200
+ {/* Comparison chart + By hour */}
201
+ <div className="grid gap-4 lg:grid-cols-2">
202
+ <Card>
203
+ <CardHeader>
204
+ <CardTitle>Image vs Text-Only Sessions</CardTitle>
205
+ <CardDescription>Average metrics comparison</CardDescription>
206
+ </CardHeader>
207
+ <CardContent>
208
+ <ChartContainer config={comparisonConfig} className="min-h-[300px] w-full">
209
+ <BarChart data={comparisonData} accessibilityLayer>
210
+ <CartesianGrid vertical={false} />
211
+ <XAxis dataKey="metric" tickLine={false} axisLine={false} />
212
+ <YAxis tickLine={false} axisLine={false} />
213
+ <ChartTooltip content={<ChartTooltipContent />} />
214
+ <ChartLegend content={<ChartLegendContent />} />
215
+ <Bar dataKey="withImages" fill="var(--color-withImages)" radius={[4, 4, 0, 0]} />
216
+ <Bar dataKey="withoutImages" fill="var(--color-withoutImages)" radius={[4, 4, 0, 0]} />
217
+ </BarChart>
218
+ </ChartContainer>
219
+ </CardContent>
220
+ </Card>
221
+
222
+ <Card>
223
+ <CardHeader>
224
+ <CardTitle>Images by Hour</CardTitle>
225
+ <CardDescription>When do you share images?</CardDescription>
226
+ </CardHeader>
227
+ <CardContent>
228
+ <ChartContainer config={hourConfig} className="min-h-[300px] w-full">
229
+ <BarChart data={data.byHour} accessibilityLayer>
230
+ <CartesianGrid vertical={false} />
231
+ <XAxis
232
+ dataKey="hour"
233
+ tickLine={false}
234
+ axisLine={false}
235
+ tickFormatter={(h) => `${h}:00`}
236
+ />
237
+ <YAxis tickLine={false} axisLine={false} />
238
+ <ChartTooltip
239
+ content={
240
+ <ChartTooltipContent
241
+ labelFormatter={(h) => `${h}:00 - ${h}:59`}
242
+ />
243
+ }
244
+ />
245
+ <Bar dataKey="count" fill="var(--color-count)" radius={[4, 4, 0, 0]} />
246
+ </BarChart>
247
+ </ChartContainer>
248
+ </CardContent>
249
+ </Card>
250
+ </div>
251
+
252
+ {/* Daily timeline + By project */}
253
+ <div className="grid gap-4 lg:grid-cols-2">
254
+ <Card>
255
+ <CardHeader>
256
+ <CardTitle>Daily Image Activity</CardTitle>
257
+ <CardDescription>Images shared per day</CardDescription>
258
+ </CardHeader>
259
+ <CardContent>
260
+ <ChartContainer config={dateConfig} className="min-h-[300px] w-full">
261
+ <BarChart data={data.byDate} accessibilityLayer>
262
+ <CartesianGrid vertical={false} />
263
+ <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
264
+ <YAxis tickLine={false} axisLine={false} />
265
+ <ChartTooltip content={<ChartTooltipContent />} />
266
+ <Bar dataKey="count" fill="var(--color-count)" radius={[4, 4, 0, 0]} />
267
+ </BarChart>
268
+ </ChartContainer>
269
+ </CardContent>
270
+ </Card>
271
+
272
+ <Card>
273
+ <CardHeader>
274
+ <CardTitle>Images by Project</CardTitle>
275
+ <CardDescription>Which projects need the most visual feedback?</CardDescription>
276
+ </CardHeader>
277
+ <CardContent>
278
+ <ChartContainer config={projectConfig} className="min-h-[300px] w-full">
279
+ <BarChart data={data.byProject} layout="vertical" accessibilityLayer margin={{ left: 8 }}>
280
+ <XAxis type="number" tickLine={false} axisLine={false} />
281
+ <YAxis type="category" dataKey="name" tickLine={false} axisLine={false} width={80} />
282
+ <ChartTooltip content={<ChartTooltipContent />} />
283
+ <Bar dataKey="count" fill="var(--color-count)" radius={[0, 4, 4, 0]} />
284
+ </BarChart>
285
+ </ChartContainer>
286
+ </CardContent>
287
+ </Card>
288
+ </div>
289
+
290
+ {/* Most image-heavy sessions */}
291
+ <Card>
292
+ <CardHeader>
293
+ <CardTitle>Most Image-Heavy Sessions</CardTitle>
294
+ <CardDescription>Your biggest visual collaboration marathons</CardDescription>
295
+ </CardHeader>
296
+ <CardContent>
297
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
298
+ {data.topSessions.map((s, i) => (
299
+ <div key={s.sessionId} className="rounded-lg border p-3 space-y-1">
300
+ <div className="flex items-center justify-between">
301
+ <Badge variant={i < 3 ? 'default' : 'secondary'} className="text-xs">
302
+ #{i + 1}
303
+ </Badge>
304
+ <span className="text-xs text-muted-foreground">{s.date}</span>
305
+ </div>
306
+ <div className="text-2xl font-bold">{s.imageCount}</div>
307
+ <div className="text-xs text-muted-foreground">images</div>
308
+ <div className="text-xs">
309
+ <span className="text-muted-foreground">{s.project}</span>
310
+ {' · '}{formatCost(s.cost)}{' · '}{formatNumber(s.messages)} msgs
311
+ </div>
312
+ </div>
313
+ ))}
314
+ </div>
315
+ </CardContent>
316
+ </Card>
317
+
318
+ {/* Recent images gallery */}
319
+ <Card>
320
+ <CardHeader>
321
+ <CardTitle>Recent Images</CardTitle>
322
+ <CardDescription>Latest images from your sessions</CardDescription>
323
+ </CardHeader>
324
+ <CardContent>
325
+ <div className="columns-1 gap-4 sm:columns-2 lg:columns-3">
326
+ {data.recentImages.map((img) => (
327
+ <div
328
+ key={img.filename}
329
+ className="group relative mb-4 break-inside-avoid overflow-hidden rounded-xl border bg-card shadow-sm transition-all duration-300 hover:shadow-lg hover:-translate-y-0.5"
330
+ >
331
+ <div className="relative">
332
+ <img
333
+ src={`/api/images/${img.filename}`}
334
+ alt={`Image from ${img.project}`}
335
+ className="w-full object-cover object-top"
336
+ loading="lazy"
337
+ />
338
+ {/* Hover overlay */}
339
+ <div className="absolute inset-0 bg-black/0 transition-colors duration-300 group-hover:bg-black/5" />
340
+ {/* Project badge — always visible */}
341
+ <div className="absolute top-2.5 left-2.5">
342
+ <span className="inline-flex items-center rounded-md bg-black/60 px-2 py-0.5 text-[11px] font-medium text-white backdrop-blur-sm">
343
+ {img.project}
344
+ </span>
345
+ </div>
346
+ </div>
347
+ {/* Metadata footer */}
348
+ <div className="flex items-center justify-between px-3 py-2.5 text-[11px] text-muted-foreground">
349
+ <span>{new Date(img.timestamp).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}</span>
350
+ <span>{Math.round(img.sizeBytes / 1024)} KB</span>
351
+ </div>
352
+ </div>
353
+ ))}
354
+ </div>
355
+ </CardContent>
356
+ </Card>
357
+ </div>
358
+ )
359
+ }
@@ -0,0 +1,97 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
5
+ import {
6
+ Table,
7
+ TableBody,
8
+ TableCell,
9
+ TableHead,
10
+ TableHeader,
11
+ TableRow,
12
+ } from '@/components/ui/table'
13
+ import {
14
+ Select,
15
+ SelectContent,
16
+ SelectItem,
17
+ SelectTrigger,
18
+ SelectValue,
19
+ } from '@/components/ui/select'
20
+ import { Badge } from '@/components/ui/badge'
21
+ import { PaginationControls } from '@/components/pagination-controls'
22
+ import { usePagination } from '@/hooks/use-pagination'
23
+ import type { SessionSummary } from '@/lib/parse-logs'
24
+ import { formatCost, formatTokens, formatDuration, formatNumber } from '@/lib/format'
25
+
26
+ export function SessionsTable({ sessions }: { sessions: SessionSummary[] }) {
27
+ const [projectFilter, setProjectFilter] = useState<string>('all')
28
+
29
+ const projects = [...new Set(sessions.map((s) => s.project))].sort()
30
+ const filtered =
31
+ projectFilter === 'all' ? sessions : sessions.filter((s) => s.project === projectFilter)
32
+
33
+ const pagination = usePagination(filtered, 20)
34
+
35
+ return (
36
+ <Card>
37
+ <CardHeader>
38
+ <div className="flex items-center justify-between">
39
+ <div>
40
+ <CardTitle>Sessions</CardTitle>
41
+ <CardDescription>{filtered.length} sessions recorded</CardDescription>
42
+ </div>
43
+ <Select value={projectFilter} onValueChange={(v) => setProjectFilter(v ?? 'all')}>
44
+ <SelectTrigger className="w-[200px]">
45
+ <SelectValue placeholder="Filter by project" />
46
+ </SelectTrigger>
47
+ <SelectContent>
48
+ <SelectItem value="all">All projects</SelectItem>
49
+ {projects.map((p) => (
50
+ <SelectItem key={p} value={p}>
51
+ {p}
52
+ </SelectItem>
53
+ ))}
54
+ </SelectContent>
55
+ </Select>
56
+ </div>
57
+ </CardHeader>
58
+ <CardContent>
59
+ <Table>
60
+ <TableHeader>
61
+ <TableRow>
62
+ <TableHead>Date</TableHead>
63
+ <TableHead>Project</TableHead>
64
+ <TableHead>Model</TableHead>
65
+ <TableHead className="text-right">Messages</TableHead>
66
+ <TableHead className="text-right">Tokens</TableHead>
67
+ <TableHead className="text-right">Cost</TableHead>
68
+ <TableHead className="text-right">Duration</TableHead>
69
+ <TableHead className="text-right">Tools</TableHead>
70
+ </TableRow>
71
+ </TableHeader>
72
+ <TableBody>
73
+ {pagination.pageItems.map((s) => (
74
+ <TableRow key={s.sessionId}>
75
+ <TableCell className="whitespace-nowrap text-sm">
76
+ {s.startTime ? new Date(s.startTime).toLocaleDateString() : 'N/A'}
77
+ </TableCell>
78
+ <TableCell>
79
+ <Badge variant="secondary">{s.project}</Badge>
80
+ </TableCell>
81
+ <TableCell className="text-xs text-muted-foreground">{s.model}</TableCell>
82
+ <TableCell className="text-right">
83
+ {formatNumber(s.userMessages)} / {formatNumber(s.assistantMessages)}
84
+ </TableCell>
85
+ <TableCell className="text-right">{formatTokens(s.totalTokens)}</TableCell>
86
+ <TableCell className="text-right">{formatCost(s.costUSD)}</TableCell>
87
+ <TableCell className="text-right">{formatDuration(s.durationMinutes)}</TableCell>
88
+ <TableCell className="text-right">{formatNumber(s.toolCallsTotal)}</TableCell>
89
+ </TableRow>
90
+ ))}
91
+ </TableBody>
92
+ </Table>
93
+ <PaginationControls pagination={pagination} noun="sessions" />
94
+ </CardContent>
95
+ </Card>
96
+ )
97
+ }
@@ -0,0 +1,71 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes"
5
+
6
+ function ThemeProvider({
7
+ children,
8
+ ...props
9
+ }: React.ComponentProps<typeof NextThemesProvider>) {
10
+ return (
11
+ <NextThemesProvider
12
+ attribute="class"
13
+ defaultTheme="system"
14
+ enableSystem
15
+ disableTransitionOnChange
16
+ {...props}
17
+ >
18
+ <ThemeHotkey />
19
+ {children}
20
+ </NextThemesProvider>
21
+ )
22
+ }
23
+
24
+ function isTypingTarget(target: EventTarget | null) {
25
+ if (!(target instanceof HTMLElement)) {
26
+ return false
27
+ }
28
+
29
+ return (
30
+ target.isContentEditable ||
31
+ target.tagName === "INPUT" ||
32
+ target.tagName === "TEXTAREA" ||
33
+ target.tagName === "SELECT"
34
+ )
35
+ }
36
+
37
+ function ThemeHotkey() {
38
+ const { resolvedTheme, setTheme } = useTheme()
39
+
40
+ React.useEffect(() => {
41
+ function onKeyDown(event: KeyboardEvent) {
42
+ if (event.defaultPrevented || event.repeat) {
43
+ return
44
+ }
45
+
46
+ if (event.metaKey || event.ctrlKey || event.altKey) {
47
+ return
48
+ }
49
+
50
+ if (event.key.toLowerCase() !== "d") {
51
+ return
52
+ }
53
+
54
+ if (isTypingTarget(event.target)) {
55
+ return
56
+ }
57
+
58
+ setTheme(resolvedTheme === "dark" ? "light" : "dark")
59
+ }
60
+
61
+ window.addEventListener("keydown", onKeyDown)
62
+
63
+ return () => {
64
+ window.removeEventListener("keydown", onKeyDown)
65
+ }
66
+ }, [resolvedTheme, setTheme])
67
+
68
+ return null
69
+ }
70
+
71
+ export { ThemeProvider }