Repository Public
1'use client';2import { useModels, useSpend } from '@/lib/queries';3import { DASH, fmtTokens, fmtUsd } from '@/lib/format';4import { PROVIDER_LABEL } from '@/lib/status';5import { EmptyState, ErrorState, Section, Skeleton } from '@/components/ui/misc';6import { IllusSpend } from '@/components/ui/illustrations';7import { BudgetBar } from '@/components/missions/bits';8import { cn } from '@/lib/cn';910const dayLabel = (day: string) => {11 const d = new Date(`${day.slice(0, 10)}T12:00:00`);12 return Number.isNaN(d.getTime()) ? { short: day, long: day } : { short: d.toLocaleDateString('en-CA', { weekday: 'short' }), long: d.toLocaleDateString('en-CA', { weekday: 'long', month: 'long', day: 'numeric' }) };13};1415/** Seven daily bars with the cap as a reference line. Values are also in a visually hidden table. */16function WeekChart({ days, cap }: { days: { day: string; usd: number }[]; cap: number }) {17 const top = Math.max(...days.map((d) => d.usd), 0.01);18 const capVisible = cap <= top * 1.6;19 const max = capVisible ? Math.max(cap, top) * 1.08 : top * 1.2;20 const capPct = (cap / max) * 82; // bars use 82 % of the height (the value label sits above them)21 return (22 <figure className="px-4 pb-4 pt-5" data-testid="week-chart">23 <p className="tnum -mt-2 mb-3 text-right text-[11px] text-muted" data-testid="week-cap">{capVisible ? `dashed line: daily cap ${fmtUsd(cap, { compact: true })}` : `daily cap ${fmtUsd(cap, { compact: true })} — above this scale`}</p>24 <div className="relative flex h-40 items-end gap-2 sm:gap-4" aria-hidden>25 {capVisible && <div className="pointer-events-none absolute inset-x-0 border-t border-dashed border-line-3" style={{ bottom: `${capPct}%` }} />}26 {days.map((d, i) => {27 const last = i === days.length - 1;28 const over = d.usd >= cap;29 return (30 <div key={d.day} className="flex h-full min-w-0 flex-1 flex-col items-center justify-end gap-1.5" title={`${dayLabel(d.day).long}: ${fmtUsd(d.usd)}`}>31 <span className={cn('tnum text-[11px]', last ? 'font-semibold text-text' : 'text-muted')}>{fmtUsd(d.usd, { compact: true })}</span>32 <div className={cn('w-full max-w-14 rounded-t-[7px] rounded-b-[3px]', over ? 'bg-error-vivid' : last ? 'bg-brand' : 'bg-[color-mix(in_srgb,var(--color-brand-2)_55%,var(--color-surface-2))]')} style={{ height: `${Math.max((d.usd / max) * 82, d.usd > 0 ? 1.5 : 0.5)}%` }} data-testid="week-bar" />33 </div>34 );35 })}36 </div>37 <div className="mt-2 flex gap-2 sm:gap-4" aria-hidden>38 {days.map((d, i) => <span key={d.day} className={cn('min-w-0 flex-1 truncate text-center text-[11px]', i === days.length - 1 ? 'font-semibold text-text' : 'text-muted')}>{i === days.length - 1 ? 'Today' : dayLabel(d.day).short}</span>)}39 </div>40 <table className="sr-only">41 <caption>Spend per day, last 7 days, in US dollars. Daily cap {fmtUsd(cap)}.</caption>42 <thead><tr><th scope="col">Day</th><th scope="col">Spend</th></tr></thead>43 <tbody>{days.map((d) => <tr key={d.day}><th scope="row">{dayLabel(d.day).long}</th><td>{fmtUsd(d.usd)}</td></tr>)}</tbody>44 </table>45 </figure>46 );47}4849export default function SpendPage() {50 const q = useSpend();51 const models = useModels().data ?? [];52 const s = q.data;53 const week = s ? s.last7Days.reduce((n, d) => n + d.usd, 0) : null;54 const byModel = s ? [...s.byModel].sort((a, b) => b.usd - a.usd) : [];55 const totalModel = byModel.reduce((n, m) => n + m.usd, 0);56 return (57 <div className="h-full overflow-y-auto" data-testid="spend-page">58 <div className="mx-auto flex w-full max-w-4xl flex-col gap-5 px-4 pb-32 pt-1 md:px-8 md:pb-12 md:pt-6">59 <div className="hidden md:block">60 <h1 className="text-xl font-bold tracking-tight">Spend</h1>61 <p className="text-[13px] text-text-2">Model usage priced from the provider price table at the time of each call. Times are server days.</p>62 </div>63 {q.isError ? (64 <div className="card"><ErrorState error={q.error} onRetry={() => q.refetch()} /></div>65 ) : !s ? (66 <><Skeleton className="h-28 rounded-md" /><Skeleton className="h-64 rounded-md" /></>67 ) : (68 <>69 <div className="grid gap-4 sm:grid-cols-3">70 <div className="card p-4 sm:col-span-2" data-testid="spend-today-card">71 <p className="text-xs font-semibold uppercase tracking-wide text-muted">Today</p>72 <p className="tnum mt-1.5 flex items-baseline gap-2">73 <span className="text-[34px] font-bold leading-none tracking-tight" data-testid="spend-today">{fmtUsd(s.todayUsd)}</span>74 <span className="text-sm text-muted">of <span data-testid="spend-cap">{fmtUsd(s.dailyBudgetUsd, { compact: true })}</span> daily cap</span>75 </p>76 <BudgetBar spent={s.todayUsd} budget={s.dailyBudgetUsd} className="mt-3.5 h-2" label="Daily cap used" />77 <p className="tnum mt-2 text-xs text-muted">{s.todayUsd >= s.dailyBudgetUsd ? 'Cap reached — model calls are stopped until tomorrow.' : `${fmtUsd(s.dailyBudgetUsd - s.todayUsd)} left today · ${((s.todayUsd / s.dailyBudgetUsd) * 100).toFixed(0)} % used`}</p>78 </div>79 <div className="card p-4">80 <p className="text-xs font-semibold uppercase tracking-wide text-muted">Last 7 days</p>81 <p className="tnum mt-1.5 text-[34px] font-bold leading-none tracking-tight" data-testid="spend-week">{fmtUsd(week)}</p>82 <p className="tnum mt-3 text-xs text-muted">{s.last7Days.length > 0 && week != null ? `${fmtUsd(week / s.last7Days.length)} per day on average` : DASH}</p>83 </div>84 </div>85 <Section title="Daily spend">86 {s.last7Days.length === 0 ? <EmptyState illustration={<IllusSpend />} title="No spend recorded" description="Days appear here once a mission has made a model call." /> : <WeekChart days={s.last7Days} cap={s.dailyBudgetUsd} />}87 </Section>88 <Section title="By model">89 {byModel.length === 0 ? (90 <p className="px-4 py-6 text-center text-[13px] text-muted">No model call recorded yet.</p>91 ) : (92 <div className="overflow-x-auto">93 <table className="w-full min-w-[480px] text-[13px]" data-testid="by-model">94 <thead>95 <tr className="text-left text-[11px] uppercase tracking-wide text-muted">96 <th scope="col" className="px-4 py-2 font-semibold">Model</th>97 <th scope="col" className="px-3 py-2 text-right font-semibold">Input</th>98 <th scope="col" className="px-3 py-2 text-right font-semibold">Output</th>99 <th scope="col" className="px-3 py-2 text-right font-semibold">Share</th>100 <th scope="col" className="px-4 py-2 text-right font-semibold">Spend</th>101 </tr>102 </thead>103 <tbody className="tnum divide-y divide-line border-t border-line">104 {byModel.map((m) => (105 <tr key={`${m.provider}/${m.model}`}>106 <th scope="row" className="px-4 py-2.5 text-left font-normal">107 <span className="block font-medium text-text">{models.find((x) => x.id === m.model)?.label ?? m.model}</span>108 <span className="block text-[11px] text-muted">{PROVIDER_LABEL[m.provider]} · <span className="font-mono">{m.model}</span></span>109 </th>110 <td className="px-3 py-2.5 text-right text-text-2">{fmtTokens(m.inputTokens)}</td>111 <td className="px-3 py-2.5 text-right text-text-2">{fmtTokens(m.outputTokens)}</td>112 <td className="px-3 py-2.5 text-right text-text-2">{totalModel > 0 ? `${((m.usd / totalModel) * 100).toFixed(0)} %` : DASH}</td>113 <td className="px-4 py-2.5 text-right font-semibold text-text">{fmtUsd(m.usd)}</td>114 </tr>115 ))}116 </tbody>117 </table>118 </div>119 )}120 </Section>121 </>122 )}123 </div>124 </div>125 );126}127