/**
* 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 (
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
| Model |
Avg Health |
Samples |
{data.perModel.map((m) => (
| {m.modelId} |
{(m.avgComposite * 100).toFixed(0)}%
|
{m.sampleCount} |
))}
)}
);
}