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.
- package/.github/workflows/release.yml +111 -0
- package/README.md +41 -38
- package/app/(dashboard)/daily/page.tsx +1 -1
- package/app/(dashboard)/data-management/page.tsx +180 -0
- package/app/(dashboard)/flow/page.tsx +17 -0
- package/app/(dashboard)/layout.tsx +2 -0
- package/app/(dashboard)/page.tsx +24 -5
- package/app/(dashboard)/reports/[id]/page.tsx +72 -0
- package/app/(dashboard)/reports/page.tsx +132 -0
- package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
- package/app/api/backup/route.ts +215 -0
- package/app/api/check/route.ts +11 -1
- package/app/api/command-insights/route.ts +13 -0
- package/app/api/commands/route.ts +55 -1
- package/app/api/images-analysis/route.ts +3 -4
- package/app/api/reports/[id]/route.ts +23 -0
- package/app/api/reports/route.ts +50 -0
- package/app/api/reset/route.ts +21 -0
- package/app/api/session/route.ts +40 -0
- package/app/api/usage/route.ts +26 -1
- package/app/layout.tsx +1 -1
- package/bin/agentfit.mjs +2 -2
- package/components/agent-coach.tsx +256 -129
- package/components/app-sidebar.tsx +45 -10
- package/components/backup-section.tsx +236 -0
- package/components/daily-chart.tsx +447 -83
- package/components/dashboard-shell.tsx +29 -31
- package/components/data-provider.tsx +88 -8
- package/components/fitness-score.tsx +95 -54
- package/components/overview-cards.tsx +148 -41
- package/components/report-view.tsx +307 -0
- package/components/screenshots-analysis.tsx +51 -46
- package/components/session-chatlog.tsx +124 -0
- package/components/session-timeline.tsx +184 -0
- package/components/session-workflow.tsx +183 -0
- package/components/sessions-table.tsx +9 -1
- package/components/tool-flow-graph.tsx +144 -0
- package/components/ui/carousel.tsx +242 -0
- package/components/ui/sidebar.tsx +1 -1
- package/components/ui/sonner.tsx +51 -0
- package/electron/entitlements.mac.plist +16 -0
- package/electron/init-db.mjs +37 -0
- package/electron/main.mjs +203 -0
- package/generated/prisma/browser.ts +5 -0
- package/generated/prisma/client.ts +5 -0
- package/generated/prisma/internal/class.ts +14 -4
- package/generated/prisma/internal/prismaNamespace.ts +97 -2
- package/generated/prisma/internal/prismaNamespaceBrowser.ts +21 -1
- package/generated/prisma/models/Report.ts +1219 -0
- package/generated/prisma/models/Session.ts +221 -1
- package/generated/prisma/models.ts +1 -0
- package/lib/coach.ts +571 -211
- package/lib/command-insights.ts +231 -0
- package/lib/db.ts +2 -2
- package/lib/parse-codex.ts +6 -0
- package/lib/parse-logs.ts +80 -1
- package/lib/queries-codex.ts +24 -0
- package/lib/queries.ts +45 -0
- package/lib/report.ts +156 -0
- package/lib/session-detail.ts +382 -0
- package/lib/sync.ts +87 -0
- package/lib/tool-flow.ts +71 -0
- package/next.config.mjs +6 -1
- package/package.json +17 -2
- package/plugins/cost-heatmap/component.tsx +72 -50
- package/prisma/migrations/20260401144555_add_system_prompt_edits/migration.sql +80 -0
- package/prisma/schema.prisma +18 -0
- package/prisma/schema.sql +81 -0
- package/.claude/settings.local.json +0 -26
- package/CONTRIBUTING.md +0 -209
- package/prisma/migrations/20260328152517_init/migration.sql +0 -41
- package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
- package/prisma.config.ts +0 -14
- package/setup.sh +0 -73
|
@@ -23,53 +23,131 @@ import {
|
|
|
23
23
|
Cpu,
|
|
24
24
|
Sun,
|
|
25
25
|
CalendarClock,
|
|
26
|
+
AlertTriangle,
|
|
27
|
+
Ban,
|
|
28
|
+
Flame,
|
|
29
|
+
Zap,
|
|
30
|
+
Calendar,
|
|
26
31
|
HelpCircle,
|
|
27
32
|
} from 'lucide-react'
|
|
28
33
|
|
|
29
|
-
function
|
|
34
|
+
function computeTopHours(sessions: SessionSummary[]): string {
|
|
30
35
|
if (sessions.length === 0) return 'N/A'
|
|
31
36
|
const hourCounts = new Array(24).fill(0)
|
|
32
37
|
for (const s of sessions) {
|
|
33
|
-
|
|
38
|
+
const timestamps = s.messageTimestamps
|
|
39
|
+
if (timestamps && timestamps.length > 0) {
|
|
40
|
+
for (const ts of timestamps) {
|
|
41
|
+
hourCounts[new Date(ts).getHours()]++
|
|
42
|
+
}
|
|
43
|
+
} else if (s.startTime) {
|
|
44
|
+
hourCounts[new Date(s.startTime).getHours()]++
|
|
45
|
+
}
|
|
34
46
|
}
|
|
35
|
-
const peak = hourCounts.indexOf(Math.max(...hourCounts))
|
|
36
|
-
const end = (peak + 1) % 24
|
|
37
47
|
const fmt = (h: number) => `${h % 12 || 12}${h < 12 ? 'am' : 'pm'}`
|
|
38
|
-
|
|
48
|
+
const top2 = hourCounts
|
|
49
|
+
.map((count, hour) => ({ hour, count }))
|
|
50
|
+
.sort((a, b) => b.count - a.count)
|
|
51
|
+
.slice(0, 2)
|
|
52
|
+
return top2.map((t) => fmt(t.hour)).join(', ')
|
|
39
53
|
}
|
|
40
54
|
|
|
55
|
+
const IDLE_THRESHOLD_MS = 30 * 60 * 1000 // 30 min — gaps longer than this are idle time
|
|
56
|
+
|
|
41
57
|
function computeAvgDailyTime(sessions: SessionSummary[]): number {
|
|
42
58
|
if (sessions.length === 0) return 0
|
|
43
|
-
//
|
|
44
|
-
|
|
59
|
+
// Collect all message timestamps grouped by local date.
|
|
60
|
+
// Sum only gaps between consecutive messages that are under the idle threshold.
|
|
61
|
+
const dayTimestamps = new Map<string, number[]>()
|
|
62
|
+
|
|
45
63
|
for (const s of sessions) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
const timestamps = s.messageTimestamps
|
|
65
|
+
if (!timestamps || timestamps.length === 0) continue
|
|
66
|
+
for (const ts of timestamps) {
|
|
67
|
+
const d = new Date(ts)
|
|
68
|
+
const ms = d.getTime()
|
|
69
|
+
const date = d.toLocaleDateString('en-CA')
|
|
70
|
+
if (!dayTimestamps.has(date)) dayTimestamps.set(date, [])
|
|
71
|
+
dayTimestamps.get(date)!.push(ms)
|
|
72
|
+
}
|
|
53
73
|
}
|
|
54
74
|
|
|
55
75
|
let totalMinutes = 0
|
|
56
|
-
for (const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
last[1] = Math.max(last[1], intervals[i][1])
|
|
63
|
-
} else {
|
|
64
|
-
merged.push(intervals[i])
|
|
76
|
+
for (const timestamps of dayTimestamps.values()) {
|
|
77
|
+
timestamps.sort((a, b) => a - b)
|
|
78
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
79
|
+
const gap = timestamps[i] - timestamps[i - 1]
|
|
80
|
+
if (gap <= IDLE_THRESHOLD_MS) {
|
|
81
|
+
totalMinutes += gap / 60000
|
|
65
82
|
}
|
|
66
83
|
}
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return dayTimestamps.size > 0 ? totalMinutes / dayTimestamps.size : 0
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function computeAvgParallelSessions(sessions: SessionSummary[]): string {
|
|
90
|
+
if (sessions.length === 0) return '0'
|
|
91
|
+
// Group sessions by local date, count how many are active per day
|
|
92
|
+
const daySessions = new Map<string, number>()
|
|
93
|
+
for (const s of sessions) {
|
|
94
|
+
if (!s.startTime) continue
|
|
95
|
+
const date = new Date(s.startTime).toLocaleDateString('en-CA')
|
|
96
|
+
daySessions.set(date, (daySessions.get(date) || 0) + 1)
|
|
97
|
+
}
|
|
98
|
+
if (daySessions.size === 0) return '0'
|
|
99
|
+
const total = Array.from(daySessions.values()).reduce((a, b) => a + b, 0)
|
|
100
|
+
const avg = total / daySessions.size
|
|
101
|
+
return avg.toFixed(1)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function computeStreaks(sessions: SessionSummary[]): { longest: number; current: number; activeDays: number } {
|
|
105
|
+
if (sessions.length === 0) return { longest: 0, current: 0, activeDays: 0 }
|
|
106
|
+
|
|
107
|
+
const activeDates = new Set<string>()
|
|
108
|
+
for (const s of sessions) {
|
|
109
|
+
if (s.startTime) {
|
|
110
|
+
activeDates.add(new Date(s.startTime).toLocaleDateString('en-CA'))
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (activeDates.size === 0) return { longest: 0, current: 0, activeDays: 0 }
|
|
115
|
+
|
|
116
|
+
const sorted = Array.from(activeDates).sort()
|
|
117
|
+
let longest = 1
|
|
118
|
+
let streak = 1
|
|
119
|
+
|
|
120
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
121
|
+
const prev = new Date(sorted[i - 1])
|
|
122
|
+
const curr = new Date(sorted[i])
|
|
123
|
+
const diffDays = (curr.getTime() - prev.getTime()) / (1000 * 60 * 60 * 24)
|
|
124
|
+
if (diffDays === 1) {
|
|
125
|
+
streak++
|
|
126
|
+
if (streak > longest) longest = streak
|
|
127
|
+
} else {
|
|
128
|
+
streak = 1
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Current streak: count backwards from today
|
|
133
|
+
const today = new Date().toLocaleDateString('en-CA')
|
|
134
|
+
const yesterday = new Date(Date.now() - 86400000).toLocaleDateString('en-CA')
|
|
135
|
+
let current = 0
|
|
136
|
+
if (activeDates.has(today) || activeDates.has(yesterday)) {
|
|
137
|
+
const start = activeDates.has(today) ? today : yesterday
|
|
138
|
+
current = 1
|
|
139
|
+
let d = new Date(start)
|
|
140
|
+
while (true) {
|
|
141
|
+
d = new Date(d.getTime() - 86400000)
|
|
142
|
+
if (activeDates.has(d.toLocaleDateString('en-CA'))) {
|
|
143
|
+
current++
|
|
144
|
+
} else {
|
|
145
|
+
break
|
|
146
|
+
}
|
|
69
147
|
}
|
|
70
148
|
}
|
|
71
149
|
|
|
72
|
-
return
|
|
150
|
+
return { longest, current, activeDays: activeDates.size }
|
|
73
151
|
}
|
|
74
152
|
|
|
75
153
|
interface MetricDef {
|
|
@@ -80,8 +158,9 @@ interface MetricDef {
|
|
|
80
158
|
}
|
|
81
159
|
|
|
82
160
|
export function OverviewCards({ overview, sessions }: { overview: OverviewStats; sessions: SessionSummary[] }) {
|
|
83
|
-
const
|
|
161
|
+
const topHours = useMemo(() => computeTopHours(sessions), [sessions])
|
|
84
162
|
const avgDailyTime = useMemo(() => computeAvgDailyTime(sessions), [sessions])
|
|
163
|
+
const streaks = useMemo(() => computeStreaks(sessions), [sessions])
|
|
85
164
|
|
|
86
165
|
const cards: MetricDef[] = [
|
|
87
166
|
{
|
|
@@ -121,22 +200,22 @@ export function OverviewCards({ overview, sessions }: { overview: OverviewStats;
|
|
|
121
200
|
explanation: 'Total number of tool invocations by the assistant (Read, Edit, Bash, Grep, Write, Agent, etc.). Each tool_use content block in an assistant message counts as one call.',
|
|
122
201
|
},
|
|
123
202
|
{
|
|
124
|
-
title: '
|
|
203
|
+
title: 'Est. Daily Time',
|
|
125
204
|
value: formatDuration(avgDailyTime),
|
|
126
205
|
icon: CalendarClock,
|
|
127
|
-
explanation: 'Average active time per day.
|
|
206
|
+
explanation: 'Average active coding time per day. Computed from individual message timestamps: sums gaps between consecutive messages that are under 30 minutes (idle gaps are excluded). Then divides by the number of active days.',
|
|
128
207
|
},
|
|
129
208
|
{
|
|
130
|
-
title: '
|
|
131
|
-
value:
|
|
209
|
+
title: 'Top Hours',
|
|
210
|
+
value: topHours,
|
|
132
211
|
icon: Sun,
|
|
133
|
-
explanation: 'The
|
|
212
|
+
explanation: 'The two hours of day with the most message activity. Based on individual message timestamps across all sessions, grouped by hour in your local timezone. Note: logs only store UTC timestamps, so this conversion uses your browser\'s current timezone — it is only accurate if you mostly code from one timezone.',
|
|
134
213
|
},
|
|
135
214
|
{
|
|
136
|
-
title: '
|
|
137
|
-
value:
|
|
215
|
+
title: 'Avg Parallel',
|
|
216
|
+
value: computeAvgParallelSessions(sessions),
|
|
138
217
|
icon: Clock,
|
|
139
|
-
explanation: '
|
|
218
|
+
explanation: 'Average number of sessions active in parallel per day. For each active day, counts how many sessions have overlapping time ranges, then averages across all days.',
|
|
140
219
|
},
|
|
141
220
|
{
|
|
142
221
|
title: 'Top Model',
|
|
@@ -144,6 +223,36 @@ export function OverviewCards({ overview, sessions }: { overview: OverviewStats;
|
|
|
144
223
|
icon: Cpu,
|
|
145
224
|
explanation: 'The Claude model used in the most sessions. Determined by the model field in assistant responses. If a session uses multiple models, the last one seen is recorded.',
|
|
146
225
|
},
|
|
226
|
+
{
|
|
227
|
+
title: 'Longest Streak',
|
|
228
|
+
value: `${streaks.longest}d`,
|
|
229
|
+
icon: Flame,
|
|
230
|
+
explanation: 'The longest consecutive-day coding streak. A day counts as active if at least one session was started that day. Helps you see your most sustained periods of AI-assisted coding.',
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
title: 'Current Streak',
|
|
234
|
+
value: `${streaks.current}d`,
|
|
235
|
+
icon: Zap,
|
|
236
|
+
explanation: 'Your current consecutive-day coding streak, counting back from today (or yesterday if you haven\'t coded today yet). Keep it going!',
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
title: 'Active Days',
|
|
240
|
+
value: formatNumber(streaks.activeDays),
|
|
241
|
+
icon: Calendar,
|
|
242
|
+
explanation: 'Total number of unique days with at least one coding session. Shows how consistently you\'ve been using AI coding tools over time.',
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
title: 'Rate Limit Days',
|
|
246
|
+
value: formatNumber(overview.totalRateLimitDays),
|
|
247
|
+
icon: AlertTriangle,
|
|
248
|
+
explanation: 'Number of unique days where you hit API rate limits. Rate limits (HTTP 429/529, "Rate limit reached/exceeded") indicate heavy token usage that day. This is a better signal of usage intensity than raw API error counts, which include unrelated server errors.',
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
title: 'Interruptions',
|
|
252
|
+
value: formatNumber(overview.totalUserInterruptions),
|
|
253
|
+
icon: Ban,
|
|
254
|
+
explanation: 'Number of times you interrupted or rejected a tool execution. Detected from tool_result messages containing "The user doesn\'t want to proceed". High counts may indicate the agent is taking unwanted actions.',
|
|
255
|
+
},
|
|
147
256
|
]
|
|
148
257
|
|
|
149
258
|
return (
|
|
@@ -161,14 +270,12 @@ function MetricCard({ metric }: { metric: MetricDef }) {
|
|
|
161
270
|
return (
|
|
162
271
|
<Card className="group relative">
|
|
163
272
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
164
|
-
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
273
|
+
<CardTitle className="flex items-center gap-1 text-sm font-medium text-muted-foreground">
|
|
165
274
|
{metric.title}
|
|
166
|
-
</CardTitle>
|
|
167
|
-
<div className="flex items-center gap-1">
|
|
168
275
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
169
276
|
<DialogTrigger
|
|
170
277
|
render={<button />}
|
|
171
|
-
className="
|
|
278
|
+
className="text-muted-foreground/50 hover:text-foreground transition-colors"
|
|
172
279
|
>
|
|
173
280
|
<HelpCircle className="h-3.5 w-3.5" />
|
|
174
281
|
</DialogTrigger>
|
|
@@ -187,8 +294,8 @@ function MetricCard({ metric }: { metric: MetricDef }) {
|
|
|
187
294
|
</div>
|
|
188
295
|
</DialogContent>
|
|
189
296
|
</Dialog>
|
|
190
|
-
|
|
191
|
-
|
|
297
|
+
</CardTitle>
|
|
298
|
+
<metric.icon className="h-4 w-4 text-muted-foreground" />
|
|
192
299
|
</CardHeader>
|
|
193
300
|
<CardContent>
|
|
194
301
|
<div className="text-2xl font-bold tracking-tight">{metric.value}</div>
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
4
|
+
import { Badge } from '@/components/ui/badge'
|
|
5
|
+
import {
|
|
6
|
+
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
|
7
|
+
} from '@/components/ui/table'
|
|
8
|
+
import {
|
|
9
|
+
Trophy, AlertTriangle, Lightbulb, Brain, Flame, Wrench, Clock,
|
|
10
|
+
MessageSquare, FolderOpen, LayoutList, DollarSign, Terminal,
|
|
11
|
+
Zap, Search, Ban, Calendar,
|
|
12
|
+
} from 'lucide-react'
|
|
13
|
+
import { formatCost, formatDuration, formatNumber, formatTokens } from '@/lib/format'
|
|
14
|
+
import type { ReportContent } from '@/lib/report'
|
|
15
|
+
import type { CoachInsight, InsightSeverity, InsightCategory } from '@/lib/coach'
|
|
16
|
+
|
|
17
|
+
// ─── Score Ring ──────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function ScoreRing({ score, size = 140 }: { score: number; size?: number }) {
|
|
20
|
+
const radius = (size - 14) / 2
|
|
21
|
+
const circumference = 2 * Math.PI * radius
|
|
22
|
+
const offset = circumference - (score / 100) * circumference
|
|
23
|
+
const getColor = (s: number) => {
|
|
24
|
+
if (s >= 85) return 'var(--chart-2)'
|
|
25
|
+
if (s >= 70) return 'var(--chart-1)'
|
|
26
|
+
if (s >= 55) return 'var(--chart-3)'
|
|
27
|
+
return 'var(--chart-5)'
|
|
28
|
+
}
|
|
29
|
+
return (
|
|
30
|
+
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
|
|
31
|
+
<circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke="var(--muted)" strokeWidth="9" />
|
|
32
|
+
<circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke={getColor(score)} strokeWidth="9"
|
|
33
|
+
strokeLinecap="round" strokeDasharray={circumference} strokeDashoffset={offset}
|
|
34
|
+
transform={`rotate(-90 ${size / 2} ${size / 2})`} />
|
|
35
|
+
<text x={size / 2} y={size / 2 - 4} textAnchor="middle" dominantBaseline="middle" className="fill-foreground font-bold" style={{ fontSize: size * 0.22 }}>{score}</text>
|
|
36
|
+
<text x={size / 2} y={size / 2 + 16} textAnchor="middle" className="fill-muted-foreground" style={{ fontSize: size * 0.09 }}>/ 100</text>
|
|
37
|
+
</svg>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Insight Card ────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const SEVERITY_BORDER: Record<InsightSeverity, string> = {
|
|
44
|
+
achievement: 'border-l-chart-2',
|
|
45
|
+
warning: 'border-l-chart-5',
|
|
46
|
+
tip: 'border-l-chart-1',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const SEVERITY_ICON: Record<InsightSeverity, typeof Trophy> = {
|
|
50
|
+
achievement: Trophy,
|
|
51
|
+
warning: AlertTriangle,
|
|
52
|
+
tip: Lightbulb,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function InsightCard({ insight }: { insight: CoachInsight }) {
|
|
56
|
+
const Icon = SEVERITY_ICON[insight.severity]
|
|
57
|
+
return (
|
|
58
|
+
<div className={`rounded-lg border border-l-4 ${SEVERITY_BORDER[insight.severity]} p-4`}>
|
|
59
|
+
<div className="flex items-start gap-3">
|
|
60
|
+
<Icon className="h-4 w-4 shrink-0 mt-0.5 text-muted-foreground" />
|
|
61
|
+
<div className="space-y-1">
|
|
62
|
+
<div className="text-sm font-semibold">{insight.title}</div>
|
|
63
|
+
<p className="text-xs text-muted-foreground">{insight.description}</p>
|
|
64
|
+
{insight.recommendation && (
|
|
65
|
+
<div className="rounded-md bg-muted/50 p-2 text-xs mt-2">{insight.recommendation}</div>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
{insight.metric && (
|
|
69
|
+
<span className="shrink-0 rounded-md bg-muted px-2 py-0.5 text-xs font-mono">{insight.metric}</span>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Main Report View ────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export function ReportView({ content }: { content: ReportContent }) {
|
|
79
|
+
const g = content.atAGlance
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="space-y-6 max-w-4xl">
|
|
83
|
+
{/* At a Glance */}
|
|
84
|
+
<Card>
|
|
85
|
+
<CardHeader>
|
|
86
|
+
<CardTitle>At a Glance</CardTitle>
|
|
87
|
+
<CardDescription>{g.dateRange.from} to {g.dateRange.to}</CardDescription>
|
|
88
|
+
</CardHeader>
|
|
89
|
+
<CardContent>
|
|
90
|
+
<div className="flex items-center gap-8">
|
|
91
|
+
<ScoreRing score={g.fitnessScore} />
|
|
92
|
+
<div className="flex-1">
|
|
93
|
+
<div className="text-lg font-semibold mb-1">Fitness: {g.scoreLabel}</div>
|
|
94
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-3">
|
|
95
|
+
<Flame className="h-3.5 w-3.5" />
|
|
96
|
+
{g.currentStreak > 0 ? `${g.currentStreak}d streak` : 'No active streak'}
|
|
97
|
+
{g.longestStreak > 0 && <span>(best: {g.longestStreak}d)</span>}
|
|
98
|
+
</div>
|
|
99
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4 text-sm">
|
|
100
|
+
<div><span className="text-muted-foreground">Sessions</span><div className="font-semibold">{formatNumber(g.totalSessions)}</div></div>
|
|
101
|
+
<div><span className="text-muted-foreground">Projects</span><div className="font-semibold">{formatNumber(g.totalProjects)}</div></div>
|
|
102
|
+
<div><span className="text-muted-foreground">Messages</span><div className="font-semibold">{formatNumber(g.totalMessages)}</div></div>
|
|
103
|
+
<div><span className="text-muted-foreground">Total Time</span><div className="font-semibold">{formatDuration(g.totalDurationMinutes)}</div></div>
|
|
104
|
+
<div><span className="text-muted-foreground">Tool Calls</span><div className="font-semibold">{formatNumber(g.totalToolCalls)}</div></div>
|
|
105
|
+
<div><span className="text-muted-foreground">Total Cost</span><div className="font-semibold">{formatCost(g.totalCostUSD)}</div></div>
|
|
106
|
+
<div><span className="text-muted-foreground">API Errors</span><div className="font-semibold">{formatNumber(g.totalApiErrors)}</div></div>
|
|
107
|
+
<div><span className="text-muted-foreground">Interruptions</span><div className="font-semibold">{formatNumber(g.totalUserInterruptions)}</div></div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</CardContent>
|
|
112
|
+
</Card>
|
|
113
|
+
|
|
114
|
+
{/* Project Areas */}
|
|
115
|
+
{content.projectAreas.length > 0 && (
|
|
116
|
+
<Card>
|
|
117
|
+
<CardHeader>
|
|
118
|
+
<div className="flex items-center gap-2">
|
|
119
|
+
<FolderOpen className="h-5 w-5 text-muted-foreground" />
|
|
120
|
+
<div>
|
|
121
|
+
<CardTitle>What You Work On</CardTitle>
|
|
122
|
+
<CardDescription>Top projects by session count</CardDescription>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</CardHeader>
|
|
126
|
+
<CardContent>
|
|
127
|
+
<Table>
|
|
128
|
+
<TableHeader>
|
|
129
|
+
<TableRow>
|
|
130
|
+
<TableHead>Project</TableHead>
|
|
131
|
+
<TableHead className="text-right">Sessions</TableHead>
|
|
132
|
+
<TableHead className="text-right">Messages</TableHead>
|
|
133
|
+
<TableHead className="text-right">Duration</TableHead>
|
|
134
|
+
<TableHead>Top Tools</TableHead>
|
|
135
|
+
</TableRow>
|
|
136
|
+
</TableHeader>
|
|
137
|
+
<TableBody>
|
|
138
|
+
{content.projectAreas.map(p => (
|
|
139
|
+
<TableRow key={p.name}>
|
|
140
|
+
<TableCell className="font-medium">{p.name}</TableCell>
|
|
141
|
+
<TableCell className="text-right">{p.sessions}</TableCell>
|
|
142
|
+
<TableCell className="text-right">{formatNumber(p.totalMessages)}</TableCell>
|
|
143
|
+
<TableCell className="text-right">{formatDuration(p.totalDurationMinutes)}</TableCell>
|
|
144
|
+
<TableCell className="text-xs text-muted-foreground">
|
|
145
|
+
{p.topTools.map(([name, count]) => `${name} (${count})`).join(', ')}
|
|
146
|
+
</TableCell>
|
|
147
|
+
</TableRow>
|
|
148
|
+
))}
|
|
149
|
+
</TableBody>
|
|
150
|
+
</Table>
|
|
151
|
+
</CardContent>
|
|
152
|
+
</Card>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
{/* Interaction Style */}
|
|
156
|
+
<Card>
|
|
157
|
+
<CardHeader>
|
|
158
|
+
<div className="flex items-center gap-2">
|
|
159
|
+
<Brain className="h-5 w-5 text-muted-foreground" />
|
|
160
|
+
<div>
|
|
161
|
+
<CardTitle>How You Use Your Agent</CardTitle>
|
|
162
|
+
<CardDescription>Interaction patterns and coding style</CardDescription>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</CardHeader>
|
|
166
|
+
<CardContent>
|
|
167
|
+
<div className="flex items-start gap-6 mb-4">
|
|
168
|
+
<div>
|
|
169
|
+
<div className="text-3xl font-bold tracking-wider text-primary">{content.interactionStyle.mbtiType}</div>
|
|
170
|
+
<div className="text-xs text-muted-foreground mt-1">{content.interactionStyle.mbtiDescription}</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4 text-sm">
|
|
174
|
+
<div>
|
|
175
|
+
<span className="text-muted-foreground">Avg Messages/Session</span>
|
|
176
|
+
<div className="font-semibold">{Math.round(content.interactionStyle.avgMessagesPerSession)}</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div>
|
|
179
|
+
<span className="text-muted-foreground">Avg Duration</span>
|
|
180
|
+
<div className="font-semibold">{formatDuration(content.interactionStyle.avgDurationMinutes)}</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div>
|
|
183
|
+
<span className="text-muted-foreground">Read/Edit Ratio</span>
|
|
184
|
+
<div className="font-semibold">{content.interactionStyle.readEditRatio.toFixed(1)}x</div>
|
|
185
|
+
</div>
|
|
186
|
+
<div>
|
|
187
|
+
<span className="text-muted-foreground">Bash Usage</span>
|
|
188
|
+
<div className="font-semibold">{Math.round(content.interactionStyle.bashRatio * 100)}%</div>
|
|
189
|
+
</div>
|
|
190
|
+
<div>
|
|
191
|
+
<span className="text-muted-foreground">Subagent Calls</span>
|
|
192
|
+
<div className="font-semibold">{formatNumber(content.interactionStyle.agentCallsTotal)}</div>
|
|
193
|
+
</div>
|
|
194
|
+
<div>
|
|
195
|
+
<span className="text-muted-foreground">Peak Hour</span>
|
|
196
|
+
<div className="font-semibold">{content.interactionStyle.peakHour}:00</div>
|
|
197
|
+
</div>
|
|
198
|
+
<div>
|
|
199
|
+
<span className="text-muted-foreground">Most Active Day</span>
|
|
200
|
+
<div className="font-semibold">{content.interactionStyle.mostActiveDay}</div>
|
|
201
|
+
</div>
|
|
202
|
+
<div>
|
|
203
|
+
<span className="text-muted-foreground">Avg Cost/Session</span>
|
|
204
|
+
<div className="font-semibold">{formatCost(content.interactionStyle.avgCostPerSession)}</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</CardContent>
|
|
208
|
+
</Card>
|
|
209
|
+
|
|
210
|
+
{/* What Works */}
|
|
211
|
+
{content.whatWorks.length > 0 && (
|
|
212
|
+
<Card>
|
|
213
|
+
<CardHeader>
|
|
214
|
+
<div className="flex items-center gap-2">
|
|
215
|
+
<Trophy className="h-5 w-5 text-chart-2" />
|
|
216
|
+
<div>
|
|
217
|
+
<CardTitle>What's Working</CardTitle>
|
|
218
|
+
<CardDescription>Patterns and habits that are paying off</CardDescription>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</CardHeader>
|
|
222
|
+
<CardContent className="space-y-3">
|
|
223
|
+
{content.whatWorks.map(i => <InsightCard key={i.id} insight={i} />)}
|
|
224
|
+
</CardContent>
|
|
225
|
+
</Card>
|
|
226
|
+
)}
|
|
227
|
+
|
|
228
|
+
{/* Friction Analysis */}
|
|
229
|
+
{content.frictionAnalysis.length > 0 && (
|
|
230
|
+
<Card>
|
|
231
|
+
<CardHeader>
|
|
232
|
+
<div className="flex items-center gap-2">
|
|
233
|
+
<AlertTriangle className="h-5 w-5 text-chart-5" />
|
|
234
|
+
<div>
|
|
235
|
+
<CardTitle>Where Things Go Wrong</CardTitle>
|
|
236
|
+
<CardDescription>Friction points that slow you down</CardDescription>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</CardHeader>
|
|
240
|
+
<CardContent className="space-y-3">
|
|
241
|
+
{content.frictionAnalysis.map(i => <InsightCard key={i.id} insight={i} />)}
|
|
242
|
+
</CardContent>
|
|
243
|
+
</Card>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{/* Suggestions */}
|
|
247
|
+
<Card>
|
|
248
|
+
<CardHeader>
|
|
249
|
+
<div className="flex items-center gap-2">
|
|
250
|
+
<Lightbulb className="h-5 w-5 text-chart-1" />
|
|
251
|
+
<div>
|
|
252
|
+
<CardTitle>Suggestions</CardTitle>
|
|
253
|
+
<CardDescription>Actionable ways to improve your workflow</CardDescription>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</CardHeader>
|
|
257
|
+
<CardContent className="space-y-6">
|
|
258
|
+
{/* CLAUDE.md rules */}
|
|
259
|
+
{content.suggestions.claudeMdRules.length > 0 && (
|
|
260
|
+
<div>
|
|
261
|
+
<h4 className="text-sm font-semibold mb-2 flex items-center gap-1.5">
|
|
262
|
+
<Terminal className="h-4 w-4" /> Add to CLAUDE.md
|
|
263
|
+
</h4>
|
|
264
|
+
<div className="space-y-2">
|
|
265
|
+
{content.suggestions.claudeMdRules.map((rule, i) => (
|
|
266
|
+
<div key={i} className="rounded-md bg-muted p-3 text-xs font-mono whitespace-pre-wrap">{rule}</div>
|
|
267
|
+
))}
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
{/* Coach tips */}
|
|
273
|
+
{content.suggestions.coachTips.length > 0 && (
|
|
274
|
+
<div>
|
|
275
|
+
<h4 className="text-sm font-semibold mb-2 flex items-center gap-1.5">
|
|
276
|
+
<Zap className="h-4 w-4" /> Workflow Tips
|
|
277
|
+
</h4>
|
|
278
|
+
<div className="space-y-3">
|
|
279
|
+
{content.suggestions.coachTips.map(i => <InsightCard key={i.id} insight={i} />)}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{/* Command tips */}
|
|
285
|
+
{content.suggestions.commandTips.length > 0 && (
|
|
286
|
+
<div>
|
|
287
|
+
<h4 className="text-sm font-semibold mb-2 flex items-center gap-1.5">
|
|
288
|
+
<Search className="h-4 w-4" /> Commands to Discover
|
|
289
|
+
</h4>
|
|
290
|
+
<div className="space-y-2">
|
|
291
|
+
{content.suggestions.commandTips.map(i => (
|
|
292
|
+
<div key={i.id} className="rounded-lg border p-3">
|
|
293
|
+
<div className="text-sm font-semibold">{i.title}</div>
|
|
294
|
+
<p className="text-xs text-muted-foreground mt-0.5">{i.description}</p>
|
|
295
|
+
{i.recommendation && (
|
|
296
|
+
<div className="rounded-md bg-muted/50 p-2 text-xs mt-2">{i.recommendation}</div>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
))}
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
</CardContent>
|
|
304
|
+
</Card>
|
|
305
|
+
</div>
|
|
306
|
+
)
|
|
307
|
+
}
|