/** * Chat Message Component * Renders user / assistant / system / toolresult messages * with markdown, thinking sections, images, and tool cards. */ import { useState, useCallback, memo } from 'react'; import { User, Sparkles, Copy, Check, ChevronDown, ChevronRight, Wrench } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import type { RawMessage } from '@/stores/chat'; import { extractText, extractThinking, extractImages, extractToolUse, formatTimestamp } from './message-utils'; interface ChatMessageProps { message: RawMessage; showThinking: boolean; isStreaming?: boolean; } export const ChatMessage = memo(function ChatMessage({ message, showThinking, isStreaming = false, }: ChatMessageProps) { const isUser = message.role === 'user'; const isToolResult = message.role === 'toolresult'; const text = extractText(message); const thinking = extractThinking(message); const images = extractImages(message); const tools = extractToolUse(message); // Don't render empty tool results when thinking is hidden if (isToolResult && !showThinking) return null; // Don't render empty messages if (!text && !thinking && images.length === 0 && tools.length === 0) return null; return (
{/* Avatar */}
{isUser ? : }
{/* Content */}
{/* Thinking section */} {showThinking && thinking && ( )} {/* Tool use cards */} {showThinking && tools.length > 0 && (
{tools.map((tool, i) => ( ))}
)} {/* Main text bubble */} {text && ( )} {/* Images */} {images.length > 0 && (
{images.map((img, i) => ( attachment ))}
)}
); }); // ── Message Bubble ────────────────────────────────────────────── function MessageBubble({ text, isUser, isStreaming, timestamp, }: { text: string; isUser: boolean; isStreaming: boolean; timestamp?: number; }) { const [copied, setCopied] = useState(false); const copyContent = useCallback(() => { navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); }, [text]); return (
{isUser ? (

{text}

) : (
{children} ); } return (
                    
                      {children}
                    
                  
); }, a({ href, children }) { return ( {children} ); }, }} > {text}
{isStreaming && ( )}
)} {/* Footer: timestamp + copy */}
{timestamp && ( {formatTimestamp(timestamp)} )} {!isUser && ( )}
); } // ── Thinking Block ────────────────────────────────────────────── function ThinkingBlock({ content }: { content: string }) { const [expanded, setExpanded] = useState(false); return (
{expanded && (
{content}
)}
); } // ── Tool Card ─────────────────────────────────────────────────── function ToolCard({ name, input }: { name: string; input: unknown }) { const [expanded, setExpanded] = useState(false); return (
{expanded && input != null && (
          {typeof input === 'string' ? input : JSON.stringify(input, null, 2) as string}
        
)}
); }