diff --git a/frontend/src/pages/Status/StatusPage.tsx b/frontend/src/pages/Status/StatusPage.tsx index c3fbb38..ebafee2 100644 --- a/frontend/src/pages/Status/StatusPage.tsx +++ b/frontend/src/pages/Status/StatusPage.tsx @@ -3,6 +3,8 @@ import { Topbar } from '../../components/layout/Topbar'; import { Search, Upload, Download, RefreshCw, CheckCircle, XCircle, AlertTriangle, Info } from 'lucide-react'; import { UploadModal } from '../Docs/UploadModal'; import { useLanguage } from '../../contexts/LanguageContext'; +import { getModelUsage, pingModelConnections } from '../../api/status'; +import type { ModelUsageEntry } from '../../api/index'; const TOKEN_KEY = 'auth_token'; function authHeader(): Record { @@ -85,22 +87,29 @@ export function StatusPage() { const [refreshKey, setRefreshKey] = useState(0); const [showUpload, setShowUpload] = useState(false); const [lastRefresh, setLastRefresh] = useState(null); + const [modelUsage, setModelUsage] = useState(null); + const [pinging, setPinging] = useState(false); useEffect(() => { setLoading(true); setHealthLoading(true); - // Fetch all three endpoints in parallel + // Fetch all endpoints in parallel. The first three use raw fetch() (legacy + // pattern already established in this file); model usage uses the typed + // fetchAPI-based client from api/status.ts — new code should prefer that. Promise.allSettled([ fetch('/api/v1/status/stats', { headers: authHeader() }).then(r => r.json()), fetch('/api/v1/status/health', { headers: authHeader() }).then(r => r.json()), fetch('/api/v1/status/config', { headers: authHeader() }).then(r => r.json()), - ]).then(([statsRes, healthRes, configRes]) => { + getModelUsage(), + ]).then(([statsRes, healthRes, configRes, modelsRes]) => { if (statsRes.status === 'fulfilled') setStats(statsRes.value); else setStats({ documents_total: 0, documents_indexed: 0, documents_failed: 0, chunks_total: 0 }); if (healthRes.status === 'fulfilled') setHealth(healthRes.value); if (configRes.status === 'fulfilled') setConfig(configRes.value); + if (modelsRes.status === 'fulfilled') setModelUsage(modelsRes.value.models); + else setModelUsage(null); setLoading(false); setHealthLoading(false); @@ -136,6 +145,38 @@ export function StatusPage() { URL.revokeObjectURL(url); } + async function handleTestConnections() { + setPinging(true); + try { + const res = await pingModelConnections(); + setModelUsage(res.models); + } catch { + // Leave modelUsage as-is; the card below already shows a muted + // "never_called"/error state per row when data can't be refreshed. + } finally { + setPinging(false); + } + } + + function modelBadgeStatus(status: ModelUsageEntry['status']): 'ok' | 'error' | 'warn' | 'info' { + if (status === 'ok') return 'ok'; + if (status === 'error') return 'error'; + if (status === 'disabled') return 'info'; + return 'info'; // never_called + } + + function modelStatusLabel(entry: ModelUsageEntry): string { + if (entry.status === 'never_called') return t.status.modelStatusNeverCalled; + if (entry.status === 'disabled') return t.status.modelStatusDisabled; + return entry.status === 'ok' ? t.status.badgeOnline : t.status.badgeError; + } + + /** Small relative-ish hint shown next to provider/model — "Never" or a local time string. */ + function modelLastCalledLabel(entry: ModelUsageEntry): string { + if (!entry.last_called_at) return t.status.lastCalledNever; + return new Date(entry.last_called_at).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); + } + return (
+ {/* AI Models — connection status + cumulative token usage */} +
+
+ {t.status.cardModels} + +
+ + {!modelUsage ? ( +
+ {[1, 2, 3, 4].map(i => ( +
+ ))} +
+ ) : ( + modelUsage.map(entry => { + const roleLabel = entry.role === 'main_llm' ? t.status.roleMainLlm + : entry.role === 'hyde_llm' ? t.status.roleHydeLlm + : entry.role === 'embedding' ? t.status.roleEmbedding + : t.status.roleReranker; + return ( +
+ + {roleLabel} + + {entry.provider}/{entry.model} + {entry.shares_usage_with && ` · ${t.status.sharesUsageWithMain}`} + {` · ${modelLastCalledLabel(entry)}`} + + + {entry.total_tokens > 0 || entry.status === 'ok' || entry.status === 'error' + ? entry.total_tokens.toLocaleString() + : '—'} + + + {modelStatusLabel(entry)} + +
+ ); + }) + )} +
+ {/* System config (collapsible) */}
-
- {t.status.labelReranker} - - {health.reranker.enabled ? (health.reranker.model ?? t.status.serviceEnabled) : t.status.serviceDisabled} - -
{t.status.labelBM25}