cost-breakdown.tsx
2,944 bytes
| 1 | 'use client'; |
|---|---|
| 2 | |
| 3 | import { Bar, BarChart, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; |
| 4 | import type { DebateView } from '@/lib/debate-view'; |
| 5 | import { displayNameForModel } from '@/core/types'; |
| 6 | import { participantColor } from '@/lib/model-visuals'; |
| 7 | import { formatUsd } from '@/lib/utils'; |
| 8 | |
| 9 | /** Post-debate cost breakdown per model (in micro-dollars for readability). */ |
| 10 | export function CostBreakdown({ view }: { view: DebateView }) { |
| 11 | const modelIndex = new Map(view.participants.map((p, i) => [p.model, i])); |
| 12 | const data = Object.entries(view.totals.costByModel) |
| 13 | .map(([model, cost]) => ({ |
| 14 | model, |
| 15 | name: displayNameForModel(model), |
| 16 | cost, |
| 17 | index: modelIndex.get(model) ?? 5, |
| 18 | })) |
| 19 | .sort((a, b) => b.cost - a.cost); |
| 20 | |
| 21 | if (data.length === 0) { |
| 22 | return <p className="text-sm text-muted-foreground">No cost recorded yet.</p>; |
| 23 | } |
| 24 | |
| 25 | return ( |
| 26 | <div className="space-y-4"> |
| 27 | <div className="grid grid-cols-2 gap-3 sm:grid-cols-4"> |
| 28 | <Stat label="Total cost" value={formatUsd(view.totals.costUsd)} /> |
| 29 | <Stat label="Prompt tokens" value={view.totals.promptTokens.toLocaleString()} /> |
| 30 | <Stat label="Completion tokens" value={view.totals.completionTokens.toLocaleString()} /> |
| 31 | <Stat label="Models" value={String(data.length)} /> |
| 32 | </div> |
| 33 | |
| 34 | <div className="h-56 w-full"> |
| 35 | <ResponsiveContainer width="100%" height="100%"> |
| 36 | <BarChart data={data} layout="vertical" margin={{ left: 8, right: 16 }}> |
| 37 | <XAxis |
| 38 | type="number" |
| 39 | tickFormatter={(v: number) => formatUsd(v)} |
| 40 | tick={{ fontSize: 11, fill: 'hsl(var(--muted-foreground))' }} |
| 41 | stroke="hsl(var(--border))" |
| 42 | /> |
| 43 | <YAxis |
| 44 | type="category" |
| 45 | dataKey="name" |
| 46 | width={110} |
| 47 | tick={{ fontSize: 11, fill: 'hsl(var(--muted-foreground))' }} |
| 48 | stroke="hsl(var(--border))" |
| 49 | /> |
| 50 | <Tooltip |
| 51 | cursor={{ fill: 'hsl(var(--muted) / 0.4)' }} |
| 52 | contentStyle={{ |
| 53 | background: 'hsl(var(--popover))', |
| 54 | border: '1px solid hsl(var(--border))', |
| 55 | borderRadius: 8, |
| 56 | fontSize: 12, |
| 57 | }} |
| 58 | formatter={(v: number) => [formatUsd(v), 'Cost']} |
| 59 | /> |
| 60 | <Bar dataKey="cost" radius={[0, 4, 4, 0]}> |
| 61 | {data.map((d) => ( |
| 62 | <Cell key={d.model} fill={participantColor(d.index)} /> |
| 63 | ))} |
| 64 | </Bar> |
| 65 | </BarChart> |
| 66 | </ResponsiveContainer> |
| 67 | </div> |
| 68 | </div> |
| 69 | ); |
| 70 | } |
| 71 | |
| 72 | function Stat({ label, value }: { label: string; value: string }) { |
| 73 | return ( |
| 74 | <div className="rounded-lg border bg-card p-3"> |
| 75 | <div className="text-[11px] uppercase tracking-wide text-muted-foreground">{label}</div> |
| 76 | <div className="mt-0.5 text-lg font-semibold">{value}</div> |
| 77 | </div> |
| 78 | ); |
| 79 | } |
| 80 | |