MessageContent Component

The MessageContent component handles the rendering of chat message content with advanced markdown processing, streaming support, custom component integration, and robust copy functionality. It’s the core component responsible for displaying and interacting with message content in the Aitana chat interface.

Overview

MessageContent provides comprehensive message rendering capabilities including:

  • Advanced markdown processing with math and custom components
  • Streaming text display with typewriter effects
  • Content chunking and error boundary protection
  • Multiple copy strategies (rendered, markdown, HTML)
  • Role-based styling and theming
  • Custom component integration through registry

Architecture

Core Dependencies

import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import rehypeRaw from 'rehype-raw';
import rehypeKatex from 'rehype-katex';
import { markdownComponents } from './markdown';
import { useTextStream } from "@/components/ui/response-stream";
import { copyContentToClipboard, copyAsMarkdown } from '@/utils/copyToClipboard';

Component Structure

MessageContent
├── ChunkErrorBoundary[] (per content chunk)
│   └── StreamingChunk
│       └── Markdown (with custom components)
└── Copy Button (for non-user messages)

Props Interface

interface MessageContentProps {
  // Content Management
  content: string;
  isUser: boolean;
  role: Role;
  className?: ClassNameValue;
  
  // Custom Component Integration
  additionalComponents?: ComponentRegistry;
  
  // Streaming Configuration
  streaming?: boolean;
  streamSpeed?: number;
  streamMode?: "typewriter";
  onStreamComplete?: () => void;
  isCurrentlyStreaming?: boolean;
  isLastMessage?: boolean;
}

Core Functionality

1. Content Cleaning and Processing

// From MessageContent.tsx:89-94
const cleanContent = (content: string): string => {
  // Remove all thinking tags from content
  const allTagsRegex = /<thinking>[\s\S]*?<\/thinking>/g;
  return content.replace(allTagsRegex, '');
};

// From MessageContent.tsx:383-384
const cleanedContent = React.useMemo(() => cleanContent(content), [content]);

Purpose: Removes thinking tags that are meant for internal AI processing and shouldn’t be displayed to users.

2. Advanced Content Chunking

The component implements sophisticated content splitting to handle custom components properly:

// From MessageContent.tsx:96-282
const splitContent = (content: string): string[] => {
  // Components that need to be kept on their own lines (block components)
  const blockComponents = [
    { open: '<networkgraph', close: '/>' },
    { open: '<preview', close: '/>' },
    { open: '<plot', close: '/>' },
    { open: '<googlesearch', close: '</googlesearch>' },
    { open: '<alert', close: '</alert>' },
    { open: '<svg', close: '</svg>' },
    { open: '<toolconfirmation', close: '/>' }
  ];
  
  // Inline components that should not break text flow
  const inlineComponents = [
    { open: '<tooltip', close: '</tooltip>' }
  ];
  
  // Process content, separating only block components into their own chunks
  // ... (detailed implementation with safety guards)
};

Key Features:

  • Block Component Separation: Isolates complex components for proper rendering
  • Inline Component Preservation: Keeps inline components with surrounding text
  • Code Block Detection: Special handling for markdown code blocks
  • Safety Guards: Prevents infinite loops and memory issues
  • Overlap Prevention: Handles nested component scenarios

Important Development Note: When adding new custom UI components, they must be added to the blockComponents list to prevent content disappearing issues.

3. Streaming Text Display

// From MessageContent.tsx:298-365
const StreamingChunk: React.FC<{
  content: string;
  components: any;
  streaming: boolean;
  streamSpeed?: number;
  streamMode?: "typewriter";
  onStreamComplete?: () => void;
  index: number;
  isCurrentlyStreaming?: boolean;
}> = ({ /* props */ }) => {
  const shouldUseStreaming = streaming && isCurrentlyStreaming;
  
  const { displayedText, startStreaming, isComplete } = useTextStream({
    textStream: content,
    mode: streamMode,
    speed: streamSpeed,
  });

  useEffect(() => {
    if (shouldUseStreaming) {
      startStreaming();
    }
  }, [shouldUseStreaming, startStreaming]);

  // For non-streaming messages, show content directly
  if (!shouldUseStreaming) {
    return (
      <Markdown
        remarkPlugins={[remarkGfm, remarkMath]}
        rehypePlugins={[rehypeRaw, rehypeKatex]}
        components={components}
        skipHtml={false}
      >
        {content}
      </Markdown>
    );
  }

  return (
    <Markdown /* streaming display with displayedText */ />
  );
};

Features:

  • Selective Streaming: Only applies to currently streaming messages
  • Typewriter Effect: Character-by-character text reveal
  • Completion Callbacks: Notifies when streaming finishes
  • Performance Optimization: Non-streaming chunks render immediately

4. Role-Based Styling

// From MessageContent.tsx:34-53
const roleStyles: Record<Role, { container: string }> = {
  bot: {
    container: 'bg-bot-message rounded-lg border-l-4'
  },
  assistant: {
    container: 'bg-bot-message rounded-lg border-l-4'
  },
  admin: {
    container: 'bg-[var(--chat-admin-bg)] border-l-4 border-[var(--chat-admin)] rounded-lg'
  },
  receiver: {
    container: 'bg-[var(--chat-receiver-bg)] border-l-4 border-[var(--chat-receiver)] rounded-lg'
  },
  user: {
    container: 'bg-user-message rounded-lg border-l-4 text-foreground'
  },
  other: {
    container: 'bg-muted text-muted-foreground rounded-lg border-l-4 border-muted'
  }
};

Features:

  • CSS Variable Integration: Uses theme-aware CSS variables
  • Role Differentiation: Visual distinction between user types
  • Consistent Branding: Maintains design system consistency

5. Advanced Copy Functionality

The component provides multiple copy strategies for different use cases:

Rendered Content Copy (Primary)

// From MessageContent.tsx:429-455
const handleCopyRendered = async () => {
  try {
    const messageContainer = document.querySelector(`[data-message-content="${cleanedContent.substring(0, 50)}"]`) as HTMLElement;
    if (!messageContainer) {
      return handleCopyAsHTML();
    }

    const success = await copyContentToClipboard({
      messageContainer,
      cleanedContent,
      markdownComponents: components
    });

    if (success) {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } else {
      handleCopyAsHTML();
    }
  } catch (err) {
    handleCopyAsHTML();
  }
};

HTML Content Copy (Fallback)

// From MessageContent.tsx:458-495
const handleCopyAsHTML = async () => {
  try {
    const ReactDOMServer = (await import('react-dom/server')).default;
    
    const htmlContent = ReactDOMServer.renderToStaticMarkup(
      <div>
        {contentChunks.map((chunk, index) => (
          <Markdown
            key={index}
            remarkPlugins={[remarkGfm, remarkMath]}
            rehypePlugins={[rehypeRaw, rehypeKatex]}
            components={components}
            skipHtml={false}
          >
            {chunk}
          </Markdown>
        ))}
      </div>
    );
    
    const blob = new Blob([htmlContent], { type: 'text/html' });
    const clipboardItem = new ClipboardItem({ 
      'text/html': blob, 
      'text/plain': new Blob([cleanedContent], { type: 'text/plain' }) 
    });
    
    await navigator.clipboard.write([clipboardItem]);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  } catch (err) {
    handleCopyMarkdown();
  }
};

Markdown Copy (Final Fallback)

// From MessageContent.tsx:417-427
const handleCopyMarkdown = async () => {
  try {
    const success = await copyAsMarkdown(cleanedContent);
    if (success) {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    }
  } catch (err) {
    console.error('Failed to copy markdown content:', err);
  }
};

Copy Strategy Features:

  • Progressive Fallback: Tries advanced methods first, falls back gracefully
  • Visual Feedback: Shows checkmark when copy succeeds
  • Multiple Formats: Supports HTML, plain text, and markdown simultaneously
  • Error Resilience: Handles clipboard API failures gracefully

6. Error Boundary Protection

// From MessageContent.tsx:55-87
const ChunkErrorBoundary: React.FC<{
  children: React.ReactNode;
  index: number;
}> = ({ children, index }) => {
  const [hasError, setHasError] = React.useState(false);
  const [error, setError] = React.useState<Error | null>(null);

  React.useEffect(() => {
    const handleError = (event: ErrorEvent) => {
      const target = event.target as Node;
      const boundary = document.getElementById(`chunk-boundary-${index}`);
      if (boundary?.contains(target)) {
        event.preventDefault();
        setHasError(true);
        setError(event.error);
      }
    };

    window.addEventListener('error', handleError, true);
    return () => window.removeEventListener('error', handleError, true);
  }, [index]);

  if (hasError) {
    return (
      <div className="p-2 my-2 text-sm text-red-500 bg-red-50 rounded">
        Error in chunk {index + 1}: {error?.message || 'Failed to render content'}
      </div>
    );
  }

  return <div id={`chunk-boundary-${index}`}>{children}</div>;
};

Features:

  • Isolated Error Handling: Errors in one chunk don’t break others
  • Visual Error Display: Shows user-friendly error messages
  • Graceful Degradation: Application continues functioning despite errors
  • Debugging Support: Provides error context for development

Content Deduplication

// From MessageContent.tsx:387-403
const contentChunks = React.useMemo(() => {
  const chunks = splitContent(cleanedContent);
  
  // Deduplicate toolconfirmation chunks (keep only the first one)
  const seenToolConfirmations = new Set<string>();
  return chunks.filter(chunk => {
    if (chunk.toLowerCase().includes('<toolconfirmation')) {
      const normalized = chunk.trim();
      if (seenToolConfirmations.has(normalized)) {
        console.log('[MessageContent] Filtering out duplicate toolconfirmation chunk');
        return false;
      }
      seenToolConfirmations.add(normalized);
    }
    return true;
  });
}, [cleanedContent]);

Purpose: Prevents duplicate tool confirmation dialogs from appearing in the same message.

Performance Optimizations

Memoization Strategy

// Content cleaning memoization
const cleanedContent = React.useMemo(() => cleanContent(content), [content]);

// Content chunking memoization
const contentChunks = React.useMemo(() => {
  // Complex chunking logic
}, [cleanedContent]);

// Component registry memoization
const components = React.useMemo(() => ({
  ...roleComponents[role],
  ...additionalComponents,
}), [role, additionalComponents]);

Role Components Optimization

// From MessageContent.tsx:284-292
const roleComponents = {
  user: markdownComponents,
  bot: markdownComponents,
  assistant: markdownComponents,
  other: markdownComponents,
  receiver: markdownComponents,
  admin: markdownComponents
};

Purpose: Pre-computed component registry to avoid recreation on each render.

User Interface

Copy Button Implementation

// From MessageContent.tsx:512-527
{!isUser && cleanedContent.trim() && (
  <Button
    onClick={handleCopyRendered}
    variant="ghost"
    size="sm"
    className="copy-button absolute bottom-2 right-2 h-8 w-8 p-0 opacity-60 hover:opacity-100 transition-opacity"
    title="Copy formatted content (preserves visual appearance)"
  >
    {copied ? (
      <Check className="h-3 w-3 text-green-500" />
    ) : (
      <Copy className="h-3 w-3" />
    )}
  </Button>
)}

Features:

  • Contextual Display: Only shows for non-user messages with content
  • Visual Feedback: Icon changes to checkmark when copied
  • Accessibility: Includes descriptive title
  • Positioning: Absolute positioning for consistent placement

Main Container Structure

// From MessageContent.tsx:500-545
<div className={twMerge("space-y-2", className)}>
  <div
    className={twMerge(
      'prose prose-sm max-w-none relative',
      'rounded-lg border p-3',
      styles.container,
      className
    )}
    data-message-content={cleanedContent.substring(0, 50)}
  >
    {/* Copy button */}
    
    {contentChunks.map((chunk, index) => (
      <ChunkErrorBoundary key={`chunk-${index}`} index={index}>
        <StreamingChunk
          content={chunk}
          components={components}
          streaming={shouldStreamThisMessage}
          streamSpeed={streamSpeed}
          streamMode={streamMode}
          onStreamComplete={onStreamComplete}
          index={index}
          isCurrentlyStreaming={isCurrentlyStreaming}
        />
      </ChunkErrorBoundary>
    ))}
  </div>
</div>

Integration Points

Custom Component System

The component integrates with the markdown component registry:

import { markdownComponents } from './markdown';

// Components get merged with additional components
const components = React.useMemo(() => ({
  ...roleComponents[role],
  ...additionalComponents,
}), [role, additionalComponents]);

Streaming Integration

import { useTextStream } from "@/components/ui/response-stream";

// Conditional streaming based on message state
const shouldStreamThisMessage = streaming && 
                     (role === 'assistant' || role === 'bot') && 
                     isCurrentlyStreaming;

Usage Examples

Basic Message Display

import MessageContent from '@/components/MessageContent';

function MyMessage() {
  return (
    <MessageContent
      content="Hello, this is a **markdown** message!"
      isUser={false}
      role="assistant"
      streaming={false}
    />
  );
}

Streaming Message

<MessageContent
  content={streamingContent}
  isUser={false}
  role="assistant"
  streaming={true}
  streamSpeed={50}
  streamMode="typewriter"
  isCurrentlyStreaming={true}
  isLastMessage={true}
  onStreamComplete={() => console.log('Streaming complete!')}
/>

With Custom Components

<MessageContent
  content="<customcomponent>Custom content</customcomponent>"
  isUser={false}
  role="assistant"
  additionalComponents={{
    customcomponent: ({ children }) => (
      <div className="custom-styling">{children}</div>
    )
  }}
/>

Testing Considerations

Key Test Areas

  1. Content Cleaning: Verify thinking tags are removed
  2. Content Chunking: Test block vs inline component separation
  3. Streaming Behavior: Test streaming vs non-streaming display
  4. Copy Functionality: Test all copy strategies
  5. Error Boundaries: Test error handling for problematic content
  6. Role Styling: Verify correct styling for different roles

Mock Requirements

// Required mocks for testing
jest.mock('react-markdown');
jest.mock('@/components/ui/response-stream');
jest.mock('@/utils/copyToClipboard');
jest.mock('./markdown', () => ({
  markdownComponents: {}
}));

Example Test Structure

describe('MessageContent', () => {
  it('should clean thinking tags from content', () => {
    // Test content cleaning
  });
  
  it('should chunk content with block components', () => {
    // Test content chunking
  });
  
  it('should stream text for current streaming messages', () => {
    // Test streaming behavior
  });
  
  it('should handle copy operations gracefully', () => {
    // Test copy functionality
  });
});

Error Handling

Graceful Degradation

  • Chunk Errors: Individual chunks can fail without breaking the message
  • Copy Failures: Progressive fallback through copy strategies
  • Streaming Issues: Falls back to immediate display if streaming fails
  • Component Errors: Error boundaries provide user-friendly error messages

Safety Guards

  • Loop Prevention: Maximum iteration counts prevent infinite loops
  • Memory Protection: Maximum component limits prevent memory exhaustion
  • Content Validation: Validates component positions before processing

Future Enhancements

Potential Improvements

  1. Virtual Scrolling: For very long message content
  2. Lazy Loading: Defer rendering of complex components
  3. Enhanced Copy: Rich text preservation for more formats
  4. Interactive Elements: Support for interactive message components
  5. Accessibility: Enhanced screen reader support for complex content

The MessageContent component is a sophisticated solution for rendering rich, interactive message content with robust error handling, multiple copy strategies, and optimized performance for real-time streaming scenarios.