Skip to content
4 changes: 2 additions & 2 deletions messages/zh-TW/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@
"cacheWrite1h": "快取寫入(1h)",
"cacheRead": "快取讀取",
"cacheTtl": "快取 TTL",
"cacheTtlSwapped": "計費 TTL (已互換)",
"cacheTtlSwapped": "計費 TTL已互換",
"multiplier": "供應商倍率",
"totalCost": "總費用",
"context1m": "1M 上下文",
Expand Down Expand Up @@ -366,7 +366,7 @@
"cacheWrite1h": "快取寫入(1h)",
"cacheRead": "快取讀取",
"cacheTtl": "快取 TTL",
"cacheTtlSwapped": "計費 TTL (已互換)",
"cacheTtlSwapped": "計費 TTL已互換",
"multiplier": "供應商倍率",
"totalCost": "總費用",
"context1m": "1M 上下文長度",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export async function UsageLogsActiveSessionsSection() {
currencyCode={systemSettings.currencyDisplay}
maxHeight="200px"
showTokensCost={false}
compactEmpty
/>
);
}
Expand Down
176 changes: 55 additions & 121 deletions src/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useTranslations } from "next-intl";
import { useCallback, useEffect, useState } from "react";
import { getUsageLogsStats } from "@/actions/usage-logs";
import { Skeleton } from "@/components/ui/skeleton";
import { cn, formatTokenAmount } from "@/lib/utils";
import { formatTokenAmount } from "@/lib/utils";
import type { CurrencyCode } from "@/lib/utils/currency";
import { formatCurrency } from "@/lib/utils/currency";
import type { UsageLogSummary } from "@/repository/usage-logs";
Expand All @@ -27,21 +27,14 @@ interface UsageLogsStatsPanelProps {
currencyCode?: CurrencyCode;
}

/**
* Stats panel component with glass morphism UI
* Always expanded (not collapsible), loads data asynchronously
* Re-fetches when filters change
*/
export function UsageLogsStatsPanel({ filters, currencyCode = "USD" }: UsageLogsStatsPanelProps) {
const t = useTranslations("dashboard");
const [stats, setStats] = useState<UsageLogSummary | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

// Create stable filter key for dependency comparison
const filtersKey = JSON.stringify(filters);

// Load stats data
const loadStats = useCallback(async () => {
setIsLoading(true);
setError(null);
Expand All @@ -61,79 +54,30 @@ export function UsageLogsStatsPanel({ filters, currencyCode = "USD" }: UsageLogs
}
}, [filters, t]);

// Load data on mount and when filters change
// biome-ignore lint/correctness/useExhaustiveDependencies: filtersKey is used to detect filter changes
useEffect(() => {
loadStats();
}, [filtersKey, loadStats]);

return (
<div
className={cn(
// Glass morphism base
"relative overflow-hidden rounded-xl border bg-card/30 backdrop-blur-sm",
"transition-all duration-200",
"border-border/50 hover:border-border"
)}
>
{/* Glassmorphism gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.02] to-transparent pointer-events-none" />

<div className="relative z-10">
{/* Header */}
<div className="flex items-center gap-3 px-4 py-3 border-b border-border/30">
<span
className={cn(
"flex items-center justify-center w-8 h-8 rounded-lg shrink-0",
"bg-muted text-muted-foreground"
)}
>
<BarChart3 className="h-4 w-4" />
</span>
<div className="space-y-0.5">
<h3 className="text-sm font-semibold text-foreground leading-none">
{t("logs.stats.title")}
</h3>
<p className="text-xs text-muted-foreground leading-relaxed hidden sm:block">
{t("logs.stats.description")}
</p>
</div>
</div>

{/* Content */}
<div className="px-4 py-4">
{isLoading ? (
<StatsSkeletons />
) : error ? (
<div className="text-center py-4 text-destructive">{error}</div>
) : stats ? (
<StatsContent stats={stats} currencyCode={currencyCode} />
) : null}
</div>
</div>
</div>
);
}

/**
* Stats data skeletons
*/
function StatsSkeletons() {
return (
<div className="grid gap-4 md:grid-cols-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="space-y-2 p-4 border border-border/50 rounded-lg bg-card/20">
<div className="flex items-center gap-3 px-4 py-2.5 rounded-lg border border-border/50 bg-card/30 text-sm flex-wrap">
<BarChart3 className="h-4 w-4 text-muted-foreground shrink-0" />
{isLoading ? (
<div className="flex items-center gap-4 flex-wrap">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-8 w-32" />
<Skeleton className="h-4 w-20" />
<Skeleton className="h-4 w-32" />
<Skeleton className="h-4 w-28" />
</div>
))}
) : error ? (
<span className="text-destructive">{error}</span>
) : stats ? (
<StatsContent stats={stats} currencyCode={currencyCode} />
) : null}
</div>
);
}

/**
* Stats data content
*/
function StatsContent({
stats,
currencyCode,
Expand All @@ -144,58 +88,48 @@ function StatsContent({
const t = useTranslations("dashboard");

return (
<div className="grid gap-4 md:grid-cols-4">
{/* Total Requests */}
<div className="p-4 border border-border/50 rounded-lg bg-card/20">
<div className="text-sm text-muted-foreground mb-1">{t("logs.stats.totalRequests")}</div>
<div className="text-2xl font-mono font-semibold">
{stats.totalRequests.toLocaleString()}
</div>
</div>

{/* Total Amount */}
<div className="p-4 border border-border/50 rounded-lg bg-card/20">
<div className="text-sm text-muted-foreground mb-1">{t("logs.stats.totalAmount")}</div>
<div className="text-2xl font-mono font-semibold">
{formatCurrency(stats.totalCost, currencyCode)}
</div>
</div>

{/* Total Tokens */}
<div className="p-4 border border-border/50 rounded-lg bg-card/20">
<div className="text-sm text-muted-foreground mb-1">{t("logs.stats.totalTokens")}</div>
<div className="text-2xl font-mono font-semibold">
{formatTokenAmount(stats.totalTokens)}
</div>
<div className="mt-2 text-xs text-muted-foreground space-y-1">
<div className="flex justify-between">
<span>{t("logs.stats.input")}:</span>
<span className="font-mono">{formatTokenAmount(stats.totalInputTokens)}</span>
</div>
<div className="flex justify-between">
<span>{t("logs.stats.output")}:</span>
<span className="font-mono">{formatTokenAmount(stats.totalOutputTokens)}</span>
</div>
</div>
</div>

{/* Cache Tokens */}
<div className="p-4 border border-border/50 rounded-lg bg-card/20">
<div className="text-sm text-muted-foreground mb-1">{t("logs.stats.cacheTokens")}</div>
<div className="text-2xl font-mono font-semibold">
{formatTokenAmount(stats.totalCacheCreationTokens + stats.totalCacheReadTokens)}
</div>
<div className="mt-2 text-xs text-muted-foreground space-y-1">
<div className="flex justify-between">
<span>{t("logs.stats.write")}:</span>
<span className="font-mono">{formatTokenAmount(stats.totalCacheCreationTokens)}</span>
</div>
<div className="flex justify-between">
<span>{t("logs.stats.read")}:</span>
<span className="font-mono">{formatTokenAmount(stats.totalCacheReadTokens)}</span>
</div>
</div>
</div>
<div className="flex items-center gap-x-4 gap-y-1 flex-wrap">
<StatItem
label={t("logs.stats.totalRequests")}
value={stats.totalRequests.toLocaleString()}
/>

<Separator />

<StatItem
label={t("logs.stats.totalAmount")}
value={formatCurrency(stats.totalCost, currencyCode)}
/>

<Separator />

<StatItem
label={t("logs.stats.totalTokens")}
value={formatTokenAmount(stats.totalTokens)}
detail={`${t("logs.stats.input")} ${formatTokenAmount(stats.totalInputTokens)} / ${t("logs.stats.output")} ${formatTokenAmount(stats.totalOutputTokens)}`}
/>

<Separator />

<StatItem
label={t("logs.stats.cacheTokens")}
value={formatTokenAmount(stats.totalCacheCreationTokens + stats.totalCacheReadTokens)}
detail={`${t("logs.stats.write")} ${formatTokenAmount(stats.totalCacheCreationTokens)} / ${t("logs.stats.read")} ${formatTokenAmount(stats.totalCacheReadTokens)}`}
/>
</div>
);
}

function StatItem({ label, value, detail }: { label: string; value: string; detail?: string }) {
return (
<span className="flex items-center gap-1.5">
<span className="text-muted-foreground">{label}</span>
<span className="font-mono font-medium">{value}</span>
{detail ? <span className="text-xs text-muted-foreground/70">({detail})</span> : null}
</span>
);
}

function Separator() {
return <span className="text-border hidden sm:inline">|</span>;
}
Comment on lines +133 to +135
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separator color should use proper border color variable

The separator uses text-border which applies the border color to text, creating a grey separator. However, for semantic correctness and proper theming, use text-muted-foreground or create a proper divider element with border styling.

Suggested change
function Separator() {
return <span className="text-border hidden sm:inline">|</span>;
}
function Separator() {
return <span className="text-muted-foreground/30 hidden sm:inline">|</span>;
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsx
Line: 133-135

Comment:
separator color should use proper border color variable

The separator uses `text-border` which applies the border color to text, creating a grey separator. However, for semantic correctness and proper theming, use `text-muted-foreground` or create a proper divider element with border styling.

```suggestion
function Separator() {
  return <span className="text-muted-foreground/30 hidden sm:inline">|</span>;
}
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Loading
Loading