import { useEffect, useReducer, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ChevronLeft, ChevronRight, X, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useGatewayStore } from '@/stores/gateway'; import { useSettingsStore } from '@/stores/settings'; import { hostApiFetch } from '@/lib/host-api'; import { trackUiEvent } from '@/lib/telemetry'; import { ProvidersSettings } from '@/components/settings/ProvidersSettings'; import { FeedbackState } from '@/components/common/FeedbackState'; import { filterUsageHistoryByWindow, groupUsageHistory, type UsageGroupBy, type UsageHistoryEntry, type UsageWindow, } from './usage-history'; const DEFAULT_USAGE_FETCH_MAX_ATTEMPTS = 2; const WINDOWS_USAGE_FETCH_MAX_ATTEMPTS = 3; const USAGE_FETCH_RETRY_DELAY_MS = 1500; export function Models() { const { t } = useTranslation(['dashboard', 'settings']); const gatewayStatus = useGatewayStore((state) => state.status); const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked); const isGatewayRunning = gatewayStatus.state === 'running'; const usageFetchMaxAttempts = window.electron.platform === 'win32' ? WINDOWS_USAGE_FETCH_MAX_ATTEMPTS : DEFAULT_USAGE_FETCH_MAX_ATTEMPTS; const [usageGroupBy, setUsageGroupBy] = useState('model'); const [usageWindow, setUsageWindow] = useState('7d'); const [usagePage, setUsagePage] = useState(1); const [selectedUsageEntry, setSelectedUsageEntry] = useState(null); const HIDDEN_USAGE_SOURCES = new Set([ 'gateway-injected', 'delivery-mirror', ]); function isHiddenUsageSource(source?: string): boolean { if (!source) return false; const normalizedSource = source.trim().toLowerCase(); return ( HIDDEN_USAGE_SOURCES.has(normalizedSource) || normalizedSource.includes('gateway-injected') || normalizedSource.includes('delivery-mirror') ); } function formatUsageSource(source?: string): string | undefined { if (!source) return undefined; if (isHiddenUsageSource(source)) { return undefined; } return source; } function shouldHideUsageEntry(entry: UsageHistoryEntry): boolean { return ( isHiddenUsageSource(entry.provider) || isHiddenUsageSource(entry.model) ); } type FetchState = { status: 'idle' | 'loading' | 'done'; data: UsageHistoryEntry[]; }; type FetchAction = | { type: 'start' } | { type: 'done'; data: UsageHistoryEntry[] } | { type: 'reset' }; const [fetchState, dispatchFetch] = useReducer( (state: FetchState, action: FetchAction): FetchState => { switch (action.type) { case 'start': return { status: 'loading', data: state.data }; case 'done': return { status: 'done', data: action.data }; case 'reset': return { status: 'idle', data: [] }; default: return state; } }, { status: 'idle' as const, data: [] as UsageHistoryEntry[] }, ); const usageFetchTimerRef = useRef | null>(null); const usageFetchGenerationRef = useRef(0); useEffect(() => { trackUiEvent('models.page_viewed'); }, []); useEffect(() => { if (usageFetchTimerRef.current) { clearTimeout(usageFetchTimerRef.current); usageFetchTimerRef.current = null; } if (!isGatewayRunning) { dispatchFetch({ type: 'reset' }); return; } dispatchFetch({ type: 'start' }); const generation = usageFetchGenerationRef.current + 1; usageFetchGenerationRef.current = generation; const restartMarker = `${gatewayStatus.pid ?? 'na'}:${gatewayStatus.connectedAt ?? 'na'}`; trackUiEvent('models.token_usage_fetch_started', { generation, restartMarker, }); // Safety timeout: if the fetch cycle hasn't resolved after 30 s, // force-resolve to "done" with empty data to avoid an infinite spinner. const safetyTimeout = setTimeout(() => { if (usageFetchGenerationRef.current !== generation) return; trackUiEvent('models.token_usage_fetch_safety_timeout', { generation, restartMarker, }); dispatchFetch({ type: 'done', data: [] }); }, 30_000); const fetchUsageHistoryWithRetry = async (attempt: number) => { trackUiEvent('models.token_usage_fetch_attempt', { generation, attempt, restartMarker, }); try { const entries = await hostApiFetch('/api/usage/recent-token-history'); if (usageFetchGenerationRef.current !== generation) return; const normalized = Array.isArray(entries) ? entries : []; setUsagePage(1); trackUiEvent('models.token_usage_fetch_succeeded', { generation, attempt, records: normalized.length, restartMarker, }); if (normalized.length === 0 && attempt < usageFetchMaxAttempts) { trackUiEvent('models.token_usage_fetch_retry_scheduled', { generation, attempt, reason: 'empty', restartMarker, }); usageFetchTimerRef.current = setTimeout(() => { void fetchUsageHistoryWithRetry(attempt + 1); }, USAGE_FETCH_RETRY_DELAY_MS); } else { if (normalized.length === 0) { trackUiEvent('models.token_usage_fetch_exhausted', { generation, attempt, reason: 'empty', restartMarker, }); } dispatchFetch({ type: 'done', data: normalized }); } } catch (error) { if (usageFetchGenerationRef.current !== generation) return; trackUiEvent('models.token_usage_fetch_failed_attempt', { generation, attempt, restartMarker, message: error instanceof Error ? error.message : String(error), }); if (attempt < usageFetchMaxAttempts) { trackUiEvent('models.token_usage_fetch_retry_scheduled', { generation, attempt, reason: 'error', restartMarker, }); usageFetchTimerRef.current = setTimeout(() => { void fetchUsageHistoryWithRetry(attempt + 1); }, USAGE_FETCH_RETRY_DELAY_MS); return; } dispatchFetch({ type: 'done', data: [] }); trackUiEvent('models.token_usage_fetch_exhausted', { generation, attempt, reason: 'error', restartMarker, }); } }; void fetchUsageHistoryWithRetry(1); return () => { clearTimeout(safetyTimeout); if (usageFetchTimerRef.current) { clearTimeout(usageFetchTimerRef.current); usageFetchTimerRef.current = null; } }; }, [isGatewayRunning, gatewayStatus.connectedAt, gatewayStatus.pid, usageFetchMaxAttempts]); const visibleUsageHistory = isGatewayRunning ? fetchState.data.filter((entry) => !shouldHideUsageEntry(entry)) : []; const filteredUsageHistory = filterUsageHistoryByWindow(visibleUsageHistory, usageWindow); const usageGroups = groupUsageHistory(filteredUsageHistory, usageGroupBy); const usagePageSize = 5; const usageTotalPages = Math.max(1, Math.ceil(filteredUsageHistory.length / usagePageSize)); const safeUsagePage = Math.min(usagePage, usageTotalPages); const pagedUsageHistory = filteredUsageHistory.slice((safeUsagePage - 1) * usagePageSize, safeUsagePage * usagePageSize); const usageLoading = isGatewayRunning && fetchState.status === 'loading'; return (
{/* Header */}

{t('dashboard:models.title')}

{t('dashboard:models.subtitle')}

{/* Content Area */}
{/* AI Providers Section */} {/* Token Usage History Section */}

{t('dashboard:recentTokenHistory.title', 'Token Usage History')}

{usageLoading ? (
) : visibleUsageHistory.length === 0 ? (
) : filteredUsageHistory.length === 0 ? (
) : (

{t('dashboard:recentTokenHistory.showingLast', { count: filteredUsageHistory.length })}

{pagedUsageHistory.map((entry) => (

{entry.model || t('dashboard:recentTokenHistory.unknownModel')}

{[formatUsageSource(entry.provider), formatUsageSource(entry.agentId), entry.sessionId].filter(Boolean).join(' • ')}

{formatUsageTotal(entry)}

{entry.usageStatus === 'missing' && (

{t('dashboard:recentTokenHistory.noUsage')}

)} {entry.usageStatus === 'error' && (

{t('dashboard:recentTokenHistory.usageParseError')}

)}

{formatUsageTimestamp(entry.timestamp)}

{entry.usageStatus === 'available' || entry.usageStatus === undefined ? ( <>
{t('dashboard:recentTokenHistory.input', { value: formatTokenCount(entry.inputTokens) })}
{t('dashboard:recentTokenHistory.output', { value: formatTokenCount(entry.outputTokens) })}
{entry.cacheReadTokens > 0 && (
{t('dashboard:recentTokenHistory.cacheRead', { value: formatTokenCount(entry.cacheReadTokens) })}
)} {entry.cacheWriteTokens > 0 && (
{t('dashboard:recentTokenHistory.cacheWrite', { value: formatTokenCount(entry.cacheWriteTokens) })}
)} ) : ( {entry.usageStatus === 'missing' ? t('dashboard:recentTokenHistory.noUsage') : t('dashboard:recentTokenHistory.usageParseError')} )} {typeof entry.costUsd === 'number' && Number.isFinite(entry.costUsd) && ( {t('dashboard:recentTokenHistory.cost', { amount: entry.costUsd.toFixed(4) })} )} {devModeUnlocked && entry.content && ( )}
))}

{t('dashboard:recentTokenHistory.page', { current: safeUsagePage, total: usageTotalPages })}

)}
{devModeUnlocked && selectedUsageEntry && ( setSelectedUsageEntry(null)} title={t('dashboard:recentTokenHistory.contentDialogTitle')} closeLabel={t('dashboard:recentTokenHistory.close')} unknownModelLabel={t('dashboard:recentTokenHistory.unknownModel')} /> )}
); } function formatTokenCount(value: number): string { return Intl.NumberFormat().format(value); } function getUsageTotalClass(entry: UsageHistoryEntry): string { if (entry.usageStatus === 'error') return 'font-bold text-[15px] text-red-500 dark:text-red-400'; if (entry.usageStatus === 'missing') return 'font-bold text-[15px] text-muted-foreground'; return 'font-bold text-[15px]'; } function formatUsageTotal(entry: UsageHistoryEntry): string { if (entry.usageStatus === 'error') return '✕'; if (entry.usageStatus === 'missing') return '—'; return formatTokenCount(entry.totalTokens); } function formatUsageTimestamp(timestamp: string): string { const date = new Date(timestamp); if (Number.isNaN(date.getTime())) return timestamp; return new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }).format(date); } function UsageBarChart({ groups, emptyLabel, totalLabel, inputLabel, outputLabel, cacheLabel, }: { groups: Array<{ label: string; totalTokens: number; inputTokens: number; outputTokens: number; cacheTokens: number; }>; emptyLabel: string; totalLabel: string; inputLabel: string; outputLabel: string; cacheLabel: string; }) { if (groups.length === 0) { return (
{emptyLabel}
); } const maxTokens = Math.max(...groups.map((group) => group.totalTokens), 1); return (
{inputLabel} {outputLabel} {cacheLabel}
{groups.map((group) => (
{group.label} {totalLabel}: {formatTokenCount(group.totalTokens)}
0 ? `${Math.max((group.totalTokens / maxTokens) * 100, 6)}%` : '0%', }} > {group.inputTokens > 0 && (
)} {group.outputTokens > 0 && (
)} {group.cacheTokens > 0 && (
)}
))}
); } export default Models; function UsageContentPopup({ entry, onClose, title, closeLabel, unknownModelLabel, }: { entry: UsageHistoryEntry; onClose: () => void; title: string; closeLabel: string; unknownModelLabel: string; }) { return (

{title}

{(entry.model || unknownModelLabel)} • {formatUsageTimestamp(entry.timestamp)}

            {entry.content}
          
); }