diff --git a/app/overview-client.tsx b/app/overview-client.tsx index b12f449..c1aab73 100644 --- a/app/overview-client.tsx +++ b/app/overview-client.tsx @@ -6,7 +6,7 @@ import Link from "next/link"; import { BarChart3, PieChart } from "lucide-react"; import { UsageOverTimeChart } from "@/components/overview/usage-over-time-chart"; import { ModelBreakdownDonut } from "@/components/overview/model-breakdown-donut"; -import { ProjectActivityDonut } from "@/components/overview/project-activity-donut"; +import { ProjectActivityChart } from "@/components/overview/project-activity-chart"; import { PeakHoursChart } from "@/components/overview/peak-hours-chart"; import { OverviewConversationTable } from "@/components/overview/conversation-table"; import { formatTokens, formatBytes } from "@/lib/decode"; @@ -283,7 +283,7 @@ export function OverviewClient() { icon={} title="Project activity distribution" > - + diff --git a/components/overview/model-breakdown-donut.tsx b/components/overview/model-breakdown-donut.tsx index d5f04ad..671928c 100644 --- a/components/overview/model-breakdown-donut.tsx +++ b/components/overview/model-breakdown-donut.tsx @@ -47,18 +47,17 @@ function CustomTooltip({ active, payload }: any) { } export function ModelBreakdownDonut({ modelUsage }: Props) { + // I/O tokens only — cache tokens are a different dimension and inflate Opus disproportionately const data = Object.entries(modelUsage) .map(([model, usage]) => ({ name: shortModelName(model), - value: - (usage.inputTokens ?? 0) + - (usage.outputTokens ?? 0) + - (usage.cacheReadInputTokens ?? 0) + - (usage.cacheCreationInputTokens ?? 0), + value: (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0), })) .filter((d) => d.value > 0) .sort((a, b) => b.value - a.value); + const total = data.reduce((s, d) => s + d.value, 0); + if (data.length === 0) { return (
@@ -90,11 +89,17 @@ export function ModelBreakdownDonut({ modelUsage }: Props) { iconType="circle" iconSize={8} wrapperStyle={{ fontSize: 12 }} - formatter={(value) => ( - - {value} - - )} + formatter={(value: string, _entry: unknown, index: number) => { + const pct = + total > 0 ? Math.round((data[index].value / total) * 100) : 0; + return ( + + {value} ({pct}%) + + ); + }} /> diff --git a/components/overview/project-activity-chart.tsx b/components/overview/project-activity-chart.tsx new file mode 100644 index 0000000..dc9408f --- /dev/null +++ b/components/overview/project-activity-chart.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + CartesianGrid, +} from "recharts"; +import type { ProjectSummary } from "@/types/claude"; +import { formatTokens } from "@/lib/decode"; + +interface Props { + projects: ProjectSummary[]; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function CustomTooltip({ active, payload, label }: any) { + if (!active || !payload?.length) return null; + return ( +
+

{label}

+

+ {formatTokens(payload[0].value)} tokens +

+ {payload[0].payload.sessions != null && ( +

+ {payload[0].payload.sessions} sessions +

+ )} +
+ ); +} + +export function ProjectActivityChart({ projects }: Props) { + const data = projects + .slice(0, 8) + .map((p) => ({ + name: + p.display_name.length > 20 + ? p.display_name.slice(0, 18) + "..." + : p.display_name, + fullName: p.display_name, + value: (p.input_tokens ?? 0) + (p.output_tokens ?? 0), + sessions: p.session_count ?? 0, + })) + .filter((d) => d.value > 0); + + if (data.length === 0) { + return ( +
+ no project data +
+ ); + } + + return ( +
+ + + + formatTokens(v)} + /> + + } /> + + + +
+ ); +}