import { ReactNode, useCallback, useEffect, useMemo, useRef, memo, useState } from 'react' // Import useMemo import { Message } from '@/api/lightrag' import useTheme from '@/hooks/useTheme' import Button from '@/components/ui/Button' import { cn } from '@/lib/utils' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import rehypeReact from 'rehype-react' import remarkMath from 'remark-math' import mermaid from 'mermaid' import type { Element } from 'hast' import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' import { oneLight, oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism' import { LoaderIcon, CopyIcon } from 'lucide-react' import { useTranslation } from 'react-i18next' export type MessageWithError = Message & { id: string // Unique identifier for stable React keys isError?: boolean /** * Indicates if the mermaid diagram in this message has been rendered. * Used to persist the rendering state across updates and prevent flickering. */ mermaidRendered?: boolean } // Restore original component definition and export export const ChatMessage = ({ message }: { message: MessageWithError }) => { // Remove isComplete prop const { t } = useTranslation() const { theme } = useTheme() const [katexPlugin, setKatexPlugin] = useState(null) // Load KaTeX dynamically useEffect(() => { const loadKaTeX = async () => { try { const [{ default: rehypeKatex }] = await Promise.all([ import('rehype-katex'), import('katex/dist/katex.min.css') ]) setKatexPlugin(() => rehypeKatex) } catch (error) { console.error('Failed to load KaTeX:', error) } } loadKaTeX() }, []) const handleCopyMarkdown = useCallback(async () => { if (message.content) { try { await navigator.clipboard.writeText(message.content) } catch (err) { console.error(t('chat.copyError'), err) } } }, [message, t]) // Added t to dependency array return (
({ code: (props: any) => ( // Add type annotation if needed, e.g., props: CodeProps from 'react-markdown/lib/ast-to-react' ), p: ({ children }: { children?: ReactNode }) =>

{children}

, h1: ({ children }: { children?: ReactNode }) =>

{children}

, h2: ({ children }: { children?: ReactNode }) =>

{children}

, h3: ({ children }: { children?: ReactNode }) =>

{children}

, h4: ({ children }: { children?: ReactNode }) =>

{children}

, ul: ({ children }: { children?: ReactNode }) =>
    {children}
, ol: ({ children }: { children?: ReactNode }) =>
    {children}
, li: ({ children }: { children?: ReactNode }) =>
  • {children}
  • }), [message.mermaidRendered])} // Dependency ensures update if mermaid state changes > {message.content}
    {message.role === 'assistant' && message.content && message.content.length > 0 && ( // Added check for message.content existence )}
    {message.content === '' && } {/* Check for empty string specifically */}
    ) } // Remove the incorrect memo export line interface CodeHighlightProps { inline?: boolean className?: string children?: ReactNode node?: Element // Keep node for inline check renderAsDiagram?: boolean // Flag to indicate if rendering as diagram should be attempted } // Helper function remains the same const isInlineCode = (node?: Element): boolean => { if (!node || !node.children) return false; const textContent = node.children .filter((child) => child.type === 'text') .map((child) => (child as any).value) .join(''); // Consider inline if it doesn't contain newline or is very short return !textContent.includes('\n') || textContent.length < 40; }; // Check if it is a large JSON const isLargeJson = (language: string | undefined, content: string | undefined): boolean => { if (!content || language !== 'json') return false; return content.length > 5000; // JSON larger than 5KB is considered large JSON }; // Memoize the CodeHighlight component const CodeHighlight = memo(({ className, children, node, renderAsDiagram = false, ...props }: CodeHighlightProps) => { const { theme } = useTheme(); const [hasRendered, setHasRendered] = useState(false); // State to track successful render const match = className?.match(/language-(\w+)/); const language = match ? match[1] : undefined; const inline = isInlineCode(node); // Use the helper function const mermaidRef = useRef(null); const debounceTimerRef = useRef | null>(null); // Use ReturnType for better typing // Get the content string, check if it is a large JSON const contentStr = String(children || '').replace(/\n$/, ''); const isLargeJsonBlock = isLargeJson(language, contentStr); // Handle Mermaid rendering with debounce useEffect(() => { // Effect should run when renderAsDiagram becomes true or hasRendered changes. // The actual rendering logic inside checks language and hasRendered state. if (renderAsDiagram && !hasRendered && language === 'mermaid' && mermaidRef.current) { const container = mermaidRef.current; // Capture ref value // Clear previous timer if dependencies change before timeout (e.g., renderAsDiagram flips quickly) if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } debounceTimerRef.current = setTimeout(() => { if (!container) return; // Container might have unmounted // Double check hasRendered state inside timeout, in case it changed rapidly if (hasRendered) return; try { // Initialize mermaid config mermaid.initialize({ startOnLoad: false, theme: theme === 'dark' ? 'dark' : 'default', securityLevel: 'loose', suppressErrorRendering: true, }); // Show loading indicator container.innerHTML = '
    '; // Preprocess mermaid content const rawContent = String(children).replace(/\n$/, '').trim(); // Heuristic check for potentially complete graph definition const looksPotentiallyComplete = rawContent.length > 10 && ( rawContent.startsWith('graph') || rawContent.startsWith('sequenceDiagram') || rawContent.startsWith('classDiagram') || rawContent.startsWith('stateDiagram') || rawContent.startsWith('gantt') || rawContent.startsWith('pie') || rawContent.startsWith('flowchart') || rawContent.startsWith('erDiagram') ); if (!looksPotentiallyComplete) { console.log('Mermaid content might be incomplete, skipping render attempt:', rawContent); // Optionally keep loading indicator or show a message // container.innerHTML = '

    Waiting for complete diagram...

    '; return; } const processedContent = rawContent .split('\n') .map(line => { const trimmedLine = line.trim(); if (trimmedLine.startsWith('subgraph')) { const parts = trimmedLine.split(' '); if (parts.length > 1) { const title = parts.slice(1).join(' ').replace(/["']/g, ''); return `subgraph "${title}"`; } } return trimmedLine; }) .filter(line => !line.trim().startsWith('linkStyle')) .join('\n'); const mermaidId = `mermaid-${Date.now()}`; mermaid.render(mermaidId, processedContent) .then(({ svg, bindFunctions }) => { // Check ref and hasRendered state again inside async callback if (mermaidRef.current === container && !hasRendered) { container.innerHTML = svg; setHasRendered(true); // Mark as rendered successfully if (bindFunctions) { try { bindFunctions(container); } catch (bindError) { console.error('Mermaid bindFunctions error:', bindError); container.innerHTML += '

    Diagram interactions might be limited.

    '; } } } else if (mermaidRef.current !== container) { console.log('Mermaid container changed before rendering completed.'); } }) .catch(error => { console.error('Mermaid rendering promise error (debounced):', error); console.error('Failed content (debounced):', processedContent); if (mermaidRef.current === container) { const errorMessage = error instanceof Error ? error.message : String(error); const errorPre = document.createElement('pre'); errorPre.className = 'text-red-500 text-xs whitespace-pre-wrap break-words'; errorPre.textContent = `Mermaid diagram error: ${errorMessage}\n\nContent:\n${processedContent}`; container.innerHTML = ''; container.appendChild(errorPre); } }); } catch (error) { console.error('Mermaid synchronous error (debounced):', error); console.error('Failed content (debounced):', String(children)); if (mermaidRef.current === container) { const errorMessage = error instanceof Error ? error.message : String(error); const errorPre = document.createElement('pre'); errorPre.className = 'text-red-500 text-xs whitespace-pre-wrap break-words'; errorPre.textContent = `Mermaid diagram setup error: ${errorMessage}`; container.innerHTML = ''; container.appendChild(errorPre); } } }, 300); // Debounce delay } // Cleanup function to clear the timer on unmount or before re-running effect return () => { if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } }; // Dependencies: renderAsDiagram ensures effect runs when diagram should be shown. // Dependencies include all values used inside the effect to satisfy exhaustive-deps. // The !hasRendered check prevents re-execution of render logic after success. }, [renderAsDiagram, hasRendered, language, children, theme]); // Add children and theme back // For large JSON, skip syntax highlighting completely and use a simple pre tag if (isLargeJsonBlock) { return (
            {contentStr}
          
    ); } // Render based on language type // If it's a mermaid language block and rendering as diagram is not requested (e.g., incomplete stream), display as plain text if (language === 'mermaid' && !renderAsDiagram) { return ( {contentStr} ); } // If it's a mermaid language block and the message is complete, render as diagram if (language === 'mermaid') { // Container for Mermaid diagram return
    ; } // Handle non-Mermaid code blocks return !inline ? ( {contentStr} ) : ( // Handle inline code {children} ); }); // Assign display name for React DevTools CodeHighlight.displayName = 'CodeHighlight';