"use client"; import React, { useState, useMemo } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; import rehypeRaw from 'rehype-raw'; import rehypeSlug from 'rehype-slug'; import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus, coy } from 'react-syntax-highlighter/dist/cjs/styles/prism'; import 'katex/dist/katex.min.css'; import { useTheme } from 'next-themes'; // --- TYPE DEFINITIONS --- interface MarkdownRendererProps { content: string; className?: string; } type MarkdownComponentProps = { node?: any; children?: React.ReactNode; className?: string; [key: string]: any; }; // --- PRE-PROCESSOR --- function preprocessMarkdown(content: string): string { if (!content) { return ""; } let processed = content.replace(/\[cite:\s*(\d+(?:,\s*\d+)*)\]/g, '[$1]'); return processed; } // --- HELPER: Copy to Clipboard --- const copyToClipboard = (text: string, onCopy: () => void) => { if (navigator.clipboard) { navigator.clipboard.writeText(text).then(onCopy).catch(err => { console.error('Async clipboard copy failed:', err); }); } else { const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.opacity = '0'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); onCopy(); } catch (err) { console.error('Fallback execCommand copy failed:', err); } document.body.removeChild(textArea); } }; // --- CUSTOM RENDERER COMPONENTS --- const CodeBlock: React.FC = React.memo(({ node, inline, className, children }) => { const { theme } = useTheme(); const [isCopied, setIsCopied] = useState(false); const match = /language-(\w+)/.exec(className || ''); const lang = match ? match[1] : 'text'; const codeString = String(children).replace(/\n$/, ''); const handleCopy = () => { copyToClipboard(codeString, () => { setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); }); }; if (inline) { return ( {children} ); } return (
{lang}
1} wrapLines={true} customStyle={{ margin: 0, padding: '1rem', background: 'transparent', fontSize: '0.9rem', }} codeTagProps={{ style: { fontFamily: '"JetBrains Mono", "Fira Code", monospace', lineHeight: 1.6, }, }} lineNumberStyle={{ minWidth: '2.5em', paddingRight: '1em', textAlign: 'right', userSelect: 'none', color: theme === 'dark' ? '#6b7280' : '#9ca3af', }} > {codeString}
); }); CodeBlock.displayName = 'CodeBlock'; const CustomImage: React.FC> = React.memo(({ src, alt, title, ...props }) => { const [isZoomed, setIsZoomed] = useState(false); return (
{alt setIsZoomed(true)} loading="lazy" {...props} /> {title &&

{title}

} {isZoomed && (
setIsZoomed(false)} > {alt
)}
); }); CustomImage.displayName = 'CustomImage'; // --- MAIN RENDERER COMPONENT --- const MarkdownRenderer: React.FC = ({ content, className = '' }) => { const processedContent = useMemo(() => preprocessMarkdown(content), [content]); const components = useMemo(() => ({ pre: ({ node, ...props }: MarkdownComponentProps) => { const childrenArray = React.Children.toArray(props.children); const child = childrenArray[0]; if (React.isValidElement(child) && (child.props as any)?.className?.includes('language-')) { return ; } return
;
    },
    code: ({ node, inline, className, children, ...props }: MarkdownComponentProps) => {
        return {children};
    },
    div: ({ className, children, ...props }: MarkdownComponentProps) => {
        if (className?.includes('math-display')) {
            return 
{children}
; } return
{children}
; }, img: CustomImage, h1: ({node, ...props}: MarkdownComponentProps) =>

, h2: ({node, ...props}: MarkdownComponentProps) =>

, h3: ({node, ...props}: MarkdownComponentProps) =>

, h4: ({node, ...props}: MarkdownComponentProps) =>

, p: ({node, ...props}: MarkdownComponentProps) =>

, a: ({node, ...props}: MarkdownComponentProps) => , blockquote: ({node, ...props}: MarkdownComponentProps) =>

, ul: ({node, ...props}: MarkdownComponentProps) =>
    , ol: ({node, ...props}: MarkdownComponentProps) =>
      , li: ({node, ...props}: MarkdownComponentProps) =>
    1. , table: ({node, ...props}: MarkdownComponentProps) => (
      ), thead: ({node, ...props}: MarkdownComponentProps) => , th: ({node, ...props}: MarkdownComponentProps) => (
      ), td: ({node, ...props}: MarkdownComponentProps) => ( ), hr: ({node, ...props}: MarkdownComponentProps) =>
      , }), []); const remarkPlugins = [remarkGfm, remarkMath]; const rehypePlugins: any[] = [ rehypeRaw, rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'wrap' }], [rehypeKatex, { throwOnError: false, errorColor: '#EF4444', strict: false, output: 'html', macros: { "\\vec": "\\overrightarrow{#1}", "\\hat": "\\hat{#1}", "\\perp": "\\perp", "\\ihat": "\\hat{\\imath}", "\\jhat": "\\hat{\\jmath}", "\\khat": "\\hat{k}", "\\R": "\\mathbb{R}", "\\N": "\\mathbb{N}", "\\Z": "\\mathbb{Z}", "\\Q": "\\mathbb{Q}", "\\C": "\\mathbb{C}" } }], ]; return ( <>
      {processedContent}
      ); }; export default React.memo(MarkdownRenderer);