"use client"; import { useId, useState } from "react"; import { AlertTriangle } from "lucide-react"; import { cn, timeAgo } from "@/lib/utils"; import { ms, providerTone, usd } from "./format"; export interface UsagePayload { days: number; byModel: { model: string; provider: string; calls: number; errors: number; inputTokens: number; outputTokens: number; costUsd: number; latency: number | null; ttft: number | null }[]; byDay: { day: string; costUsd: number; calls: number }[]; byTask: { task: string; calls: number; costUsd: number }[]; recent: { id: string; model: string; provider: string; task: string; ok: boolean; error: string | null; costUsd: number | null; latencyMs: number | null; createdAt: string }[] } const n = (v: unknown) => Number(v ?? 0); function SpendBars({ byDay, days }: { byDay: UsagePayload["byDay"]; days: number }) { const id = useId(); const [hover, setHover] = useState(null); const series: { day: string; cost: number; calls: number }[] = []; const map = new Map(byDay.map((d) => [d.day, d])); for (let i = days - 1; i >= 0; i--) { const d = new Date(Date.now() - i * 864e5).toISOString().slice(0, 10); const r = map.get(d); series.push({ day: d, cost: n(r?.costUsd), calls: n(r?.calls) }); } const W = 600, H = 150, P = { l: 40, r: 8, t: 10, b: 22 }; const max = Math.max(0.01, ...series.map((s) => s.cost)); const bw = (W - P.l - P.r) / series.length; const Y = (v: number) => P.t + (1 - v / max) * (H - P.t - P.b); const total = series.reduce((a, s) => a + s.cost, 0); const ticks = [0, max / 2, max]; return (
{usd(total, 2)}spent in the last {days} days · {series.reduce((a, s) => a + s.calls, 0)} calls
Daily AI spend {ticks.map((t) => {usd(t, 2)})} {series.map((s, i) => { const x = P.l + i * bw; const h = Math.max(s.cost > 0 ? 2 : 0, (H - P.t - P.b) * (s.cost / max)); return setHover(i)} onMouseLeave={() => setHover(null)}> {h > 0 && } {(i === 0 || i === series.length - 1 || (i % 7 === 0 && i < series.length - 3)) && {s.day.slice(5)}} ; })} {hover != null &&
{series[hover].day}
{usd(series[hover].cost, 3)} · {series[hover].calls} calls
}
); } export function UsagePanel({ usage, budget, onBudget }: { usage: UsagePayload | undefined; budget: number | null; onBudget: (usd: number) => void }) { if (!usage) return
Loading usage…
; const errors = usage.recent.filter((r) => !r.ok).slice(0, 12); return (
Spend by day
{usage.byTask.map((t) => {t.task} {n(t.calls)} · {usd(n(t.costUsd), 3)})}{!usage.byTask.length && No usage recorded yet.}
By model
{usage.byModel.map((m) => )}{!usage.byModel.length && }
ModelCallsTokensp50Cost
{m.model}{n(m.errors) > 0 && ·{n(m.errors)}✕}{n(m.calls)}{Math.round((n(m.inputTokens) + n(m.outputTokens)) / 1000)}K{ms(m.latency)}{usd(n(m.costUsd), 3)}
No calls yet.
Recent errors & refusals
{errors.length ?
    {errors.map((e) =>
  • {e.model}{e.task}{e.error}{timeAgo(e.createdAt)}
  • )}
:
No errors in the last 50 calls.
}
); }