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.
- package/README.md +30 -34
- package/app/(dashboard)/daily/page.tsx +1 -1
- 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/(dashboard)/settings/page.tsx +180 -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/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 +25 -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 +258 -8
- package/components/backup-section.tsx +236 -0
- package/components/daily-chart.tsx +404 -83
- package/components/dashboard-shell.tsx +9 -24
- package/components/data-provider.tsx +66 -2
- 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/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 +96 -2
- package/generated/prisma/internal/prismaNamespaceBrowser.ts +20 -1
- package/generated/prisma/models/Report.ts +1219 -0
- package/generated/prisma/models/Session.ts +187 -1
- package/generated/prisma/models.ts +1 -0
- package/lib/coach.ts +530 -211
- package/lib/command-insights.ts +231 -0
- package/lib/db.ts +1 -1
- package/lib/parse-codex.ts +5 -0
- package/lib/parse-logs.ts +65 -0
- package/lib/queries-codex.ts +22 -0
- package/lib/queries.ts +42 -0
- package/lib/report.ts +156 -0
- package/lib/session-detail.ts +382 -0
- package/lib/sync.ts +77 -0
- package/lib/tool-flow.ts +71 -0
- package/next.config.mjs +6 -1
- package/package.json +16 -2
- package/plugins/cost-heatmap/component.tsx +72 -50
- package/prisma/schema.prisma +17 -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/migrations/migration_lock.toml +0 -3
- package/prisma.config.ts +0 -14
- package/setup.sh +0 -73
package/README.md
CHANGED
|
@@ -10,56 +10,64 @@ Fitness tracker dashboard for AI coding agents. Reads your local Claude Code and
|
|
|
10
10
|
curl -fsSL https://raw.githubusercontent.com/harrywang/agentfit/main/setup.sh | bash
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Then start the dashboard:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
cd agentfit && npm start
|
|
17
|
-
```
|
|
18
|
-
|
|
19
13
|
### Option 2: npx
|
|
20
14
|
|
|
21
15
|
```bash
|
|
22
16
|
npx agentfit
|
|
23
17
|
```
|
|
24
18
|
|
|
25
|
-
This handles first-run setup (database creation, build) automatically.
|
|
26
|
-
|
|
27
19
|
### Option 3: Manual
|
|
28
20
|
|
|
29
21
|
```bash
|
|
30
22
|
git clone https://github.com/harrywang/agentfit.git
|
|
31
23
|
cd agentfit
|
|
32
24
|
npm install
|
|
33
|
-
echo 'DATABASE_URL="file:./
|
|
25
|
+
echo 'DATABASE_URL="file:./agentfit.db"' > .env
|
|
34
26
|
npx prisma migrate deploy
|
|
35
27
|
npm run build
|
|
36
28
|
npm start
|
|
37
29
|
```
|
|
38
30
|
|
|
39
|
-
Open [http://localhost:3000](http://localhost:3000). The app auto-syncs your `~/.claude/projects/` logs on first load.
|
|
31
|
+
Open [http://localhost:3000](http://localhost:3000). The app auto-syncs your Claude Code (`~/.claude/projects/`) and Codex (`~/.codex/sessions/`) logs on first load.
|
|
40
32
|
|
|
41
33
|
**Requirements:** Node.js 20+
|
|
42
34
|
|
|
35
|
+
## The CRAFT Framework
|
|
36
|
+
|
|
37
|
+
AgentFit scores your AI coding proficiency using **CRAFT** — a Human-AI coding proficiency framework by [Harry Wang](https://harrywang.me). All metrics are computed from your local conversation logs.
|
|
38
|
+
|
|
39
|
+
| | Dimension | What it measures | Key metrics |
|
|
40
|
+
|---|---|---|---|
|
|
41
|
+
| **C** | **Context** | How effectively you engineer context for the AI | CLAUDE.md usage, memory writes, cache hit rate |
|
|
42
|
+
| **R** | **Reach** | How broadly you leverage available capabilities | Tool diversity, subagent usage, skill adoption |
|
|
43
|
+
| **A** | **Autonomy** | How independently the agent works for you | Message ratio, interruption rate, delegation |
|
|
44
|
+
| **F** | **Flow** | How consistently you maintain a coding rhythm | Streak length, daily consistency, active days |
|
|
45
|
+
| **T** | **Throughput** | How much output you get for your investment | Cost efficiency, output volume, error rate |
|
|
46
|
+
|
|
47
|
+
Inspired by [DORA Metrics](https://dora.dev) and Microsoft's [SPACE framework](https://queue.acm.org/detail.cfm?id=3454124). Behavioral signals from your logs — no surveys, no guesswork. Each dimension is scored 0–100.
|
|
48
|
+
|
|
43
49
|
## Features
|
|
44
50
|
|
|
45
51
|
- **Dashboard** — overview stats (cost, tokens, sessions, projects, messages, tool calls, duration)
|
|
46
|
-
- **
|
|
52
|
+
- **CRAFT Coach** — fitness score, achievements, and actionable improvement tips
|
|
47
53
|
- **Daily Usage** — daily cost and activity charts
|
|
48
54
|
- **Token Breakdown** — pie chart + stacked area chart of token types
|
|
49
|
-
- **Tool Usage** — top
|
|
55
|
+
- **Tool Usage** — top tools by invocation count
|
|
50
56
|
- **Projects** — per-project breakdown with top tools and sessions
|
|
51
|
-
- **
|
|
57
|
+
- **Sessions** — individual session details with chat logs and tool flow graphs
|
|
58
|
+
- **Personality Fit** — MBTI-style behavioral analysis
|
|
52
59
|
- **Command Usage** — slash command pattern tracking
|
|
53
60
|
- **Images** — screenshot analysis across sessions
|
|
54
|
-
- **Community Plugins** — extensible analysis views
|
|
61
|
+
- **Community Plugins** — extensible analysis views
|
|
55
62
|
|
|
56
|
-
##
|
|
63
|
+
## Desktop App
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
Download a pre-built installer from the [Releases](https://github.com/harrywang/agentfit/releases) page, or build yourself:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm run electron:build:mac # Mac (.dmg)
|
|
69
|
+
npm run electron:build:win # Windows (.exe)
|
|
70
|
+
```
|
|
63
71
|
|
|
64
72
|
## Development
|
|
65
73
|
|
|
@@ -70,23 +78,11 @@ npm run lint # ESLint
|
|
|
70
78
|
npm run format # Prettier
|
|
71
79
|
npm run typecheck # TypeScript check
|
|
72
80
|
npm test # Run tests
|
|
73
|
-
npm run test:watch # Tests in watch mode
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
### Database
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
npx prisma migrate dev --name <name> # Create/apply migration
|
|
80
|
-
npx prisma generate # Regenerate Prisma client
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
## Community Plugins
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide on creating your own plugin.
|
|
88
|
-
|
|
89
|
-
**Quick version:**
|
|
85
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide. Quick version:
|
|
90
86
|
|
|
91
87
|
1. Create `plugins/<your-slug>/manifest.ts` and `component.tsx`
|
|
92
88
|
2. Register in `plugins/index.ts`
|
|
@@ -98,7 +94,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide on creating your own p
|
|
|
98
94
|
| Variable | Default | Description |
|
|
99
95
|
|----------|---------|-------------|
|
|
100
96
|
| `PORT` or `AGENTFIT_PORT` | `3000` | Server port |
|
|
101
|
-
| `DATABASE_URL` | `file:./
|
|
97
|
+
| `DATABASE_URL` | `file:./agentfit.db` | SQLite database path |
|
|
102
98
|
|
|
103
99
|
## Credits
|
|
104
100
|
|
|
@@ -11,7 +11,7 @@ export default function DailyPage() {
|
|
|
11
11
|
return (
|
|
12
12
|
<>
|
|
13
13
|
<div className="grid gap-4 lg:grid-cols-3">
|
|
14
|
-
<DailyChart daily={data.daily} />
|
|
14
|
+
<DailyChart daily={data.daily} sessions={data.sessions} />
|
|
15
15
|
</div>
|
|
16
16
|
<DailyTable daily={data.daily} sessions={data.sessions} />
|
|
17
17
|
</>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useData } from '@/components/data-provider'
|
|
4
|
+
import { ToolFlowGraph } from '@/components/tool-flow-graph'
|
|
5
|
+
import { SessionTimeline } from '@/components/session-timeline'
|
|
6
|
+
|
|
7
|
+
export default function FlowPage() {
|
|
8
|
+
const { data } = useData()
|
|
9
|
+
if (!data) return null
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<SessionTimeline sessions={data.sessions} />
|
|
14
|
+
<ToolFlowGraph data={data} />
|
|
15
|
+
</>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { DataProvider } from '@/components/data-provider'
|
|
4
4
|
import { DashboardShell } from '@/components/dashboard-shell'
|
|
5
|
+
import { Toaster } from '@/components/ui/sonner'
|
|
5
6
|
|
|
6
7
|
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
7
8
|
return (
|
|
8
9
|
<DataProvider>
|
|
9
10
|
<DashboardShell>{children}</DashboardShell>
|
|
11
|
+
<Toaster />
|
|
10
12
|
</DataProvider>
|
|
11
13
|
)
|
|
12
14
|
}
|
package/app/(dashboard)/page.tsx
CHANGED
|
@@ -3,21 +3,40 @@
|
|
|
3
3
|
import { useData } from '@/components/data-provider'
|
|
4
4
|
import { FitnessScore } from '@/components/fitness-score'
|
|
5
5
|
import { OverviewCards } from '@/components/overview-cards'
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { DailyCostChart, TopCommandsChart, TokenUsageHeatmap, UserVsAssistantChart, InterruptionRateChart, ToolMixChart } from '@/components/daily-chart'
|
|
7
|
+
import { RefreshCw } from 'lucide-react'
|
|
8
8
|
|
|
9
9
|
export default function DashboardPage() {
|
|
10
|
-
const { data } = useData()
|
|
10
|
+
const { data, newSessionsAvailable, handleSync, syncing } = useData()
|
|
11
11
|
if (!data) return null
|
|
12
12
|
|
|
13
13
|
return (
|
|
14
14
|
<>
|
|
15
|
+
{newSessionsAvailable > 0 && (
|
|
16
|
+
<div className="flex items-center justify-between rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-sm dark:border-blue-800 dark:bg-blue-950">
|
|
17
|
+
<span>
|
|
18
|
+
<strong>{newSessionsAvailable}</strong> new session{newSessionsAvailable !== 1 ? 's' : ''} detected on disk. Sync to update your dashboard.
|
|
19
|
+
</span>
|
|
20
|
+
<button
|
|
21
|
+
onClick={handleSync}
|
|
22
|
+
disabled={syncing}
|
|
23
|
+
className="inline-flex items-center gap-1.5 rounded-md bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
|
24
|
+
>
|
|
25
|
+
<RefreshCw className={`h-3 w-3 ${syncing ? 'animate-spin' : ''}`} />
|
|
26
|
+
{syncing ? 'Syncing…' : 'Sync now'}
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
15
30
|
<FitnessScore data={data} />
|
|
16
31
|
<OverviewCards overview={data.overview} sessions={data.sessions} />
|
|
17
32
|
<div className="grid gap-4 lg:grid-cols-3">
|
|
18
|
-
<
|
|
33
|
+
<DailyCostChart daily={data.daily} />
|
|
34
|
+
<TopCommandsChart />
|
|
35
|
+
<TokenUsageHeatmap daily={data.daily} />
|
|
36
|
+
<UserVsAssistantChart daily={data.daily} />
|
|
37
|
+
<InterruptionRateChart daily={data.daily} />
|
|
38
|
+
<ToolMixChart daily={data.daily} />
|
|
19
39
|
</div>
|
|
20
|
-
<ToolUsageChart toolUsage={data.toolUsage} />
|
|
21
40
|
</>
|
|
22
41
|
)
|
|
23
42
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useParams } from 'next/navigation'
|
|
5
|
+
import Link from 'next/link'
|
|
6
|
+
import { ArrowLeft, Loader2 } from 'lucide-react'
|
|
7
|
+
import { ReportView } from '@/components/report-view'
|
|
8
|
+
import type { ReportContent } from '@/lib/report'
|
|
9
|
+
|
|
10
|
+
interface ReportData {
|
|
11
|
+
id: string
|
|
12
|
+
title: string
|
|
13
|
+
generatedAt: string
|
|
14
|
+
contentJson: ReportContent
|
|
15
|
+
sessionCount: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function ReportDetailPage() {
|
|
19
|
+
const params = useParams()
|
|
20
|
+
const id = params.id as string
|
|
21
|
+
const [report, setReport] = useState<ReportData | null>(null)
|
|
22
|
+
const [loading, setLoading] = useState(true)
|
|
23
|
+
const [error, setError] = useState<string | null>(null)
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
async function load() {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(`/api/reports/${id}`)
|
|
29
|
+
if (!res.ok) throw new Error('Report not found')
|
|
30
|
+
setReport(await res.json())
|
|
31
|
+
} catch (e) {
|
|
32
|
+
setError((e as Error).message)
|
|
33
|
+
} finally {
|
|
34
|
+
setLoading(false)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
load()
|
|
38
|
+
}, [id])
|
|
39
|
+
|
|
40
|
+
if (loading) {
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex items-center gap-2 py-8 text-muted-foreground">
|
|
43
|
+
<Loader2 className="h-4 w-4 animate-spin" /> Loading report...
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (error || !report) {
|
|
49
|
+
return (
|
|
50
|
+
<div className="space-y-4">
|
|
51
|
+
<Link href="/reports" className="flex items-center gap-1 text-sm text-primary hover:underline">
|
|
52
|
+
<ArrowLeft className="h-4 w-4" /> Back to reports
|
|
53
|
+
</Link>
|
|
54
|
+
<div className="text-destructive">{error || 'Report not found'}</div>
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="space-y-4">
|
|
61
|
+
<div className="flex items-center justify-between">
|
|
62
|
+
<Link href="/reports" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
|
|
63
|
+
<ArrowLeft className="h-4 w-4" /> Back to reports
|
|
64
|
+
</Link>
|
|
65
|
+
<span className="text-xs text-muted-foreground">
|
|
66
|
+
Generated {new Date(report.generatedAt).toLocaleString()} · {report.sessionCount} sessions
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
<ReportView content={report.contentJson} />
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { Card, CardContent } from '@/components/ui/card'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Badge } from '@/components/ui/badge'
|
|
8
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
|
9
|
+
import { FileText, Plus, Loader2, ChevronRight } from 'lucide-react'
|
|
10
|
+
import { toast } from 'sonner'
|
|
11
|
+
import { useData } from '@/components/data-provider'
|
|
12
|
+
|
|
13
|
+
interface ReportSummary {
|
|
14
|
+
id: string
|
|
15
|
+
title: string
|
|
16
|
+
generatedAt: string
|
|
17
|
+
sessionCount: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function ReportsPage() {
|
|
21
|
+
const { data } = useData()
|
|
22
|
+
const [reports, setReports] = useState<ReportSummary[]>([])
|
|
23
|
+
const [loading, setLoading] = useState(true)
|
|
24
|
+
const [generating, setGenerating] = useState(false)
|
|
25
|
+
|
|
26
|
+
async function fetchReports() {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch('/api/reports')
|
|
29
|
+
setReports(await res.json())
|
|
30
|
+
} catch {
|
|
31
|
+
// ignore
|
|
32
|
+
} finally {
|
|
33
|
+
setLoading(false)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
useEffect(() => { fetchReports() }, [])
|
|
38
|
+
|
|
39
|
+
const currentSessionCount = data?.overview.totalSessions || 0
|
|
40
|
+
const lastReportSessionCount = reports[0]?.sessionCount || 0
|
|
41
|
+
const hasNewData = reports.length === 0 || currentSessionCount !== lastReportSessionCount
|
|
42
|
+
|
|
43
|
+
async function handleGenerate() {
|
|
44
|
+
setGenerating(true)
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch('/api/reports', { method: 'POST' })
|
|
47
|
+
const report = await res.json()
|
|
48
|
+
if (res.status === 409) {
|
|
49
|
+
toast.info('No new data', { description: report.error })
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
if (!res.ok) throw new Error(report.error || 'Failed to generate report')
|
|
53
|
+
toast.success('Report generated', { description: report.title })
|
|
54
|
+
await fetchReports()
|
|
55
|
+
} catch (e) {
|
|
56
|
+
toast.error('Failed to generate report', { description: (e as Error).message })
|
|
57
|
+
} finally {
|
|
58
|
+
setGenerating(false)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const buttonDisabled = generating || !hasNewData
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="space-y-6 max-w-3xl">
|
|
66
|
+
<div className="flex items-center justify-between">
|
|
67
|
+
<div>
|
|
68
|
+
<h2 className="text-lg font-semibold">Insights Reports</h2>
|
|
69
|
+
<p className="text-sm text-muted-foreground">
|
|
70
|
+
Generate point-in-time snapshots of your coding agent fitness
|
|
71
|
+
</p>
|
|
72
|
+
</div>
|
|
73
|
+
<Tooltip>
|
|
74
|
+
<TooltipTrigger
|
|
75
|
+
render={<span />}
|
|
76
|
+
>
|
|
77
|
+
<Button onClick={handleGenerate} disabled={buttonDisabled} className="gap-2">
|
|
78
|
+
{generating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
|
|
79
|
+
{generating ? 'Generating...' : 'Generate Report'}
|
|
80
|
+
</Button>
|
|
81
|
+
</TooltipTrigger>
|
|
82
|
+
{!hasNewData && (
|
|
83
|
+
<TooltipContent>
|
|
84
|
+
No new sessions since last report. Sync new data first.
|
|
85
|
+
</TooltipContent>
|
|
86
|
+
)}
|
|
87
|
+
</Tooltip>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{loading ? (
|
|
91
|
+
<div className="flex items-center gap-2 py-8 text-muted-foreground">
|
|
92
|
+
<Loader2 className="h-4 w-4 animate-spin" /> Loading reports...
|
|
93
|
+
</div>
|
|
94
|
+
) : reports.length === 0 ? (
|
|
95
|
+
<Card>
|
|
96
|
+
<CardContent className="py-12 text-center">
|
|
97
|
+
<FileText className="h-10 w-10 text-muted-foreground/30 mx-auto mb-3" />
|
|
98
|
+
<p className="text-sm text-muted-foreground">
|
|
99
|
+
No reports yet. Click "Generate Report" to create your first fitness snapshot.
|
|
100
|
+
</p>
|
|
101
|
+
</CardContent>
|
|
102
|
+
</Card>
|
|
103
|
+
) : (
|
|
104
|
+
<div className="space-y-2">
|
|
105
|
+
{reports.map(report => (
|
|
106
|
+
<Link key={report.id} href={`/reports/${report.id}`}>
|
|
107
|
+
<Card className="hover:bg-muted/50 transition-colors cursor-pointer">
|
|
108
|
+
<CardContent className="flex items-center justify-between py-4">
|
|
109
|
+
<div className="flex items-center gap-3">
|
|
110
|
+
<FileText className="h-5 w-5 text-muted-foreground" />
|
|
111
|
+
<div>
|
|
112
|
+
<div className="text-sm font-medium">{report.title}</div>
|
|
113
|
+
<div className="text-xs text-muted-foreground">
|
|
114
|
+
{new Date(report.generatedAt).toLocaleString()}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="flex items-center gap-3">
|
|
119
|
+
<Badge variant="secondary" className="text-xs">
|
|
120
|
+
{report.sessionCount} sessions
|
|
121
|
+
</Badge>
|
|
122
|
+
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
|
123
|
+
</div>
|
|
124
|
+
</CardContent>
|
|
125
|
+
</Card>
|
|
126
|
+
</Link>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useParams } from 'next/navigation'
|
|
5
|
+
import Link from 'next/link'
|
|
6
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
7
|
+
import { ArrowLeft, Clock, Coins, MessageSquare, Wrench, CheckCircle, XCircle } from 'lucide-react'
|
|
8
|
+
import { SessionWorkflow } from '@/components/session-workflow'
|
|
9
|
+
import { SessionChatLog } from '@/components/session-chatlog'
|
|
10
|
+
import type { SessionDetail } from '@/lib/session-detail'
|
|
11
|
+
|
|
12
|
+
export default function SessionDetailPage() {
|
|
13
|
+
const params = useParams()
|
|
14
|
+
const sessionId = params.id as string
|
|
15
|
+
const [detail, setDetail] = useState<SessionDetail | null>(null)
|
|
16
|
+
const [loading, setLoading] = useState(true)
|
|
17
|
+
const [error, setError] = useState<string | null>(null)
|
|
18
|
+
const [activeTab, setActiveTab] = useState<'workflow' | 'dialog'>('dialog')
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
async function load() {
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(`/api/session?id=${sessionId}`)
|
|
24
|
+
if (!res.ok) throw new Error('Session not found')
|
|
25
|
+
const data = await res.json()
|
|
26
|
+
setDetail(data)
|
|
27
|
+
} catch (e) {
|
|
28
|
+
setError((e as Error).message)
|
|
29
|
+
} finally {
|
|
30
|
+
setLoading(false)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
load()
|
|
34
|
+
}, [sessionId])
|
|
35
|
+
|
|
36
|
+
if (loading) {
|
|
37
|
+
return <div className="text-muted-foreground">Loading session...</div>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (error || !detail) {
|
|
41
|
+
return (
|
|
42
|
+
<div className="space-y-4">
|
|
43
|
+
<Link href="/sessions" className="flex items-center gap-1 text-sm text-primary hover:underline">
|
|
44
|
+
<ArrowLeft className="h-4 w-4" /> Back to sessions
|
|
45
|
+
</Link>
|
|
46
|
+
<div className="text-destructive">{error || 'Session not found'}</div>
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const s = detail.stats
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="space-y-4">
|
|
55
|
+
<Link href="/sessions" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
|
|
56
|
+
<ArrowLeft className="h-4 w-4" /> Back to sessions
|
|
57
|
+
</Link>
|
|
58
|
+
|
|
59
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-6">
|
|
60
|
+
<Card>
|
|
61
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
62
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">Duration</CardTitle>
|
|
63
|
+
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
64
|
+
</CardHeader>
|
|
65
|
+
<CardContent>
|
|
66
|
+
<div className="text-2xl font-bold tracking-tight">{s.duration}</div>
|
|
67
|
+
</CardContent>
|
|
68
|
+
</Card>
|
|
69
|
+
<Card>
|
|
70
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
71
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">Tokens</CardTitle>
|
|
72
|
+
<Coins className="h-4 w-4 text-muted-foreground" />
|
|
73
|
+
</CardHeader>
|
|
74
|
+
<CardContent>
|
|
75
|
+
<div className="text-2xl font-bold tracking-tight">{s.tokens.toLocaleString()}</div>
|
|
76
|
+
</CardContent>
|
|
77
|
+
</Card>
|
|
78
|
+
<Card>
|
|
79
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
80
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">Messages</CardTitle>
|
|
81
|
+
<MessageSquare className="h-4 w-4 text-muted-foreground" />
|
|
82
|
+
</CardHeader>
|
|
83
|
+
<CardContent>
|
|
84
|
+
<div className="text-2xl font-bold tracking-tight">{s.totalMessages}</div>
|
|
85
|
+
<p className="text-xs text-muted-foreground">{s.userTurns} user / {s.assistantTurns} assistant</p>
|
|
86
|
+
</CardContent>
|
|
87
|
+
</Card>
|
|
88
|
+
<Card>
|
|
89
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
90
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">Tool Calls</CardTitle>
|
|
91
|
+
<Wrench className="h-4 w-4 text-muted-foreground" />
|
|
92
|
+
</CardHeader>
|
|
93
|
+
<CardContent>
|
|
94
|
+
<div className="text-2xl font-bold tracking-tight">{s.toolCalls}</div>
|
|
95
|
+
</CardContent>
|
|
96
|
+
</Card>
|
|
97
|
+
<Card>
|
|
98
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
99
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">Success</CardTitle>
|
|
100
|
+
<CheckCircle className="h-4 w-4 text-muted-foreground" />
|
|
101
|
+
</CardHeader>
|
|
102
|
+
<CardContent>
|
|
103
|
+
<div className="text-2xl font-bold tracking-tight">{s.successCount}</div>
|
|
104
|
+
</CardContent>
|
|
105
|
+
</Card>
|
|
106
|
+
<Card>
|
|
107
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
108
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">Failures</CardTitle>
|
|
109
|
+
<XCircle className="h-4 w-4 text-muted-foreground" />
|
|
110
|
+
</CardHeader>
|
|
111
|
+
<CardContent>
|
|
112
|
+
<div className={`text-2xl font-bold tracking-tight ${s.failureCount > 0 ? 'text-destructive' : ''}`}>{s.failureCount}</div>
|
|
113
|
+
</CardContent>
|
|
114
|
+
</Card>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="flex gap-1.5 border-b pb-1">
|
|
118
|
+
<button
|
|
119
|
+
onClick={() => setActiveTab('dialog')}
|
|
120
|
+
className={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
|
|
121
|
+
activeTab === 'dialog'
|
|
122
|
+
? 'bg-primary text-primary-foreground'
|
|
123
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
124
|
+
}`}
|
|
125
|
+
>
|
|
126
|
+
Dialog
|
|
127
|
+
</button>
|
|
128
|
+
<button
|
|
129
|
+
onClick={() => setActiveTab('workflow')}
|
|
130
|
+
className={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
|
|
131
|
+
activeTab === 'workflow'
|
|
132
|
+
? 'bg-primary text-primary-foreground'
|
|
133
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
134
|
+
}`}
|
|
135
|
+
>
|
|
136
|
+
Workflow
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{activeTab === 'dialog' && (
|
|
141
|
+
<Card>
|
|
142
|
+
<CardHeader>
|
|
143
|
+
<CardTitle>Chat Log</CardTitle>
|
|
144
|
+
<CardDescription>{detail.chatLog.length} records</CardDescription>
|
|
145
|
+
</CardHeader>
|
|
146
|
+
<CardContent>
|
|
147
|
+
<SessionChatLog messages={detail.chatLog} />
|
|
148
|
+
</CardContent>
|
|
149
|
+
</Card>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
{activeTab === 'workflow' && (
|
|
153
|
+
<Card>
|
|
154
|
+
<CardHeader>
|
|
155
|
+
<CardTitle>Workflow</CardTitle>
|
|
156
|
+
<CardDescription>
|
|
157
|
+
Tool calls, reasoning, and execution flow — {detail.workflowNodes.length} nodes
|
|
158
|
+
</CardDescription>
|
|
159
|
+
</CardHeader>
|
|
160
|
+
<CardContent>
|
|
161
|
+
<SessionWorkflow nodes={detail.workflowNodes} edges={detail.workflowEdges} />
|
|
162
|
+
</CardContent>
|
|
163
|
+
</Card>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
)
|
|
167
|
+
}
|