"use client";
import * as React from "react";
import { ArrowDownTrayIcon, ChartBarIcon, CheckCircleIcon, XCircleIcon } from "@heroicons-animated/react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { BarChart, Sparkline, StatTile } from "@/components/pandacoderz-ui/mini-chart";
import { UsageMeter, UsageMeterGroup } from "@/components/pandacoderz-ui/usage-meter";
type Range = "7d" | "30d";
const daily = {
"7d": { requests: [2100, 2400, 1900, 3100, 2800, 3600, 4100], spend: [41, 47, 38, 62, 55, 71, 84], errors: [0.8, 0.6, 1.1, 0.4, 0.5, 0.3, 0.4], labels: ["Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon"] },
"30d": { requests: Array.from({ length: 30 }, (_, i) => 1500 + Math.round(Math.sin(i / 3) * 500 + i * 60)), spend: Array.from({ length: 30 }, (_, i) => 30 + Math.round(Math.sin(i / 4) * 12 + i * 1.4)), errors: Array.from({ length: 30 }, (_, i) => 0.3 + Math.abs(Math.sin(i)) * 0.9), labels: Array.from({ length: 30 }, (_, i) => `${i + 1}`) },
};
const byModel = [
{ label: "Fable 5.1", value: 162 },
{ label: "Opus 5", value: 94 },
{ label: "Sonnet 5", value: 118 },
{ label: "Haiku 4.5", value: 24 },
];
const recent = [
{ id: "req_91ka", at: "14:02:11", model: "claude-sonnet-5", tokens: 2252, latency: 3.12, ok: true },
{ id: "req_91k9", at: "14:01:47", model: "claude-haiku-4-5", tokens: 760, latency: 0.88, ok: true },
{ id: "req_91k2", at: "13:58:03", model: "claude-opus-5", tokens: 3200, latency: 5.21, ok: false },
{ id: "req_91jx", at: "13:57:30", model: "claude-fable-5-1", tokens: 5410, latency: 6.4, ok: true },
{ id: "req_91jq", at: "13:55:12", model: "claude-sonnet-5", tokens: 1890, latency: 2.7, ok: true },
{ id: "req_91jm", at: "13:54:58", model: "claude-haiku-4-5", tokens: 410, latency: 0.6, ok: true },
{ id: "req_91jh", at: "13:52:41", model: "claude-sonnet-5", tokens: 2010, latency: 2.9, ok: true },
];
const sum = (xs: number[]) => xs.reduce((a, b) => a + b, 0);
const money = (n: number) => `$${n.toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
export type UsageDashboardProps = { className?: string };
export default function UsageDashboard({ className }: UsageDashboardProps) {
const [range, setRange] = React.useState<Range>("7d");
const d = daily[range];
const requests = sum(d.requests);
const spend = sum(d.spend);
const errorRate = sum(d.errors) / d.errors.length;
return (
<div data-slot="usage-dashboard" className={cn("flex h-full min-h-0 w-full flex-col overflow-hidden rounded-3xl border bg-background shadow-xs", className)}>
<header className="flex h-12 shrink-0 items-center justify-between gap-3 border-b px-4">
<div className="flex items-center gap-2 text-sm font-medium">
<span className="flex size-6 items-center justify-center rounded-md bg-brand text-primary-foreground"><ChartBarIcon size={14} className="flex" /></span>
Usage
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 rounded-lg bg-muted p-1 text-xs">
{(["7d", "30d"] as Range[]).map((r) => (
<button key={r} type="button" onClick={() => setRange(r)} aria-pressed={range === r} className="rounded-md px-2.5 py-1 font-medium text-muted-foreground transition-colors aria-pressed:bg-background aria-pressed:text-foreground aria-pressed:shadow-xs">{r === "7d" ? "7 days" : "30 days"}</button>
))}
</div>
<Button size="sm" variant="outline" className="rounded-full"><ArrowDownTrayIcon size={14} className="flex" /> Export</Button>
</div>
</header>
<div className="min-h-0 flex-1 overflow-y-auto bg-surface p-4">
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<StatTile label="Requests" value={requests.toLocaleString()} delta="+12.4%" deltaTone="up" chart={<Sparkline data={d.requests} />} />
<StatTile label="Spend" value={money(spend)} delta="+8.1%" deltaTone="up" chart={<Sparkline data={d.spend} className="text-chart-2" />} />
<StatTile label="Avg tokens / request" value="2,140" delta="−3.2%" deltaTone="down" chart={<Sparkline data={d.requests.map((r, i) => r / (i + 5))} className="text-chart-3" />} />
<StatTile label="Error rate" value={`${errorRate.toFixed(2)}%`} delta="−0.3 pts" deltaTone="up" chart={<Sparkline data={d.errors} className="text-chart-5" />} />
</div>
<div className="mt-3 grid gap-3 lg:grid-cols-[2fr_1fr]">
<section className="rounded-2xl border bg-card p-4">
<div className="mb-3 flex items-center justify-between"><h3 className="text-sm font-medium">Spend by day</h3><span className="text-xs text-muted-foreground">USD</span></div>
<BarChart data={d.spend.map((v, i) => ({ label: d.labels[i], value: v }))} height={140} format={money} showValues className={range === "30d" ? "[&_span]:text-[8px]" : ""} />
</section>
<section className="flex flex-col gap-4 rounded-2xl border bg-card p-4">
<div><h3 className="text-sm font-medium">Rate limits</h3><p className="text-xs text-muted-foreground">Current minute</p></div>
<UsageMeterGroup>
<UsageMeter label="Requests / min" value={860} max={1000} showPercent size="sm" />
<UsageMeter label="Input tokens / min" value={312_000} max={400_000} showPercent size="sm" />
<UsageMeter label="Output tokens / min" value={38_000} max={80_000} showPercent size="sm" />
<UsageMeter label="Monthly budget" value={spend} max={range === "7d" ? 500 : 2000} format={(v, m) => `${money(v)} / ${money(m)}`} showPercent size="sm" />
</UsageMeterGroup>
</section>
</div>
<div className="mt-3 grid gap-3 lg:grid-cols-[1fr_2fr]">
<section className="rounded-2xl border bg-card p-4">
<h3 className="mb-3 text-sm font-medium">Spend by model</h3>
<BarChart data={byModel} height={110} format={money} showValues />
</section>
<section className="overflow-hidden rounded-2xl border bg-card">
<div className="flex items-center justify-between px-4 py-3"><h3 className="text-sm font-medium">Recent requests</h3><Button size="xs" variant="ghost">View all</Button></div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead className="border-y bg-surface text-left text-[11px] text-muted-foreground">
<tr><th className="px-4 py-2 font-medium">Request</th><th className="px-4 py-2 font-medium">Time</th><th className="px-4 py-2 font-medium">Model</th><th className="px-4 py-2 text-right font-medium">Tokens</th><th className="px-4 py-2 text-right font-medium">Latency</th><th className="px-4 py-2 text-right font-medium">Status</th></tr>
</thead>
<tbody>
{recent.map((r) => (
<tr key={r.id} className="border-b last:border-b-0 hover:bg-accent/40">
<td className="px-4 py-2 font-mono">{r.id}</td>
<td className="px-4 py-2 font-mono tabular-nums text-muted-foreground">{r.at}</td>
<td className="px-4 py-2">{r.model}</td>
<td className="px-4 py-2 text-right font-mono tabular-nums">{r.tokens.toLocaleString()}</td>
<td className="px-4 py-2 text-right font-mono tabular-nums">{r.latency.toFixed(2)}s</td>
<td className="px-4 py-2 text-right">{r.ok ? <CheckCircleIcon size={14} className="ml-auto flex text-emerald-600 dark:text-emerald-400" /> : <XCircleIcon size={14} className="ml-auto flex text-red-600 dark:text-red-400" />}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</div>
</div>
</div>
);
}What’s inside
- Stat Tile and Sparkline for requests, spend, tokens, and error rate.
- Bar Chart for spend by day and by model.
- Usage Meter for per-minute rate limits and the monthly budget.
- Table of recent requests.
Installation
npx shadcn@latest add https://ui.spencerwueste.com/r/usage-dashboard.jsonpnpm dlx shadcn@latest add https://ui.spencerwueste.com/r/usage-dashboard.jsonyarn dlx shadcn@latest add https://ui.spencerwueste.com/r/usage-dashboard.jsonbunx --bun shadcn@latest add https://ui.spencerwueste.com/r/usage-dashboard.jsonInstall the dependencies:
npm install @heroicons-animated/react motionAdd the shadcn primitives it builds on:
npx shadcn@latest add buttonCopy the source into your project:
"use client";
import * as React from "react";
import { ArrowDownTrayIcon, ChartBarIcon, CheckCircleIcon, XCircleIcon } from "@heroicons-animated/react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { BarChart, Sparkline, StatTile } from "@/components/pandacoderz-ui/mini-chart";
import { UsageMeter, UsageMeterGroup } from "@/components/pandacoderz-ui/usage-meter";
type Range = "7d" | "30d";
const daily = {
"7d": { requests: [2100, 2400, 1900, 3100, 2800, 3600, 4100], spend: [41, 47, 38, 62, 55, 71, 84], errors: [0.8, 0.6, 1.1, 0.4, 0.5, 0.3, 0.4], labels: ["Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon"] },
"30d": { requests: Array.from({ length: 30 }, (_, i) => 1500 + Math.round(Math.sin(i / 3) * 500 + i * 60)), spend: Array.from({ length: 30 }, (_, i) => 30 + Math.round(Math.sin(i / 4) * 12 + i * 1.4)), errors: Array.from({ length: 30 }, (_, i) => 0.3 + Math.abs(Math.sin(i)) * 0.9), labels: Array.from({ length: 30 }, (_, i) => `${i + 1}`) },
};
const byModel = [
{ label: "Fable 5.1", value: 162 },
{ label: "Opus 5", value: 94 },
{ label: "Sonnet 5", value: 118 },
{ label: "Haiku 4.5", value: 24 },
];
const recent = [
{ id: "req_91ka", at: "14:02:11", model: "claude-sonnet-5", tokens: 2252, latency: 3.12, ok: true },
{ id: "req_91k9", at: "14:01:47", model: "claude-haiku-4-5", tokens: 760, latency: 0.88, ok: true },
{ id: "req_91k2", at: "13:58:03", model: "claude-opus-5", tokens: 3200, latency: 5.21, ok: false },
{ id: "req_91jx", at: "13:57:30", model: "claude-fable-5-1", tokens: 5410, latency: 6.4, ok: true },
{ id: "req_91jq", at: "13:55:12", model: "claude-sonnet-5", tokens: 1890, latency: 2.7, ok: true },
{ id: "req_91jm", at: "13:54:58", model: "claude-haiku-4-5", tokens: 410, latency: 0.6, ok: true },
{ id: "req_91jh", at: "13:52:41", model: "claude-sonnet-5", tokens: 2010, latency: 2.9, ok: true },
];
const sum = (xs: number[]) => xs.reduce((a, b) => a + b, 0);
const money = (n: number) => `$${n.toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
export type UsageDashboardProps = { className?: string };
export default function UsageDashboard({ className }: UsageDashboardProps) {
const [range, setRange] = React.useState<Range>("7d");
const d = daily[range];
const requests = sum(d.requests);
const spend = sum(d.spend);
const errorRate = sum(d.errors) / d.errors.length;
return (
<div data-slot="usage-dashboard" className={cn("flex h-full min-h-0 w-full flex-col overflow-hidden rounded-3xl border bg-background shadow-xs", className)}>
<header className="flex h-12 shrink-0 items-center justify-between gap-3 border-b px-4">
<div className="flex items-center gap-2 text-sm font-medium">
<span className="flex size-6 items-center justify-center rounded-md bg-brand text-primary-foreground"><ChartBarIcon size={14} className="flex" /></span>
Usage
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 rounded-lg bg-muted p-1 text-xs">
{(["7d", "30d"] as Range[]).map((r) => (
<button key={r} type="button" onClick={() => setRange(r)} aria-pressed={range === r} className="rounded-md px-2.5 py-1 font-medium text-muted-foreground transition-colors aria-pressed:bg-background aria-pressed:text-foreground aria-pressed:shadow-xs">{r === "7d" ? "7 days" : "30 days"}</button>
))}
</div>
<Button size="sm" variant="outline" className="rounded-full"><ArrowDownTrayIcon size={14} className="flex" /> Export</Button>
</div>
</header>
<div className="min-h-0 flex-1 overflow-y-auto bg-surface p-4">
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<StatTile label="Requests" value={requests.toLocaleString()} delta="+12.4%" deltaTone="up" chart={<Sparkline data={d.requests} />} />
<StatTile label="Spend" value={money(spend)} delta="+8.1%" deltaTone="up" chart={<Sparkline data={d.spend} className="text-chart-2" />} />
<StatTile label="Avg tokens / request" value="2,140" delta="−3.2%" deltaTone="down" chart={<Sparkline data={d.requests.map((r, i) => r / (i + 5))} className="text-chart-3" />} />
<StatTile label="Error rate" value={`${errorRate.toFixed(2)}%`} delta="−0.3 pts" deltaTone="up" chart={<Sparkline data={d.errors} className="text-chart-5" />} />
</div>
<div className="mt-3 grid gap-3 lg:grid-cols-[2fr_1fr]">
<section className="rounded-2xl border bg-card p-4">
<div className="mb-3 flex items-center justify-between"><h3 className="text-sm font-medium">Spend by day</h3><span className="text-xs text-muted-foreground">USD</span></div>
<BarChart data={d.spend.map((v, i) => ({ label: d.labels[i], value: v }))} height={140} format={money} showValues className={range === "30d" ? "[&_span]:text-[8px]" : ""} />
</section>
<section className="flex flex-col gap-4 rounded-2xl border bg-card p-4">
<div><h3 className="text-sm font-medium">Rate limits</h3><p className="text-xs text-muted-foreground">Current minute</p></div>
<UsageMeterGroup>
<UsageMeter label="Requests / min" value={860} max={1000} showPercent size="sm" />
<UsageMeter label="Input tokens / min" value={312_000} max={400_000} showPercent size="sm" />
<UsageMeter label="Output tokens / min" value={38_000} max={80_000} showPercent size="sm" />
<UsageMeter label="Monthly budget" value={spend} max={range === "7d" ? 500 : 2000} format={(v, m) => `${money(v)} / ${money(m)}`} showPercent size="sm" />
</UsageMeterGroup>
</section>
</div>
<div className="mt-3 grid gap-3 lg:grid-cols-[1fr_2fr]">
<section className="rounded-2xl border bg-card p-4">
<h3 className="mb-3 text-sm font-medium">Spend by model</h3>
<BarChart data={byModel} height={110} format={money} showValues />
</section>
<section className="overflow-hidden rounded-2xl border bg-card">
<div className="flex items-center justify-between px-4 py-3"><h3 className="text-sm font-medium">Recent requests</h3><Button size="xs" variant="ghost">View all</Button></div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead className="border-y bg-surface text-left text-[11px] text-muted-foreground">
<tr><th className="px-4 py-2 font-medium">Request</th><th className="px-4 py-2 font-medium">Time</th><th className="px-4 py-2 font-medium">Model</th><th className="px-4 py-2 text-right font-medium">Tokens</th><th className="px-4 py-2 text-right font-medium">Latency</th><th className="px-4 py-2 text-right font-medium">Status</th></tr>
</thead>
<tbody>
{recent.map((r) => (
<tr key={r.id} className="border-b last:border-b-0 hover:bg-accent/40">
<td className="px-4 py-2 font-mono">{r.id}</td>
<td className="px-4 py-2 font-mono tabular-nums text-muted-foreground">{r.at}</td>
<td className="px-4 py-2">{r.model}</td>
<td className="px-4 py-2 text-right font-mono tabular-nums">{r.tokens.toLocaleString()}</td>
<td className="px-4 py-2 text-right font-mono tabular-nums">{r.latency.toFixed(2)}s</td>
<td className="px-4 py-2 text-right">{r.ok ? <CheckCircleIcon size={14} className="ml-auto flex text-emerald-600 dark:text-emerald-400" /> : <XCircleIcon size={14} className="ml-auto flex text-red-600 dark:text-red-400" />}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</div>
</div>
</div>
);
}The registry item pulls in every component it depends on.
Usage
import UsageDashboard from "@/components/blocks/usage-dashboard/usage-dashboard";
export default function Page() {
return (
<div className="h-dvh p-4">
<UsageDashboard />
</div>
);
}Give the parent a bounded height. The block fills it and scrolls internally.
Going live
Replace the daily, byModel, and recent constants with data from your billing and logging APIs. The range toggle just picks a dataset, so wire it to a query parameter.