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,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 <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 }
|