"use client"; import type { ChatTurn } from "@/lib/chatStore"; import type { SourceDoc } from "@/lib/types"; import { GroundingPill } from "./GroundingPill"; import { SourceChips } from "./SourceChips"; import { useState, type ReactNode } from "react"; import clsx from "clsx"; import ReactMarkdown from "react-markdown"; export function UserMessage({ text }: { text: string }) { return (
{text}
); } export function AssistantMessage({ turn, onOpenSource, }: { turn: ChatTurn; onOpenSource?: (doc: SourceDoc, index: number) => void; }) { const r = turn.response; const [copied, setCopied] = useState(false); if (!r) return null; const copy = async () => { try { await navigator.clipboard.writeText(r.answer); setCopied(true); setTimeout(() => setCopied(false), 1500); } catch {} }; const sources = r.source_docs ?? []; const rejected = r._grounding_status === "rejected_low_similarity"; // Inline citation renderer: replaces [1] [2] markers with clickable badges. // Numbers are 1-indexed against `sources` (clamped to length). const renderWithCitations = (text: string): ReactNode[] => { if (!sources.length || !text) return [text]; const out: ReactNode[] = []; const re = /\[(\d{1,3})\]/g; let last = 0; let m: RegExpExecArray | null; let key = 0; while ((m = re.exec(text)) !== null) { const idx = parseInt(m[1], 10); if (idx >= 1 && idx <= sources.length) { if (m.index > last) out.push(text.slice(last, m.index)); const doc = sources[idx - 1]; out.push( ); last = m.index + m[0].length; } } if (last < text.length) out.push(text.slice(last)); return out.length ? out : [text]; }; // Wrap a markdown children array, expanding string nodes through citation regex. const transformChildren = (children: ReactNode): ReactNode => { if (typeof children === "string") return renderWithCitations(children); if (Array.isArray(children)) return children.map((c, i) => typeof c === "string" ? ( {renderWithCitations(c)} ) : ( c ) ); return children; }; return (
{/* Header: grounding + actions */}
{/* Answer */}
(

{transformChildren(children)}

), ul: ({ children }) => (
    {children}
), ol: ({ children }) => (
    {children}
), li: ({ children }) =>
  • {transformChildren(children)}
  • , strong: ({ children }) => ( {children} ), em: ({ children }) => ( {children} ), code: ({ children }) => ( {children} ), pre: ({ children }) => (
                      {children}
                    
    ), h1: ({ children }) => (

    {children}

    ), h2: ({ children }) => (

    {children}

    ), h3: ({ children }) => (

    {children}

    ), a: ({ href, children }) => ( {children} ), blockquote: ({ children }) => (
    {children}
    ), hr: () =>
    , }} > {r.answer}
    {/* Sources */} {sources.length > 0 && ( )} {/* Spec sheet — collapsed by default */}
    ); } function SpecSheet({ response: r }: { response: NonNullable }) { const [open, setOpen] = useState(false); return ( ); } export function ErrorMessage({ turn }: { turn: ChatTurn }) { const e = turn.error; if (!e) return null; return (
    error · http {e.status || "—"}
    {e.message}
    {e.status === 502 && (
    Backend not reachable. Check the HF Space stage.
    )}
    ); } function Spec({ label, value }: { label: string; value: string }) { return (
    {label} {value}
    ); } function Avatar({ tone = "amber" }: { tone?: "amber" | "err" }) { const bg = tone === "err" ? "linear-gradient(135deg, rgba(255,122,122,0.95), rgba(195,70,70,1))" : "linear-gradient(135deg, rgba(255,181,69,0.95), rgba(214,138,31,1))"; const ring = tone === "err" ? "0 0 0 1px rgba(255,122,122,0.45), 0 0 14px -2px rgba(255,122,122,0.4)" : "0 0 0 1px rgba(255,181,69,0.45), 0 0 14px -2px rgba(255,181,69,0.45)"; return (
    ); }