/** * HealthTab.tsx — Context Health dashboard tab. * Shows composite health gauge, sparkline trend, sub-score bars, * alerts, and per-model comparison. Polls /api/context-health every 5s. */ import { useState, useEffect, useCallback } from "react"; import { fetchContextHealth, type ContextHealthResponse } from "../api/health"; import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card"; import { Badge } from "../components/ui/badge"; function healthColor(score: number): string { if (score > 0.8) return "#4ade80"; if (score >= 0.5) return "#facc15"; return "#f87171"; } function HealthGauge({ score }: { score: number }): React.ReactElement { const r = 70; const circ = 2 * Math.PI * r; const offset = circ * (1 - Math.max(0, Math.min(1, score))); const color = healthColor(score); return (
{(score * 100).toFixed(0)} / 100
Context Health
); } function HealthSparkline({ trend }: { trend: number[] }): React.ReactElement { if (trend.length < 2) return
No trend data yet
; const w = 400; const h = 60; const max = 1; const min = 0; const pts = trend.map((v, i) => { const x = (i / (trend.length - 1)) * w; const y = h - ((v - min) / (max - min)) * h; return `${x.toFixed(1)},${y.toFixed(1)}`; }); const last = trend[trend.length - 1]; return ( ); } function SubScoreBar({ label, value }: { label: string; value: number }): React.ReactElement { return (
{label}
{(value * 100).toFixed(0)}%
); } export default function HealthTab(): React.ReactElement { const [data, setData] = useState(null); const [err, setError] = useState(null); const [loading, setLoading] = useState(true); const poll = useCallback(() => { fetchContextHealth() .then(setData) .catch((e: unknown) => setError(e instanceof Error ? e.message : String(e))) .finally(() => setLoading(false)); }, []); useEffect(() => { poll(); const id = setInterval(poll, 5000); return () => clearInterval(id); }, [poll]); if (loading && !data) return
Loading health data…
; if (err && !data) return
Error: {err}
; if (!data) return
No health data available.
; const latest = data.latest; const composite = latest?.composite ?? 0; return (
Sub-Scores
Health Trend (last {data.trend.length} turns) {data.alerts.length > 0 && ( Recent Alerts ({data.alerts.length})
{data.alerts.slice(-10).map((a) => (
{new Date(a.ts).toLocaleTimeString()} {a.modelId ?? "(unknown)"} {(a.composite * 100).toFixed(0)} {a.cachePoison < 0.3 && CACHE POISON}
))}
)} {data.perModel.length > 0 && ( Health by Model {data.perModel.map((m) => ( ))}
Model Avg Health Samples
{m.modelId} {(m.avgComposite * 100).toFixed(0)}% {m.sampleCount}
)}
); }