import { useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Card } from '@/components/ui/Card'; import { LATENCY_SOURCE_FIELD, formatCompactNumber, formatDurationMs, formatUsd, type ModelStatsSummary, } from '@/utils/usage'; import styles from '@/pages/UsagePage.module.scss'; function ModelStatsTitle({ title, subtitle, eyebrow }: { title: string; subtitle: string; eyebrow: string }) { return (
{eyebrow}

{title}

{subtitle}

); } export type ModelStat = ModelStatsSummary; export interface ModelStatsCardProps { modelStats: ModelStat[]; loading: boolean; hasPrices: boolean; } type SortKey = | 'model' | 'requests' | 'tokens' | 'cost' | 'successRate' | 'averageLatencyMs' | 'totalLatencyMs'; type SortDir = 'asc' | 'desc'; interface ModelStatWithRate extends ModelStat { successRate: number; } export function ModelStatsCard({ modelStats, loading, hasPrices }: ModelStatsCardProps) { const { t } = useTranslation(); const [sortKey, setSortKey] = useState('requests'); const [sortDir, setSortDir] = useState('desc'); const latencyHint = t('usage_stats.latency_unit_hint', { field: LATENCY_SOURCE_FIELD, unit: t('usage_stats.duration_unit_ms'), }); const handleSort = (key: SortKey) => { if (sortKey === key) { setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')); } else { setSortKey(key); setSortDir(key === 'model' ? 'asc' : 'desc'); } }; const sorted = useMemo((): ModelStatWithRate[] => { const list: ModelStatWithRate[] = modelStats.map((s) => ({ ...s, successRate: s.requests > 0 ? (s.successCount / s.requests) * 100 : 100, })); const dir = sortDir === 'asc' ? 1 : -1; list.sort((a, b) => { if (sortKey === 'model') return dir * a.model.localeCompare(b.model); const left = a[sortKey]; const right = b[sortKey]; const leftValid = typeof left === 'number' && Number.isFinite(left); const rightValid = typeof right === 'number' && Number.isFinite(right); if (!leftValid && !rightValid) return 0; if (!leftValid) return 1; if (!rightValid) return -1; return dir * (left - right); }); return list; }, [modelStats, sortKey, sortDir]); const arrow = (key: SortKey) => (sortKey === key ? (sortDir === 'asc' ? ' ▲' : ' ▼') : ''); const ariaSort = (key: SortKey): 'none' | 'ascending' | 'descending' => sortKey === key ? (sortDir === 'asc' ? 'ascending' : 'descending') : 'none'; const hasLatencyData = sorted.some((stat) => stat.latencySampleCount > 0); return ( } className={styles.detailsFixedCard} > {loading ? (
{t('common.loading')}
) : sorted.length > 0 ? ( <> {hasLatencyData &&
{latencyHint}
}
{hasPrices && ( )} {sorted.map((stat) => ( {hasPrices && } ))}
{stat.model} {stat.requests.toLocaleString()} ( {stat.successCount.toLocaleString()} {' '} {stat.failureCount.toLocaleString()} ) {formatCompactNumber(stat.tokens)} {formatDurationMs(stat.averageLatencyMs)} {formatDurationMs(stat.totalLatencyMs)} = 95 ? styles.statSuccess : stat.successRate >= 80 ? styles.statNeutral : styles.statFailure } > {stat.successRate.toFixed(1)}% {stat.cost > 0 ? formatUsd(stat.cost) : '--'}
) : (
{t('usage_stats.no_data')}
)}
); }