fix: polish usage statistics ui (#3426)

* fix: improve usage statistics ui

* chore: remove unused token suffix translation

---------

Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
Allen Xu
2026-06-05 08:12:17 +08:00
committed by GitHub
parent 8e7d167ace
commit 03a9296c1f
6 changed files with 140 additions and 137 deletions
+29 -31
View File
@@ -80,53 +80,51 @@ export function UsageDashboard() {
transition={{ duration: 0.4 }}
className="space-y-8 pb-8"
>
<div className="flex flex-col gap-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex flex-col gap-1">
<h2 className="text-2xl font-bold">{t("usage.title")}</h2>
<p className="text-sm text-muted-foreground">
{t("usage.subtitle")}
</p>
</div>
<div className="flex flex-col lg:flex-row lg:items-end justify-between gap-4 mb-2">
<div className="flex flex-col gap-1">
<h2 className="text-2xl font-bold tracking-tight">
{t("usage.title")}
</h2>
<p className="text-sm text-muted-foreground">{t("usage.subtitle")}</p>
</div>
<div className="rounded-xl border border-border/50 bg-card/40 backdrop-blur-sm p-4">
<div className="flex flex-wrap items-center gap-1.5">
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center p-1 bg-muted/30 rounded-lg border border-border/50">
{APP_FILTER_OPTIONS.map((type) => (
<button
key={type}
type="button"
onClick={() => setAppType(type)}
className={cn(
"px-4 py-1.5 rounded-lg text-sm font-medium transition-all",
"px-3 py-1.5 rounded-md text-sm font-medium transition-all",
appType === type
? "bg-primary/10 text-primary shadow-sm border border-primary/20"
: "text-muted-foreground hover:text-primary hover:bg-muted/50 border border-transparent",
? "bg-background text-primary shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-muted/50",
)}
>
{t(`usage.appFilter.${type}`)}
</button>
))}
</div>
<div className="ml-auto flex items-center gap-2">
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 px-2 text-xs text-muted-foreground"
title={t("common.refresh", "刷新")}
onClick={changeRefreshInterval}
>
<RefreshCw className="mr-1 h-3.5 w-3.5" />
{refreshIntervalMs > 0 ? `${refreshIntervalMs / 1000}s` : "--"}
</Button>
<div className="flex items-center gap-2 ml-auto lg:ml-0">
<Button
type="button"
variant="outline"
size="sm"
className="h-9 px-3 text-xs"
title={t("common.refresh", "刷新")}
onClick={changeRefreshInterval}
>
<RefreshCw className="mr-2 h-3.5 w-3.5" />
{refreshIntervalMs > 0 ? `${refreshIntervalMs / 1000}s` : "--"}
</Button>
<UsageDateRangePicker
selection={range}
triggerLabel={rangeLabel}
onApply={(nextRange) => setRange(nextRange)}
/>
</div>
<UsageDateRangePicker
selection={range}
triggerLabel={rangeLabel}
onApply={(nextRange) => setRange(nextRange)}
/>
</div>
</div>
</div>
+111 -102
View File
@@ -201,112 +201,121 @@ export function UsageHero({
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<Card className="relative overflow-hidden border border-border/50 bg-gradient-to-br from-primary/5 via-card/50 to-background/50 backdrop-blur-xl shadow-sm">
<CardContent className="p-6 md:p-8">
{/* Header: title + cost */}
<div className="flex flex-wrap items-start justify-between gap-4 mb-4">
<div className="flex items-center gap-2">
<div className={cn("p-2 rounded-lg", titleTheme.iconBg)}>
<Zap className={cn("h-4 w-4", titleTheme.accent)} />
</div>
<span className="text-sm font-medium text-muted-foreground">
{appLabel && (
<>
<span className={cn("font-semibold", titleTheme.accent)}>
{appLabel}
<Card className="relative overflow-hidden border border-border/50 bg-card/60 backdrop-blur-xl shadow-sm">
<CardContent className="p-4 md:p-5">
<div className="flex flex-col gap-4">
{/* Top row: Main Token Count, Requests, Cost */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div
className={cn(
"p-2.5 rounded-xl bg-gradient-to-br shadow-sm",
titleTheme.iconBg,
)}
>
<Zap className={cn("h-5 w-5", titleTheme.accent)} />
</div>
<div>
<div className="text-xs font-medium text-muted-foreground flex items-center gap-1.5 mb-0.5">
{appLabel && (
<>
<span
className={cn("font-semibold", titleTheme.accent)}
>
{appLabel}
</span>
<span className="text-muted-foreground/30"></span>
</>
)}
{t("usage.realTotal", "真实消耗 Tokens")}
</div>
<div className="flex items-baseline gap-2">
<span
className="text-2xl md:text-3xl font-bold tabular-nums tracking-tight leading-none"
title={realTotal.toLocaleString()}
>
{realTotal.toLocaleString()}
</span>
<span className="mx-1.5 text-muted-foreground/40">·</span>
</>
)}
{t("usage.realTotal", "真实消耗 Tokens")}
</span>
</div>
<div className="flex items-center gap-4 text-right">
<div className="flex flex-col">
<span className="text-xs text-muted-foreground">
{t("usage.totalRequests")}
</span>
<span className="text-sm font-semibold flex items-center gap-1 justify-end">
<Activity className="h-3.5 w-3.5 text-blue-500" />
{requests.toLocaleString()}
</span>
<span className="text-xs text-muted-foreground font-medium bg-muted/40 px-1.5 py-0.5 rounded-md">
{formatTokensShort(realTotal, lang, 2)}
</span>
</div>
</div>
</div>
<div className="flex flex-col">
<span className="text-xs text-muted-foreground">
{t("usage.totalCost")}
</span>
<span className="text-sm font-semibold text-green-500">
{totalCost == null ? "--" : fmtUsd(totalCost, 4)}
</span>
<div className="flex items-center gap-5 bg-background/50 px-4 py-2.5 rounded-xl border border-border/40 shadow-sm">
<div className="flex flex-col">
<span className="text-[10px] text-muted-foreground uppercase tracking-wider font-medium">
{t("usage.totalRequests")}
</span>
<span className="font-semibold flex items-center gap-1.5 text-sm tabular-nums">
<Activity className="h-3.5 w-3.5 text-blue-500" />
{requests.toLocaleString()}
</span>
</div>
<div className="w-px h-8 bg-border/60" />
<div className="flex flex-col">
<span className="text-[10px] text-muted-foreground uppercase tracking-wider font-medium">
{t("usage.totalCost")}
</span>
<span className="font-semibold text-green-500 text-sm tabular-nums">
{totalCost == null ? "--" : fmtUsd(totalCost, 4)}
</span>
</div>
</div>
</div>
</div>
{/* Hero number */}
<div className="flex flex-col items-start mb-6">
<div
className="text-4xl md:text-5xl font-bold tracking-tight tabular-nums leading-tight"
title={realTotal.toLocaleString()}
>
{realTotal.toLocaleString()}
</div>
<div className="text-sm text-muted-foreground mt-1">
{formatTokensShort(realTotal, lang, 2)}{" "}
{t("usage.tokensSuffix", "tokens")}
</div>
</div>
{/* Breakdown row: 4 mini stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-5">
<MiniStat
icon={<ArrowDownToLine className="h-3.5 w-3.5" />}
label={t("usage.freshInput", "新增输入")}
value={formatTokensShort(input, lang)}
accent="text-blue-500"
/>
<MiniStat
icon={<ArrowUpFromLine className="h-3.5 w-3.5" />}
label={t("usage.output")}
value={formatTokensShort(output, lang)}
accent="text-purple-500"
/>
<MiniStat
icon={<Database className="h-3.5 w-3.5" />}
label={t("usage.cacheWrite", "缓存写入")}
value={cacheWriteDisplay.value}
accent="text-amber-500"
muted={cacheWriteDisplay.muted}
tooltip={cacheWriteDisplay.tooltip}
/>
<MiniStat
icon={<Sparkles className="h-3.5 w-3.5" />}
label={t("usage.cacheRead", "缓存命中")}
value={formatTokensShort(cacheRead, lang)}
accent="text-emerald-500"
/>
</div>
{/* Hit rate progress */}
<div className="space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground">
{t("usage.cacheHitRate", "缓存命中率")}
</span>
<span className="font-semibold text-emerald-500 tabular-nums">
{hitPercentLabel}%
</span>
</div>
<div className="relative h-2 rounded-full bg-muted/50 overflow-hidden">
<motion.div
className="absolute inset-y-0 left-0 bg-gradient-to-r from-emerald-500/80 to-emerald-400 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${hitPercent}%` }}
transition={{ duration: 0.8, ease: "easeOut" }}
{/* Bottom row: Breakdown and Hit Rate */}
<div className="grid grid-cols-2 lg:grid-cols-5 gap-3">
<MiniStat
icon={<ArrowDownToLine className="h-3.5 w-3.5" />}
label={t("usage.freshInput", "新增输入")}
value={formatTokensShort(input, lang)}
accent="text-blue-500"
/>
<MiniStat
icon={<ArrowUpFromLine className="h-3.5 w-3.5" />}
label={t("usage.output")}
value={formatTokensShort(output, lang)}
accent="text-purple-500"
/>
<MiniStat
icon={<Database className="h-3.5 w-3.5" />}
label={t("usage.cacheWrite", "缓存写入")}
value={cacheWriteDisplay.value}
accent="text-amber-500"
muted={cacheWriteDisplay.muted}
tooltip={cacheWriteDisplay.tooltip}
/>
<MiniStat
icon={<Sparkles className="h-3.5 w-3.5" />}
label={t("usage.cacheRead", "缓存命中")}
value={formatTokensShort(cacheRead, lang)}
accent="text-emerald-500"
/>
<div className="col-span-2 lg:col-span-1 flex flex-col justify-center rounded-xl border border-border/40 bg-background/40 p-3 shadow-sm">
<div className="flex items-center justify-between text-[11px] mb-2">
<span className="text-muted-foreground font-medium">
{t("usage.cacheHitRate", "缓存命中率")}
</span>
<span className="font-bold text-emerald-500 tabular-nums">
{hitPercentLabel}%
</span>
</div>
<div className="relative h-1.5 rounded-full bg-muted/60 overflow-hidden">
<motion.div
className="absolute inset-y-0 left-0 bg-emerald-500 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${hitPercent}%` }}
transition={{ duration: 0.8, ease: "easeOut" }}
/>
</div>
</div>
</div>
</div>
</CardContent>
@@ -336,21 +345,21 @@ function MiniStat({
}: MiniStatProps) {
return (
<div
className="flex flex-col gap-1 rounded-lg border border-border/40 bg-background/40 px-3 py-2.5"
className="flex flex-col gap-1 rounded-xl border border-border/40 bg-background/40 p-3 shadow-sm"
title={tooltip}
>
<div
className={`flex items-center gap-1.5 text-xs text-muted-foreground ${accent}`}
className={`flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground ${accent}`}
>
{icon}
<span className="text-foreground/70">{label}</span>
<span className="text-foreground/70 tracking-wide">{label}</span>
{tooltip && (
<Info className="h-3 w-3 text-muted-foreground/60 shrink-0" />
<Info className="h-3 w-3 text-muted-foreground/60 shrink-0 ml-auto" />
)}
</div>
<div
className={cn(
"text-base font-semibold tabular-nums",
"text-sm font-semibold tabular-nums",
muted && "text-muted-foreground/70",
)}
>
-1
View File
@@ -1345,7 +1345,6 @@
"cacheTokens": "Cache Tokens",
"realTotal": "Tokens Processed",
"realTotalHint": "Input + Output + Cache Write + Cache Read",
"tokensSuffix": "tokens",
"freshInput": "Fresh Input",
"cacheHitRate": "Cache Hit Rate",
"requestLogs": "Request Logs",
-1
View File
@@ -1345,7 +1345,6 @@
"cacheTokens": "キャッシュトークン",
"realTotal": "実消費トークン",
"realTotalHint": "Input + Output + キャッシュ書込 + キャッシュ命中",
"tokensSuffix": "tokens",
"freshInput": "新規入力",
"cacheHitRate": "キャッシュヒット率",
"requestLogs": "リクエストログ",
-1
View File
@@ -1291,7 +1291,6 @@
"cacheTokens": "快取 Token",
"realTotal": "真實消耗 Tokens",
"realTotalHint": "Input + Output + 快取建立 + 快取命中",
"tokensSuffix": "Tokens",
"freshInput": "新增輸入",
"cacheHitRate": "快取命中率",
"requestLogs": "請求日誌",
-1
View File
@@ -1345,7 +1345,6 @@
"cacheTokens": "缓存 Token",
"realTotal": "真实消耗 Tokens",
"realTotalHint": "Input + Output + 缓存写入 + 缓存命中",
"tokensSuffix": "tokens",
"freshInput": "新增输入",
"cacheHitRate": "缓存命中率",
"requestLogs": "请求日志",