/** * Search-box.ai * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: apps/web/components/AnswerPanel.tsx * Description: Streaming answer renderer — sanitized markdown, citation links, copy action. */ "use client"; import { useMemo, useState } from "react"; import { marked } from "marked"; import DOMPurify from "dompurify"; import { Pastille } from "./Pastille"; export function AnswerPanel({ markdown, done, live }: { markdown: string; done: boolean; live: boolean; }) { const [copied, setCopied] = useState(false); const html = useMemo(() => { if (!markdown) return ""; const raw = marked.parse(markdown, { async: false, gfm: true }); const safe = DOMPurify.sanitize(raw, { USE_PROFILES: { html: true } }); // Citations are mechanical [n] markers from state — link them to the sources board. return safe.replace( /\[(\d{1,3})\]/g, (_m, n: string) => `[${n}]` ); }, [markdown]); async function copy() { try { await navigator.clipboard.writeText(markdown); setCopied(true); setTimeout(() => setCopied(false), 1600); } catch { // clipboard unavailable — ignore } } return (
0 && !done} /> {done ? "final answer" : markdown ? "writing…" : "answer"} {done && ( )}
{!markdown ? (

{live ? "The answer is written here, token by token, once the evidence base is ready." : "No answer was produced for this session."}

{live && ( <> )}
) : ( <>
{!done && } )}
); }