SPB Git

Repository Public

page.tsx 8.3 KB 127 lines tsx

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
Files main
Menu

Theme