'use client'; import { useState } from 'react'; import { Copy, ThumbsUp, ThumbsDown, RefreshCw, ChevronDown, ChevronUp, Brain, Loader2, AlertCircle, Check, FileText, FileCode } from 'lucide-react'; import { Avatar } from '@/components/ui/Avatar'; import { AILogo } from '@/components/ui/AILogo'; import { MarkdownRenderer } from '@/components/markdown/MarkdownRenderer'; import { CodeExecutionResult, PyodideLoading } from '@/components/features/CodeExecutionResult'; import { ImageLightbox } from '@/components/ui/ImageLightbox'; import { DocumentPreview, type DocumentData } from '@/components/ui/DocumentPreview'; import { cn } from '@/lib/utils'; import type { Message, User, ToolResult } from '@/types'; import type { UploadedDocument } from '@/hooks/useStreamChat'; interface MessageBubbleProps { message: Message; user?: User; thinkingContent?: string; isStreaming?: boolean; error?: string; /** 代码执行产生的图片(Base64) */ images?: string[]; /** 用户上传的图片(Base64 或 URL) */ uploadedImages?: string[]; /** 用户上传的文档 */ uploadedDocuments?: UploadedDocument[]; /** Pyodide 加载状态 */ pyodideStatus?: { stage: 'loading' | 'ready' | 'error'; message: string; progress?: number; }; } // 格式化文件大小 function formatFileSize(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // 根据文件类型获取图标 function getDocumentIcon(type: string) { if (type === 'code' || type === 'markdown') { return FileCode; } return FileText; } export function MessageBubble({ message, user, thinkingContent, isStreaming, error, images, uploadedImages, uploadedDocuments, pyodideStatus }: MessageBubbleProps) { const isUser = message.role === 'user'; const [thinkingExpanded, setThinkingExpanded] = useState(false); const [copied, setCopied] = useState(false); // 图片 Lightbox 状态 const [lightboxOpen, setLightboxOpen] = useState(false); const [lightboxImages, setLightboxImages] = useState([]); const [lightboxInitialIndex, setLightboxInitialIndex] = useState(0); // 文档预览状态 const [documentPreviewOpen, setDocumentPreviewOpen] = useState(false); const [documentPreviewData, setDocumentPreviewData] = useState(null); // 打开图片 Lightbox const openLightbox = (imageArray: string[], index: number) => { setLightboxImages(imageArray); setLightboxInitialIndex(index); setLightboxOpen(true); }; // 关闭图片 Lightbox const closeLightbox = () => { setLightboxOpen(false); }; // 打开文档预览 const openDocumentPreview = (doc: UploadedDocument) => { setDocumentPreviewData({ name: doc.name, content: doc.content, size: doc.size, type: doc.type, }); setDocumentPreviewOpen(true); }; // 关闭文档预览 const closeDocumentPreview = () => { setDocumentPreviewOpen(false); setDocumentPreviewData(null); }; // 复制消息内容 const handleCopy = async () => { try { await navigator.clipboard.writeText(message.content); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (error) { console.error('Failed to copy:', error); } }; if (isUser) { return (
{/* 用户上传的图片 */} {uploadedImages && uploadedImages.length > 0 && (
{uploadedImages.map((img, index) => (
openLightbox(uploadedImages, index)} > {`上传的图片
))}
)} {/* 用户上传的文档 */} {uploadedDocuments && uploadedDocuments.length > 0 && (
{uploadedDocuments.map((doc, index) => { const Icon = getDocumentIcon(doc.type); return (
openDocumentPreview(doc)} title="点击预览" >
{doc.name} {formatFileSize(doc.size)}
); })}
)}
{message.content || ((uploadedImages && uploadedImages.length > 0) || (uploadedDocuments && uploadedDocuments.length > 0) ? '(附件)' : '')}
{/* 悬停显示复制按钮 */}
{user && } {/* 图片 Lightbox */} {/* 文档预览 */}
); } return (
{/* AI 图标 */}
{/* 消息内容 */}
{/* 思考内容 */} {thinkingContent && (
{thinkingExpanded && (
                  {thinkingContent}
                
)}
)} {/* 错误提示 */} {error && (
{error}
)} {/* 主要内容 */}
{message.content ? ( ) : isStreaming ? (
正在思考...
) : null}
{/* 工具调用结果 - 代码执行图片展示 */} {message.toolResults && message.toolResults.length > 0 && (
{message.toolResults.map((result, index) => ( ))}
)} {/* Pyodide 加载状态 */} {pyodideStatus && (
)} {/* 代码执行图片(从 props 传入) */} {images && images.length > 0 && (() => { // 规范化所有图片 URL 用于 lightbox const normalizedImages = images.map(i => i.startsWith('data:') ? i : `data:image/png;base64,${i}`); return (
{normalizedImages.map((img, index) => (
openLightbox(normalizedImages, index)} > {`图表
图表 {index + 1}
))}
); })()} {/* 流式状态指示器 */} {isStreaming && message.content && (
生成中...
)} {/* 操作按钮(非流式状态下显示) */} {!isStreaming && message.content && ( <>
{/* 免责声明 */}
LionCode can make mistakes
)}
{/* 图片 Lightbox */} {/* 文档预览 */}
); } interface ActionButtonProps { icon: React.ComponentType<{ size?: number }>; title: string; onClick?: () => void; } function ActionButton({ icon: Icon, title, onClick }: ActionButtonProps) { return ( ); } /** * 工具调用结果显示组件 * 专门处理代码执行结果和图片显示 */ interface ToolResultDisplayProps { result: ToolResult; } function ToolResultDisplay({ result }: ToolResultDisplayProps) { // 只有代码执行工具才显示图片 if (result.toolName !== 'code_execution') { return null; } // 如果没有图片,不显示 if (!result.images || result.images.length === 0) { return null; } return ( ); }