'use client' import { useMemo, useState, useEffect } from 'react' import { Line, LineChart, Bar, BarChart, Area, AreaChart, CartesianGrid, XAxis, YAxis, Legend, Cell, Tooltip as RechartsTooltip, } from 'recharts' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig, } from '@/components/ui/chart' import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip' import type { DailyUsage, SessionSummary } from '@/lib/parse-logs' import { formatCost } from '@/lib/format' // ─── 1. Daily Cost ────────────────────────────────────────────────── const costConfig = { cost: { label: 'Cost', color: 'var(--chart-5)' }, } satisfies ChartConfig export function DailyCostChart({ daily }: { daily: DailyUsage[] }) { const data = daily.map((d) => ({ date: d.date.slice(5), cost: Number(d.costUSD.toFixed(2)), })) return ( Daily Cost USD spent per day `$${v}`} /> formatCost(Number(value))} />} /> ) } // ─── 2. Tool Mix Over Time ────────────────────────────────────────── const TOP_TOOLS = ['Read', 'Edit', 'Write', 'Bash', 'Grep', 'Agent'] as const const toolMixConfig = { Read: { label: 'Read', color: 'var(--chart-1)' }, Edit: { label: 'Edit', color: 'var(--chart-2)' }, Write: { label: 'Write', color: 'var(--chart-3)' }, Bash: { label: 'Bash', color: 'var(--chart-4)' }, Grep: { label: 'Grep', color: 'var(--chart-6)' }, Agent: { label: 'Agent', color: 'var(--chart-7)' }, } satisfies ChartConfig export function ToolMixChart({ daily }: { daily: DailyUsage[] }) { const data = daily.map((d) => { const row: Record = { date: d.date.slice(5) } for (const tool of TOP_TOOLS) { row[tool] = d.toolCallsDetail[tool] || 0 } return row }) return ( Tool Mix Over Time Daily tool calls — explore (Read/Grep) vs build (Edit/Write) } /> {TOP_TOOLS.map((tool) => ( ))} ) } // ─── 3. Interruption Rate Trend ───────────────────────────────────── const interruptionConfig = { rate: { label: 'Interruption Rate', color: 'var(--chart-5)' }, } satisfies ChartConfig export function InterruptionRateChart({ daily }: { daily: DailyUsage[] }) { const data = daily.map((d) => ({ date: d.date.slice(5), rate: d.messages > 0 ? Number(((d.interruptions / d.messages) * 100).toFixed(1)) : 0, interruptions: d.interruptions, })) return ( Interruption Rate Daily interruptions as % of messages — lower is better `${v}%`} /> name === 'rate' ? `${value}%` : String(value) } /> } /> ) } // ─── 4. Top Skills Used ───────────────────────────────────────────── const commandConfig = { count: { label: 'Uses', color: 'var(--chart-1)' }, } satisfies ChartConfig interface CommandBarEntry { name: string count: number fill: string historyCount: number sessionCount: number } export function TopCommandsChart() { const [data, setData] = useState([]) useEffect(() => { fetch('/api/commands') .then(r => r.json()) .then(analysis => { const dbSkills: Record = analysis.dbSkillCounts || {} const bi = analysis.commands .filter((c: { used: boolean; count: number }) => c.used && c.count > 0) .map((c: { command: string; count: number }) => { const sk = dbSkills[c.command] || 0 return { name: c.command, count: c.count, fill: 'var(--chart-1)', historyCount: c.count - sk, sessionCount: sk } }) .sort((a: { count: number }, b: { count: number }) => b.count - a.count) .slice(0, 5) const cu = analysis.customCommands .sort((a: { count: number }, b: { count: number }) => b.count - a.count) .slice(0, 5) .map((c: { command: string; count: number; historyCount?: number; sessionCount?: number }) => ({ name: c.command, count: c.count, fill: 'var(--chart-4)', historyCount: c.historyCount || 0, sessionCount: c.sessionCount || 0, })) const combined = [...bi, ...cu].sort((a, b) => b.count - a.count) setData(combined) }) .catch(() => {}) }, []) if (data.length === 0) { return ( Top Commands & Skills Most used built-in commands and custom skills
No commands used yet
) } return ( Top Commands & Skills Built-in Custom skill { if (!active || !payload?.length) return null const d = payload[0].payload as CommandBarEntry return (
{d.name}
Slash command {d.historyCount ?? 0}
Skill invocation {d.sessionCount ?? 0}
Total {d.count ?? 0}
) }} /> {data.map((entry, i) => ( ))}
) } // ─── 5. User vs Assistant Messages ────────────────────────────────── const msgRatioConfig = { userMessages: { label: 'User', color: 'var(--chart-1)' }, assistantMessages: { label: 'Assistant', color: 'var(--chart-3)' }, } satisfies ChartConfig export function UserVsAssistantChart({ daily }: { daily: DailyUsage[] }) { const data = daily.map((d) => ({ date: d.date.slice(5), userMessages: d.userMessages, assistantMessages: d.assistantMessages, })) return ( User vs Assistant Messages Low user ratio = Claude doing more autonomous turns } /> ) } // ─── 6. Token Usage Heatmap ───────────────────────────────────────── const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] function formatTokensShort(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K` return String(n) } function formatDateLabel(dateStr: string): string { const d = new Date(dateStr + 'T00:00:00') const month = MONTH_NAMES[d.getMonth()] const day = d.getDate() const suffix = day === 1 || day === 21 || day === 31 ? 'st' : day === 2 || day === 22 ? 'nd' : day === 3 || day === 23 ? 'rd' : 'th' return `${month} ${day}${suffix}` } export function TokenUsageHeatmap({ daily }: { daily: DailyUsage[] }) { const { tokensByDate, rateLimitByDate, maxTokens, totalTokens } = useMemo(() => { const tMap = new Map() const rMap = new Map() let max = 0 let total = 0 for (const d of daily) { tMap.set(d.date, d.totalTokens) total += d.totalTokens if (d.totalTokens > max) max = d.totalTokens if (d.rateLimitErrors > 0) rMap.set(d.date, d.rateLimitErrors) } return { tokensByDate: tMap, rateLimitByDate: rMap, maxTokens: max, totalTokens: total } }, [daily]) const { weeks, monthLabels } = useMemo(() => { if (daily.length === 0) return { weeks: [], monthLabels: [] } const lastDate = new Date(daily[daily.length - 1].date) const result: { date: string; tokens: number; rateLimits: number; dayOfWeek: number }[][] = [] const startDate = new Date(lastDate) startDate.setDate(startDate.getDate() - 83) // Align to Sunday startDate.setDate(startDate.getDate() - startDate.getDay()) let currentWeek: { date: string; tokens: number; rateLimits: number; dayOfWeek: number }[] = [] const d = new Date(startDate) while (d <= lastDate) { const dateStr = d.toLocaleDateString('en-CA') currentWeek.push({ date: dateStr, tokens: tokensByDate.get(dateStr) || 0, rateLimits: rateLimitByDate.get(dateStr) || 0, dayOfWeek: d.getDay(), }) if (d.getDay() === 6) { result.push(currentWeek) currentWeek = [] } d.setDate(d.getDate() + 1) } if (currentWeek.length > 0) result.push(currentWeek) // Compute month labels with column positions const labels: { label: string; col: number }[] = [] let lastMonth = -1 for (let wi = 0; wi < result.length; wi++) { const firstDay = result[wi][0] if (!firstDay) continue const month = new Date(firstDay.date).getMonth() if (month !== lastMonth) { labels.push({ label: MONTH_NAMES[month], col: wi }) lastMonth = month } } return { weeks: result, monthLabels: labels } }, [daily, tokensByDate, rateLimitByDate]) const totalRateLimitDays = rateLimitByDate.size function getIntensity(tokens: number): string { if (tokens === 0) return 'bg-muted' const ratio = tokens / maxTokens if (ratio <= 0.25) return 'bg-blue-200 dark:bg-blue-900/50' if (ratio <= 0.5) return 'bg-blue-300 dark:bg-blue-700/60' if (ratio <= 0.75) return 'bg-blue-400 dark:bg-blue-600/70' return 'bg-blue-500 dark:bg-blue-500' } return ( {formatTokensShort(totalTokens)} tokens in the last {daily.length} days Daily token volume {totalRateLimitDays > 0 && ( · = rate limited ({totalRateLimitDays}d) )}
{/* Day labels column — rendered as a matching grid so heights stay in sync */}
Sun
Mon
Tue
Wed
Thu
Fri
Sat
{/* Week columns */} {weeks.map((week, wi) => { const monthLabel = monthLabels.find((m) => m.col === wi) return (
{monthLabel?.label ?? ''}
{Array.from({ length: 7 }, (_, dayIdx) => { const cell = week.find((c) => c.dayOfWeek === dayIdx) if (!cell) return
const hasRateLimit = cell.rateLimits > 0 const label = cell.tokens > 0 ? `${formatTokensShort(cell.tokens)} tokens on ${formatDateLabel(cell.date)}.${hasRateLimit ? ` Rate limited ${cell.rateLimits}×.` : ''}` : `No tokens on ${formatDateLabel(cell.date)}.` return ( } className={`aspect-square w-full rounded-sm ${getIntensity(cell.tokens)} ${hasRateLimit ? 'ring-2 ring-rose-400 ring-inset' : ''}`} /> {label} ) })}
) })}
{/* Legend */}
Less
More
) } // ─── Export ────────────────────────────────────────────────────────── export function DailyChart({ daily, sessions }: { daily: DailyUsage[]; sessions: SessionSummary[] }) { return ( <> ) }