From 1f04c62d0d35a2f74ec96b5ed6fc7fdb8d471caa Mon Sep 17 00:00:00 2001 From: ooodc <549496103@qq.com> Date: Sun, 7 Jun 2026 18:47:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E8=BD=BB=E7=AE=B1=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E7=82=B9=E5=87=BB=E5=9B=BE=E7=89=87=E9=A2=84=E8=A7=88=E5=92=8C?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E9=99=84=E4=BB=B6?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/Chat/MessageBubble.tsx | 103 ++++++++++++++++++++-- 1 file changed, 97 insertions(+), 6 deletions(-) diff --git a/web/src/components/Chat/MessageBubble.tsx b/web/src/components/Chat/MessageBubble.tsx index 510f0f0..38a02bf 100644 --- a/web/src/components/Chat/MessageBubble.tsx +++ b/web/src/components/Chat/MessageBubble.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react' -import { User, Bot, Wrench, CheckCircle, AlertCircle, Terminal, File, Image, FileText, Music, Video, Download, ChevronDown, ChevronRight, Copy, Check, Loader2, XCircle, Clock, Loader } from 'lucide-react' +import { useState, useEffect } from 'react' +import { User, Bot, Wrench, CheckCircle, AlertCircle, Terminal, File, Image, FileText, Music, Video, Download, ChevronDown, ChevronRight, Copy, Check, Loader2, XCircle, Clock, Loader, X } from 'lucide-react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import type { ChatMessage, Attachment, TaskToolResult } from '../../types/protocol' @@ -144,6 +144,68 @@ function AttachmentCard({ attachment }: { attachment: Attachment }) { ) } +function ImageLightbox({ src, mimeType, fileName, onClose }: { src: string; mimeType: string; fileName?: string; onClose: () => void }) { + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [onClose]) + + const handleDownload = (e: React.MouseEvent) => { + e.stopPropagation() + const byteChars = atob(src) + const byteNums = new Array(byteChars.length) + for (let i = 0; i < byteChars.length; i++) { + byteNums[i] = byteChars.charCodeAt(i) + } + const byteArr = new Uint8Array(byteNums) + const blob = new Blob([byteArr], { type: mimeType }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = fileName || `image.${mimeType.split('/')[1] || 'png'}` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + + return ( +
+ {/* 顶部工具栏 */} +
+ + +
+ e.stopPropagation()} + alt="图片预览" + /> +
+ ) +} + function CopyButton({ text, className = '' }: { text: string; className?: string }) { const [copied, setCopied] = useState(false) @@ -198,6 +260,16 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr const isTool = message.role === 'tool' const isMergedTool = message.type === 'merged_tool' const [toolExpanded, setToolExpanded] = useState(false) + const [lightboxImage, setLightboxImage] = useState<{ base64: string; mimeType: string; fileName?: string } | null>(null) + + const lightboxElement = lightboxImage ? ( + setLightboxImage(null)} + /> + ) : null if (isMergedTool) { const status = message.status || 'calling' @@ -468,6 +540,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr )} + {lightboxElement} ) } @@ -616,14 +689,32 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr )} {message.attachments && message.attachments.length > 0 && ( -
- {message.attachments.map((att: Attachment, idx: number) => ( - - ))} +
+ {message.attachments.some(att => att.media_type === 'image' && att.content_base64) && ( +
+ {message.attachments + .filter(att => att.media_type === 'image' && att.content_base64) + .map((att, idx) => ( + {att.file_name setLightboxImage({ base64: att.content_base64!, mimeType: att.mime_type || 'image/png', fileName: att.file_name || att.path })} + /> + ))} +
+ )} + {message.attachments + .filter(att => att.media_type !== 'image' || !att.content_base64) + .map((att, idx) => ( + + ))}
)}
+ {lightboxElement} ) }