"use client"; import { useState } from "react"; import ReactMarkdown from "react-markdown"; import rehypeKatex from "rehype-katex"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import { Copy } from "lucide-react"; import { markdownComponents } from "./StudyCardPrimitives"; import { cleanLegacySourceSafetyText } from "./utils"; // Markdown tokens would otherwise be read aloud as raw symbols ("hash", "asterisk"). function plainAnnouncementText(text: string) { return text .replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1") .replace(/[*_`>#]+/g, " ") .replace(/\s+/g, " ") .trim(); } /** * A streamed answer grows one token at a time. Announcing that raw string would * make a screen reader repeat itself on every token, so we announce only the * sentences that are already complete. Each finished sentence becomes its own * node in the live region below, so assistive tech reads just the new sentence * and never a half-typed word. */ function settledSentences(text: string): string[] { const sentences: string[] = []; let start = 0; const boundary = /[.!?](?=\s|$)|\n{2,}/g; let match: RegExpExecArray | null; while ((match = boundary.exec(text)) !== null) { const end = match.index + match[0].length; const sentence = plainAnnouncementText(text.slice(start, end)); if (sentence) sentences.push(sentence); start = end; } return sentences; } export function PlainAssistantMessage({ text, streaming = false }: { text: string; streaming?: boolean }) { const [copied, setCopied] = useState(false); const displayText = cleanLegacySourceSafetyText(text); const announcement = streaming ? settledSentences(displayText) : []; async function copyText() { try { await navigator.clipboard.writeText(displayText); setCopied(true); window.setTimeout(() => setCopied(false), 1200); } catch { setCopied(false); } } return (
{streaming ? (
{announcement.map((sentence, index) => ( {sentence} ))}
) : null}
{displayText || (streaming ? " " : "")}
{!streaming ? ( ) : null}
); }