Spaces:
Running
Running
| 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: <IconSatellite size={16} />, | |
| accent: '#8b8680', | |
| accentSoft: 'rgba(139, 134, 128, 0.18)', | |
| accentBorder: 'rgba(139, 134, 128, 0.35)', | |
| value: loading ? '-' : (usageSnapshot?.total_requests ?? 0).toLocaleString(), | |
| meta: ( | |
| <> | |
| <span className={styles.statMetaItem}> | |
| <span className={styles.statMetaDot} style={{ backgroundColor: '#10b981' }} /> | |
| {t('usage_stats.success_requests')}: {loading ? '-' : (usageSnapshot?.success_count ?? 0)} | |
| </span> | |
| <span className={styles.statMetaItem}> | |
| <span className={styles.statMetaDot} style={{ backgroundColor: '#c65746' }} /> | |
| {t('usage_stats.failed_requests')}: {loading ? '-' : (usageSnapshot?.failure_count ?? 0)} | |
| </span> | |
| </> | |
| ), | |
| trend: sparklines.requests, | |
| }, | |
| { | |
| key: 'tokens', | |
| label: t('usage_stats.total_tokens'), | |
| icon: <IconDiamond size={16} />, | |
| accent: '#8b5cf6', | |
| accentSoft: 'rgba(139, 92, 246, 0.18)', | |
| accentBorder: 'rgba(139, 92, 246, 0.35)', | |
| value: loading ? '-' : formatCompactNumber(usageSnapshot?.total_tokens ?? 0), | |
| meta: ( | |
| <> | |
| <span className={styles.statMetaItem}> | |
| {t('usage_stats.cached_tokens')}:{' '} | |
| {loading ? '-' : formatCompactNumber(tokenBreakdown.cachedTokens)} | |
| </span> | |
| <span className={styles.statMetaItem}> | |
| {t('usage_stats.reasoning_tokens')}:{' '} | |
| {loading ? '-' : formatCompactNumber(tokenBreakdown.reasoningTokens)} | |
| </span> | |
| </> | |
| ), | |
| trend: sparklines.tokens, | |
| }, | |
| { | |
| key: 'rpm', | |
| label: t('usage_stats.rpm'), | |
| icon: <IconTimer size={16} />, | |
| accent: '#22c55e', | |
| accentSoft: 'rgba(34, 197, 94, 0.18)', | |
| accentBorder: 'rgba(34, 197, 94, 0.32)', | |
| value: loading ? '-' : formatPerMinuteValue(rateStats.rpm), | |
| meta: ( | |
| <span className={styles.statMetaItem}> | |
| {t('usage_stats.total_requests')}:{' '} | |
| {loading ? '-' : rateStats.requestCount.toLocaleString()} | |
| </span> | |
| ), | |
| trend: sparklines.rpm, | |
| }, | |
| { | |
| key: 'tpm', | |
| label: t('usage_stats.tpm'), | |
| icon: <IconTrendingUp size={16} />, | |
| accent: '#f97316', | |
| accentSoft: 'rgba(249, 115, 22, 0.18)', | |
| accentBorder: 'rgba(249, 115, 22, 0.32)', | |
| value: loading ? '-' : formatPerMinuteValue(rateStats.tpm), | |
| meta: ( | |
| <span className={styles.statMetaItem}> | |
| {t('usage_stats.total_tokens')}:{' '} | |
| {loading ? '-' : formatCompactNumber(rateStats.tokenCount)} | |
| </span> | |
| ), | |
| trend: sparklines.tpm, | |
| }, | |
| { | |
| key: 'cost', | |
| label: t('usage_stats.total_cost'), | |
| icon: <IconDollarSign size={16} />, | |
| accent: '#f59e0b', | |
| accentSoft: 'rgba(245, 158, 11, 0.18)', | |
| accentBorder: 'rgba(245, 158, 11, 0.32)', | |
| value: loading ? '-' : formatUsd(totalCost), | |
| meta: ( | |
| <> | |
| <span className={styles.statMetaItem}> | |
| {t('usage_stats.total_tokens')}:{' '} | |
| {loading ? '-' : formatCompactNumber(usageSnapshot?.total_tokens ?? 0)} | |
| </span> | |
| {!costAvailable && ( | |
| <span className={`${styles.statMetaItem} ${styles.statSubtle}`}> | |
| {t('usage_stats.cost_need_price')} | |
| </span> | |
| )} | |
| </> | |
| ), | |
| trend: sparklines.cost, | |
| }, | |
| ]; | |
| return ( | |
| <div className={styles.statsGrid}> | |
| {statsCards.map((card) => ( | |
| <div | |
| key={card.key} | |
| className={styles.statCard} | |
| style={ | |
| { | |
| '--accent': card.accent, | |
| '--accent-soft': card.accentSoft, | |
| '--accent-border': card.accentBorder, | |
| } as CSSProperties | |
| } | |
| > | |
| <div className={styles.statCardHeader}> | |
| <div className={styles.statLabelGroup}> | |
| <span className={styles.statLabel}>{card.label}</span> | |
| </div> | |
| <span className={styles.statIconBadge}>{card.icon}</span> | |
| </div> | |
| <div className={styles.statValue}>{card.value}</div> | |
| {card.meta && <div className={styles.statMetaRow}>{card.meta}</div>} | |
| <div className={styles.statTrend}> | |
| {card.trend ? ( | |
| <Line | |
| className={styles.sparkline} | |
| data={card.trend.data} | |
| options={sparklineOptions} | |
| /> | |
| ) : ( | |
| <div className={styles.statTrendPlaceholder}></div> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| } | |