"use client"; import * as React from "react"; import { ResponsiveContainer, AreaChart, Area, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, PieChart, Pie, Cell, LabelList, type TooltipContentProps } from "recharts"; import { ProviderIcon } from "@/components/brand/provider-icon"; import { PROVIDERS, providerName } from "@/lib/client/providers"; import { formatUsd, formatTokens, formatNumber, formatMs, cn } from "@/lib/utils"; import type { ProviderId } from "@/lib/client/types"; /* ------------------------------------------------------------------------------------------------ * Theme-bound chart primitives. Rules (dataviz): one hue per entity (provider color follows the * provider, never its rank), thin marks (≤ 24 px, 4 px rounded data-end), 2 px surface gaps between * stacked fills, hairline solid grid, text in text tokens (never the series color), tooltips on * every plot, no dual axes, no default recharts colors anywhere. * ---------------------------------------------------------------------------------------------- */ const GRID = "var(--border)"; const TEXT = "var(--fg-muted)"; const SURFACE = "var(--bg-elevated)"; const AXIS_TICK = { fill: TEXT, fontSize: 11, fontFamily: "var(--font-mono)" } as const; const BAR_MAX = 24; export function providerColor(p: string): string { return p in PROVIDERS ? PROVIDERS[p as ProviderId].colorVar : "var(--p-custom)"; } export interface SeriesPoint { bucket: string; requests: number; failures: number; inputTokens: number; outputTokens: number; costUsd: number; } /** `YYYY-MM-DD` → "Sep 11" · `YYYY-MM-DDTHH:00` → "3 PM" (parsed as wall-clock, never shifted by the browser zone). */ export function bucketLabel(key: string, hourly: boolean, opts: { long?: boolean } = {}): string { const m = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}))?$/.exec(key); if (!m) return key; const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), Number(m[4] ?? 0), Number(m[5] ?? 0)); if (hourly) return opts.long ? d.toLocaleString("en-US", { month: "short", day: "numeric", hour: "numeric" }) : d.toLocaleTimeString("en-US", { hour: "numeric" }); return d.toLocaleDateString("en-US", opts.long ? { weekday: "short", month: "short", day: "numeric", year: "numeric" } : { month: "short", day: "numeric" }); } interface TooltipRow { name: string; value: React.ReactNode; color?: string; } function ChartTooltipBox({ title, rows }: { title?: React.ReactNode; rows: TooltipRow[] }) { return (
{title ?

{title}

: null}
); } type RechartsTooltipProps = TooltipContentProps; function num(v: unknown): number { return typeof v === "number" ? v : Number(v ?? 0); } /* ---------- chart card frame ---------- */ export function ChartCard({ title, hint, children, className, legend, action }: { title: string; hint?: React.ReactNode; children: React.ReactNode; className?: string; legend?: React.ReactNode; action?: React.ReactNode }) { return (

{title}

{hint ?

{hint}

: null}
{legend ?? action}
{children}
); } export function Legend({ items }: { items: { label: string; color: string; icon?: React.ReactNode; opacity?: number }[] }) { return ( ); } export function ChartEmpty({ children = "Nothing recorded for this period." }: { children?: React.ReactNode }) { return

{children}

; } /* ---------- time series ---------- */ const H = 200; export function CostOverTime({ data, hourly }: { data: SeriesPoint[]; hourly: boolean }) { if (!data.some((d) => d.costUsd > 0)) return No priced requests in this period.; return ( bucketLabel(String(v), hourly)} tick={AXIS_TICK} axisLine={false} tickLine={false} minTickGap={28} interval="preserveStartEnd" /> (num(v) === 0 ? "$0" : formatUsd(num(v)))} width={64} /> { if (!active || !payload?.length) return null; const row = payload[0].payload as SeriesPoint; return ; }} /> ); } export function RequestsOverTime({ data, hourly }: { data: SeriesPoint[]; hourly: boolean }) { if (!data.some((d) => d.requests > 0)) return ; const hasFailures = data.some((d) => d.failures > 0); return ( bucketLabel(String(v), hourly)} tick={AXIS_TICK} axisLine={false} tickLine={false} minTickGap={28} interval="preserveStartEnd" /> { if (!active || !payload?.length) return null; const row = payload[0].payload as SeriesPoint; return ( ); }} /> {/* stacked: ok (accent) + failed (status danger, only when > 0); 2 px surface gap via stroke */} d.requests - d.failures} name="ok" stackId="r" fill="var(--accent)" stroke={SURFACE} strokeWidth={2} radius={hasFailures ? undefined : [4, 4, 0, 0]} isAnimationActive={false} maxBarSize={BAR_MAX} /> {hasFailures ? : null} ); } export function TokensOverTime({ data, hourly }: { data: SeriesPoint[]; hourly: boolean }) { if (!data.some((d) => d.inputTokens + d.outputTokens > 0)) return ; return ( bucketLabel(String(v), hourly)} tick={AXIS_TICK} axisLine={false} tickLine={false} minTickGap={28} interval="preserveStartEnd" /> formatTokens(num(v))} width={56} /> { if (!active || !payload?.length) return null; const row = payload[0].payload as SeriesPoint; return ( ); }} /> ); } /* ---------- by provider ---------- */ export interface ProviderSlice { provider: string; requests: number; failures: number; costUsd: number; inputTokens: number; outputTokens: number; } /** Part-to-whole donut (≤ 6 slices, tail folded into "Other") with a labelled list — identity comes from the list, not color alone. */ export function ProviderDonut({ data, metric }: { data: ProviderSlice[]; metric: "requests" | "costUsd" }) { const total = data.reduce((a, d) => a + num(d[metric]), 0); const sorted = [...data].filter((d) => num(d[metric]) > 0).sort((a, b) => num(b[metric]) - num(a[metric])); const shown = sorted.length > 6 ? [...sorted.slice(0, 5), { ...sorted[5], provider: "other", [metric]: sorted.slice(5).reduce((a, d) => a + num(d[metric]), 0) }] : sorted; if (!total) return {metric === "costUsd" ? "No priced requests in this period." : "Nothing recorded for this period."}; const color = (p: string) => (p === "other" ? "var(--fg-subtle)" : providerColor(p)); const name = (p: string) => (p === "other" ? "Other" : providerName(p)); return (
{shown.map((d) => ( ))} { if (!active || !payload?.length) return null; const d = payload[0].payload as ProviderSlice; const v = num(d[metric]); return ; }} />
{metric === "costUsd" ? "Est. cost" : "Requests"} {metric === "costUsd" ? formatUsd(total) : formatNumber(total)}
    {shown.map((d) => { const v = num(d[metric]); const pct = (v / total) * 100; return (
  • {d.provider === "other" ? : } {name(d.provider)} {metric === "costUsd" ? formatUsd(v) : `${pct.toFixed(0)}%`}
  • ); })}
); } /* ---------- by model ---------- */ export interface ModelRow { modelKey: string; provider: string; label: string; requests: number; costUsd: number; inputTokens: number; outputTokens: number; tokensPerSec: number | null; avgLatencyMs: number; avgTtftMs: number | null; failures: number; } /** Horizontal bars, one row per model, colored by provider (identity), values labelled at the bar end. */ export function ModelBars({ data, metric, format, max = 8, ascending = false, emptyText }: { data: ModelRow[]; metric: keyof ModelRow; format: (v: number) => string; max?: number; ascending?: boolean; emptyText?: string }) { const rows = [...data] .filter((d) => num(d[metric]) > 0) .sort((a, b) => (ascending ? num(a[metric]) - num(b[metric]) : num(b[metric]) - num(a[metric]))) .slice(0, max); if (rows.length === 0) return {emptyText ?? "Not enough data."}; const height = Math.max(120, rows.length * 32 + 12); return ( (v.length > 17 ? `${v.slice(0, 16)}…` : v)} /> { if (!active || !payload?.length) return null; const d = payload[0].payload as ModelRow; return ( {d.label} } rows={[ { name: "Value", value: format(num(d[metric])) }, { name: "Requests", value: formatNumber(d.requests) }, { name: "Est. cost", value: formatUsd(d.costUsd, { precise: true }) }, { name: "Tokens in / out", value: `${formatTokens(d.inputTokens)} / ${formatTokens(d.outputTokens)}` }, ]} /> ); }} /> {rows.map((d) => ( ))} format(num(v))} style={{ fill: TEXT, fontSize: 11, fontFamily: "var(--font-mono)" }} /> ); } /** Two small multiples (never a dual axis): time to first token (lower is better) and output speed (higher is better). */ export function LatencyByModel({ data, max = 6 }: { data: ModelRow[]; max?: number }) { return (

Time to first token · lower is better

formatMs(v)} max={max} ascending emptyText="No first-token timings yet." />

Output speed · higher is better

`${v.toFixed(0)} tok/s`} max={max} emptyText="No streaming speed measured yet." />
); } /** Tiny 12-point sparkline for KPI tiles (de-emphasised hue, no axes). */ export function Sparkline({ values, color = "var(--accent)", height = 28 }: { values: number[]; color?: string; height?: number }) { const pts = values.slice(-12); if (pts.length < 2 || !pts.some((v) => v > 0)) return null; const maxV = Math.max(...pts); const w = 72; const step = w / (pts.length - 1); const d = pts.map((v, i) => `${i === 0 ? "M" : "L"}${(i * step).toFixed(1)},${(height - 2 - (v / maxV) * (height - 4)).toFixed(1)}`).join(" "); return ( ); }