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.
Files changed (68) hide show
  1. package/README.md +30 -34
  2. package/app/(dashboard)/daily/page.tsx +1 -1
  3. package/app/(dashboard)/flow/page.tsx +17 -0
  4. package/app/(dashboard)/layout.tsx +2 -0
  5. package/app/(dashboard)/page.tsx +24 -5
  6. package/app/(dashboard)/reports/[id]/page.tsx +72 -0
  7. package/app/(dashboard)/reports/page.tsx +132 -0
  8. package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
  9. package/app/(dashboard)/settings/page.tsx +180 -0
  10. package/app/api/backup/route.ts +215 -0
  11. package/app/api/check/route.ts +11 -1
  12. package/app/api/command-insights/route.ts +13 -0
  13. package/app/api/images-analysis/route.ts +3 -4
  14. package/app/api/reports/[id]/route.ts +23 -0
  15. package/app/api/reports/route.ts +50 -0
  16. package/app/api/reset/route.ts +21 -0
  17. package/app/api/session/route.ts +40 -0
  18. package/app/api/usage/route.ts +25 -1
  19. package/app/layout.tsx +1 -1
  20. package/bin/agentfit.mjs +2 -2
  21. package/components/agent-coach.tsx +256 -129
  22. package/components/app-sidebar.tsx +258 -8
  23. package/components/backup-section.tsx +236 -0
  24. package/components/daily-chart.tsx +404 -83
  25. package/components/dashboard-shell.tsx +9 -24
  26. package/components/data-provider.tsx +66 -2
  27. package/components/fitness-score.tsx +95 -54
  28. package/components/overview-cards.tsx +148 -41
  29. package/components/report-view.tsx +307 -0
  30. package/components/screenshots-analysis.tsx +51 -46
  31. package/components/session-chatlog.tsx +124 -0
  32. package/components/session-timeline.tsx +184 -0
  33. package/components/session-workflow.tsx +183 -0
  34. package/components/sessions-table.tsx +9 -1
  35. package/components/tool-flow-graph.tsx +144 -0
  36. package/components/ui/carousel.tsx +242 -0
  37. package/components/ui/sidebar.tsx +1 -1
  38. package/components/ui/sonner.tsx +51 -0
  39. package/generated/prisma/browser.ts +5 -0
  40. package/generated/prisma/client.ts +5 -0
  41. package/generated/prisma/internal/class.ts +14 -4
  42. package/generated/prisma/internal/prismaNamespace.ts +96 -2
  43. package/generated/prisma/internal/prismaNamespaceBrowser.ts +20 -1
  44. package/generated/prisma/models/Report.ts +1219 -0
  45. package/generated/prisma/models/Session.ts +187 -1
  46. package/generated/prisma/models.ts +1 -0
  47. package/lib/coach.ts +530 -211
  48. package/lib/command-insights.ts +231 -0
  49. package/lib/db.ts +1 -1
  50. package/lib/parse-codex.ts +5 -0
  51. package/lib/parse-logs.ts +65 -0
  52. package/lib/queries-codex.ts +22 -0
  53. package/lib/queries.ts +42 -0
  54. package/lib/report.ts +156 -0
  55. package/lib/session-detail.ts +382 -0
  56. package/lib/sync.ts +77 -0
  57. package/lib/tool-flow.ts +71 -0
  58. package/next.config.mjs +6 -1
  59. package/package.json +16 -2
  60. package/plugins/cost-heatmap/component.tsx +72 -50
  61. package/prisma/schema.prisma +17 -0
  62. package/.claude/settings.local.json +0 -26
  63. package/CONTRIBUTING.md +0 -209
  64. package/prisma/migrations/20260328152517_init/migration.sql +0 -41
  65. package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
  66. package/prisma/migrations/migration_lock.toml +0 -3
  67. package/prisma.config.ts +0 -14
  68. package/setup.sh +0 -73
@@ -1,8 +1,9 @@
1
1
  'use client'
2
2
 
3
- import { useMemo } from 'react'
3
+ import { useMemo, useEffect, useState } from 'react'
4
4
  import type { UsageData } from '@/lib/parse-logs'
5
- import { generateCoachInsights, type CoachInsight, type InsightCategory, type InsightSeverity } from '@/lib/coach'
5
+ import { generateCoachInsights, type CoachInsight, type InsightCategory, type InsightSeverity, type CraftDimension, type CraftScores } from '@/lib/coach'
6
+ import type { CommandInsight } from '@/lib/command-insights'
6
7
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
7
8
  import { formatCost, formatDuration, formatNumber } from '@/lib/format'
8
9
  import {
@@ -18,8 +19,11 @@ import {
18
19
  Calendar,
19
20
  Search,
20
21
  Clock,
21
- TrendingUp,
22
- MessageSquare,
22
+ Brain,
23
+ Radar,
24
+ Bot,
25
+ Activity,
26
+ Gauge,
23
27
  } from 'lucide-react'
24
28
 
25
29
  const CATEGORY_ICONS: Record<InsightCategory, typeof Trophy> = {
@@ -33,48 +37,90 @@ const CATEGORY_ICONS: Record<InsightCategory, typeof Trophy> = {
33
37
  streak: Flame,
34
38
  }
35
39
 
36
- const CATEGORY_LABELS: Record<InsightCategory, string> = {
37
- cost: 'Cost',
38
- efficiency: 'Efficiency',
39
- tools: 'Tools',
40
- context: 'Context',
41
- model: 'Model',
42
- habits: 'Habits',
43
- discovery: 'Discovery',
44
- streak: 'Streak',
45
- }
46
-
47
40
  const SEVERITY_STYLES: Record<InsightSeverity, { border: string; icon: typeof Trophy; iconClass: string }> = {
48
41
  achievement: { border: 'border-l-chart-2', icon: Trophy, iconClass: 'text-chart-2' },
49
42
  warning: { border: 'border-l-chart-5', icon: AlertTriangle, iconClass: 'text-chart-5' },
50
43
  tip: { border: 'border-l-chart-1', icon: Lightbulb, iconClass: 'text-chart-1' },
51
44
  }
52
45
 
46
+ const CRAFT_DIMENSIONS: {
47
+ key: CraftDimension
48
+ letter: string
49
+ label: string
50
+ color: string
51
+ icon: typeof Brain
52
+ description: string
53
+ metrics: string[]
54
+ }[] = [
55
+ {
56
+ key: 'context',
57
+ letter: 'C',
58
+ label: 'Context',
59
+ color: 'var(--chart-1)',
60
+ icon: Brain,
61
+ description: 'How well you engineer the context available to the AI — not just window size, but the holistic curation of tokens: system prompts (CLAUDE.md), just-in-time retrieval, structured notes, sub-agent isolation, and cache efficiency.',
62
+ metrics: ['Cache reuse (20%)', 'Overflow avoidance (20%)', 'Just-in-time retrieval (20%)', 'Session length (10%)', 'Note-taking (10%)', 'Output density (10%)', 'Sub-agent isolation (10%)'],
63
+ },
64
+ {
65
+ key: 'reach',
66
+ letter: 'R',
67
+ label: 'Reach',
68
+ color: 'var(--chart-2)',
69
+ icon: Radar,
70
+ description: 'How broadly you leverage available capabilities. Using diverse tools, subagents, and custom skills means you\'re getting more out of the AI assistant.',
71
+ metrics: ['Tool diversity (35%)', 'Subagent parallelization (35%)', 'Skill/command adoption (30%)'],
72
+ },
73
+ {
74
+ key: 'autonomy',
75
+ letter: 'A',
76
+ label: 'Autonomy',
77
+ color: 'var(--chart-3)',
78
+ icon: Bot,
79
+ description: 'How independently the agent works for you. High autonomy means clear prompts, fewer interruptions, the agent reading before editing, and trusting it with permissions.',
80
+ metrics: ['Assistant/user message ratio (25%)', 'Low interruption rate (25%)', 'Read-before-edit ratio (25%)', 'Permission trust level (25%)'],
81
+ },
82
+ {
83
+ key: 'flow',
84
+ letter: 'F',
85
+ label: 'Flow',
86
+ color: 'var(--chart-4)',
87
+ icon: Activity,
88
+ description: 'How consistently you maintain a coding rhythm. Regular usage builds mastery faster than sporadic intense sessions.',
89
+ metrics: ['Current streak length (35%)', 'Daily consistency (35%)', 'Active days coverage (30%)'],
90
+ },
91
+ {
92
+ key: 'throughput',
93
+ letter: 'T',
94
+ label: 'Throughput',
95
+ color: 'var(--chart-6)',
96
+ icon: Gauge,
97
+ description: 'How much output you get for your investment. Efficient sessions produce more output per dollar with fewer errors, and parallel sessions multiply your throughput.',
98
+ metrics: ['Cost efficiency (25%)', 'Output volume (25%)', 'Parallel sessions (25%)', 'Low error rate (25%)'],
99
+ },
100
+ ]
101
+
53
102
  function FitnessRing({ score, label }: { score: number; label: string }) {
54
103
  const radius = 70
55
104
  const circumference = 2 * Math.PI * radius
56
105
  const offset = circumference - (score / 100) * circumference
57
106
 
107
+ const getColor = (s: number) => {
108
+ if (s >= 85) return 'var(--chart-2)'
109
+ if (s >= 70) return 'var(--chart-1)'
110
+ if (s >= 55) return 'var(--chart-3)'
111
+ return 'var(--chart-5)'
112
+ }
113
+
58
114
  return (
59
115
  <div className="flex flex-col items-center gap-2">
60
116
  <div className="relative">
61
117
  <svg width="180" height="180" viewBox="0 0 180 180">
118
+ <circle cx="90" cy="90" r={radius} fill="none" stroke="var(--muted)" strokeWidth="10" />
62
119
  <circle
63
120
  cx="90" cy="90" r={radius}
64
- fill="none"
65
- stroke="var(--muted)"
66
- strokeWidth="10"
67
- />
68
- <circle
69
- cx="90" cy="90" r={radius}
70
- fill="none"
71
- stroke="var(--chart-2)"
72
- strokeWidth="10"
73
- strokeLinecap="round"
74
- strokeDasharray={circumference}
75
- strokeDashoffset={offset}
121
+ fill="none" stroke={getColor(score)} strokeWidth="10"
122
+ strokeLinecap="round" strokeDasharray={circumference} strokeDashoffset={offset}
76
123
  transform="rotate(-90 90 90)"
77
- className="transition-all duration-1000"
78
124
  />
79
125
  </svg>
80
126
  <div className="absolute inset-0 flex flex-col items-center justify-center">
@@ -86,9 +132,19 @@ function FitnessRing({ score, label }: { score: number; label: string }) {
86
132
  )
87
133
  }
88
134
 
135
+ function DimensionBar({ value, color }: { value: number; color: string }) {
136
+ return (
137
+ <div className="flex-1 h-3 rounded-full bg-muted overflow-hidden">
138
+ <div
139
+ className="h-full rounded-full transition-all duration-500"
140
+ style={{ width: `${value}%`, backgroundColor: color }}
141
+ />
142
+ </div>
143
+ )
144
+ }
145
+
89
146
  function InsightCard({ insight }: { insight: CoachInsight }) {
90
147
  const style = SEVERITY_STYLES[insight.severity]
91
- const CategoryIcon = CATEGORY_ICONS[insight.category]
92
148
  const SeverityIcon = style.icon
93
149
 
94
150
  return (
@@ -98,17 +154,11 @@ function InsightCard({ insight }: { insight: CoachInsight }) {
98
154
  <div className="flex-1 space-y-1.5">
99
155
  <div className="flex items-center justify-between gap-2">
100
156
  <h4 className="text-sm font-semibold">{insight.title}</h4>
101
- <div className="flex items-center gap-1.5 shrink-0">
102
- {insight.metric && (
103
- <span className="rounded-md bg-muted px-2 py-0.5 text-xs font-mono font-medium">
104
- {insight.metric}
105
- </span>
106
- )}
107
- <span className="flex items-center gap-1 text-xs text-muted-foreground">
108
- <CategoryIcon className="h-3 w-3" />
109
- {CATEGORY_LABELS[insight.category]}
157
+ {insight.metric && (
158
+ <span className="rounded-md bg-muted px-2 py-0.5 text-xs font-mono font-medium shrink-0">
159
+ {insight.metric}
110
160
  </span>
111
- </div>
161
+ )}
112
162
  </div>
113
163
  <p className="text-sm text-muted-foreground">{insight.description}</p>
114
164
  {insight.recommendation && (
@@ -125,124 +175,201 @@ function InsightCard({ insight }: { insight: CoachInsight }) {
125
175
 
126
176
  export function AgentCoach({ data }: { data: UsageData }) {
127
177
  const coach = useMemo(() => generateCoachInsights(data), [data])
178
+ const [cmdInsights, setCmdInsights] = useState<CommandInsight[]>([])
128
179
 
129
- const achievements = coach.insights.filter(i => i.severity === 'achievement')
130
- const warnings = coach.insights.filter(i => i.severity === 'warning')
131
- const tips = coach.insights.filter(i => i.severity === 'tip')
180
+ useEffect(() => {
181
+ fetch('/api/command-insights')
182
+ .then(r => r.json())
183
+ .then(setCmdInsights)
184
+ .catch(() => {})
185
+ }, [])
132
186
 
133
- return (
134
- <div className="space-y-6">
135
- {/* Fitness Score + Quick Stats */}
136
- <div className="grid gap-4 lg:grid-cols-3">
137
- <Card className="lg:row-span-2">
138
- <CardHeader>
139
- <CardTitle>Agent Fitness Score</CardTitle>
140
- <CardDescription>Overall health of your human-AI collaboration</CardDescription>
141
- </CardHeader>
142
- <CardContent className="flex justify-center pb-6">
143
- <FitnessRing score={coach.score} label={coach.scoreLabel} />
144
- </CardContent>
145
- </Card>
187
+ const allInsights: CoachInsight[] = [
188
+ ...coach.insights,
189
+ ...cmdInsights.map(i => ({ ...i, category: 'discovery' as InsightCategory, craft: 'reach' as CraftDimension })),
190
+ ]
146
191
 
147
- <Card>
148
- <CardHeader className="flex flex-row items-center justify-between pb-2">
149
- <CardTitle className="text-sm font-medium text-muted-foreground">Avg Cost / Session</CardTitle>
150
- <DollarSign className="h-4 w-4 text-muted-foreground" />
151
- </CardHeader>
152
- <CardContent>
153
- <div className="text-2xl font-bold">{formatCost(coach.stats.avgCostPerSession)}</div>
154
- </CardContent>
155
- </Card>
192
+ // Group insights by CRAFT dimension
193
+ const insightsByDimension = (dim: CraftDimension) =>
194
+ allInsights.filter(i => i.craft === dim)
156
195
 
157
- <Card>
158
- <CardHeader className="flex flex-row items-center justify-between pb-2">
159
- <CardTitle className="text-sm font-medium text-muted-foreground">Avg Duration</CardTitle>
160
- <Clock className="h-4 w-4 text-muted-foreground" />
161
- </CardHeader>
162
- <CardContent>
163
- <div className="text-2xl font-bold">{formatDuration(coach.stats.avgDurationMinutes)}</div>
164
- </CardContent>
165
- </Card>
196
+ // Ungrouped insights (no craft tag)
197
+ const ungroupedWarnings = allInsights.filter(i => !i.craft && i.severity === 'warning')
198
+ const ungroupedTips = allInsights.filter(i => !i.craft && i.severity === 'tip')
199
+ const ungroupedAchievements = allInsights.filter(i => !i.craft && i.severity === 'achievement')
166
200
 
201
+ return (
202
+ <div className="space-y-6">
203
+ {/* Hero: CRAFT Score + Framework Overview */}
204
+ <div className="grid gap-4 lg:grid-cols-2">
167
205
  <Card>
168
- <CardHeader className="flex flex-row items-center justify-between pb-2">
169
- <CardTitle className="text-sm font-medium text-muted-foreground">Avg Messages</CardTitle>
170
- <MessageSquare className="h-4 w-4 text-muted-foreground" />
206
+ <CardHeader className="pb-2">
207
+ <CardTitle>CRAFT Score</CardTitle>
208
+ <CardDescription>
209
+ Your overall AI coding proficiency, measured across 5 dimensions
210
+ </CardDescription>
171
211
  </CardHeader>
172
- <CardContent>
173
- <div className="text-2xl font-bold">{formatNumber(Math.round(coach.stats.avgMessagesPerSession))}</div>
212
+ <CardContent className="flex items-center gap-6">
213
+ <FitnessRing score={coach.score} label={coach.scoreLabel} />
214
+ <div className="flex-1 space-y-3">
215
+ <div className="flex items-center gap-3 text-sm text-muted-foreground">
216
+ <span className="flex items-center gap-1">
217
+ <Flame className="h-3.5 w-3.5" />
218
+ {coach.stats.currentStreak > 0 ? `${coach.stats.currentStreak}d streak` : 'No streak'}
219
+ {coach.stats.longestStreak > 0 && (
220
+ <span className="text-xs"> (best: {coach.stats.longestStreak}d)</span>
221
+ )}
222
+ </span>
223
+ </div>
224
+ <div className="space-y-2">
225
+ {CRAFT_DIMENSIONS.map(({ key, letter, label, color }) => (
226
+ <div key={key} className="flex items-center gap-2">
227
+ <span className="w-5 text-xs font-bold" style={{ color }}>{letter}</span>
228
+ <span className="w-20 text-xs text-muted-foreground">{label}</span>
229
+ <DimensionBar value={coach.craft[key]} color={color} />
230
+ <span className="w-7 text-xs font-semibold text-right">{coach.craft[key]}</span>
231
+ </div>
232
+ ))}
233
+ </div>
234
+ <div className="text-[10px] text-muted-foreground">
235
+ Weights: A 25% · C/R/T 20% each · F 15%
236
+ </div>
237
+ </div>
174
238
  </CardContent>
175
239
  </Card>
176
240
 
241
+ {/* What is CRAFT */}
177
242
  <Card>
178
- <CardHeader className="flex flex-row items-center justify-between pb-2">
179
- <CardTitle className="text-sm font-medium text-muted-foreground">Coding Streak</CardTitle>
180
- <Flame className="h-4 w-4 text-muted-foreground" />
243
+ <CardHeader className="pb-2">
244
+ <CardTitle>What is CRAFT?</CardTitle>
245
+ <CardDescription>
246
+ A framework for measuring Human-AI coding proficiency
247
+ </CardDescription>
181
248
  </CardHeader>
182
- <CardContent>
183
- <div className="text-2xl font-bold">
184
- {coach.stats.currentStreak > 0 ? `${coach.stats.currentStreak}d` : '—'}
185
- <span className="text-sm font-normal text-muted-foreground ml-2">
186
- best: {coach.stats.longestStreak}d
187
- </span>
249
+ <CardContent className="text-sm space-y-3">
250
+ <p className="text-muted-foreground">
251
+ Every CRAFT metric is derived directly from your local conversation logs.
252
+ No external integrations or surveys needed.
253
+ </p>
254
+ <div className="grid grid-cols-5 gap-2 text-center">
255
+ {CRAFT_DIMENSIONS.map(({ letter, label, color, icon: Icon }) => (
256
+ <div key={letter} className="space-y-1">
257
+ <div className="flex justify-center">
258
+ <Icon className="h-5 w-5" style={{ color }} />
259
+ </div>
260
+ <div className="text-xs font-bold" style={{ color }}>{letter}</div>
261
+ <div className="text-[10px] text-muted-foreground">{label}</div>
262
+ </div>
263
+ ))}
188
264
  </div>
265
+ <p className="text-xs text-muted-foreground">
266
+ Each dimension is scored 0-100 based on your actual usage patterns. The overall score is a
267
+ weighted average prioritizing behavioral quality (Autonomy) over volume (Throughput).
268
+ </p>
189
269
  </CardContent>
190
270
  </Card>
191
271
  </div>
192
272
 
193
- {/* Achievements */}
194
- {achievements.length > 0 && (
195
- <Card>
196
- <CardHeader>
197
- <div className="flex items-center gap-2">
198
- <Trophy className="h-5 w-5 text-chart-2" />
199
- <div>
200
- <CardTitle>Achievements</CardTitle>
201
- <CardDescription>Things you're doing well</CardDescription>
273
+ {/* Per-dimension breakdown with insights */}
274
+ {CRAFT_DIMENSIONS.map(({ key, letter, label, color, icon: Icon, description, metrics }) => {
275
+ const dimInsights = insightsByDimension(key)
276
+ const dimScore = coach.craft[key]
277
+ return (
278
+ <Card key={key}>
279
+ <CardHeader>
280
+ <div className="flex items-center justify-between">
281
+ <div className="flex items-center gap-3">
282
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg" style={{ backgroundColor: `color-mix(in srgb, ${color} 15%, transparent)` }}>
283
+ <Icon className="h-5 w-5" style={{ color }} />
284
+ </div>
285
+ <div>
286
+ <CardTitle className="flex items-center gap-2">
287
+ <span className="text-lg font-bold" style={{ color }}>{letter}</span>
288
+ {label}
289
+ </CardTitle>
290
+ <CardDescription>{description}</CardDescription>
291
+ </div>
292
+ </div>
293
+ <div className="text-right">
294
+ <div className="text-2xl font-bold" style={{ color }}>{dimScore}</div>
295
+ <div className="text-xs text-muted-foreground">/ 100</div>
296
+ </div>
297
+ </div>
298
+ </CardHeader>
299
+ <CardContent className="space-y-4">
300
+ {/* How this score is calculated */}
301
+ <div className="rounded-md bg-muted/50 p-3">
302
+ <div className="text-xs font-medium mb-2">How this score is calculated:</div>
303
+ <div className="grid gap-1">
304
+ {metrics.map((m) => (
305
+ <div key={m} className="text-xs text-muted-foreground flex items-center gap-2">
306
+ <div className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: color }} />
307
+ {m}
308
+ </div>
309
+ ))}
310
+ </div>
202
311
  </div>
203
- </div>
204
- </CardHeader>
205
- <CardContent className="space-y-3">
206
- {achievements.map(i => <InsightCard key={i.id} insight={i} />)}
207
- </CardContent>
208
- </Card>
209
- )}
210
312
 
211
- {/* Warnings */}
212
- {warnings.length > 0 && (
213
- <Card>
214
- <CardHeader>
215
- <div className="flex items-center gap-2">
216
- <AlertTriangle className="h-5 w-5 text-chart-5" />
217
- <div>
218
- <CardTitle>Areas to Improve</CardTitle>
219
- <CardDescription>Issues that may be costing you time or money</CardDescription>
313
+ {/* Score bar */}
314
+ <div className="flex items-center gap-3">
315
+ <DimensionBar value={dimScore} color={color} />
316
+ <span className="text-sm font-semibold w-10 text-right">{dimScore}/100</span>
220
317
  </div>
221
- </div>
222
- </CardHeader>
223
- <CardContent className="space-y-3">
224
- {warnings.map(i => <InsightCard key={i.id} insight={i} />)}
225
- </CardContent>
226
- </Card>
227
- )}
228
318
 
229
- {/* Tips */}
230
- {tips.length > 0 && (
319
+ {/* Insights for this dimension */}
320
+ {dimInsights.length > 0 ? (
321
+ <div className="space-y-3">
322
+ {dimInsights.map(i => <InsightCard key={i.id} insight={i} />)}
323
+ </div>
324
+ ) : (
325
+ <p className="text-sm text-muted-foreground italic">No specific insights for this dimension yet.</p>
326
+ )}
327
+ </CardContent>
328
+ </Card>
329
+ )
330
+ })}
331
+
332
+ {/* Ungrouped insights (if any) */}
333
+ {(ungroupedWarnings.length > 0 || ungroupedTips.length > 0 || ungroupedAchievements.length > 0) && (
231
334
  <Card>
232
335
  <CardHeader>
233
- <div className="flex items-center gap-2">
234
- <Lightbulb className="h-5 w-5 text-chart-1" />
235
- <div>
236
- <CardTitle>Tips & Suggestions</CardTitle>
237
- <CardDescription>Ways to get more out of your coding agent</CardDescription>
238
- </div>
239
- </div>
336
+ <CardTitle>Other Insights</CardTitle>
337
+ <CardDescription>General recommendations not tied to a specific CRAFT dimension</CardDescription>
240
338
  </CardHeader>
241
339
  <CardContent className="space-y-3">
242
- {tips.map(i => <InsightCard key={i.id} insight={i} />)}
340
+ {[...ungroupedWarnings, ...ungroupedTips, ...ungroupedAchievements].map(i => (
341
+ <InsightCard key={i.id} insight={i} />
342
+ ))}
243
343
  </CardContent>
244
344
  </Card>
245
345
  )}
346
+
347
+ {/* Session Averages */}
348
+ <Card>
349
+ <CardHeader>
350
+ <CardTitle className="text-sm">Session Averages</CardTitle>
351
+ </CardHeader>
352
+ <CardContent>
353
+ <div className="grid grid-cols-2 gap-4 sm:grid-cols-4 text-sm">
354
+ <div>
355
+ <div className="text-muted-foreground">Cost / Session</div>
356
+ <div className="font-semibold">{formatCost(coach.stats.avgCostPerSession)}</div>
357
+ </div>
358
+ <div>
359
+ <div className="text-muted-foreground">Duration</div>
360
+ <div className="font-semibold">{formatDuration(coach.stats.avgDurationMinutes)}</div>
361
+ </div>
362
+ <div>
363
+ <div className="text-muted-foreground">Messages</div>
364
+ <div className="font-semibold">{formatNumber(Math.round(coach.stats.avgMessagesPerSession))}</div>
365
+ </div>
366
+ <div>
367
+ <div className="text-muted-foreground">Peak Hour</div>
368
+ <div className="font-semibold">{coach.stats.peakHour}:00</div>
369
+ </div>
370
+ </div>
371
+ </CardContent>
372
+ </Card>
246
373
  </div>
247
374
  )
248
375
  }