/** * dashboard-client/src/components/RagDashboard.tsx — full RAG metrics section * (H3.2). Rendered in the Metrics tab. * * Fetches /api/rag-metrics and renders: * 1. Stat row: telemetry turns, HyDE-ran count, avg lift, pass rate. * 2. HyDE Recall Lift Bar (BarChart over daily avg lift). * 3. CRAG Quality Line (LineChart over daily avg score). * 4. Per-flag status dots (hydeEnabled, recallMetricsEnabled). * 5. Recall Latency Stacked Bar (hyde vs base turn counts) + Hit-Rate Area. * * PREVENT-PI-004: relative-path fetch to the same-origin dashboard server. * recharts is bundled into this localhost-served static bundle (no runtime * network calls). */ import type React from "react"; import { useMemo } from "react"; import { useApi } from "../hooks/useApi"; import { fetchRagMetrics } from "../api/client"; import type { RagMetricsResponse } from "@contracts"; /** Daily-aggregate row shape (derived from the rag-metrics contract). */ type DailyTelemetry = RagMetricsResponse["daily"][number]; import { ResponsiveContainer, BarChart, Bar, LineChart, Line, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, } from "recharts"; import { Card, CardHeader, CardTitle, CardContent } from "./ui/card"; import { Badge } from "./ui/badge"; const AXIS = "#8b949e"; const GRID = "#30363d"; const TOOLTIP = { background: "#161b22", border: "1px solid #30363d", borderRadius: "6px", color: "#e6edf3", fontSize: 12, } as const; /** One row per day, with lead/lag-safe derived series for the charts. */ interface DailyRow { day: string; avgLift: number; avgScore: number; avgGenMs: number; passRate: number; hydeTurns: number; baseTurns: number; recallCount: number; } function toRows(daily: DailyTelemetry[]): DailyRow[] { return daily.map((d) => { const baseTurns = Math.max(0, d.recallCount - d.hydeRanCount); return { day: d.day.slice(5), // "MM-DD" avgLift: d.avgLift ?? 0, avgScore: d.avgScore ?? 0, avgGenMs: d.avgGenMs ?? 0, passRate: d.avgScore == null ? 0 : d.avgScore, hydeTurns: d.hydeRanCount, baseTurns, recallCount: d.recallCount, }; }); } export interface RagDashboardProps { /** Optional pre-fetched metrics; when absent, the card fetches inline. */ metrics?: RagMetricsResponse | null; } export const RagDashboard: React.FC = ({ metrics }) => { const { data, loading } = useApi( useMemo(() => () => fetchRagMetrics(), []), { pollInterval: 30_000 }, ); const m = metrics ?? data; const rows = useMemo(() => (m ? toRows(m.daily) : []), [m]); if (loading && !m) { return
Loading RAG metrics…
; } if (!m) { return (
No RAG metrics available yet. They appear once turns record HyDE/recall telemetry.
); } const { totals, flags } = m; return (
HyDE Recall Lift CRAG Quality Recall Latency (avg gen ms) HyDE vs Base Turn Volume
); }; function StatRow({ totals, flags, }: { totals: RagMetricsResponse["totals"]; flags: RagMetricsResponse["flags"]; }): React.ReactElement { const stats: Array<{ label: string; value: string; hint: string }> = [ { label: "Telemetry turns", value: String(totals.telemetryTurns), hint: "turns with HyDE or recall data", }, { label: "HyDE ran", value: String(totals.hydeRanTurns), hint: "turns where HyDE generated a doc", }, { label: "Avg lift", value: `${totals.avgLift.toFixed(2)}×`, hint: "fused / raw hit breadth", }, { label: "Pass rate", value: `${Math.round(totals.recentPassRate * 100)}%`, hint: totals.avgScore == null ? "no scored turns" : `avg score ${totals.avgScore.toFixed(2)}`, }, ]; return (
{stats.map((s) => (
{s.label}
{s.value}
{s.hint}
))}
Flags
); } function FlagDot({ label, on }: { label: string; on: boolean }): React.ReactElement { return ( ); }