multica/apps/web/features/runtimes/components/charts/model-distribution-chart.tsx
Jiayuan ceba8556f5 refactor(runtimes): split monolithic page, add zustand store, time range selector, and delete support
- Split 1189-line runtimes-page.tsx into focused sub-components (list, detail, ping, usage, 5 chart files, shared UI, utils)
- Add useRuntimeStore zustand store for shared runtime state across pages (agents page now uses it too)
- Add 7d/30d/90d time range selector to usage charts
- Add full-stack runtime delete: SQL query, Go handler, API route, frontend with confirmation dialog
- Remove unused daemon:heartbeat WS listener (server never broadcasts it)
2026-03-29 17:02:25 +08:00

99 lines
3.3 KiB
TypeScript

import { PieChart, Pie, Cell, Label } from "recharts";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
import type { ModelDistribution } from "../../utils";
import { formatTokens } from "../../utils";
const MODEL_COLORS = [
"hsl(var(--chart-1))",
"hsl(var(--chart-2))",
"hsl(var(--chart-3))",
"hsl(var(--chart-4))",
"hsl(var(--chart-5))",
];
export function ModelDistributionChart({ data }: { data: ModelDistribution[] }) {
if (data.length === 0) return null;
const totalTokens = data.reduce((sum, d) => sum + d.tokens, 0);
const chartConfig = Object.fromEntries(
data.map((d, i) => [
d.model,
{ label: d.model, color: MODEL_COLORS[i % MODEL_COLORS.length] },
]),
) satisfies ChartConfig;
return (
<div className="rounded-lg border p-4">
<h4 className="text-xs font-medium text-muted-foreground mb-3">Token Usage by Model</h4>
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[200px]">
<PieChart>
<ChartTooltip
content={
<ChartTooltipContent
formatter={(value) =>
typeof value === "number" ? formatTokens(value) : String(value)
}
nameKey="model"
/>
}
/>
<Pie
data={data}
dataKey="tokens"
nameKey="model"
innerRadius={50}
outerRadius={80}
paddingAngle={2}
>
{data.map((entry, i) => (
<Cell
key={entry.model}
fill={MODEL_COLORS[i % MODEL_COLORS.length]}
/>
))}
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text x={viewBox.cx} y={viewBox.cy} textAnchor="middle" dominantBaseline="middle">
<tspan x={viewBox.cx} y={viewBox.cy} className="fill-foreground text-lg font-bold">
{formatTokens(totalTokens)}
</tspan>
<tspan x={viewBox.cx} y={(viewBox.cy ?? 0) + 18} className="fill-muted-foreground text-xs">
tokens
</tspan>
</text>
);
}
return null;
}}
/>
</Pie>
</PieChart>
</ChartContainer>
{/* Model legend with cost */}
<div className="mt-3 space-y-1.5">
{data.map((d, i) => (
<div key={d.model} className="flex items-center justify-between text-xs">
<div className="flex items-center gap-2 min-w-0">
<div
className="h-2 w-2 shrink-0 rounded-full"
style={{ backgroundColor: MODEL_COLORS[i % MODEL_COLORS.length] }}
/>
<span className="truncate font-mono">{d.model}</span>
</div>
<div className="flex items-center gap-3 shrink-0 text-muted-foreground tabular-nums">
<span>{formatTokens(d.tokens)}</span>
{d.cost > 0 && <span>${d.cost.toFixed(2)}</span>}
</div>
</div>
))}
</div>
</div>
);
}