import React, { useState, useRef, useEffect } from 'react'; import ReactMarkdown from 'react-markdown'; import { Reply, Copy, Check, Maximize2, Info, FileText, Hash, Target } from 'lucide-react'; import { advisors, getAdvisorColors } from '../data/advisors'; import { useTheme } from '../contexts/ThemeContext'; const MessageBubble = ({ message, onReply, onCopy, onExpand, showReplyButton = false }) => { const { isDark } = useTheme(); const [showTooltip, setShowTooltip] = useState(null); const [copiedStates, setCopiedStates] = useState({}); const [showInfoOverlay, setShowInfoOverlay] = useState(false); const overlayRef = useRef(null); const handleCopy = async (messageId, content) => { try { await navigator.clipboard.writeText(content); setCopiedStates(prev => ({ ...prev, [messageId]: true })); if (onCopy) onCopy(messageId, content); setTimeout(() => { setCopiedStates(prev => ({ ...prev, [messageId]: false })); }, 2000); } catch (err) { console.error('Failed to copy text: ', err); } }; const handleExpand = (messageId, persona_id) => { if (onExpand) onExpand(messageId, persona_id); }; const handleInfoToggle = () => { setShowInfoOverlay(!showInfoOverlay); }; const showTooltipWithDelay = (tooltipType) => { setTimeout(() => setShowTooltip(tooltipType), 500); }; const hideTooltip = () => { setShowTooltip(null); }; // Close overlay when clicking outside useEffect(() => { const handleClickOutside = (event) => { if (overlayRef.current && !overlayRef.current.contains(event.target)) { setShowInfoOverlay(false); } }; if (showInfoOverlay) { document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; } }, [showInfoOverlay]); // Preprocess markdown content to fix common formatting issues const preprocessMarkdown = (content) => { if (!content) return ''; // Ensure proper line breaks before numbered lists let processed = content.replace(/(\d+\.\s\*\*[^*]+\*\*)/g, '\n\n$1'); // Ensure proper line breaks after list items processed = processed.replace(/(\d+\.\s[^\n]+)(?=\s+\d+\.)/g, '$1\n'); // Fix spacing around bold headers processed = processed.replace(/(\*\*[^*]+\*\*)/g, '\n\n$1\n\n'); // Clean up multiple consecutive line breaks processed = processed.replace(/\n{3,}/g, '\n\n'); // Ensure proper paragraph breaks processed = processed.replace(/([.!?])\s+([A-Z])/g, '$1\n\n$2'); return processed.trim(); }; // ENHANCED MARKDOWN COMPONENTS WITH BETTER STYLING const markdownComponents = { // Bold text styling - for headers and key terms strong: ({ children }) => ( {children} ), // Italic text styling em: ({ children }) => ( {children} ), // Paragraph styling with proper spacing p: ({ children }) => (

{children}

), // Unordered list styling ul: ({ children }) => ( ), // Ordered list styling with better spacing ol: ({ children }) => (
    {children}
), // List item styling with proper spacing li: ({ children }) => (
  • {children}
  • ), // Headers (in case they use them) h1: ({ children }) => (

    {children}

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

    {children}

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

    {children}

    ), // Code styling code: ({ children }) => ( {children} ), // Block quote styling blockquote: ({ children }) => (
    {children}
    ) }; // RAG Metadata Component const RagInfoOverlay = ({ ragMetadata, colors }) => { const hasDocuments = ragMetadata?.usedDocuments || false; const chunksUsed = ragMetadata?.chunksUsed || 0; const documentChunks = ragMetadata?.documentChunks || []; return (
    RAG Information
    Used Documents:
    {hasDocuments ? 'Yes' : 'No'}
    Document Chunks:
    {chunksUsed}
    {hasDocuments && documentChunks.length > 0 && (
    Referenced Sources
    {documentChunks.map((chunk, index) => (
    {chunk.metadata?.filename || 'Unknown file'} {Math.round((chunk.relevance_score || 0) * 100)}%
    {chunk.text && (
    {chunk.text.substring(0, 120)} {chunk.text.length > 120 && '...'}
    )}
    ))}
    )} {!hasDocuments && (
    This response was generated without referencing uploaded documents.
    )}
    ); }; if (message.type === 'user') { return (
    {message.replyTo && (
    to {message.replyTo.advisorName}
    )}

    {message.content}

    ); } if (message.type === 'advisor') { const advisor = advisors[message.persona_id]; const Icon = advisor.icon; const colors = getAdvisorColors(message.persona_id, isDark); const isCopied = copiedStates[message.id]; return (

    {advisor.name} {message.isReply && ↳ Reply} {message.isExpansion && ⤴ Expanded}

    {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
    {/* Enhanced markdown rendering with preprocessing */}
    {preprocessMarkdown(message.content)}
    {showReplyButton && (
    {showTooltip === 'reply' && (
    Reply to this message
    )}
    {showTooltip === 'copy' && (
    {isCopied ? 'Copied!' : 'Copy response'}
    )}
    {showTooltip === 'expand' && (
    Expand on this response
    )}
    )} {showInfoOverlay && ( )}
    ); } if (message.type === 'error') { return (

    {message.content}

    ); } return null; }; export default MessageBubble;