/** * dashboard-client/src/components/HydeDetailPanel.tsx — per-turn HyDE + recall * quality detail (H3.1). * * Fetches /api/rag-metrics once and indexes telemetry rows by the composite * `conversationId:turnIndex` key (the dashboard TurnRow has no DB id, so the * pair is the stable identifier). Renders one of three HyDE states: * - HyDE ran: hypothetical doc (collapsible), raw/hyde/fused hit counts, * lift multiplier, generation latency. * - HyDE skipped: reason (disabled / no-llm / generation-failed). * - No telemetry: nothing (the parent only renders us when flagged). * Plus a CRAG-style mini score line when recall metrics exist. * * PREVENT-PI-004: relative-path fetch to the same-origin dashboard server. */ import type React from "react"; import { useMemo, useState } from "react"; import { useApi } from "../hooks/useApi"; import { fetchRagMetrics } from "../api/client"; import type { RagMetricsResponse } from "@contracts"; import { Card } from "./ui/card"; import { Badge } from "./ui/badge"; /** Per-turn telemetry row shape derived from the rag-metrics contract. */ type TelemetryRow = RagMetricsResponse["recent"][number]; export interface HydeDetailPanelProps { conversationId: string; turnIndex: number; } /** Composite key matching a telemetry row to a rendered turn. */ function rowKey(conversationId: string, turnIndex: number): string { return `${conversationId}:${turnIndex}`; } /** First N lines of the hypothetical doc (or full when expanded). */ function truncateDoc(doc: string, maxLines: number): string { const lines = doc.split("\n"); return lines.length > maxLines ? lines.slice(0, maxLines).join("\n") : doc; } /** CRAG-style mini score line when recall metrics were captured. */ function ScoreLine({ row }: { row: TelemetryRow }): React.ReactElement { const hasScore = row.recallScore > 0 || row.recallPass === 1; if (!hasScore) { return (
{visibleDoc}
)}
{showDoc && (
)}
>
) : (