Usage Dashboard
A coding-agent usage overview with daily token trends, provider and model totals, billed spend, and a daily breakdown.
Preview
Illustrative data · not live usage
Usage overview
Aug 28 – Sep 26, 2026 · UTC
Processed tokens
70M
Across 7,387 requests
- Codex36.2MOpenAI · 52%$418.49
- Claude Code33.8MAnthropic · 48%$644.50
Daily processed tokens
CodexClaude Code
Inspect a day
Use the Day tab in Breakdown for exact daily values.
Totals
- Cached input
- 19.8M
- Uncached input
- 36.5M
- Output
- 13.6M
- Billed spend · USD
- $1,062.99
Breakdown
| Model | Share | Cost | Tokens |
|---|---|---|---|
| gpt-6-solOpenAI | 52% | $418.49 | 36.2M |
| claude-sonnet-5Anthropic | 38% | $355.28 | 26.7M |
| claude-opus-5-5Anthropic | 10% | $289.22 | 7.1M |
TSXcomponents/previews/agents/usage-dashboard-example.tsx
"use client";
import {
UsageDashboard,
type UsageRecord,
} from "@/components/agents/usage-dashboard";
// Illustrative records with real coding agents, providers, and model IDs.
// Token counts and billed amounts are examples, not live usage or price quotes.
export const sampleUsageRecords: UsageRecord[] = Array.from(
{ length: 30 },
(_, index) => {
const date = new Date(Date.UTC(2026, 7, index + 28))
.toISOString()
.slice(0, 10);
const codexSwing = Math.sin(index * 0.62) * 0.25 + Math.cos(index * 0.17) * 0.12 + (index / 29) * 0.12;
const claudeSwing = Math.cos(index * 0.47 + 0.8) * 0.22 + Math.sin(index * 0.21) * 0.1;
const opusSwing = Math.sin(index * 0.86 + 1.2) * 0.2;
const codexInput = Math.round(940_000 * (1 + codexSwing));
const claudeInput = Math.round(710_000 * (1 + claudeSwing));
const opusInput = Math.round(185_000 * (1 + opusSwing));
return [
{
date,
agent: "Codex",
provider: "OpenAI",
model: "gpt-6-sol",
inputTokens: codexInput,
cachedInputTokens: Math.round(codexInput * 0.42),
outputTokens: Math.round(220_000 * (1 + codexSwing * 0.8)),
requests: Math.round(125 * (1 + codexSwing * 0.4)),
costUsd: Number((13.4 * (1 + codexSwing)).toFixed(2)),
},
{
date,
agent: "Claude Code",
provider: "Anthropic",
model: "claude-sonnet-5",
inputTokens: claudeInput,
cachedInputTokens: Math.round(claudeInput * 0.3),
outputTokens: Math.round(176_000 * (1 + claudeSwing * 0.65)),
requests: Math.round(97 * (1 + claudeSwing * 0.32)),
costUsd: Number((11.8 * (1 + claudeSwing * 0.7)).toFixed(2)),
},
{
date,
agent: "Claude Code",
provider: "Anthropic",
model: "claude-opus-5-5",
inputTokens: opusInput,
cachedInputTokens: Math.round(opusInput * 0.19),
outputTokens: Math.round(51_000 * (1 + opusSwing * 0.75)),
requests: Math.round(22 * (1 + opusSwing * 0.35)),
costUsd: Number((9.6 * (1 + opusSwing * 0.9)).toFixed(2)),
},
];
},
).flat();
export function UsageDashboardUsage() {
return <UsageDashboard records={sampleUsageRecords} defaultRange={30} />;
}
TSXcomponents/agents/usage-dashboard.tsx
"use client";
// www.agentui.pro/components/agents/usage-dashboard
import { motion, useReducedMotion } from "motion/react";
import { type KeyboardEvent, type PointerEvent, useId, useMemo, useState } from "react";
import { SPRING_LAYOUT } from "@/lib/ease";
import { cn } from "@/lib/utils";
import {
summarizeUsage,
type DailyUsage,
type UsageBreakdown,
type UsageRecord,
type UsageTotals,
} from "./usage-dashboard-data";
import { UsageProviderMark } from "./usage-provider-mark";
export type { UsageRecord } from "./usage-dashboard-data";
export interface UsageDashboardProps {
records: UsageRecord[];
title?: string;
/** Controlled range, in UTC calendar days. */
range?: 7 | 30;
defaultRange?: 7 | 30;
onRangeChange?: (range: 7 | 30) => void;
className?: string;
}
type AgentSeries = { name: string; values: number[] };
const seriesColors = [
"var(--usage-first)",
"var(--usage-second)",
"var(--usage-third)",
"var(--usage-fourth)",
];
const compact = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
});
const integer = new Intl.NumberFormat("en-US");
const money = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
function formatMoney(value: number) {
return value > 0 && value < 0.01 ? "<$0.01" : money.format(value);
}
function formatDate(date: string, options: Intl.DateTimeFormatOptions) {
return new Intl.DateTimeFormat("en-US", { timeZone: "UTC", ...options }).format(
new Date(`${date}T00:00:00.000Z`),
);
}
function tokenCount(totals: UsageTotals) {
return totals.inputTokens + totals.outputTokens;
}
function chartPath(values: number[], max: number) {
return values
.map((value, index) => {
const x = 46 + (index / Math.max(values.length - 1, 1)) * 684;
const y = 22 + 178 * (1 - value / max);
return `${index === 0 ? "M" : "L"}${x.toFixed(2)} ${y.toFixed(2)}`;
})
.join(" ");
}
function AgentChart({
daily,
series,
}: {
daily: DailyUsage[];
series: AgentSeries[];
}) {
const gradientId = useId().replaceAll(":", "");
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const peak = Math.max(1, ...series.flatMap((item) => item.values));
const step = 10 ** Math.floor(Math.log10(peak));
const max = Math.ceil((peak * 1.16) / step) * step;
const activeDay = activeIndex === null ? null : daily[activeIndex];
const activeX = activeIndex === null ? 0 : 46 + (activeIndex / Math.max(daily.length - 1, 1)) * 684;
const labels = [0, Math.floor((daily.length - 1) / 2), daily.length - 1];
const inspectPointer = (event: PointerEvent<HTMLButtonElement>) => {
const bounds = event.currentTarget.getBoundingClientRect();
const plotX = ((event.clientX - bounds.left) / bounds.width) * 760;
const index = Math.round(((plotX - 46) / 684) * (daily.length - 1));
setActiveIndex(Math.max(0, Math.min(daily.length - 1, index)));
};
const inspectKeyboard = (event: KeyboardEvent<HTMLButtonElement>) => {
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
event.preventDefault();
setActiveIndex((previous) => {
if (event.key === "Home") return 0;
if (event.key === "End") return daily.length - 1;
const current = previous ?? daily.length - 1;
return Math.max(0, Math.min(daily.length - 1, current + (event.key === "ArrowRight" ? 1 : -1)));
});
};
return (
<div className="min-w-0">
<div className="flex min-h-20 items-start justify-between gap-3">
<div className="min-w-0">
<h3 className="text-sm font-semibold text-foreground">Daily processed tokens</h3>
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-1.5">
{series.map((item, index) => (
<span key={item.name} className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
<span className="size-2 rounded-full" style={{ backgroundColor: seriesColors[index % seriesColors.length] }} />
{item.name}
</span>
))}
</div>
</div>
<div className="min-w-28 shrink-0 text-right text-xs tabular-nums" aria-live="polite">
{activeDay ? (
<div className="rounded-lg border border-border bg-popover px-2.5 py-2 text-popover-foreground shadow-sm">
<p className="font-medium">{formatDate(activeDay.date, { month: "short", day: "numeric" })}</p>
{series.map((item) => (
<p key={item.name} className="mt-0.5 flex items-center justify-between gap-3 text-muted-foreground">
<span className="truncate">{item.name}</span>
<span className="shrink-0 text-popover-foreground">{compact.format(item.values[activeIndex ?? 0])}</span>
</p>
))}
</div>
) : (
<p className="pt-1 text-muted-foreground">Inspect a day</p>
)}
</div>
</div>
<div className="relative mt-2 w-full">
<button
type="button"
className="absolute inset-0 z-10 w-full cursor-crosshair rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
aria-label="Daily token chart. Move the pointer or use left and right arrow keys to inspect a day."
onPointerMove={inspectPointer}
onPointerDown={inspectPointer}
onPointerLeave={(event) => { if (event.pointerType === "mouse") setActiveIndex(null); }}
onFocus={() => setActiveIndex((previous) => previous ?? daily.length - 1)}
onBlur={() => setActiveIndex(null)}
onKeyDown={inspectKeyboard}
/>
<svg viewBox="0 0 760 235" className="block w-full" aria-hidden="true">
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="var(--usage-first)" stopOpacity="0.14" />
<stop offset="1" stopColor="var(--usage-first)" stopOpacity="0" />
</linearGradient>
</defs>
{[0, 0.5, 1].map((fraction) => {
const y = 200 - fraction * 178;
return (
<g key={fraction}>
<line x1="46" x2="730" y1={y} y2={y} className="stroke-border/80" strokeDasharray={fraction === 0 ? undefined : "3 5"} />
<text x="38" y={y + 4} textAnchor="end" className="fill-muted-foreground text-[10px] tabular-nums">{compact.format(max * fraction)}</text>
</g>
);
})}
{series[0] && <path d={`${chartPath(series[0].values, max)} L730 200 L46 200 Z`} fill={`url(#${gradientId})`} />}
{series.map((item, index) => (
<path key={item.name} d={chartPath(item.values, max)} fill="none" stroke={seriesColors[index % seriesColors.length]} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" vectorEffect="non-scaling-stroke" />
))}
{activeDay && (
<g>
<line x1={activeX} x2={activeX} y1="22" y2="200" className="stroke-foreground/30" strokeDasharray="3 4" />
{series.map((item, index) => (
<circle key={item.name} cx={activeX} cy={22 + 178 * (1 - item.values[activeIndex ?? 0] / max)} r="4.5" fill={seriesColors[index % seriesColors.length]} stroke="var(--usage-surface)" strokeWidth="2" />
))}
</g>
)}
{labels.map((index, labelIndex) => (
<text key={index} x={46 + (index / Math.max(daily.length - 1, 1)) * 684} y="230" textAnchor={labelIndex === 0 ? "start" : labelIndex === 2 ? "end" : "middle"} className="fill-muted-foreground text-[10px]">
{formatDate(daily[index].date, { month: "short", day: "numeric" })}
</text>
))}
</svg>
</div>
<p className="sr-only">Use the Day tab in Breakdown for exact daily values.</p>
</div>
);
}
function AgentSummary({ agents, totals }: { agents: UsageBreakdown[]; totals: UsageTotals }) {
return (
<div className="min-w-0">
<p className="text-xs font-medium text-muted-foreground">Processed tokens</p>
<p className="mt-1 text-[clamp(2.6rem,7vw,4.1rem)] font-semibold leading-none tracking-[-0.07em] tabular-nums text-foreground">{compact.format(tokenCount(totals))}</p>
<p className="mt-2 text-xs text-muted-foreground">Across {integer.format(totals.requests)} requests</p>
<ul className="mt-7 space-y-4">
{agents.map((agent, index) => (
<li key={`${agent.name}\0${agent.provider}`} className="border-t border-border pt-3">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="flex min-w-0 items-center gap-2 font-medium text-foreground"><span className="size-2 shrink-0 rounded-full" style={{ backgroundColor: seriesColors[index % seriesColors.length] }} /><UsageProviderMark provider={agent.provider} className="size-[18px] text-foreground" /><span className="truncate">{agent.name}</span></span>
<span className="shrink-0 font-medium tabular-nums text-foreground">{compact.format(tokenCount(agent))}</span>
</div>
<div className="mt-1 flex items-center justify-between gap-3 pl-4 text-xs text-muted-foreground">
<span className="truncate">{agent.provider} · {Math.round((tokenCount(agent) / Math.max(tokenCount(totals), 1)) * 100)}%</span>
<span className="shrink-0 tabular-nums">{formatMoney(agent.costUsd)}</span>
</div>
</li>
))}
</ul>
</div>
);
}
function Total({ label, value }: { label: string; value: string }) {
return (
<div className="min-w-0">
<dt className="text-xs text-muted-foreground">{label}</dt>
<dd className="mt-1.5 text-xl font-semibold tracking-[-0.04em] tabular-nums text-foreground sm:text-2xl">{value}</dd>
</div>
);
}
function Breakdown({
models,
daily,
totals,
reduceMotion,
layoutId,
view,
onViewChange,
}: {
models: UsageBreakdown[];
daily: DailyUsage[];
totals: UsageTotals;
reduceMotion: boolean;
layoutId: string;
view: "model" | "day";
onViewChange: (view: "model" | "day") => void;
}) {
const [dayPage, setDayPage] = useState(0);
const pageCount = Math.ceil(daily.length / 7);
const pageIndex = Math.min(dayPage, pageCount - 1);
const pageEnd = daily.length - pageIndex * 7;
const visibleDays = daily.slice(Math.max(0, pageEnd - 7), pageEnd);
return (
<div className="pt-7">
<div className="flex flex-wrap items-center justify-between gap-4">
<h3 className="text-base font-semibold tracking-tight text-foreground">Breakdown</h3>
<fieldset className="inline-flex rounded-full border border-border bg-muted/50 p-1">
<legend className="sr-only">Breakdown view</legend>
{(["model", "day"] as const).map((option) => (
<button
key={option}
type="button"
aria-pressed={view === option}
onClick={() => onViewChange(option)}
className={cn("relative isolate min-h-8 rounded-full px-3.5 text-xs font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring", view === option ? "text-foreground" : "text-muted-foreground hover:text-foreground")}
>
{view === option && <motion.span layoutId={layoutId} className="absolute inset-0 -z-10 rounded-full border border-border/70 bg-background shadow-sm" transition={reduceMotion ? { duration: 0 } : SPRING_LAYOUT} />}
{option === "model" ? "Model" : "Day"}
</button>
))}
</fieldset>
</div>
<div>
{view === "model" ? (
<table className="mt-5 w-full border-collapse text-left text-xs sm:text-sm">
<thead><tr className="border-b border-border text-muted-foreground"><th scope="col" className="pb-3 font-medium">Model</th><th scope="col" className="hidden pb-3 text-right font-medium sm:table-cell">Share</th><th scope="col" className="pb-3 text-right font-medium">Cost</th><th scope="col" className="pb-3 text-right font-medium">Tokens</th></tr></thead>
<tbody>{models.map((model) => (
<tr key={`${model.provider}\0${model.name}`} className="border-b border-border/65 last:border-0">
<th scope="row" className="min-w-0 py-3 pr-2 font-medium text-foreground"><span className="flex items-center gap-2"><UsageProviderMark provider={model.provider} className="size-4 text-foreground" /><span className="break-all">{model.name}</span></span><span className="block font-normal text-muted-foreground">{model.provider}</span></th>
<td className="hidden py-3 text-right tabular-nums text-muted-foreground sm:table-cell">{Math.round((tokenCount(model) / Math.max(tokenCount(totals), 1)) * 100)}%</td>
<td className="py-3 text-right tabular-nums text-muted-foreground">{formatMoney(model.costUsd)}</td>
<td className="py-3 text-right font-medium tabular-nums text-foreground">{compact.format(tokenCount(model))}</td>
</tr>
))}</tbody>
</table>
) : (
<div className="mt-5">
<table className="w-full border-collapse text-left text-xs sm:text-sm">
<thead className="sticky top-0 bg-card"><tr className="border-b border-border text-muted-foreground"><th scope="col" className="pb-3 font-medium">Day · UTC</th><th scope="col" className="hidden pb-3 text-right font-medium sm:table-cell">Input</th><th scope="col" className="hidden pb-3 text-right font-medium sm:table-cell">Output</th><th scope="col" className="pb-3 text-right font-medium">Cost</th><th scope="col" className="pb-3 text-right font-medium">Tokens</th></tr></thead>
<tbody>{visibleDays.map((day) => (
<tr key={day.date} className="border-b border-border/65 last:border-0">
<th scope="row" className="py-3 font-medium text-foreground">{formatDate(day.date, { month: "short", day: "numeric" })}</th>
<td className="hidden py-3 text-right tabular-nums text-muted-foreground sm:table-cell">{integer.format(day.inputTokens)}</td>
<td className="hidden py-3 text-right tabular-nums text-muted-foreground sm:table-cell">{integer.format(day.outputTokens)}</td>
<td className="py-3 text-right tabular-nums text-muted-foreground">{formatMoney(day.costUsd)}</td>
<td className="py-3 text-right font-medium tabular-nums text-foreground">{compact.format(tokenCount(day))}</td>
</tr>
))}</tbody>
</table>
{pageCount > 1 && (
<div className="mt-4 flex flex-wrap items-center justify-between gap-3 border-t border-border pt-4">
<p className="text-xs text-muted-foreground">
{formatDate(visibleDays[0].date, { month: "short", day: "numeric" })} – {formatDate(visibleDays[visibleDays.length - 1].date, { month: "short", day: "numeric" })} · {visibleDays.length} of {daily.length} days
</p>
<div className="flex items-center gap-2">
<button
type="button"
disabled={pageIndex >= pageCount - 1}
onClick={() => setDayPage(pageIndex + 1)}
className="min-h-10 rounded-full border border-border px-3 text-xs font-medium text-foreground outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-40"
>
← Older
</button>
<button
type="button"
disabled={pageIndex === 0}
onClick={() => setDayPage(pageIndex - 1)}
className="min-h-10 rounded-full border border-border px-3 text-xs font-medium text-foreground outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-40"
>
Newer →
</button>
</div>
</div>
)}
</div>
)}
</div>
</div>
);
}
/** Provider-billed usage overview. Pass real per-day records; the component never estimates prices. */
export function UsageDashboard({
records,
title = "Usage overview",
range,
defaultRange = 7,
onRangeChange,
className,
}: UsageDashboardProps) {
const reduceMotion = useReducedMotion() ?? false;
const rangeId = useId();
const breakdownId = useId();
const [localRange, setLocalRange] = useState<7 | 30>(defaultRange);
const [breakdownView, setBreakdownView] = useState<"model" | "day">("model");
const selectedRange = range ?? localRange;
const usage = useMemo(() => summarizeUsage(records, selectedRange), [records, selectedRange]);
const changeRange = (next: 7 | 30) => {
if (range === undefined) setLocalRange(next);
onRangeChange?.(next);
};
return (
<section
className={cn(
"w-full max-w-5xl rounded-[1.35rem] border border-border bg-card p-4 text-card-foreground [--usage-first:#0e857e] [--usage-second:#7468bd] [--usage-third:#bd8562] [--usage-fourth:#6b9aad] [--usage-surface:#fff] dark:[--usage-first:#65d5c3] dark:[--usage-second:#b6a6f7] dark:[--usage-third:#d9a778] dark:[--usage-fourth:#87bdca] dark:[--usage-surface:#151515] sm:p-7",
className,
)}
aria-label={title}
>
<div className="flex flex-wrap items-start justify-between gap-5">
<div>
<h2 className="text-2xl font-semibold tracking-[-0.045em] text-foreground sm:text-3xl">{title}</h2>
<p className="mt-1.5 text-sm text-muted-foreground">
{usage
? `${formatDate(usage.startDate, { month: "short", day: "numeric" })} – ${formatDate(usage.endDate, { month: "short", day: "numeric", year: "numeric" })} · UTC`
: "No usage data yet"}
</p>
</div>
<fieldset className="inline-flex rounded-full border border-border bg-muted/50 p-1">
<legend className="sr-only">Usage period</legend>
{([7, 30] as const).map((days) => (
<button
key={days}
type="button"
aria-pressed={selectedRange === days}
onClick={() => changeRange(days)}
className={cn("relative isolate min-h-9 rounded-full px-3.5 text-xs font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring", selectedRange === days ? "text-foreground" : "text-muted-foreground hover:text-foreground")}
>
{selectedRange === days && <motion.span layoutId={`${rangeId}-range`} className="absolute inset-0 -z-10 rounded-full border border-border/70 bg-background shadow-sm" transition={reduceMotion ? { duration: 0 } : SPRING_LAYOUT} />}
{days} days
</button>
))}
</fieldset>
</div>
{!usage || usage.recordCount === 0 ? (
<div className="mt-8 flex min-h-64 flex-col items-center justify-center rounded-xl border border-dashed border-border px-5 text-center">
<p className="text-sm font-medium text-foreground">No usage in this period</p>
<p className="mt-1 max-w-sm text-sm text-muted-foreground">Pass daily, provider-billed records to see agent and model usage.</p>
</div>
) : (
<div>
<div className="mt-7 grid gap-8 border-b border-border pb-7 lg:grid-cols-[minmax(200px,0.85fr)_minmax(0,2fr)] lg:gap-10">
<AgentSummary agents={usage.agents} totals={usage.totals} />
<AgentChart daily={usage.daily} series={usage.agentSeries} />
</div>
<div className="border-b border-border py-7">
<h3 className="text-sm font-semibold text-foreground">Totals</h3>
<dl className="mt-5 grid grid-cols-2 gap-x-5 gap-y-6 sm:grid-cols-4">
<Total label="Cached input" value={compact.format(usage.totals.cachedInputTokens)} />
<Total label="Uncached input" value={compact.format(usage.totals.inputTokens - usage.totals.cachedInputTokens)} />
<Total label="Output" value={compact.format(usage.totals.outputTokens)} />
<Total label="Billed spend · USD" value={formatMoney(usage.totals.costUsd)} />
</dl>
</div>
<Breakdown key={selectedRange} models={usage.models} daily={usage.daily} totals={usage.totals} reduceMotion={reduceMotion} layoutId={`${breakdownId}-view`} view={breakdownView} onViewChange={setBreakdownView} />
</div>
)}
</section>
);
}
Install
Add it with the shadcn CLI, or copy the source manually.
$ bunx --bun shadcn add https://www.agentui.pro/r/usage-dashboard.json
Needs the theme tokens once. Already ran
shadcn init? You are set. Theme setupInstall dependencies
npm i clsx motion tailwind-mergeAdd util files
TSXlib/ease.ts
// Shared motion tokens. Easing curves mirror the CSS custom properties in
// globals.css; springs are the canonical physics used across components.
// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.
export const EASE_OUT = [0.16, 1, 0.3, 1] as const;
export const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;
export const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;
/** CSS string form of EASE_OUT for inline style transitions. */
export const EASE_OUT_CSS = "cubic-bezier(0.16, 1, 0.3, 1)";
/** Press feedback on buttons and other tappable surfaces. */
export const SPRING_PRESS = {
type: "spring",
stiffness: 500,
damping: 30,
mass: 0.6,
} as const;
/** Content swaps — label/icon slots trading places inside a control. */
export const SPRING_SWAP = {
type: "spring",
stiffness: 460,
damping: 30,
mass: 0.55,
} as const;
/** Overlay panel entrances — modals and sheets summoned by pointer. */
export const SPRING_PANEL = {
type: "spring",
stiffness: 420,
damping: 40,
mass: 0.5,
} as const;
/** Shared-layout glides — pills, indicators and panels morphing between positions. */
export const SPRING_LAYOUT = {
type: "spring",
stiffness: 360,
damping: 32,
mass: 0.6,
} as const;
/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */
export const SPRING_MOUSE = {
stiffness: 200,
damping: 15,
mass: 0.3,
} as const;
/** Dragged handles and fills (sliders) — critically damped `useSpring` config,
* so the value follows the pointer butterily and never rebounds off an end. */
export const SPRING_GLIDE = {
stiffness: 700,
damping: 50,
mass: 0.5,
} as const;
TSXlib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy the source code
TSXcomponents/agents/usage-dashboard.tsx
"use client";
// www.agentui.pro/components/agents/usage-dashboard
import { motion, useReducedMotion } from "motion/react";
import { type KeyboardEvent, type PointerEvent, useId, useMemo, useState } from "react";
import { SPRING_LAYOUT } from "@/lib/ease";
import { cn } from "@/lib/utils";
import {
summarizeUsage,
type DailyUsage,
type UsageBreakdown,
type UsageRecord,
type UsageTotals,
} from "./usage-dashboard-data";
import { UsageProviderMark } from "./usage-provider-mark";
export type { UsageRecord } from "./usage-dashboard-data";
export interface UsageDashboardProps {
records: UsageRecord[];
title?: string;
/** Controlled range, in UTC calendar days. */
range?: 7 | 30;
defaultRange?: 7 | 30;
onRangeChange?: (range: 7 | 30) => void;
className?: string;
}
type AgentSeries = { name: string; values: number[] };
const seriesColors = [
"var(--usage-first)",
"var(--usage-second)",
"var(--usage-third)",
"var(--usage-fourth)",
];
const compact = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
});
const integer = new Intl.NumberFormat("en-US");
const money = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
function formatMoney(value: number) {
return value > 0 && value < 0.01 ? "<$0.01" : money.format(value);
}
function formatDate(date: string, options: Intl.DateTimeFormatOptions) {
return new Intl.DateTimeFormat("en-US", { timeZone: "UTC", ...options }).format(
new Date(`${date}T00:00:00.000Z`),
);
}
function tokenCount(totals: UsageTotals) {
return totals.inputTokens + totals.outputTokens;
}
function chartPath(values: number[], max: number) {
return values
.map((value, index) => {
const x = 46 + (index / Math.max(values.length - 1, 1)) * 684;
const y = 22 + 178 * (1 - value / max);
return `${index === 0 ? "M" : "L"}${x.toFixed(2)} ${y.toFixed(2)}`;
})
.join(" ");
}
function AgentChart({
daily,
series,
}: {
daily: DailyUsage[];
series: AgentSeries[];
}) {
const gradientId = useId().replaceAll(":", "");
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const peak = Math.max(1, ...series.flatMap((item) => item.values));
const step = 10 ** Math.floor(Math.log10(peak));
const max = Math.ceil((peak * 1.16) / step) * step;
const activeDay = activeIndex === null ? null : daily[activeIndex];
const activeX = activeIndex === null ? 0 : 46 + (activeIndex / Math.max(daily.length - 1, 1)) * 684;
const labels = [0, Math.floor((daily.length - 1) / 2), daily.length - 1];
const inspectPointer = (event: PointerEvent<HTMLButtonElement>) => {
const bounds = event.currentTarget.getBoundingClientRect();
const plotX = ((event.clientX - bounds.left) / bounds.width) * 760;
const index = Math.round(((plotX - 46) / 684) * (daily.length - 1));
setActiveIndex(Math.max(0, Math.min(daily.length - 1, index)));
};
const inspectKeyboard = (event: KeyboardEvent<HTMLButtonElement>) => {
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
event.preventDefault();
setActiveIndex((previous) => {
if (event.key === "Home") return 0;
if (event.key === "End") return daily.length - 1;
const current = previous ?? daily.length - 1;
return Math.max(0, Math.min(daily.length - 1, current + (event.key === "ArrowRight" ? 1 : -1)));
});
};
return (
<div className="min-w-0">
<div className="flex min-h-20 items-start justify-between gap-3">
<div className="min-w-0">
<h3 className="text-sm font-semibold text-foreground">Daily processed tokens</h3>
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-1.5">
{series.map((item, index) => (
<span key={item.name} className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
<span className="size-2 rounded-full" style={{ backgroundColor: seriesColors[index % seriesColors.length] }} />
{item.name}
</span>
))}
</div>
</div>
<div className="min-w-28 shrink-0 text-right text-xs tabular-nums" aria-live="polite">
{activeDay ? (
<div className="rounded-lg border border-border bg-popover px-2.5 py-2 text-popover-foreground shadow-sm">
<p className="font-medium">{formatDate(activeDay.date, { month: "short", day: "numeric" })}</p>
{series.map((item) => (
<p key={item.name} className="mt-0.5 flex items-center justify-between gap-3 text-muted-foreground">
<span className="truncate">{item.name}</span>
<span className="shrink-0 text-popover-foreground">{compact.format(item.values[activeIndex ?? 0])}</span>
</p>
))}
</div>
) : (
<p className="pt-1 text-muted-foreground">Inspect a day</p>
)}
</div>
</div>
<div className="relative mt-2 w-full">
<button
type="button"
className="absolute inset-0 z-10 w-full cursor-crosshair rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
aria-label="Daily token chart. Move the pointer or use left and right arrow keys to inspect a day."
onPointerMove={inspectPointer}
onPointerDown={inspectPointer}
onPointerLeave={(event) => { if (event.pointerType === "mouse") setActiveIndex(null); }}
onFocus={() => setActiveIndex((previous) => previous ?? daily.length - 1)}
onBlur={() => setActiveIndex(null)}
onKeyDown={inspectKeyboard}
/>
<svg viewBox="0 0 760 235" className="block w-full" aria-hidden="true">
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="var(--usage-first)" stopOpacity="0.14" />
<stop offset="1" stopColor="var(--usage-first)" stopOpacity="0" />
</linearGradient>
</defs>
{[0, 0.5, 1].map((fraction) => {
const y = 200 - fraction * 178;
return (
<g key={fraction}>
<line x1="46" x2="730" y1={y} y2={y} className="stroke-border/80" strokeDasharray={fraction === 0 ? undefined : "3 5"} />
<text x="38" y={y + 4} textAnchor="end" className="fill-muted-foreground text-[10px] tabular-nums">{compact.format(max * fraction)}</text>
</g>
);
})}
{series[0] && <path d={`${chartPath(series[0].values, max)} L730 200 L46 200 Z`} fill={`url(#${gradientId})`} />}
{series.map((item, index) => (
<path key={item.name} d={chartPath(item.values, max)} fill="none" stroke={seriesColors[index % seriesColors.length]} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" vectorEffect="non-scaling-stroke" />
))}
{activeDay && (
<g>
<line x1={activeX} x2={activeX} y1="22" y2="200" className="stroke-foreground/30" strokeDasharray="3 4" />
{series.map((item, index) => (
<circle key={item.name} cx={activeX} cy={22 + 178 * (1 - item.values[activeIndex ?? 0] / max)} r="4.5" fill={seriesColors[index % seriesColors.length]} stroke="var(--usage-surface)" strokeWidth="2" />
))}
</g>
)}
{labels.map((index, labelIndex) => (
<text key={index} x={46 + (index / Math.max(daily.length - 1, 1)) * 684} y="230" textAnchor={labelIndex === 0 ? "start" : labelIndex === 2 ? "end" : "middle"} className="fill-muted-foreground text-[10px]">
{formatDate(daily[index].date, { month: "short", day: "numeric" })}
</text>
))}
</svg>
</div>
<p className="sr-only">Use the Day tab in Breakdown for exact daily values.</p>
</div>
);
}
function AgentSummary({ agents, totals }: { agents: UsageBreakdown[]; totals: UsageTotals }) {
return (
<div className="min-w-0">
<p className="text-xs font-medium text-muted-foreground">Processed tokens</p>
<p className="mt-1 text-[clamp(2.6rem,7vw,4.1rem)] font-semibold leading-none tracking-[-0.07em] tabular-nums text-foreground">{compact.format(tokenCount(totals))}</p>
<p className="mt-2 text-xs text-muted-foreground">Across {integer.format(totals.requests)} requests</p>
<ul className="mt-7 space-y-4">
{agents.map((agent, index) => (
<li key={`${agent.name}\0${agent.provider}`} className="border-t border-border pt-3">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="flex min-w-0 items-center gap-2 font-medium text-foreground"><span className="size-2 shrink-0 rounded-full" style={{ backgroundColor: seriesColors[index % seriesColors.length] }} /><UsageProviderMark provider={agent.provider} className="size-[18px] text-foreground" /><span className="truncate">{agent.name}</span></span>
<span className="shrink-0 font-medium tabular-nums text-foreground">{compact.format(tokenCount(agent))}</span>
</div>
<div className="mt-1 flex items-center justify-between gap-3 pl-4 text-xs text-muted-foreground">
<span className="truncate">{agent.provider} · {Math.round((tokenCount(agent) / Math.max(tokenCount(totals), 1)) * 100)}%</span>
<span className="shrink-0 tabular-nums">{formatMoney(agent.costUsd)}</span>
</div>
</li>
))}
</ul>
</div>
);
}
function Total({ label, value }: { label: string; value: string }) {
return (
<div className="min-w-0">
<dt className="text-xs text-muted-foreground">{label}</dt>
<dd className="mt-1.5 text-xl font-semibold tracking-[-0.04em] tabular-nums text-foreground sm:text-2xl">{value}</dd>
</div>
);
}
function Breakdown({
models,
daily,
totals,
reduceMotion,
layoutId,
view,
onViewChange,
}: {
models: UsageBreakdown[];
daily: DailyUsage[];
totals: UsageTotals;
reduceMotion: boolean;
layoutId: string;
view: "model" | "day";
onViewChange: (view: "model" | "day") => void;
}) {
const [dayPage, setDayPage] = useState(0);
const pageCount = Math.ceil(daily.length / 7);
const pageIndex = Math.min(dayPage, pageCount - 1);
const pageEnd = daily.length - pageIndex * 7;
const visibleDays = daily.slice(Math.max(0, pageEnd - 7), pageEnd);
return (
<div className="pt-7">
<div className="flex flex-wrap items-center justify-between gap-4">
<h3 className="text-base font-semibold tracking-tight text-foreground">Breakdown</h3>
<fieldset className="inline-flex rounded-full border border-border bg-muted/50 p-1">
<legend className="sr-only">Breakdown view</legend>
{(["model", "day"] as const).map((option) => (
<button
key={option}
type="button"
aria-pressed={view === option}
onClick={() => onViewChange(option)}
className={cn("relative isolate min-h-8 rounded-full px-3.5 text-xs font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring", view === option ? "text-foreground" : "text-muted-foreground hover:text-foreground")}
>
{view === option && <motion.span layoutId={layoutId} className="absolute inset-0 -z-10 rounded-full border border-border/70 bg-background shadow-sm" transition={reduceMotion ? { duration: 0 } : SPRING_LAYOUT} />}
{option === "model" ? "Model" : "Day"}
</button>
))}
</fieldset>
</div>
<div>
{view === "model" ? (
<table className="mt-5 w-full border-collapse text-left text-xs sm:text-sm">
<thead><tr className="border-b border-border text-muted-foreground"><th scope="col" className="pb-3 font-medium">Model</th><th scope="col" className="hidden pb-3 text-right font-medium sm:table-cell">Share</th><th scope="col" className="pb-3 text-right font-medium">Cost</th><th scope="col" className="pb-3 text-right font-medium">Tokens</th></tr></thead>
<tbody>{models.map((model) => (
<tr key={`${model.provider}\0${model.name}`} className="border-b border-border/65 last:border-0">
<th scope="row" className="min-w-0 py-3 pr-2 font-medium text-foreground"><span className="flex items-center gap-2"><UsageProviderMark provider={model.provider} className="size-4 text-foreground" /><span className="break-all">{model.name}</span></span><span className="block font-normal text-muted-foreground">{model.provider}</span></th>
<td className="hidden py-3 text-right tabular-nums text-muted-foreground sm:table-cell">{Math.round((tokenCount(model) / Math.max(tokenCount(totals), 1)) * 100)}%</td>
<td className="py-3 text-right tabular-nums text-muted-foreground">{formatMoney(model.costUsd)}</td>
<td className="py-3 text-right font-medium tabular-nums text-foreground">{compact.format(tokenCount(model))}</td>
</tr>
))}</tbody>
</table>
) : (
<div className="mt-5">
<table className="w-full border-collapse text-left text-xs sm:text-sm">
<thead className="sticky top-0 bg-card"><tr className="border-b border-border text-muted-foreground"><th scope="col" className="pb-3 font-medium">Day · UTC</th><th scope="col" className="hidden pb-3 text-right font-medium sm:table-cell">Input</th><th scope="col" className="hidden pb-3 text-right font-medium sm:table-cell">Output</th><th scope="col" className="pb-3 text-right font-medium">Cost</th><th scope="col" className="pb-3 text-right font-medium">Tokens</th></tr></thead>
<tbody>{visibleDays.map((day) => (
<tr key={day.date} className="border-b border-border/65 last:border-0">
<th scope="row" className="py-3 font-medium text-foreground">{formatDate(day.date, { month: "short", day: "numeric" })}</th>
<td className="hidden py-3 text-right tabular-nums text-muted-foreground sm:table-cell">{integer.format(day.inputTokens)}</td>
<td className="hidden py-3 text-right tabular-nums text-muted-foreground sm:table-cell">{integer.format(day.outputTokens)}</td>
<td className="py-3 text-right tabular-nums text-muted-foreground">{formatMoney(day.costUsd)}</td>
<td className="py-3 text-right font-medium tabular-nums text-foreground">{compact.format(tokenCount(day))}</td>
</tr>
))}</tbody>
</table>
{pageCount > 1 && (
<div className="mt-4 flex flex-wrap items-center justify-between gap-3 border-t border-border pt-4">
<p className="text-xs text-muted-foreground">
{formatDate(visibleDays[0].date, { month: "short", day: "numeric" })} – {formatDate(visibleDays[visibleDays.length - 1].date, { month: "short", day: "numeric" })} · {visibleDays.length} of {daily.length} days
</p>
<div className="flex items-center gap-2">
<button
type="button"
disabled={pageIndex >= pageCount - 1}
onClick={() => setDayPage(pageIndex + 1)}
className="min-h-10 rounded-full border border-border px-3 text-xs font-medium text-foreground outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-40"
>
← Older
</button>
<button
type="button"
disabled={pageIndex === 0}
onClick={() => setDayPage(pageIndex - 1)}
className="min-h-10 rounded-full border border-border px-3 text-xs font-medium text-foreground outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-40"
>
Newer →
</button>
</div>
</div>
)}
</div>
)}
</div>
</div>
);
}
/** Provider-billed usage overview. Pass real per-day records; the component never estimates prices. */
export function UsageDashboard({
records,
title = "Usage overview",
range,
defaultRange = 7,
onRangeChange,
className,
}: UsageDashboardProps) {
const reduceMotion = useReducedMotion() ?? false;
const rangeId = useId();
const breakdownId = useId();
const [localRange, setLocalRange] = useState<7 | 30>(defaultRange);
const [breakdownView, setBreakdownView] = useState<"model" | "day">("model");
const selectedRange = range ?? localRange;
const usage = useMemo(() => summarizeUsage(records, selectedRange), [records, selectedRange]);
const changeRange = (next: 7 | 30) => {
if (range === undefined) setLocalRange(next);
onRangeChange?.(next);
};
return (
<section
className={cn(
"w-full max-w-5xl rounded-[1.35rem] border border-border bg-card p-4 text-card-foreground [--usage-first:#0e857e] [--usage-second:#7468bd] [--usage-third:#bd8562] [--usage-fourth:#6b9aad] [--usage-surface:#fff] dark:[--usage-first:#65d5c3] dark:[--usage-second:#b6a6f7] dark:[--usage-third:#d9a778] dark:[--usage-fourth:#87bdca] dark:[--usage-surface:#151515] sm:p-7",
className,
)}
aria-label={title}
>
<div className="flex flex-wrap items-start justify-between gap-5">
<div>
<h2 className="text-2xl font-semibold tracking-[-0.045em] text-foreground sm:text-3xl">{title}</h2>
<p className="mt-1.5 text-sm text-muted-foreground">
{usage
? `${formatDate(usage.startDate, { month: "short", day: "numeric" })} – ${formatDate(usage.endDate, { month: "short", day: "numeric", year: "numeric" })} · UTC`
: "No usage data yet"}
</p>
</div>
<fieldset className="inline-flex rounded-full border border-border bg-muted/50 p-1">
<legend className="sr-only">Usage period</legend>
{([7, 30] as const).map((days) => (
<button
key={days}
type="button"
aria-pressed={selectedRange === days}
onClick={() => changeRange(days)}
className={cn("relative isolate min-h-9 rounded-full px-3.5 text-xs font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring", selectedRange === days ? "text-foreground" : "text-muted-foreground hover:text-foreground")}
>
{selectedRange === days && <motion.span layoutId={`${rangeId}-range`} className="absolute inset-0 -z-10 rounded-full border border-border/70 bg-background shadow-sm" transition={reduceMotion ? { duration: 0 } : SPRING_LAYOUT} />}
{days} days
</button>
))}
</fieldset>
</div>
{!usage || usage.recordCount === 0 ? (
<div className="mt-8 flex min-h-64 flex-col items-center justify-center rounded-xl border border-dashed border-border px-5 text-center">
<p className="text-sm font-medium text-foreground">No usage in this period</p>
<p className="mt-1 max-w-sm text-sm text-muted-foreground">Pass daily, provider-billed records to see agent and model usage.</p>
</div>
) : (
<div>
<div className="mt-7 grid gap-8 border-b border-border pb-7 lg:grid-cols-[minmax(200px,0.85fr)_minmax(0,2fr)] lg:gap-10">
<AgentSummary agents={usage.agents} totals={usage.totals} />
<AgentChart daily={usage.daily} series={usage.agentSeries} />
</div>
<div className="border-b border-border py-7">
<h3 className="text-sm font-semibold text-foreground">Totals</h3>
<dl className="mt-5 grid grid-cols-2 gap-x-5 gap-y-6 sm:grid-cols-4">
<Total label="Cached input" value={compact.format(usage.totals.cachedInputTokens)} />
<Total label="Uncached input" value={compact.format(usage.totals.inputTokens - usage.totals.cachedInputTokens)} />
<Total label="Output" value={compact.format(usage.totals.outputTokens)} />
<Total label="Billed spend · USD" value={formatMoney(usage.totals.costUsd)} />
</dl>
</div>
<Breakdown key={selectedRange} models={usage.models} daily={usage.daily} totals={usage.totals} reduceMotion={reduceMotion} layoutId={`${breakdownId}-view`} view={breakdownView} onViewChange={setBreakdownView} />
</div>
)}
</section>
);
}
TSXcomponents/agents/usage-dashboard-data.ts
export interface UsageRecord {
/** UTC calendar day in YYYY-MM-DD format. */
date: string;
provider: string;
model: string;
/** Optional coding agent or client, such as Codex or Claude Code. */
agent?: string;
/** Input includes cached input; do not add cachedInputTokens to it. */
inputTokens: number;
cachedInputTokens?: number;
outputTokens: number;
requests: number;
/** Billed amount from the provider, in USD. No price is inferred. */
costUsd: number;
}
export interface UsageTotals {
inputTokens: number;
cachedInputTokens: number;
outputTokens: number;
requests: number;
costUsd: number;
}
export interface DailyUsage extends UsageTotals {
date: string;
}
export interface UsageBreakdown extends UsageTotals {
name: string;
provider?: string;
}
const DAY_MS = 86_400_000;
function dayStamp(value: string): number | null {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
const stamp = Date.parse(`${value}T00:00:00.000Z`);
return Number.isFinite(stamp) && new Date(stamp).toISOString().slice(0, 10) === value
? stamp
: null;
}
function isValidRecord(record: UsageRecord): boolean {
const values = [
record.inputTokens,
record.cachedInputTokens ?? 0,
record.outputTokens,
record.requests,
record.costUsd,
];
return (
dayStamp(record.date) !== null &&
record.provider.trim().length > 0 &&
record.model.trim().length > 0 &&
(record.agent === undefined || record.agent.trim().length > 0) &&
values.every((value) => Number.isFinite(value) && value >= 0) &&
values.slice(0, 4).every(Number.isInteger) &&
(record.cachedInputTokens ?? 0) <= record.inputTokens
);
}
function emptyTotals(): UsageTotals {
return { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, requests: 0, costUsd: 0 };
}
function addTotals(target: UsageTotals, record: UsageRecord): void {
target.inputTokens += record.inputTokens;
target.cachedInputTokens += record.cachedInputTokens ?? 0;
target.outputTokens += record.outputTokens;
target.requests += record.requests;
target.costUsd += record.costUsd;
}
/** Aggregates a complete UTC window, including zero-use days. Never estimates cost. */
export function summarizeUsage(records: UsageRecord[], days: 7 | 30) {
const valid = records.filter(isValidRecord);
const latest = valid.reduce<number | null>((max, record) => {
const stamp = dayStamp(record.date);
return stamp === null ? max : max === null ? stamp : Math.max(max, stamp);
}, null);
if (latest === null) return null;
const first = latest - (days - 1) * DAY_MS;
const daily: DailyUsage[] = Array.from({ length: days }, (_, index) => ({
date: new Date(first + index * DAY_MS).toISOString().slice(0, 10),
...emptyTotals(),
}));
const totals = emptyTotals();
const providers = new Map<string, UsageBreakdown>();
const agents = new Map<string, UsageBreakdown>();
const agentDaily = new Map<string, number[]>();
const models = new Map<string, UsageBreakdown>();
let recordCount = 0;
for (const record of valid) {
const stamp = dayStamp(record.date) ?? 0;
if (stamp < first || stamp > latest) continue;
recordCount += 1;
addTotals(daily[(stamp - first) / DAY_MS], record);
addTotals(totals, record);
let provider = providers.get(record.provider);
if (!provider) {
provider = { name: record.provider, ...emptyTotals() };
providers.set(record.provider, provider);
}
addTotals(provider, record);
const agentName = record.agent ?? record.provider;
const agentKey = `${agentName}\0${record.provider}`;
let agent = agents.get(agentKey);
if (!agent) {
agent = { name: agentName, provider: record.provider, ...emptyTotals() };
agents.set(agentKey, agent);
}
addTotals(agent, record);
let dailyTokens = agentDaily.get(agentKey);
if (!dailyTokens) {
dailyTokens = Array(days).fill(0);
agentDaily.set(agentKey, dailyTokens);
}
dailyTokens[(stamp - first) / DAY_MS] += record.inputTokens + record.outputTokens;
const key = `${record.provider}\0${record.model}`;
let model = models.get(key);
if (!model) {
model = { name: record.model, provider: record.provider, ...emptyTotals() };
models.set(key, model);
}
addTotals(model, record);
}
const byTokens = (a: UsageBreakdown, b: UsageBreakdown) =>
b.inputTokens + b.outputTokens - (a.inputTokens + a.outputTokens) || a.name.localeCompare(b.name);
const sortedAgents = [...agents.values()].sort(byTokens);
const agentNameCounts = new Map<string, number>();
for (const agent of sortedAgents) {
agentNameCounts.set(agent.name, (agentNameCounts.get(agent.name) ?? 0) + 1);
}
return {
daily,
totals,
providers: [...providers.values()].sort(byTokens),
agents: sortedAgents,
agentSeries: sortedAgents.map((agent) => ({
name: (agentNameCounts.get(agent.name) ?? 0) > 1 ? `${agent.name} · ${agent.provider}` : agent.name,
values: agentDaily.get(`${agent.name}\0${agent.provider}`) ?? Array(days).fill(0),
})),
models: [...models.values()].sort(byTokens),
recordCount,
startDate: daily[0].date,
endDate: daily[daily.length - 1].date,
};
}
TSXcomponents/agents/usage-provider-mark.tsx
import { cn } from "@/lib/utils";
/** Recognizable provider marks for the included coding-agent example. */
export function UsageProviderMark({
provider,
className,
}: {
provider?: string;
className?: string;
}) {
const name = provider?.trim().toLowerCase();
if (name !== "openai" && name !== "anthropic") return null;
return (
<svg
viewBox="0 0 24 24"
aria-hidden="true"
data-provider-mark={name}
className={cn("size-4 shrink-0", className)}
fill="currentColor"
>
<path d={name === "openai" ? OPENAI_MARK : CLAUDE_MARK} />
</svg>
);
}
const OPENAI_MARK = "M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.681zm1.097-2.365 2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5Z";
const CLAUDE_MARK = "m4.714 15.956 4.718-2.648.079-.23-.08-.128h-.23l-.79-.048-2.695-.073-2.337-.097-2.265-.122-.57-.121-.535-.704.055-.353.48-.321.685.06 1.518.104 2.277.157 1.651.098 2.447.255h.389l.054-.158-.133-.097-.103-.098-2.356-1.596-2.55-1.688-1.336-.972-.722-.491L2 6.223l-.158-1.008.655-.722.88.06.225.061.893.686 1.906 1.476 2.49 1.833.364.304.146-.104.018-.072-.164-.274-1.354-2.446-1.445-2.49-.644-1.032-.17-.619a2.972 2.972 0 0 1-.103-.729L6.287.133 6.7 0l.995.134.42.364.619 1.415L9.735 4.14l1.555 3.03.455.898.243.832.09.255h.159V9.01l.127-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.583.28.48.685-.067.444-.286 1.851-.558 2.903-.365 1.942h.213l.243-.242.983-1.306 1.652-2.064.728-.82.85-.904.547-.431h1.032l.759 1.129-.34 1.166-1.063 1.347-.88 1.142-1.263 1.7-.79 1.36.074.11.188-.02 2.853-.606 1.542-.28 1.84-.315.832.388.09.395-.327.807-1.967.486-2.307.462-3.436.813-.043.03.049.061 1.548.146.662.036h1.62l3.018.225.79.522.473.638-.08.485-1.213.62-1.64-.389-3.825-.91-1.31-.329h-.183v.11l1.093 1.068 2.003 1.81 2.508 2.33.127.578-.321.455-.34-.049-2.204-1.657-.85-.747-1.925-1.62h-.127v.17l.443.649 2.343 3.521.122 1.08-.17.353-.607.213-.668-.122-1.372-1.924-1.415-2.168-1.141-1.943-.14.08-.674 7.254-.316.37-.728.28-.607-.461-.322-.747.322-1.476.388-1.924.316-1.53.285-1.9.17-.632-.012-.042-.14.018-1.432 1.967-2.18 2.945-1.724 1.845-.413.164-.716-.37.066-.662.401-.589 2.386-3.036 1.439-1.882.929-1.086-.006-.158h-.055L4.138 18.56l-1.13.146-.485-.456.06-.746.231-.243 1.907-1.312Z";
TSXcomponents/previews/agents/usage-dashboard-example.tsx
"use client";
import {
UsageDashboard,
type UsageRecord,
} from "@/components/agents/usage-dashboard";
// Illustrative records with real coding agents, providers, and model IDs.
// Token counts and billed amounts are examples, not live usage or price quotes.
export const sampleUsageRecords: UsageRecord[] = Array.from(
{ length: 30 },
(_, index) => {
const date = new Date(Date.UTC(2026, 7, index + 28))
.toISOString()
.slice(0, 10);
const codexSwing = Math.sin(index * 0.62) * 0.25 + Math.cos(index * 0.17) * 0.12 + (index / 29) * 0.12;
const claudeSwing = Math.cos(index * 0.47 + 0.8) * 0.22 + Math.sin(index * 0.21) * 0.1;
const opusSwing = Math.sin(index * 0.86 + 1.2) * 0.2;
const codexInput = Math.round(940_000 * (1 + codexSwing));
const claudeInput = Math.round(710_000 * (1 + claudeSwing));
const opusInput = Math.round(185_000 * (1 + opusSwing));
return [
{
date,
agent: "Codex",
provider: "OpenAI",
model: "gpt-6-sol",
inputTokens: codexInput,
cachedInputTokens: Math.round(codexInput * 0.42),
outputTokens: Math.round(220_000 * (1 + codexSwing * 0.8)),
requests: Math.round(125 * (1 + codexSwing * 0.4)),
costUsd: Number((13.4 * (1 + codexSwing)).toFixed(2)),
},
{
date,
agent: "Claude Code",
provider: "Anthropic",
model: "claude-sonnet-5",
inputTokens: claudeInput,
cachedInputTokens: Math.round(claudeInput * 0.3),
outputTokens: Math.round(176_000 * (1 + claudeSwing * 0.65)),
requests: Math.round(97 * (1 + claudeSwing * 0.32)),
costUsd: Number((11.8 * (1 + claudeSwing * 0.7)).toFixed(2)),
},
{
date,
agent: "Claude Code",
provider: "Anthropic",
model: "claude-opus-5-5",
inputTokens: opusInput,
cachedInputTokens: Math.round(opusInput * 0.19),
outputTokens: Math.round(51_000 * (1 + opusSwing * 0.75)),
requests: Math.round(22 * (1 + opusSwing * 0.35)),
costUsd: Number((9.6 * (1 + opusSwing * 0.9)).toFixed(2)),
},
];
},
).flat();
export function UsageDashboardUsage() {
return <UsageDashboard records={sampleUsageRecords} defaultRange={30} />;
}
API Reference
recordsUsageRecord[]—title?stringUsage overviewrange?7 | 30Controlled range, in UTC calendar days.
—defaultRange?7 | 307onRangeChange?((range: 7 | 30) => void)—className?string—Updated