agentfit 0.1.0 → 0.1.2

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 (74) hide show
  1. package/.github/workflows/release.yml +111 -0
  2. package/README.md +41 -38
  3. package/app/(dashboard)/daily/page.tsx +1 -1
  4. package/app/(dashboard)/data-management/page.tsx +180 -0
  5. package/app/(dashboard)/flow/page.tsx +17 -0
  6. package/app/(dashboard)/layout.tsx +2 -0
  7. package/app/(dashboard)/page.tsx +24 -5
  8. package/app/(dashboard)/reports/[id]/page.tsx +72 -0
  9. package/app/(dashboard)/reports/page.tsx +132 -0
  10. package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
  11. package/app/api/backup/route.ts +215 -0
  12. package/app/api/check/route.ts +11 -1
  13. package/app/api/command-insights/route.ts +13 -0
  14. package/app/api/commands/route.ts +55 -1
  15. package/app/api/images-analysis/route.ts +3 -4
  16. package/app/api/reports/[id]/route.ts +23 -0
  17. package/app/api/reports/route.ts +50 -0
  18. package/app/api/reset/route.ts +21 -0
  19. package/app/api/session/route.ts +40 -0
  20. package/app/api/usage/route.ts +26 -1
  21. package/app/layout.tsx +1 -1
  22. package/bin/agentfit.mjs +2 -2
  23. package/components/agent-coach.tsx +256 -129
  24. package/components/app-sidebar.tsx +45 -10
  25. package/components/backup-section.tsx +236 -0
  26. package/components/daily-chart.tsx +447 -83
  27. package/components/dashboard-shell.tsx +29 -31
  28. package/components/data-provider.tsx +88 -8
  29. package/components/fitness-score.tsx +95 -54
  30. package/components/overview-cards.tsx +148 -41
  31. package/components/report-view.tsx +307 -0
  32. package/components/screenshots-analysis.tsx +51 -46
  33. package/components/session-chatlog.tsx +124 -0
  34. package/components/session-timeline.tsx +184 -0
  35. package/components/session-workflow.tsx +183 -0
  36. package/components/sessions-table.tsx +9 -1
  37. package/components/tool-flow-graph.tsx +144 -0
  38. package/components/ui/carousel.tsx +242 -0
  39. package/components/ui/sidebar.tsx +1 -1
  40. package/components/ui/sonner.tsx +51 -0
  41. package/electron/entitlements.mac.plist +16 -0
  42. package/electron/init-db.mjs +37 -0
  43. package/electron/main.mjs +203 -0
  44. package/generated/prisma/browser.ts +5 -0
  45. package/generated/prisma/client.ts +5 -0
  46. package/generated/prisma/internal/class.ts +14 -4
  47. package/generated/prisma/internal/prismaNamespace.ts +97 -2
  48. package/generated/prisma/internal/prismaNamespaceBrowser.ts +21 -1
  49. package/generated/prisma/models/Report.ts +1219 -0
  50. package/generated/prisma/models/Session.ts +221 -1
  51. package/generated/prisma/models.ts +1 -0
  52. package/lib/coach.ts +571 -211
  53. package/lib/command-insights.ts +231 -0
  54. package/lib/db.ts +2 -2
  55. package/lib/parse-codex.ts +6 -0
  56. package/lib/parse-logs.ts +80 -1
  57. package/lib/queries-codex.ts +24 -0
  58. package/lib/queries.ts +45 -0
  59. package/lib/report.ts +156 -0
  60. package/lib/session-detail.ts +382 -0
  61. package/lib/sync.ts +87 -0
  62. package/lib/tool-flow.ts +71 -0
  63. package/next.config.mjs +6 -1
  64. package/package.json +17 -2
  65. package/plugins/cost-heatmap/component.tsx +72 -50
  66. package/prisma/migrations/20260401144555_add_system_prompt_edits/migration.sql +80 -0
  67. package/prisma/schema.prisma +18 -0
  68. package/prisma/schema.sql +81 -0
  69. package/.claude/settings.local.json +0 -26
  70. package/CONTRIBUTING.md +0 -209
  71. package/prisma/migrations/20260328152517_init/migration.sql +0 -41
  72. package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
  73. package/prisma.config.ts +0 -14
  74. package/setup.sh +0 -73
@@ -3,10 +3,6 @@
3
3
  import { usePathname } from 'next/navigation'
4
4
  import { RefreshCw } from 'lucide-react'
5
5
  import { SidebarProvider, SidebarInset, SidebarTrigger } from '@/components/ui/sidebar'
6
- import { Separator } from '@/components/ui/separator'
7
- import { Button } from '@/components/ui/button'
8
- import { Badge } from '@/components/ui/badge'
9
- import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
10
6
  import {
11
7
  Select,
12
8
  SelectContent,
@@ -15,12 +11,12 @@ import {
15
11
  SelectValue,
16
12
  } from '@/components/ui/select'
17
13
  import { AppSidebar } from './app-sidebar'
18
- import { useData, type TimeRange } from './data-provider'
14
+ import { useData, type TimeRange, type AgentType } from './data-provider'
19
15
  import { getPlugin } from '@/lib/plugins'
20
16
  import '@/plugins' // ensure plugins are registered
21
17
  import type { ReactNode } from 'react'
22
18
 
23
- const VIEW_TITLES: Record<string, string> = {
19
+ const VIEW_TITLES: Record<string, ReactNode> = {
24
20
  '/': 'Dashboard',
25
21
  '/daily': 'Daily Usage',
26
22
  '/tokens': 'Token Breakdown',
@@ -29,19 +25,23 @@ const VIEW_TITLES: Record<string, string> = {
29
25
  '/sessions': 'Sessions',
30
26
  '/personality': 'Personality Fit',
31
27
  '/commands': 'Command Usage',
32
- '/images': 'Images',
33
- '/coach': 'Agent Coach',
28
+ '/images': 'Image Analysis',
29
+ '/coach': 'CRAFT Coach',
30
+ '/flow': 'Session Flow',
31
+ '/community': 'Community',
32
+ '/reports': 'Reports',
33
+ '/data-management': 'Data Management',
34
34
  }
35
35
 
36
- function resolveTitle(pathname: string): string {
36
+ function resolveTitle(pathname: string): ReactNode {
37
37
  if (VIEW_TITLES[pathname]) return VIEW_TITLES[pathname]
38
- if (pathname === '/community') return 'Community'
39
- // Community plugin routes: /community/<slug>
40
38
  const match = pathname.match(/^\/community\/([a-z0-9-]+)$/)
41
39
  if (match) {
42
40
  const plugin = getPlugin(match[1])
43
41
  if (plugin) return plugin.manifest.name
44
42
  }
43
+ if (pathname.startsWith('/reports/')) return 'Report Detail'
44
+ if (pathname.startsWith('/sessions/')) return 'Session Detail'
45
45
  return 'Dashboard'
46
46
  }
47
47
 
@@ -65,13 +65,13 @@ export function DashboardShell({ children }: { children: ReactNode }) {
65
65
  const pathname = usePathname()
66
66
  const {
67
67
  agent, setAgent, timeRange, setTimeRange,
68
- syncing, lastSyncResult, lastSyncTime, handleSync, loading,
69
- newSessionsAvailable,
68
+ selectedProject, setSelectedProject, allProjects,
69
+ loading,
70
70
  } = useData()
71
71
 
72
72
  const title = resolveTitle(pathname)
73
- const communityMatch = pathname.match(/^\/community\/([a-z0-9-]+)$/)
74
- const communityPlugin = communityMatch ? getPlugin(communityMatch[1]) : undefined
73
+ const communitySlug = pathname.match(/^\/community\/([a-z0-9-]+)$/)?.[1]
74
+ const communityPlugin = communitySlug ? getPlugin(communitySlug) : undefined
75
75
  const showTimeFilter =
76
76
  !NO_TIME_FILTER.has(pathname) && !communityPlugin?.manifest.customDataSource
77
77
 
@@ -113,7 +113,20 @@ export function DashboardShell({ children }: { children: ReactNode }) {
113
113
  ))}
114
114
  </div>
115
115
  )}
116
- <Select value={agent} onValueChange={(v) => v && setAgent(v as 'claude' | 'codex' | 'combined')}>
116
+ {allProjects.length > 1 && (
117
+ <Select value={selectedProject} onValueChange={(v) => v && setSelectedProject(v)}>
118
+ <SelectTrigger className="h-8 w-[160px] text-xs">
119
+ <SelectValue>{selectedProject === 'all' ? 'All Projects' : selectedProject}</SelectValue>
120
+ </SelectTrigger>
121
+ <SelectContent>
122
+ <SelectItem value="all">All Projects</SelectItem>
123
+ {allProjects.map((p) => (
124
+ <SelectItem key={p} value={p}>{p}</SelectItem>
125
+ ))}
126
+ </SelectContent>
127
+ </Select>
128
+ )}
129
+ <Select value={agent} onValueChange={(v) => v && setAgent(v as AgentType)}>
117
130
  <SelectTrigger className="h-8 w-[130px] text-xs">
118
131
  <SelectValue>{AGENT_LABELS[agent]}</SelectValue>
119
132
  </SelectTrigger>
@@ -123,21 +136,6 @@ export function DashboardShell({ children }: { children: ReactNode }) {
123
136
  <SelectItem value="combined">Combined</SelectItem>
124
137
  </SelectContent>
125
138
  </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
139
  </div>
142
140
  </header>
143
141
  <main className="flex-1 space-y-6 p-6">
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { createContext, useContext, useEffect, useState, useCallback, useMemo, type ReactNode } from 'react'
4
+ import { toast } from 'sonner'
4
5
  import type { UsageData, SessionSummary, DailyUsage, ProjectSummary, OverviewStats } from '@/lib/parse-logs'
5
6
 
6
7
  export type AgentType = 'claude' | 'codex' | 'combined'
@@ -22,10 +23,15 @@ interface DataContextValue {
22
23
  setAgent: (agent: AgentType) => void
23
24
  timeRange: TimeRange
24
25
  setTimeRange: (range: TimeRange) => void
26
+ selectedProject: string
27
+ setSelectedProject: (project: string) => void
28
+ allProjects: string[]
25
29
  syncing: boolean
30
+ resetting: boolean
26
31
  lastSyncResult: SyncResult | null
27
32
  lastSyncTime: Date | null
28
33
  handleSync: () => Promise<void>
34
+ handleReset: () => Promise<void>
29
35
  newSessionsAvailable: number
30
36
  }
31
37
 
@@ -44,16 +50,21 @@ const RANGE_DAYS: Record<TimeRange, number> = {
44
50
  'all': Infinity,
45
51
  }
46
52
 
47
- function filterByTimeRange(raw: UsageData | null, range: TimeRange): UsageData | null {
48
- if (!raw || range === 'all') return raw
53
+ function filterData(raw: UsageData | null, range: TimeRange, project: string): UsageData | null {
54
+ if (!raw) return raw
55
+ if (range === 'all' && project === 'all') return raw
49
56
 
50
57
  const days = RANGE_DAYS[range]
51
58
  const cutoff = new Date()
52
59
  cutoff.setDate(cutoff.getDate() - days)
53
- const cutoffISO = cutoff.toISOString()
60
+ const cutoffISO = range === 'all' ? '' : cutoff.toISOString()
54
61
 
55
- // Filter sessions
56
- const sessions = raw.sessions.filter(s => s.startTime >= cutoffISO)
62
+ // Filter sessions by time range and project
63
+ const sessions = raw.sessions.filter(s => {
64
+ if (range !== 'all' && s.startTime < cutoffISO) return false
65
+ if (project !== 'all' && s.project !== project) return false
66
+ return true
67
+ })
57
68
 
58
69
  // Re-aggregate from filtered sessions
59
70
  const projectMap = new Map<string, ProjectSummary>()
@@ -90,14 +101,18 @@ function filterByTimeRange(raw: UsageData | null, range: TimeRange): UsageData |
90
101
  if (!dailyMap.has(date)) {
91
102
  dailyMap.set(date, {
92
103
  date, sessions: 0, messages: 0,
104
+ userMessages: 0, assistantMessages: 0,
93
105
  inputTokens: 0, outputTokens: 0,
94
106
  cacheCreationTokens: 0, cacheReadTokens: 0,
95
107
  totalTokens: 0, costUSD: 0, toolCalls: 0,
108
+ toolCallsDetail: {}, interruptions: 0, rateLimitErrors: 0,
96
109
  })
97
110
  }
98
111
  const day = dailyMap.get(date)!
99
112
  day.sessions++
100
113
  day.messages += s.totalMessages
114
+ day.userMessages += s.userMessages
115
+ day.assistantMessages += s.assistantMessages
101
116
  day.inputTokens += s.inputTokens
102
117
  day.outputTokens += s.outputTokens
103
118
  day.cacheCreationTokens += s.cacheCreationTokens
@@ -105,6 +120,11 @@ function filterByTimeRange(raw: UsageData | null, range: TimeRange): UsageData |
105
120
  day.totalTokens += s.totalTokens
106
121
  day.costUSD += s.costUSD
107
122
  day.toolCalls += s.toolCallsTotal
123
+ day.interruptions += s.userInterruptions
124
+ day.rateLimitErrors += s.rateLimitErrors
125
+ for (const [tool, count] of Object.entries(s.toolCalls)) {
126
+ day.toolCallsDetail[tool] = (day.toolCallsDetail[tool] || 0) + count
127
+ }
108
128
 
109
129
  // Tools
110
130
  for (const [tool, count] of Object.entries(s.toolCalls)) {
@@ -130,9 +150,37 @@ function filterByTimeRange(raw: UsageData | null, range: TimeRange): UsageData |
130
150
  totalCacheReadTokens: sessions.reduce((a, s) => a + s.cacheReadTokens, 0),
131
151
  totalTokens: sessions.reduce((a, s) => a + s.totalTokens, 0),
132
152
  totalCostUSD: sessions.reduce((a, s) => a + s.costUSD, 0),
153
+ totalSystemPromptEdits: sessions.reduce((a, s) => a + (s.systemPromptEdits ?? 0), 0),
133
154
  totalDurationMinutes: sessions.reduce((a, s) => a + s.durationMinutes, 0),
134
155
  totalToolCalls: sessions.reduce((a, s) => a + s.toolCallsTotal, 0),
156
+ totalApiErrors: sessions.reduce((a, s) => a + s.apiErrors, 0),
157
+ totalRateLimitDays: (() => {
158
+ const days = new Set<string>()
159
+ for (const s of sessions) {
160
+ if (s.rateLimitErrors > 0) days.add(s.startTime.slice(0, 10))
161
+ }
162
+ return days.size
163
+ })(),
164
+ totalUserInterruptions: sessions.reduce((a, s) => a + s.userInterruptions, 0),
135
165
  models,
166
+ skillUsage: (() => {
167
+ const skills: Record<string, number> = {}
168
+ for (const s of sessions) {
169
+ for (const [skill, count] of Object.entries(s.skillCalls)) {
170
+ skills[skill] = (skills[skill] || 0) + count
171
+ }
172
+ }
173
+ return skills
174
+ })(),
175
+ permissionModes: (() => {
176
+ const modes: Record<string, number> = {}
177
+ for (const s of sessions) {
178
+ for (const [mode, count] of Object.entries(s.permissionModes || {})) {
179
+ modes[mode] = (modes[mode] || 0) + count
180
+ }
181
+ }
182
+ return modes
183
+ })(),
136
184
  }
137
185
 
138
186
  return { overview, sessions, projects, daily, toolUsage }
@@ -144,12 +192,19 @@ export function DataProvider({ children }: { children: ReactNode }) {
144
192
  const [error, setError] = useState<string | null>(null)
145
193
  const [agent, setAgentState] = useState<AgentType>('claude')
146
194
  const [timeRange, setTimeRange] = useState<TimeRange>('all')
195
+ const [selectedProject, setSelectedProject] = useState<string>('all')
147
196
  const [syncing, setSyncing] = useState(false)
197
+ const [resetting, setResetting] = useState(false)
148
198
  const [lastSyncResult, setLastSyncResult] = useState<SyncResult | null>(null)
149
199
  const [lastSyncTime, setLastSyncTime] = useState<Date | null>(null)
150
200
  const [newSessionsAvailable, setNewSessionsAvailable] = useState(0)
151
201
 
152
- const data = useMemo(() => filterByTimeRange(rawData, timeRange), [rawData, timeRange])
202
+ const data = useMemo(() => filterData(rawData, timeRange, selectedProject), [rawData, timeRange, selectedProject])
203
+
204
+ const allProjects = useMemo(() => {
205
+ if (!rawData) return []
206
+ return rawData.projects.map(p => p.name).sort((a, b) => a.localeCompare(b))
207
+ }, [rawData])
153
208
 
154
209
  const fetchData = useCallback(async (selectedAgent: AgentType) => {
155
210
  try {
@@ -183,7 +238,31 @@ export function DataProvider({ children }: { children: ReactNode }) {
183
238
  }
184
239
  }, [fetchData, agent])
185
240
 
186
-
241
+ const handleReset = useCallback(async () => {
242
+ setResetting(true)
243
+ try {
244
+ const res = await fetch('/api/reset', { method: 'POST' })
245
+ const body = await res.json()
246
+ if (!res.ok) {
247
+ throw new Error(body.error || 'Reset failed')
248
+ }
249
+ setLastSyncResult(body)
250
+ setLastSyncTime(new Date())
251
+ setNewSessionsAvailable(0)
252
+ toast.success('Database reset complete', {
253
+ description: `Re-imported ${body.sessionsAdded || 0} sessions from disk.`,
254
+ })
255
+ // Full reload to dashboard so all state is fresh
256
+ setTimeout(() => { window.location.href = '/' }, 1500)
257
+ return
258
+ } catch (e) {
259
+ const msg = (e as Error).message
260
+ setError(msg)
261
+ toast.error('Reset failed', { description: msg })
262
+ } finally {
263
+ setResetting(false)
264
+ }
265
+ }, [fetchData, agent])
187
266
 
188
267
  const setAgent = useCallback((newAgent: AgentType) => {
189
268
  setAgentState(newAgent)
@@ -204,7 +283,8 @@ export function DataProvider({ children }: { children: ReactNode }) {
204
283
  <DataContext.Provider value={{
205
284
  data, loading, error, agent, setAgent,
206
285
  timeRange, setTimeRange,
207
- syncing, lastSyncResult, lastSyncTime, handleSync,
286
+ selectedProject, setSelectedProject, allProjects,
287
+ syncing, resetting, lastSyncResult, lastSyncTime, handleSync, handleReset,
208
288
  newSessionsAvailable,
209
289
  }}>
210
290
  {children}
@@ -2,14 +2,15 @@
2
2
 
3
3
  import { useMemo } from 'react'
4
4
  import Link from 'next/link'
5
- import { Card, CardContent } from '@/components/ui/card'
5
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
6
6
  import type { UsageData } from '@/lib/parse-logs'
7
- import { generateCoachInsights } from '@/lib/coach'
8
- import { ArrowRight, Trophy, AlertTriangle, Lightbulb } from 'lucide-react'
7
+ import { generateCoachInsights, type CraftScores } from '@/lib/coach'
8
+ import { ArrowRight, AlertTriangle, Lightbulb, Flame } from 'lucide-react'
9
9
 
10
- function ScoreRing({ score, size = 120 }: { score: number; size?: number }) {
11
- const radius = (size - 16) / 2
10
+ function ScoreRing({ score, size = 100 }: { score: number; size?: number }) {
11
+ const radius = (size - 14) / 2
12
12
  const circumference = 2 * Math.PI * radius
13
+ const offset = circumference - (score / 100) * circumference
13
14
 
14
15
  const getColor = (s: number) => {
15
16
  if (s >= 85) return 'var(--chart-2)'
@@ -22,74 +23,114 @@ function ScoreRing({ score, size = 120 }: { score: number; size?: number }) {
22
23
  <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
23
24
  <circle
24
25
  cx={size / 2} cy={size / 2} r={radius}
25
- fill="none"
26
- stroke="var(--muted)"
27
- strokeWidth="8"
26
+ fill="none" stroke="var(--muted)" strokeWidth="7"
28
27
  />
29
28
  <circle
30
29
  cx={size / 2} cy={size / 2} r={radius}
31
- fill="none"
32
- stroke={getColor(score)}
33
- strokeWidth="8"
34
- strokeLinecap="round"
35
- strokeDasharray={circumference}
36
- strokeDashoffset={circumference - (score / 100) * circumference}
30
+ fill="none" stroke={getColor(score)} strokeWidth="7"
31
+ strokeLinecap="round" strokeDasharray={circumference}
32
+ strokeDashoffset={offset}
37
33
  transform={`rotate(-90 ${size / 2} ${size / 2})`}
38
- className="transition-all duration-1000"
39
34
  />
40
- <text x={size / 2} y={size / 2 - 4} textAnchor="middle" dominantBaseline="middle" className="fill-foreground text-2xl font-bold" style={{ fontSize: size * 0.22 }}>
35
+ <text x={size / 2} y={size / 2 - 2} textAnchor="middle" dominantBaseline="middle" className="fill-foreground font-bold" style={{ fontSize: size * 0.24 }}>
41
36
  {score}
42
37
  </text>
43
- <text x={size / 2} y={size / 2 + 16} textAnchor="middle" className="fill-muted-foreground" style={{ fontSize: size * 0.09 }}>
38
+ <text x={size / 2} y={size / 2 + 14} textAnchor="middle" className="fill-muted-foreground" style={{ fontSize: size * 0.1 }}>
44
39
  / 100
45
40
  </text>
46
41
  </svg>
47
42
  )
48
43
  }
49
44
 
45
+ const CRAFT_LABELS: { key: keyof CraftScores; letter: string; label: string; color: string }[] = [
46
+ { key: 'context', letter: 'C', label: 'Context', color: 'var(--chart-1)' },
47
+ { key: 'reach', letter: 'R', label: 'Reach', color: 'var(--chart-2)' },
48
+ { key: 'autonomy', letter: 'A', label: 'Autonomy', color: 'var(--chart-3)' },
49
+ { key: 'flow', letter: 'F', label: 'Flow', color: 'var(--chart-4)' },
50
+ { key: 'throughput', letter: 'T', label: 'Throughput', color: 'var(--chart-6)' },
51
+ ]
52
+
53
+ function CraftBars({ craft }: { craft: CraftScores }) {
54
+ return (
55
+ <div className="space-y-1.5">
56
+ {CRAFT_LABELS.map(({ key, letter, label, color }) => (
57
+ <div key={key} className="flex items-center gap-2">
58
+ <span className="w-4 text-[10px] font-bold" style={{ color }}>{letter}</span>
59
+ <span className="w-16 text-[10px] text-muted-foreground truncate">{label}</span>
60
+ <div className="flex-1 h-2 rounded-full bg-muted overflow-hidden">
61
+ <div
62
+ className="h-full rounded-full transition-all duration-500"
63
+ style={{ width: `${craft[key]}%`, backgroundColor: color }}
64
+ />
65
+ </div>
66
+ <span className="w-6 text-[10px] font-medium text-right">{craft[key]}</span>
67
+ </div>
68
+ ))}
69
+ </div>
70
+ )
71
+ }
72
+
50
73
  export function FitnessScore({ data }: { data: UsageData }) {
51
74
  const coach = useMemo(() => generateCoachInsights(data), [data])
52
75
 
53
- const achievements = coach.insights.filter(i => i.severity === 'achievement').length
54
- const warnings = coach.insights.filter(i => i.severity === 'warning').length
55
- const tips = coach.insights.filter(i => i.severity === 'tip').length
76
+ // Pick top 2 insights: warnings first, then tips
77
+ const topInsights = [
78
+ ...coach.insights.filter(i => i.severity === 'warning'),
79
+ ...coach.insights.filter(i => i.severity === 'tip'),
80
+ ].slice(0, 2)
56
81
 
57
82
  return (
58
- <Card>
59
- <CardContent className="flex items-center gap-6 pt-6">
60
- <ScoreRing score={coach.score} />
61
- <div className="flex-1 space-y-3">
62
- <div>
63
- <div className="text-lg font-semibold">Fitness Score: {coach.scoreLabel}</div>
64
- <p className="text-sm text-muted-foreground">
65
- Based on cost efficiency, tool habits, streaks, and context management
66
- </p>
83
+ <div className="grid gap-4 lg:grid-cols-3">
84
+ {/* CRAFT Status score ring + dimension bars in one card */}
85
+ <Card>
86
+ <CardHeader className="pb-2">
87
+ <CardTitle className="text-sm font-medium text-muted-foreground">CRAFT Status</CardTitle>
88
+ </CardHeader>
89
+ <CardContent className="flex items-center gap-6">
90
+ <div className="flex flex-col items-center gap-1">
91
+ <ScoreRing score={coach.score} />
92
+ <div className="text-xs font-semibold">{coach.scoreLabel}</div>
93
+ <div className="flex items-center gap-1 text-[10px] text-muted-foreground">
94
+ <Flame className="h-3 w-3" />
95
+ {coach.stats.currentStreak > 0
96
+ ? `${coach.stats.currentStreak}d streak (best: ${coach.stats.longestStreak}d)`
97
+ : 'Start a streak'}
98
+ </div>
99
+ <Link
100
+ href="/coach"
101
+ className="inline-flex items-center gap-1 text-[10px] font-medium text-primary hover:underline mt-1"
102
+ >
103
+ All insights <ArrowRight className="h-3 w-3" />
104
+ </Link>
67
105
  </div>
68
- <div className="flex items-center gap-4 text-sm">
69
- {achievements > 0 && (
70
- <span className="flex items-center gap-1 text-chart-2">
71
- <Trophy className="h-3.5 w-3.5" /> {achievements} achievement{achievements !== 1 && 's'}
72
- </span>
73
- )}
74
- {warnings > 0 && (
75
- <span className="flex items-center gap-1 text-chart-5">
76
- <AlertTriangle className="h-3.5 w-3.5" /> {warnings} warning{warnings !== 1 && 's'}
77
- </span>
78
- )}
79
- {tips > 0 && (
80
- <span className="flex items-center gap-1 text-chart-1">
81
- <Lightbulb className="h-3.5 w-3.5" /> {tips} tip{tips !== 1 && 's'}
82
- </span>
83
- )}
106
+ <div className="flex-1">
107
+ <CraftBars craft={coach.craft} />
84
108
  </div>
85
- <Link
86
- href="/coach"
87
- className="inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline"
88
- >
89
- View coaching insights <ArrowRight className="h-3.5 w-3.5" />
90
- </Link>
91
- </div>
92
- </CardContent>
93
- </Card>
109
+ </CardContent>
110
+ </Card>
111
+
112
+ {/* Top 2 Insights */}
113
+ {topInsights.map((insight, i) => (
114
+ <Card
115
+ key={insight.id}
116
+ className={`border-l-4 ${insight.severity === 'warning' ? 'border-l-chart-5' : 'border-l-chart-1'}`}
117
+ >
118
+ <CardHeader className="pb-1.5">
119
+ <CardTitle className="text-[11px] font-medium flex items-center gap-1.5 text-muted-foreground">
120
+ {insight.severity === 'warning'
121
+ ? <><AlertTriangle className="h-3 w-3 text-chart-5" /> {i === 0 ? 'Top Priority' : 'Improve'}</>
122
+ : <><Lightbulb className="h-3 w-3 text-chart-1" /> Tip</>}
123
+ {insight.craft && (
124
+ <span className="ml-auto text-[9px] uppercase tracking-wider opacity-60">{insight.craft}</span>
125
+ )}
126
+ </CardTitle>
127
+ </CardHeader>
128
+ <CardContent>
129
+ <div className="text-sm font-semibold mb-1 line-clamp-1">{insight.title}</div>
130
+ <p className="text-xs text-muted-foreground line-clamp-2">{insight.recommendation || insight.description}</p>
131
+ </CardContent>
132
+ </Card>
133
+ ))}
134
+ </div>
94
135
  )
95
136
  }