import type { ReactNode } from "react"; import type { EvalScores, EvalState, QueryResponse } from "../types"; import CitationList from "./CitationList"; import PlotlyChart from "./PlotlyChart"; interface Props { response: QueryResponse; showInlineChart?: boolean; evalState?: EvalState; } const FULL_LABELS: Record = { Ctx: "Context Relevance", Grd: "Groundedness", Ans: "Answer Relevance", }; function ScoreBadge({ label, score }: { label: string; score: number | null }) { if (score === null) return null; const pct = Math.round(score * 100); const colorClass = score >= 0.85 ? "text-emerald-600 bg-emerald-50 border-emerald-200" : score >= 0.5 ? "text-amber-600 bg-amber-50 border-amber-200" : "text-red-600 bg-red-50 border-red-200"; return ( {label} {pct}% ); } /** Shimmer pill shown while eval scores are loading. */ function EvalLoadingPill() { return ( ); } function EvalScoreBadges({ scores }: { scores: EvalScores }) { return ( <> · ); } // ── Inline markdown: **bold**, *italic*, `code` ──────────────────────────── function renderInline(text: string): ReactNode { const parts = text.split(/(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g); return parts.map((part, i) => { if (part.startsWith("**") && part.endsWith("**")) return {part.slice(2, -2)}; if (part.startsWith("*") && part.endsWith("*")) return {part.slice(1, -1)}; if (part.startsWith("`") && part.endsWith("`")) return ( {part.slice(1, -1)} ); return part; }); } // ── Block markdown parser ────────────────────────────────────────────────── function renderMarkdown(text: string): ReactNode { const lines = text.split("\n"); const out: ReactNode[] = []; let i = 0; let key = 0; while (i < lines.length) { const line = lines[i]; // Fenced code block if (line.startsWith("```")) { const lang = line.slice(3).trim(); const code: string[] = []; i++; while (i < lines.length && !lines[i].startsWith("```")) { code.push(lines[i]); i++; } out.push(
{lang && (
{lang}
)}
            {code.join("\n")}
          
, ); } // H3 else if (line.startsWith("### ")) { out.push(

{renderInline(line.slice(4))}

, ); } // H2 else if (line.startsWith("## ")) { out.push(

{renderInline(line.slice(3))}

, ); } // H1 else if (line.startsWith("# ")) { out.push(

{renderInline(line.slice(2))}

, ); } // Unordered list — collect consecutive items else if (/^[-*+] /.test(line)) { const items: string[] = []; while (i < lines.length && /^[-*+] /.test(lines[i])) { items.push(lines[i].replace(/^[-*+] /, "")); i++; } out.push(
    {items.map((item, idx) => (
  • {renderInline(item)}
  • ))}
, ); continue; } // Ordered list else if (/^\d+\. /.test(line)) { const items: string[] = []; while (i < lines.length && /^\d+\. /.test(lines[i])) { items.push(lines[i].replace(/^\d+\. /, "")); i++; } out.push(
    {items.map((item, idx) => (
  1. {idx + 1}. {renderInline(item)}
  2. ))}
, ); continue; } // Horizontal rule else if (/^---+$/.test(line.trim())) { out.push(
); } // Blank line → spacer paragraph gap else if (line.trim() === "") { // handled by space-y on the container } // Paragraph else { out.push(

{renderInline(line)}

, ); } i++; } return
{out}
; } // ── Component ────────────────────────────────────────────────────────────── export default function ResponseDisplay({ response, showInlineChart = true, evalState }: Props) { const time = new Date(response.created_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }); const PROVIDER_LABELS: Record = { openai: "OpenAI", azure_openai: "Azure OpenAI", gemini: "Gemini", }; return (
{/* User query — right-aligned bubble */}
{response.query}
{/* AI response — left-aligned with avatar */}
{/* Avatar */} {/* Response card */}
{/* Answer body */}
{renderMarkdown(response.answer)}
{/* Inline chart (only when viz panel is not active) */} {showInlineChart && response.chart_data && (
)} {/* Citations */} {response.citations.length > 0 && (
)} {/* Metadata footer */}
{time} · {PROVIDER_LABELS[response.model_provider] ?? response.model_provider} · {response.agent_steps} step{response.agent_steps !== 1 ? "s" : ""} {evalState?.loading && } {evalState && !evalState.loading && evalState.scores && ( )}
); }