authorgravatar for 269035359+t3-code[bot]@users.noreply.github.comt3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> 2026-08-09 23:26:26+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-09 23:26:26+02:00
logbd18d8d6d1feb23dd05dc357e34e353547a51d40
tree58e8d151ae20c83535614a1df24850f98100b6e5
parent659986ce384d29123269820ccb8340ab4d824cdd
signature Signed by PGP key B5690EEEBB952194

fix(web): unify usage page chrome (#5823)


1 files changed, 273 insertions(+), 267 deletions(-)

apps/web/src/components/usage/UsagePage.tsx+273-267
......@@ -1,8 +1,8 @@
11import type { UsageProviderKind } from "@t3tools/contracts";
2import { useCanGoBack, useNavigate, useRouter } from "@tanstack/react-router";
3import { ArrowLeftIcon, CheckIcon, RefreshCwIcon, XIcon } from "lucide-react";
2import { CheckIcon, RefreshCwIcon, XIcon } from "lucide-react";
43import { useMemo, useState } from "react";
54
5import { isElectron } from "../../env";
66import { cn } from "../../lib/utils";
77import { useUsage, type EnvironmentUsageStatus } from "../../state/usage";
88import {
......@@ -15,6 +15,8 @@ import {
1515 makeWindow,
1616} from "@t3tools/shared/usageFormat";
1717import { ScrollArea } from "../ui/scroll-area";
18import { SidebarInset } from "../ui/sidebar";
19import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../../workspaceTitlebar";
1820import { UsageChartLegend, UsageProviderChart, type UsageChartMetric } from "./UsageProviderChart";
1921import { PROVIDER_COLOR, PROVIDER_LABEL, PROVIDER_MARK, PROVIDER_ORDER } from "./usageProviders";
2022
......@@ -28,9 +30,6 @@ export function UsagePage() {
2830 const [windowDays, setWindowDays] = useState<number>(30);
2931 const [metric, setMetric] = useState<UsageChartMetric>("cost");
3032 const [breakdown, setBreakdown] = useState<"model" | "day">("model");
31 const canGoBack = useCanGoBack();
32 const navigate = useNavigate();
33 const router = useRouter();
3433
3534 // Recomputed only when the window length changes, so a re-render does not
3635 // shift the range and refetch every environment.
......@@ -63,143 +62,207 @@ export function UsagePage() {
6362 const cachedShare = observedInput === 0 ? 0 : merged.cachedInputTokens / observedInput;
6463
6564 return (
66 <ScrollArea className="h-full">
67 <div className="mx-auto flex w-full max-w-6xl flex-col gap-8 px-6 py-6">
68 <header className="flex flex-wrap items-start justify-between gap-4">
69 <div className="flex items-start gap-3">
70 <button
71 type="button"
72 aria-label="Back"
73 onClick={() => {
74 if (canGoBack) {
75 router.history.back();
76 return;
77 }
78 void navigate({ to: "/" });
79 }}
80 className="mt-1 cursor-pointer rounded-md border border-border p-2 text-muted-foreground hover:text-foreground"
81 >
82 <ArrowLeftIcon className="size-3.5" />
83 </button>
84 <div className="flex flex-col gap-1">
85 <h1 className="text-2xl font-semibold text-foreground">Usage</h1>
65 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate">
66 <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground">
67 {!isElectron && (
68 <header
69 className={cn(
70 "workspace-topbar px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",
71 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
72 )}
73 >
74 <h1 className="text-sm font-medium text-foreground">Usage</h1>
75 </header>
76 )}
77
78 {isElectron && (
79 <div
80 className={cn(
81 "drag-region flex h-[52px] shrink-0 items-center px-5 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none wco:h-[env(titlebar-area-height)] wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]",
82 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
83 )}
84 >
85 <h1 className="text-xs font-medium tracking-wide text-muted-foreground/70">Usage</h1>
86 </div>
87 )}
88
89 <ScrollArea className="min-h-0 flex-1">
90 <div className="mx-auto flex w-full max-w-6xl flex-col gap-8 px-6 py-6">
91 <div className="flex flex-wrap items-center justify-between gap-4">
8692 <p className="text-sm text-muted-foreground">
8793 {formatDayShort(window.sinceDay)} to {formatDayShort(window.untilDay)}
8894 </p>
89 </div>
90 </div>
91 <div className="flex items-center gap-2">
92 <div className="flex overflow-hidden rounded-md border border-border">
93 {WINDOW_OPTIONS.map((option) => (
95 <div className="flex items-center gap-2">
96 <div className="flex overflow-hidden rounded-md border border-border">
97 {WINDOW_OPTIONS.map((option) => (
98 <button
99 key={option.days}
100 type="button"
101 onClick={() => setWindowDays(option.days)}
102 className={cn(
103 "cursor-pointer px-3 py-1.5 text-xs",
104 option.days === windowDays
105 ? "bg-muted text-foreground"
106 : "text-muted-foreground hover:text-foreground",
107 )}
108 >
109 {option.label}
110 </button>
111 ))}
112 </div>
94113 <button
95 key={option.days}
96114 type="button"
97 onClick={() => setWindowDays(option.days)}
98 className={cn(
99 "cursor-pointer px-3 py-1.5 text-xs",
100 option.days === windowDays
101 ? "bg-muted text-foreground"
102 : "text-muted-foreground hover:text-foreground",
103 )}
115 onClick={refresh}
116 aria-label="Refresh usage"
117 className="cursor-pointer rounded-md border border-border p-2 text-muted-foreground hover:text-foreground"
104118 >
105 {option.label}
119 <RefreshCwIcon className="size-3.5" />
106120 </button>
107 ))}
121 </div>
108122 </div>
109 <button
110 type="button"
111 onClick={refresh}
112 aria-label="Refresh usage"
113 className="cursor-pointer rounded-md border border-border p-2 text-muted-foreground hover:text-foreground"
114 >
115 <RefreshCwIcon className="size-3.5" />
116 </button>
117 </div>
118 </header>
119123
120 {settling ? (
121 <>
122 {environments.length > 1 ? <UsageDeviceStrip environments={environments} /> : null}
123 <UsageSkeleton />
124 </>
125 ) : (
126 <>
127 <UsageCoverageNotice
128 environments={environments}
129 duplicateSources={merged.duplicateSources}
130 staleEnvironments={merged.staleEnvironments}
131 />
124 {settling ? (
125 <>
126 {environments.length > 1 ? <UsageDeviceStrip environments={environments} /> : null}
127 <UsageSkeleton />
128 </>
129 ) : (
130 <>
131 <UsageCoverageNotice
132 environments={environments}
133 duplicateSources={merged.duplicateSources}
134 staleEnvironments={merged.staleEnvironments}
135 />
132136
133 {/* Cost first: the financial answer, then the provider split. */}
134 <section className="grid gap-6 lg:grid-cols-[minmax(0,20rem)_minmax(0,1fr)]">
135 {/* The summary follows the chart toggle, so the headline and the
137 {/* Cost first: the financial answer, then the provider split. */}
138 <section className="grid gap-6 lg:grid-cols-[minmax(0,20rem)_minmax(0,1fr)]">
139 {/* The summary follows the chart toggle, so the headline and the
136140 series are always reading the same units. */}
137 <div className="flex flex-col gap-5">
138 <div className="flex flex-col gap-1">
139 <span className="text-xs tracking-wide text-muted-foreground uppercase">
140 {metric === "cost" ? "Raw token cost" : "Processed tokens"}
141 </span>
142 <span className="text-4xl font-semibold text-foreground tabular-nums">
143 {metric === "cost"
144 ? `${formatUsd(merged.costUsd)}*`
145 : formatTokens(merged.totalTokens)}
146 </span>
147 <span className="text-xs text-muted-foreground">
148 {metric === "cost"
149 ? "* if billed at full API rate"
150 : `Input, cache reads and output across ${formatCount(merged.sessions)} sessions.`}
151 </span>
152 </div>
153
154 {orderedProviders.map((provider) => {
155 const share = metric === "cost" ? provider.costShare : provider.tokenShare;
156 return (
157 <div key={provider.provider} className="flex flex-col gap-1.5">
158 <div className="flex items-baseline justify-between">
159 <span className="flex items-center gap-2 text-sm text-foreground">
160 <ProviderMark provider={provider.provider} className="size-4" />
161 {PROVIDER_LABEL[provider.provider]}
162 </span>
163 <span className="text-sm text-foreground tabular-nums">
164 {metric === "cost"
165 ? formatUsd(provider.costUsd)
166 : formatTokens(provider.totalTokens)}
167 </span>
168 </div>
169 <div className="h-1 w-full overflow-hidden rounded-full bg-muted">
170 <div
171 className="h-full"
172 style={{
173 width: `${(share * 100).toFixed(1)}%`,
174 backgroundColor: PROVIDER_COLOR[provider.provider],
175 }}
176 />
177 </div>
141 <div className="flex flex-col gap-5">
142 <div className="flex flex-col gap-1">
143 <span className="text-xs tracking-wide text-muted-foreground uppercase">
144 {metric === "cost" ? "Raw token cost" : "Processed tokens"}
145 </span>
146 <span className="text-4xl font-semibold text-foreground tabular-nums">
147 {metric === "cost"
148 ? `${formatUsd(merged.costUsd)}*`
149 : formatTokens(merged.totalTokens)}
150 </span>
178151 <span className="text-xs text-muted-foreground">
179152 {metric === "cost"
180 ? `${formatPercent(share)} of cost · ${formatTokens(provider.totalTokens)} tokens`
181 : `${formatPercent(share)} of tokens · ${formatUsd(provider.costUsd)}`}
153 ? "* if billed at full API rate"
154 : `Input, cache reads and output across ${formatCount(merged.sessions)} sessions.`}
182155 </span>
183156 </div>
184 );
185 })}
186 </div>
187157
188 <div className="flex flex-col gap-3">
189 <div className="flex flex-wrap items-center justify-between gap-3">
190 <h2 className="text-sm font-medium text-foreground">
191 Daily {metric === "tokens" ? "processed tokens" : "cost"}
192 </h2>
193 <div className="flex items-center gap-4">
158 {orderedProviders.map((provider) => {
159 const share = metric === "cost" ? provider.costShare : provider.tokenShare;
160 return (
161 <div key={provider.provider} className="flex flex-col gap-1.5">
162 <div className="flex items-baseline justify-between">
163 <span className="flex items-center gap-2 text-sm text-foreground">
164 <ProviderMark provider={provider.provider} className="size-4" />
165 {PROVIDER_LABEL[provider.provider]}
166 </span>
167 <span className="text-sm text-foreground tabular-nums">
168 {metric === "cost"
169 ? formatUsd(provider.costUsd)
170 : formatTokens(provider.totalTokens)}
171 </span>
172 </div>
173 <div className="h-1 w-full overflow-hidden rounded-full bg-muted">
174 <div
175 className="h-full"
176 style={{
177 width: `${(share * 100).toFixed(1)}%`,
178 backgroundColor: PROVIDER_COLOR[provider.provider],
179 }}
180 />
181 </div>
182 <span className="text-xs text-muted-foreground">
183 {metric === "cost"
184 ? `${formatPercent(share)} of cost · ${formatTokens(provider.totalTokens)} tokens`
185 : `${formatPercent(share)} of tokens · ${formatUsd(provider.costUsd)}`}
186 </span>
187 </div>
188 );
189 })}
190 </div>
191
192 <div className="flex flex-col gap-3">
193 <div className="flex flex-wrap items-center justify-between gap-3">
194 <h2 className="text-sm font-medium text-foreground">
195 Daily {metric === "tokens" ? "processed tokens" : "cost"}
196 </h2>
197 <div className="flex items-center gap-4">
198 <div className="flex overflow-hidden rounded-md border border-border">
199 {(["cost", "tokens"] as const).map((option) => (
200 <button
201 key={option}
202 type="button"
203 onClick={() => setMetric(option)}
204 className={cn(
205 "cursor-pointer px-2.5 py-1 text-[10px] tracking-wide uppercase",
206 option === metric
207 ? "bg-muted text-foreground"
208 : "text-muted-foreground hover:text-foreground",
209 )}
210 >
211 {option}
212 </button>
213 ))}
214 </div>
215 <UsageChartLegend />
216 </div>
217 </div>
218 <UsageProviderChart days={days} daily={merged.daily} metric={metric} />
219 </div>
220 </section>
221
222 <section className="grid grid-cols-2 gap-px border-y border-border bg-border md:grid-cols-5">
223 <Metric
224 label="Processed tokens"
225 value={formatTokens(merged.totalTokens)}
226 detail={`${formatTokens(dailyAverage)} per active day`}
227 />
228 <Metric
229 label="Cached input"
230 value={formatTokens(merged.cachedInputTokens)}
231 detail={`${formatPercent(cachedShare)} of observed input`}
232 />
233 <Metric
234 label="Uncached input"
235 value={formatTokens(merged.uncachedInputTokens)}
236 detail={`${formatTokens(merged.cacheCreationTokens)} cache writes`}
237 />
238 <Metric
239 label="Output"
240 value={formatTokens(merged.outputTokens)}
241 detail={`includes ${formatTokens(merged.reasoningTokens)} reasoning`}
242 />
243 <Metric
244 label="Cache savings"
245 value={formatUsd(merged.costQuality.cacheSavingsUsd)}
246 detail={
247 merged.costUsd > 0
248 ? `${(merged.costQuality.cacheSavingsUsd / merged.costUsd).toFixed(1)}x the raw token cost`
249 : "vs full input rates"
250 }
251 />
252 </section>
253
254 <section className="flex flex-col gap-3">
255 <div className="flex items-center justify-between gap-3">
256 <h2 className="text-sm font-medium text-foreground">Breakdown</h2>
194257 <div className="flex overflow-hidden rounded-md border border-border">
195 {(["cost", "tokens"] as const).map((option) => (
258 {(["model", "day"] as const).map((option) => (
196259 <button
197260 key={option}
198261 type="button"
199 onClick={() => setMetric(option)}
262 onClick={() => setBreakdown(option)}
200263 className={cn(
201264 "cursor-pointer px-2.5 py-1 text-[10px] tracking-wide uppercase",
202 option === metric
265 option === breakdown
203266 ? "bg-muted text-foreground"
204267 : "text-muted-foreground hover:text-foreground",
205268 )}
......@@ -208,160 +271,103 @@ export function UsagePage() {
208271 </button>
209272 ))}
210273 </div>
211 <UsageChartLegend />
212274 </div>
213 </div>
214 <UsageProviderChart days={days} daily={merged.daily} metric={metric} />
215 </div>
216 </section>
217
218 <section className="grid grid-cols-2 gap-px border-y border-border bg-border md:grid-cols-5">
219 <Metric
220 label="Processed tokens"
221 value={formatTokens(merged.totalTokens)}
222 detail={`${formatTokens(dailyAverage)} per active day`}
223 />
224 <Metric
225 label="Cached input"
226 value={formatTokens(merged.cachedInputTokens)}
227 detail={`${formatPercent(cachedShare)} of observed input`}
228 />
229 <Metric
230 label="Uncached input"
231 value={formatTokens(merged.uncachedInputTokens)}
232 detail={`${formatTokens(merged.cacheCreationTokens)} cache writes`}
233 />
234 <Metric
235 label="Output"
236 value={formatTokens(merged.outputTokens)}
237 detail={`includes ${formatTokens(merged.reasoningTokens)} reasoning`}
238 />
239 <Metric
240 label="Cache savings"
241 value={formatUsd(merged.costQuality.cacheSavingsUsd)}
242 detail={
243 merged.costUsd > 0
244 ? `${(merged.costQuality.cacheSavingsUsd / merged.costUsd).toFixed(1)}x the raw token cost`
245 : "vs full input rates"
246 }
247 />
248 </section>
249
250 <section className="flex flex-col gap-3">
251 <div className="flex items-center justify-between gap-3">
252 <h2 className="text-sm font-medium text-foreground">Breakdown</h2>
253 <div className="flex overflow-hidden rounded-md border border-border">
254 {(["model", "day"] as const).map((option) => (
255 <button
256 key={option}
257 type="button"
258 onClick={() => setBreakdown(option)}
259 className={cn(
260 "cursor-pointer px-2.5 py-1 text-[10px] tracking-wide uppercase",
261 option === breakdown
262 ? "bg-muted text-foreground"
263 : "text-muted-foreground hover:text-foreground",
264 )}
265 >
266 {option}
267 </button>
268 ))}
269 </div>
270 </div>
271275
272 {breakdown === "model" ? (
273 <table className="w-full text-sm">
274 <thead>
275 <tr className="border-b border-border text-left text-xs text-muted-foreground">
276 <th className="py-2 font-normal">Model</th>
277 <th className="py-2 text-right font-normal">Cost</th>
278 <th className="py-2 text-right font-normal">Share</th>
279 <th className="py-2 text-right font-normal">Tokens</th>
280 </tr>
281 </thead>
282 <tbody>
283 {merged.models.length === 0 ? (
284 <tr>
285 <td colSpan={4} className="py-6 text-center text-muted-foreground">
286 No activity in this window.
287 </td>
288 </tr>
289 ) : (
290 merged.models.map((model) => (
291 <tr
292 key={`${model.provider}:${model.model}`}
293 className="border-b border-border/50"
294 >
295 <td className="py-2 text-foreground">
296 <span className="flex items-center gap-2">
297 <ProviderMark provider={model.provider} className="size-3.5" />
298 {model.model}
299 </span>
300 </td>
301 <td className="py-2 text-right text-foreground tabular-nums">
302 {formatUsd(model.costUsd)}
303 </td>
304 <td className="py-2 text-right text-muted-foreground tabular-nums">
305 {formatPercent(model.costShare)}
306 </td>
307 <td className="py-2 text-right text-muted-foreground tabular-nums">
308 {formatTokens(model.totalTokens)}
309 </td>
276 {breakdown === "model" ? (
277 <table className="w-full text-sm">
278 <thead>
279 <tr className="border-b border-border text-left text-xs text-muted-foreground">
280 <th className="py-2 font-normal">Model</th>
281 <th className="py-2 text-right font-normal">Cost</th>
282 <th className="py-2 text-right font-normal">Share</th>
283 <th className="py-2 text-right font-normal">Tokens</th>
310284 </tr>
311 ))
312 )}
313 </tbody>
314 </table>
315 ) : (
316 <table className="w-full text-sm">
317 <thead>
318 <tr className="border-b border-border text-left text-xs text-muted-foreground">
319 <th className="py-2 font-normal">Day</th>
320 {PROVIDER_ORDER.map((provider) => (
321 <th key={provider} className="py-2 text-right font-normal">
322 {PROVIDER_LABEL[provider]}
323 </th>
324 ))}
325 <th className="py-2 text-right font-normal">Total</th>
326 <th className="py-2 text-right font-normal">Tokens</th>
327 </tr>
328 </thead>
329 <tbody>
330 {recentDays.length === 0 ? (
331 <tr>
332 <td colSpan={5} className="py-6 text-center text-muted-foreground">
333 No activity in this window.
334 </td>
335 </tr>
336 ) : (
337 recentDays.map((day) => (
338 <tr key={day.day} className="border-b border-border/50">
339 <td className="py-2 text-foreground">{formatDayShort(day.day)}</td>
340 {PROVIDER_ORDER.map((provider) => (
341 <td
342 key={provider}
343 className="py-2 text-right text-muted-foreground tabular-nums"
344 >
345 {formatUsd(day.byProvider.get(provider)?.costUsd ?? 0)}
285 </thead>
286 <tbody>
287 {merged.models.length === 0 ? (
288 <tr>
289 <td colSpan={4} className="py-6 text-center text-muted-foreground">
290 No activity in this window.
346291 </td>
292 </tr>
293 ) : (
294 merged.models.map((model) => (
295 <tr
296 key={`${model.provider}:${model.model}`}
297 className="border-b border-border/50"
298 >
299 <td className="py-2 text-foreground">
300 <span className="flex items-center gap-2">
301 <ProviderMark provider={model.provider} className="size-3.5" />
302 {model.model}
303 </span>
304 </td>
305 <td className="py-2 text-right text-foreground tabular-nums">
306 {formatUsd(model.costUsd)}
307 </td>
308 <td className="py-2 text-right text-muted-foreground tabular-nums">
309 {formatPercent(model.costShare)}
310 </td>
311 <td className="py-2 text-right text-muted-foreground tabular-nums">
312 {formatTokens(model.totalTokens)}
313 </td>
314 </tr>
315 ))
316 )}
317 </tbody>
318 </table>
319 ) : (
320 <table className="w-full text-sm">
321 <thead>
322 <tr className="border-b border-border text-left text-xs text-muted-foreground">
323 <th className="py-2 font-normal">Day</th>
324 {PROVIDER_ORDER.map((provider) => (
325 <th key={provider} className="py-2 text-right font-normal">
326 {PROVIDER_LABEL[provider]}
327 </th>
347328 ))}
348 <td className="py-2 text-right text-foreground tabular-nums">
349 {formatUsd(day.costUsd)}
350 </td>
351 <td className="py-2 text-right text-muted-foreground tabular-nums">
352 {formatTokens(day.totalTokens)}
353 </td>
329 <th className="py-2 text-right font-normal">Total</th>
330 <th className="py-2 text-right font-normal">Tokens</th>
354331 </tr>
355 ))
356 )}
357 </tbody>
358 </table>
359 )}
360 </section>
361 </>
362 )}
332 </thead>
333 <tbody>
334 {recentDays.length === 0 ? (
335 <tr>
336 <td colSpan={5} className="py-6 text-center text-muted-foreground">
337 No activity in this window.
338 </td>
339 </tr>
340 ) : (
341 recentDays.map((day) => (
342 <tr key={day.day} className="border-b border-border/50">
343 <td className="py-2 text-foreground">{formatDayShort(day.day)}</td>
344 {PROVIDER_ORDER.map((provider) => (
345 <td
346 key={provider}
347 className="py-2 text-right text-muted-foreground tabular-nums"
348 >
349 {formatUsd(day.byProvider.get(provider)?.costUsd ?? 0)}
350 </td>
351 ))}
352 <td className="py-2 text-right text-foreground tabular-nums">
353 {formatUsd(day.costUsd)}
354 </td>
355 <td className="py-2 text-right text-muted-foreground tabular-nums">
356 {formatTokens(day.totalTokens)}
357 </td>
358 </tr>
359 ))
360 )}
361 </tbody>
362 </table>
363 )}
364 </section>
365 </>
366 )}
367 </div>
368 </ScrollArea>
363369 </div>
364 </ScrollArea>
370 </SidebarInset>
365371 );
366372}
367373