import { createCodePlugin } from "@streamdown/code"; import { createMathPlugin } from "@streamdown/math"; import Image from "next/image"; import { Streamdown, type StreamdownProps } from "streamdown"; import "katex/dist/katex.min.css"; import { cn } from "@/lib/utils"; const code = createCodePlugin({ themes: ["nord", "nord"], }); const math = createMathPlugin({ singleDollarTextMath: true, }); interface MarkdownViewerProps { content: string; className?: string; } /** * If the entire content is wrapped in a single ```markdown or ```md * code fence, strip the fence so the inner markdown renders properly. */ function stripOuterMarkdownFence(content: string): string { const trimmed = content.trim(); // Match 3+ backtick fences (LLMs escalate to 4+ when content has triple-backtick blocks) const match = trimmed.match(/^(`{3,})(?:markdown|md)?\s*\n([\s\S]+?)\n\1\s*$/); return match ? match[2] : content; } /** * Convert all LaTeX delimiter styles to the double-dollar syntax * that Streamdown's @streamdown/math plugin understands. * * Streamdown math conventions (different from remark-math!): * $$...$$ on the SAME line → inline math * $$\n...\n$$ on SEPARATE lines → block (display) math * * Conversions performed: * \[...\] → $$\n ... \n$$ (block math) * \(...\) → $$...$$ (inline math, same line) * \begin{equation}...\end{equation} → $$\n ... \n$$ (block math) * \begin{displaymath}...\end{displaymath} → $$\n ... \n$$ (block math) * \begin{math}...\end{math} → $$...$$ (inline math, same line) * `$$ … $$` → $$ … $$ (strip wrapping backtick code) * `$ … $` → $ … $ (strip wrapping backtick code) * $...$ → $$...$$ (normalise single-$ to double-$$) */ function convertLatexDelimiters(content: string): string { // 1. Block math: \[...\] → $$\n...\n$$ (display math on separate lines) content = content.replace(/\\\[([\s\S]*?)\\\]/g, (_, inner) => `\n$$\n${inner.trim()}\n$$\n`); // 2. Inline math: \(...\) → $$...$$ (inline math on same line) content = content.replace(/\\\(([\s\S]*?)\\\)/g, (_, inner) => `$$${inner.trim()}$$`); // 3. Block: \begin{equation}...\end{equation} → $$\n...\n$$ content = content.replace( /\\begin\{equation\}([\s\S]*?)\\end\{equation\}/g, (_, inner) => `\n$$\n${inner.trim()}\n$$\n` ); // 4. Block: \begin{displaymath}...\end{displaymath} → $$\n...\n$$ content = content.replace( /\\begin\{displaymath\}([\s\S]*?)\\end\{displaymath\}/g, (_, inner) => `\n$$\n${inner.trim()}\n$$\n` ); // 5. Inline: \begin{math}...\end{math} → $$...$$ content = content.replace( /\\begin\{math\}([\s\S]*?)\\end\{math\}/g, (_, inner) => `$$${inner.trim()}$$` ); // 6. Strip backtick wrapping around math: `$$...$$` → $$...$$ and `$...$` → $...$ content = content.replace(/`(\${1,2})((?:(?!\1).)+)\1`/g, "$1$2$1"); // 7. Normalise single-dollar $...$ to double-dollar $$...$$ so they render // reliably in Streamdown (single-$ has strict no-space rules that often fail). // We match $…$ where the content starts with a backslash (LaTeX command) // to avoid converting currency like $50. content = content.replace( /(? `$$${inner.trim()}$$` ); return content; } export function MarkdownViewer({ content, className }: MarkdownViewerProps) { const processedContent = convertLatexDelimiters(stripOuterMarkdownFence(content)); const components: StreamdownProps["components"] = { p: ({ children, ...props }) => (

{children}

), a: ({ children, ...props }) => ( {children} ), li: ({ children, ...props }) =>
  • {children}
  • , ul: ({ ...props }) =>