"use client"; import { Fragment, type ReactNode } from "react"; import { CitationChip } from "@/components/CitationChip"; import { segmentAnswer } from "@/lib/citations"; import type { CitationView } from "@/lib/api"; /** * Renders a generated answer: paragraphs, a small safe subset of inline markdown, and * citation chips. * * Everything is built as React elements — there is no `dangerouslySetInnerHTML` and no * markdown library. Model output is untrusted text that may quote a hostile document * verbatim, so the renderer is deliberately incapable of producing markup: the worst a * malicious passage can do is render as literal characters. * * The supported subset is what a grounded legal answer actually uses — bold for the * provision being named, inline code, numbered and bulleted clause lists — and nothing * more. Links are intentionally NOT rendered: the only trustworthy destination in this * product is a citation chip, which resolves through the verifier. */ const BOLD = /\*\*([^*]+)\*\*/g; const CODE = /`([^`]+)`/g; export function AnswerText({ text, citations, onOpenEvidence, }: { text: string; citations: CitationView[]; onOpenEvidence: (chunkId: string) => void; }) { const blocks = text.split(/\n{2,}/).filter((block) => block.trim().length > 0); return (
{blocks.map((block, blockIndex) => { const lines = block.split("\n").filter((line) => line.trim().length > 0); const isList = lines.length > 1 && lines.every((line) => /^\s*(?:[-*•]|\d+[.)])\s/.test(line)); if (isList) { return ( ); } return (

{lines.map((line, lineIndex) => ( {lineIndex > 0 ?
: null}
))}

); })}
); } function Inline({ text, citations, onOpenEvidence, }: { text: string; citations: CitationView[]; onOpenEvidence: (chunkId: string) => void; }) { return ( <> {segmentAnswer(text, citations).map((segment, index) => segment.kind === "citation" ? ( ) : ( {formatInline(segment.value)} ), )} ); } /** Apply the supported inline marks, emitting elements rather than markup. */ function formatInline(value: string): ReactNode[] { const nodes: ReactNode[] = []; let cursor = 0; let key = 0; const pattern = new RegExp(`${BOLD.source}|${CODE.source}`, "g"); let match = pattern.exec(value); while (match !== null) { if (match.index > cursor) nodes.push(value.slice(cursor, match.index)); const [, boldBody, codeBody] = match; if (boldBody !== undefined) { nodes.push( {boldBody} , ); } else if (codeBody !== undefined) { nodes.push( {codeBody} , ); } key += 1; cursor = match.index + match[0].length; match = pattern.exec(value); } if (cursor < value.length) nodes.push(value.slice(cursor)); return nodes; }