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) => (
| {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)}%
|
{hasPrices && {stat.cost > 0 ? formatUsd(stat.cost) : '--'} | }
))}
>
) : (
{t('usage_stats.no_data')}
)}
);
}