import { useMemo, type CSSProperties, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { Line } from 'react-chartjs-2'; import { IconDiamond, IconDollarSign, IconSatellite, IconTimer, IconTrendingUp, } from '@/components/ui/icons'; import { formatCompactNumber, formatPerMinuteValue, formatUsd, } from '@/utils/usage'; import { sparklineOptions } from '@/utils/usage/chartConfig'; import type { UsageOverviewPayload } from './hooks/useUsageData'; import type { SparklineBundle } from './hooks/useSparklines'; import styles from '@/pages/UsagePage.module.scss'; interface StatCardData { key: string; label: string; icon: ReactNode; accent: string; accentSoft: string; accentBorder: string; value: string; meta?: ReactNode; trend: SparklineBundle | null; } export interface StatCardsProps { usage: UsageOverviewPayload | null; loading: boolean; sparklines: { requests: SparklineBundle | null; tokens: SparklineBundle | null; rpm: SparklineBundle | null; tpm: SparklineBundle | null; cost: SparklineBundle | null; }; } interface StatCardMetrics { tokenBreakdown: { cachedTokens: number; reasoningTokens: number }; rateStats: { rpm: number; tpm: number; windowMinutes: number; requestCount: number; tokenCount: number }; totalCost: number; costAvailable: boolean; } export function buildStatCardMetrics({ usage }: { usage: UsageOverviewPayload | null }): StatCardMetrics { if (!usage?.summary) { return { tokenBreakdown: { cachedTokens: 0, reasoningTokens: 0 }, rateStats: { rpm: 0, tpm: 0, windowMinutes: 1, requestCount: 0, tokenCount: 0 }, totalCost: 0, costAvailable: false, }; } return { tokenBreakdown: { cachedTokens: usage.summary.cached_tokens ?? 0, reasoningTokens: usage.summary.reasoning_tokens ?? 0, }, rateStats: { rpm: usage.summary.rpm ?? 0, tpm: usage.summary.tpm ?? 0, windowMinutes: usage.summary.window_minutes ?? 1, requestCount: usage.summary.request_count ?? 0, tokenCount: usage.summary.token_count ?? 0, }, totalCost: usage.summary.total_cost ?? 0, costAvailable: usage.summary.cost_available === true, }; } export function StatCards({ usage, loading, sparklines }: StatCardsProps) { const { t } = useTranslation(); const usageSnapshot = usage?.usage ?? null; const { tokenBreakdown, rateStats, totalCost, costAvailable } = useMemo( () => buildStatCardMetrics({ usage }), [usage] ); const statsCards: StatCardData[] = [ { key: 'requests', label: t('usage_stats.total_requests'), icon: , accent: '#8b8680', accentSoft: 'rgba(139, 134, 128, 0.18)', accentBorder: 'rgba(139, 134, 128, 0.35)', value: loading ? '-' : (usageSnapshot?.total_requests ?? 0).toLocaleString(), meta: ( <> {t('usage_stats.success_requests')}: {loading ? '-' : (usageSnapshot?.success_count ?? 0)} {t('usage_stats.failed_requests')}: {loading ? '-' : (usageSnapshot?.failure_count ?? 0)} ), trend: sparklines.requests, }, { key: 'tokens', label: t('usage_stats.total_tokens'), icon: , accent: '#8b5cf6', accentSoft: 'rgba(139, 92, 246, 0.18)', accentBorder: 'rgba(139, 92, 246, 0.35)', value: loading ? '-' : formatCompactNumber(usageSnapshot?.total_tokens ?? 0), meta: ( <> {t('usage_stats.cached_tokens')}:{' '} {loading ? '-' : formatCompactNumber(tokenBreakdown.cachedTokens)} {t('usage_stats.reasoning_tokens')}:{' '} {loading ? '-' : formatCompactNumber(tokenBreakdown.reasoningTokens)} ), trend: sparklines.tokens, }, { key: 'rpm', label: t('usage_stats.rpm'), icon: , accent: '#22c55e', accentSoft: 'rgba(34, 197, 94, 0.18)', accentBorder: 'rgba(34, 197, 94, 0.32)', value: loading ? '-' : formatPerMinuteValue(rateStats.rpm), meta: ( {t('usage_stats.total_requests')}:{' '} {loading ? '-' : rateStats.requestCount.toLocaleString()} ), trend: sparklines.rpm, }, { key: 'tpm', label: t('usage_stats.tpm'), icon: , accent: '#f97316', accentSoft: 'rgba(249, 115, 22, 0.18)', accentBorder: 'rgba(249, 115, 22, 0.32)', value: loading ? '-' : formatPerMinuteValue(rateStats.tpm), meta: ( {t('usage_stats.total_tokens')}:{' '} {loading ? '-' : formatCompactNumber(rateStats.tokenCount)} ), trend: sparklines.tpm, }, { key: 'cost', label: t('usage_stats.total_cost'), icon: , accent: '#f59e0b', accentSoft: 'rgba(245, 158, 11, 0.18)', accentBorder: 'rgba(245, 158, 11, 0.32)', value: loading ? '-' : formatUsd(totalCost), meta: ( <> {t('usage_stats.total_tokens')}:{' '} {loading ? '-' : formatCompactNumber(usageSnapshot?.total_tokens ?? 0)} {!costAvailable && ( {t('usage_stats.cost_need_price')} )} ), trend: sparklines.cost, }, ]; return (
{statsCards.map((card) => (
{card.label}
{card.icon}
{card.value}
{card.meta &&
{card.meta}
}
{card.trend ? ( ) : (
)}
))}
); }