"use client" import { CSSProperties, useState, ReactNode, useRef } from "react" import React from "react" import Markdown, { Components } from "react-markdown" import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" import { coldarkCold, coldarkDark } from "react-syntax-highlighter/dist/esm/styles/prism" import remarkGfm from "remark-gfm" import remarkMath from "remark-math" import { Button } from "@/components/ui/button" import { Check, Copy } from "lucide-react" import { cn } from "@/lib/utils" import "./markdown.css" interface MarkdownRendererProps { markdownText: string actualCode?: string className?: string style?: { prism?: { [key: string]: CSSProperties } } messageId?: string showCopyButton?: boolean isDarkMode?: boolean } const MarkdownRenderer: React.FC = ({ markdownText = '', className, style, actualCode, messageId = '', showCopyButton = true, isDarkMode = false }) => { const [copied, setCopied] = useState(false); const [isStreaming, setIsStreaming] = useState(true); const highlightBuffer = useRef([]); const isCollecting = useRef(false); const processedTextRef = useRef(''); const safeMarkdownText = React.useMemo(() => { return typeof markdownText === 'string' ? markdownText : ''; }, [markdownText]); const preProcessText = React.useCallback((text: unknown): string => { if (typeof text !== 'string' || !text) return ''; // Remove highlight tags initially for clean rendering return text.replace(/.*?<\/highlight>/g, (match) => { // Extract the content between tags const content = match.replace(/|<\/highlight>/g, ''); return content; }); }, []); // Reset streaming state when markdownText changes React.useEffect(() => { // Preprocess the text first processedTextRef.current = preProcessText(safeMarkdownText); setIsStreaming(true); const timer = setTimeout(() => { setIsStreaming(false); }, 500); return () => clearTimeout(timer); }, [safeMarkdownText, preProcessText]); const copyToClipboard = async (code: string) => { await navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 1000); }; const processText = React.useCallback((text: string) => { if (typeof text !== 'string') return text; // Only process highlights after streaming is complete if (!isStreaming) { if (text === '') { isCollecting.current = true; return null; } if (text === '') { isCollecting.current = false; const content = highlightBuffer.current.join(''); highlightBuffer.current = []; return ( {content} ); } if (isCollecting.current) { highlightBuffer.current.push(text); return null; } } return text; }, [isStreaming, messageId, isDarkMode]); const processChildren = React.useCallback((children: ReactNode): ReactNode => { if (typeof children === 'string') { return processText(children); } if (Array.isArray(children)) { return children.map(child => { const processed = processChildren(child); return processed === null ? null : processed; }).filter(Boolean); } return children; }, [processText]); const CodeBlock = React.useCallback(({ language, code, actualCode, showCopyButton = true, }: { language: string; code: string; actualCode?: string; showCopyButton?: boolean; }) => (
{showCopyButton && (
{language}
)}
{code}
), [copied, isDarkMode, style]); const components = { p: ({ children, ...props }: React.HTMLAttributes) => (

{processChildren(children)}

), span: ({ children, ...props }: React.HTMLAttributes) => ( {processChildren(children)} ), li: ({ children, ...props }: React.HTMLAttributes) => (
  • {processChildren(children)}
  • ), strong: ({ children, ...props }: React.HTMLAttributes) => ( {processChildren(children)} ), em: ({ children, ...props }: React.HTMLAttributes) => ( {processChildren(children)} ), code: ({ className, children, ...props }: React.HTMLAttributes) => { const match = /language-(\w+)/.exec(className || ""); if (match) { return ( ); } return ( {processChildren(children)} ); } } satisfies Components; return (
    {(isStreaming ? processedTextRef.current : safeMarkdownText)} {(isStreaming || (!isStreaming && !processedTextRef.current)) && }
    ); }; export default MarkdownRenderer;