import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Button, EmptyState, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@cherrystudio/ui' import { formatCompactNumber } from '@renderer/utils/number' import { cn } from '@renderer/utils/style' import { createDurationFormatter } from '@renderer/utils/time' import type { AiUsageRecordListSortBy, AiUsageRecordSortOrder } from '@shared/data/api/schemas/aiUsageRecords' import { type AiUsageRecordEntry, type AiUsageRecordModality, getAiUsageRecordTotalTokens } from '@shared/data/types/aiUsageRecord' import { displayModelId, getGenerationTokensPerSecond } from './usageAnalytics' import { formatCost } from './usageDisplay' import { UsageModelAvatar, UsagePanel, UsagePanelHeader, UsagePanelTitle, UsageSourceLabel } from './UsageSettingsPrimitives' const EMPTY_VALUE = '-' const MODALITY_LABEL_KEYS = { language: 'common.language', embedding: 'models.type.embedding', image: 'models.type.image', rerank: 'models.type.rerank' } as const satisfies Record interface UsageEntriesTableProps { entries: AiUsageRecordEntry[] entryTotal: number isLoading: boolean isRefreshing: boolean hasNextPage: boolean sortBy: AiUsageRecordListSortBy sortOrder: AiUsageRecordSortOrder onSort: (sortBy: AiUsageRecordListSortBy) => void onLoadNext: () => void getProviderInfo: (id: string, snapshotName?: string | null) => { id: string; name: string } dateFormatter: Intl.DateTimeFormat timeFormatter: Intl.DateTimeFormat } export function UsageEntriesTable({ entries, entryTotal, isLoading, isRefreshing, hasNextPage, sortBy, sortOrder, onSort, onLoadNext, getProviderInfo, dateFormatter, timeFormatter }: UsageEntriesTableProps) { const { t, i18n } = useTranslation() const locale = i18n.resolvedLanguage const durationFormatter = useMemo(() => createDurationFormatter(locale), [locale]) const integerFormatter = useMemo(() => new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }), [locale]) const isInitialLoading = isLoading && entries.length === 0 const getAriaSort = (column: AiUsageRecordListSortBy) => sortBy === column ? (sortOrder === 'asc' ? 'ascending' : 'descending') : 'none' const renderSortHeader = (column: AiUsageRecordListSortBy, label: string, align: 'left' | 'right' = 'left') => { const isActive = sortBy === column const Icon = isActive ? (sortOrder === 'asc' ? ArrowUp : ArrowDown) : ArrowUpDown return ( ) } const formatMilliseconds = (value: number | null | undefined) => { if (value === null || value === undefined) { return EMPTY_VALUE } return durationFormatter(value) } const formatTps = (value: number | undefined) => value === undefined ? EMPTY_VALUE : t('settings.usage.table.tpsValue', { value: integerFormatter.format(value) }) return ( {t('settings.usage.explore.entries')}
{t('settings.usage.explore.totalEntries', { count: entryTotal })}
{isInitialLoading ? (
{Array.from({ length: 6 }, (_, index) => ( ))}
) : entries.length > 0 ? ( <> {t('settings.usage.table.model')} {t('settings.usage.table.source')} {renderSortHeader('createdAt', t('settings.usage.table.date'))} {renderSortHeader('totalTokens', t('settings.usage.table.tokens'), 'right')} {renderSortHeader('cost', t('settings.usage.table.cost'), 'right')} {renderSortHeader('timeFirstTokenMs', t('settings.usage.table.ttft'), 'right')} {renderSortHeader('tokensPerSecond', t('settings.usage.table.tps'), 'right')} {entries.map((entry) => { const tps = getGenerationTokensPerSecond(entry) const totalTokens = getAiUsageRecordTotalTokens(entry) const sourceName = entry.sourceId ? entry.sourceName || entry.sourceId : t(MODALITY_LABEL_KEYS[entry.modality]) const modelName = entry.modelName || displayModelId(entry.modelId) || EMPTY_VALUE const providerName = getProviderInfo(entry.providerId ?? '', entry.providerName).name || EMPTY_VALUE const createdAt = new Date(entry.createdAt) const createdAtLabel = `${dateFormatter.format(createdAt)} ${timeFormatter.format(createdAt)}` return (
{modelName}
{providerName}
{entry.sourceId ? ( {sourceName} ) : ( sourceName )}
{totalTokens === null ? EMPTY_VALUE : formatCompactNumber(totalTokens)} {entry.cost !== null && entry.cost !== undefined ? formatCost(entry.cost, entry.costCurrency) : t('settings.usage.table.unpriced')} {formatMilliseconds(entry.timeFirstTokenMs)} {formatTps(tps)}
) })}
{hasNextPage && (
)} ) : ( )}
) }