authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-08 21:31:20-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-08 21:31:20-04:00
log70c423a5e48ab34d8b1a033e798ab378b48dde5d
tree0a8b96442e63850e5cc97da4cb21a86931c81c06
parent89c320df0b0884a8c4df1cf596564c6bc725eb54
signature Signed by PGP key B5690EEEBB952194

fix(web): usage page loses the cost quality panel, gains a back button (#5756)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

1 files changed, 128 insertions(+), 141 deletions(-)

apps/web/src/components/usage/UsagePage.tsx+128-141
......@@ -1,5 +1,6 @@
11import type { UsageProviderKind } from "@t3tools/contracts";
2import { RefreshCwIcon } from "lucide-react";
2import { useCanGoBack, useNavigate, useRouter } from "@tanstack/react-router";
3import { ArrowLeftIcon, RefreshCwIcon } from "lucide-react";
34import { useMemo, useState } from "react";
45
56import { cn } from "../../lib/utils";
......@@ -27,6 +28,9 @@ export function UsagePage() {
2728 const [windowDays, setWindowDays] = useState<number>(30);
2829 const [metric, setMetric] = useState<UsageChartMetric>("cost");
2930 const [breakdown, setBreakdown] = useState<"model" | "day">("model");
31 const canGoBack = useCanGoBack();
32 const navigate = useNavigate();
33 const router = useRouter();
3034
3135 // Recomputed only when the window length changes, so a re-render does not
3236 // shift the range and refetch every environment.
......@@ -57,11 +61,27 @@ export function UsagePage() {
5761 <ScrollArea className="h-full">
5862 <div className="mx-auto flex w-full max-w-6xl flex-col gap-8 px-6 py-6">
5963 <header className="flex flex-wrap items-start justify-between gap-4">
60 <div className="flex flex-col gap-1">
61 <h1 className="text-2xl font-semibold text-foreground">Usage</h1>
62 <p className="text-sm text-muted-foreground">
63 {formatDayShort(window.sinceDay)} to {formatDayShort(window.untilDay)}
64 </p>
64 <div className="flex items-start gap-3">
65 <button
66 type="button"
67 aria-label="Back"
68 onClick={() => {
69 if (canGoBack) {
70 router.history.back();
71 return;
72 }
73 void navigate({ to: "/" });
74 }}
75 className="mt-1 cursor-pointer rounded-md border border-border p-2 text-muted-foreground hover:text-foreground"
76 >
77 <ArrowLeftIcon className="size-3.5" />
78 </button>
79 <div className="flex flex-col gap-1">
80 <h1 className="text-2xl font-semibold text-foreground">Usage</h1>
81 <p className="text-sm text-muted-foreground">
82 {formatDayShort(window.sinceDay)} to {formatDayShort(window.untilDay)}
83 </p>
84 </div>
6585 </div>
6686 <div className="flex items-center gap-2">
6787 <div className="flex overflow-hidden rounded-md border border-border">
......@@ -222,140 +242,116 @@ export function UsagePage() {
222242 />
223243 </section>
224244
225 <section className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,18rem)]">
226 <div className="flex flex-col gap-3">
227 <div className="flex items-center justify-between gap-3">
228 <h2 className="text-sm font-medium text-foreground">Breakdown</h2>
229 <div className="flex overflow-hidden rounded-md border border-border">
230 {(["model", "day"] as const).map((option) => (
231 <button
232 key={option}
233 type="button"
234 onClick={() => setBreakdown(option)}
235 className={cn(
236 "cursor-pointer px-2.5 py-1 text-[10px] tracking-wide uppercase",
237 option === breakdown
238 ? "bg-muted text-foreground"
239 : "text-muted-foreground hover:text-foreground",
240 )}
241 >
242 {option}
243 </button>
244 ))}
245 </div>
245 <section className="flex flex-col gap-3">
246 <div className="flex items-center justify-between gap-3">
247 <h2 className="text-sm font-medium text-foreground">Breakdown</h2>
248 <div className="flex overflow-hidden rounded-md border border-border">
249 {(["model", "day"] as const).map((option) => (
250 <button
251 key={option}
252 type="button"
253 onClick={() => setBreakdown(option)}
254 className={cn(
255 "cursor-pointer px-2.5 py-1 text-[10px] tracking-wide uppercase",
256 option === breakdown
257 ? "bg-muted text-foreground"
258 : "text-muted-foreground hover:text-foreground",
259 )}
260 >
261 {option}
262 </button>
263 ))}
246264 </div>
265 </div>
247266
248 {breakdown === "model" ? (
249 <table className="w-full text-sm">
250 <thead>
251 <tr className="border-b border-border text-left text-xs text-muted-foreground">
252 <th className="py-2 font-normal">Model</th>
253 <th className="py-2 text-right font-normal">Cost</th>
254 <th className="py-2 text-right font-normal">Share</th>
255 <th className="py-2 text-right font-normal">Tokens</th>
267 {breakdown === "model" ? (
268 <table className="w-full text-sm">
269 <thead>
270 <tr className="border-b border-border text-left text-xs text-muted-foreground">
271 <th className="py-2 font-normal">Model</th>
272 <th className="py-2 text-right font-normal">Cost</th>
273 <th className="py-2 text-right font-normal">Share</th>
274 <th className="py-2 text-right font-normal">Tokens</th>
275 </tr>
276 </thead>
277 <tbody>
278 {merged.models.length === 0 ? (
279 <tr>
280 <td colSpan={4} className="py-6 text-center text-muted-foreground">
281 No activity in this window.
282 </td>
256283 </tr>
257 </thead>
258 <tbody>
259 {merged.models.length === 0 ? (
260 <tr>
261 <td colSpan={4} className="py-6 text-center text-muted-foreground">
262 No activity in this window.
284 ) : (
285 merged.models.map((model) => (
286 <tr
287 key={`${model.provider}:${model.model}`}
288 className="border-b border-border/50"
289 >
290 <td className="py-2 text-foreground">
291 <span className="flex items-center gap-2">
292 <ProviderMark provider={model.provider} className="size-3.5" />
293 {model.model}
294 </span>
295 </td>
296 <td className="py-2 text-right text-foreground tabular-nums">
297 {formatUsd(model.costUsd)}
298 </td>
299 <td className="py-2 text-right text-muted-foreground tabular-nums">
300 {formatPercent(model.costShare)}
301 </td>
302 <td className="py-2 text-right text-muted-foreground tabular-nums">
303 {formatTokens(model.totalTokens)}
263304 </td>
264305 </tr>
265 ) : (
266 merged.models.map((model) => (
267 <tr
268 key={`${model.provider}:${model.model}`}
269 className="border-b border-border/50"
270 >
271 <td className="py-2 text-foreground">
272 <span className="flex items-center gap-2">
273 <ProviderMark provider={model.provider} className="size-3.5" />
274 {model.model}
275 </span>
276 </td>
277 <td className="py-2 text-right text-foreground tabular-nums">
278 {formatUsd(model.costUsd)}
279 </td>
280 <td className="py-2 text-right text-muted-foreground tabular-nums">
281 {formatPercent(model.costShare)}
282 </td>
283 <td className="py-2 text-right text-muted-foreground tabular-nums">
284 {formatTokens(model.totalTokens)}
285 </td>
286 </tr>
287 ))
288 )}
289 </tbody>
290 </table>
291 ) : (
292 <table className="w-full text-sm">
293 <thead>
294 <tr className="border-b border-border text-left text-xs text-muted-foreground">
295 <th className="py-2 font-normal">Day</th>
296 {PROVIDER_ORDER.map((provider) => (
297 <th key={provider} className="py-2 text-right font-normal">
298 {PROVIDER_LABEL[provider]}
299 </th>
300 ))}
301 <th className="py-2 text-right font-normal">Total</th>
302 <th className="py-2 text-right font-normal">Tokens</th>
306 ))
307 )}
308 </tbody>
309 </table>
310 ) : (
311 <table className="w-full text-sm">
312 <thead>
313 <tr className="border-b border-border text-left text-xs text-muted-foreground">
314 <th className="py-2 font-normal">Day</th>
315 {PROVIDER_ORDER.map((provider) => (
316 <th key={provider} className="py-2 text-right font-normal">
317 {PROVIDER_LABEL[provider]}
318 </th>
319 ))}
320 <th className="py-2 text-right font-normal">Total</th>
321 <th className="py-2 text-right font-normal">Tokens</th>
322 </tr>
323 </thead>
324 <tbody>
325 {recentDays.length === 0 ? (
326 <tr>
327 <td colSpan={5} className="py-6 text-center text-muted-foreground">
328 No activity in this window.
329 </td>
303330 </tr>
304 </thead>
305 <tbody>
306 {recentDays.length === 0 ? (
307 <tr>
308 <td colSpan={5} className="py-6 text-center text-muted-foreground">
309 No activity in this window.
331 ) : (
332 recentDays.map((day) => (
333 <tr key={day.day} className="border-b border-border/50">
334 <td className="py-2 text-foreground">{formatDayShort(day.day)}</td>
335 {PROVIDER_ORDER.map((provider) => (
336 <td
337 key={provider}
338 className="py-2 text-right text-muted-foreground tabular-nums"
339 >
340 {formatUsd(day.byProvider.get(provider)?.costUsd ?? 0)}
341 </td>
342 ))}
343 <td className="py-2 text-right text-foreground tabular-nums">
344 {formatUsd(day.costUsd)}
345 </td>
346 <td className="py-2 text-right text-muted-foreground tabular-nums">
347 {formatTokens(day.totalTokens)}
310348 </td>
311349 </tr>
312 ) : (
313 recentDays.map((day) => (
314 <tr key={day.day} className="border-b border-border/50">
315 <td className="py-2 text-foreground">{formatDayShort(day.day)}</td>
316 {PROVIDER_ORDER.map((provider) => (
317 <td
318 key={provider}
319 className="py-2 text-right text-muted-foreground tabular-nums"
320 >
321 {formatUsd(day.byProvider.get(provider)?.costUsd ?? 0)}
322 </td>
323 ))}
324 <td className="py-2 text-right text-foreground tabular-nums">
325 {formatUsd(day.costUsd)}
326 </td>
327 <td className="py-2 text-right text-muted-foreground tabular-nums">
328 {formatTokens(day.totalTokens)}
329 </td>
330 </tr>
331 ))
332 )}
333 </tbody>
334 </table>
335 )}
336 </div>
337
338 <div className="flex flex-col gap-3">
339 <h2 className="text-sm font-medium text-foreground">Cost quality</h2>
340 <dl className="flex flex-col">
341 <QualityRow
342 label="Provider reported"
343 value={formatPercent(merged.costQuality.providerReportedShare)}
344 />
345 <QualityRow
346 label="Model priced"
347 value={formatPercent(merged.costQuality.modelPricedShare)}
348 />
349 <QualityRow
350 label="Unpriced"
351 value={formatPercent(merged.costQuality.unpricedShare)}
352 />
353 <QualityRow
354 label="Cache savings"
355 value={formatUsd(merged.costQuality.cacheSavingsUsd)}
356 />
357 </dl>
358 </div>
350 ))
351 )}
352 </tbody>
353 </table>
354 )}
359355 </section>
360356 </>
361357 )}
......@@ -394,15 +390,6 @@ function Metric({
394390 );
395391}
396392
397function QualityRow({ label, value }: { readonly label: string; readonly value: string }) {
398 return (
399 <div className="flex items-center justify-between border-b border-border/50 py-2 text-sm">
400 <dt className="text-muted-foreground">{label}</dt>
401 <dd className="text-foreground tabular-nums">{value}</dd>
402 </div>
403 );
404}
405
406393/**
407394 * Says plainly when the totals are incomplete: an environment still answering,
408395 * one that failed, or one whose transcripts another environment already