/** * Chat Message Component * Renders user / assistant / system / toolresult messages * with markdown, thinking sections, images, and tool cards. */ import { useState, useCallback, useEffect, memo } from 'react'; import { User, Sparkles, Copy, Check, ChevronDown, ChevronRight, Wrench, FileText, Film, Music, FileArchive, File, X, FolderOpen, ZoomIn, Loader2, CheckCircle2, AlertCircle } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { createPortal } from 'react-dom'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import type { RawMessage, AttachedFileMeta } from '@/stores/chat'; import { extractText, extractThinking, extractImages, extractToolUse, formatTimestamp } from './message-utils'; interface ChatMessageProps { message: RawMessage; showThinking: boolean; isStreaming?: boolean; streamingTools?: Array<{ id?: string; toolCallId?: string; name: string; status: 'running' | 'completed' | 'error'; durationMs?: number; summary?: string; }>; } interface ExtractedImage { url?: string; data?: string; mimeType: string; } /** Resolve an ExtractedImage to a displayable src string, or null if not possible. */ function imageSrc(img: ExtractedImage): string | null { if (img.url) return img.url; if (img.data) return `data:${img.mimeType};base64,${img.data}`; return null; } export const ChatMessage = memo(function ChatMessage({ message, showThinking, isStreaming = false, streamingTools = [], }: ChatMessageProps) { const isUser = message.role === 'user'; const role = typeof message.role === 'string' ? message.role.toLowerCase() : ''; const isToolResult = role === 'toolresult' || role === 'tool_result'; const text = extractText(message); const hasText = text.trim().length > 0; const thinking = extractThinking(message); const images = extractImages(message); const tools = extractToolUse(message); const visibleThinking = showThinking ? thinking : null; const visibleTools = tools; const attachedFiles = message._attachedFiles || []; const [lightboxImg, setLightboxImg] = useState<{ src: string; fileName: string; filePath?: string; base64?: string; mimeType?: string } | null>(null); // Never render tool result messages in chat UI if (isToolResult) return null; const hasStreamingToolStatus = isStreaming && streamingTools.length > 0; if (!hasText && !visibleThinking && images.length === 0 && visibleTools.length === 0 && attachedFiles.length === 0 && !hasStreamingToolStatus) return null; return (
{text}
) : (
{children}
);
},
a({ href, children }) {
return (
{children}
);
},
}}
>
{text}
{file.fileName}
{file.fileSize > 0 ? formatFileSize(file.fileSize) : 'File'}
{typeof input === 'string' ? input : JSON.stringify(input, null, 2) as string}
)}