From 8b558fb7809b67df4e9043f0d01b1dafbf0a42b6 Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Sat, 27 Dec 2025 15:02:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=BB=84=E4=BB=B6):=20=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=B0=94=E6=B3=A1=E6=94=AF=E6=8C=81=20AI=20=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 generatedImages 和 isGeneratingImage 属性 - 实现图片生成加载动画 - 添加图片操作按钮(复制、重新生成、下载、放大) - 生成图片独立显示,不包含在白色卡片内 - 优化空内容时的卡片条件渲染 --- src/components/features/MessageBubble.tsx | 158 +++++++++++++++++++++- 1 file changed, 153 insertions(+), 5 deletions(-) diff --git a/src/components/features/MessageBubble.tsx b/src/components/features/MessageBubble.tsx index 67851c5..1b53224 100644 --- a/src/components/features/MessageBubble.tsx +++ b/src/components/features/MessageBubble.tsx @@ -13,7 +13,7 @@ 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'; +import type { UploadedDocument, GeneratedImageData } from '@/hooks/useStreamChat'; // 工具名称中文映射 const TOOL_DISPLAY_NAMES: Record = { @@ -48,6 +48,10 @@ interface MessageBubbleProps { message: string; progress?: number; }; + /** AI 生成的图片(Gemini 等图片生成模型) */ + generatedImages?: GeneratedImageData[]; + /** 是否正在生成图片 */ + isGeneratingImage?: boolean; /** 重新生成回调(仅对 AI 消息有效),传入消息 ID */ onRegenerate?: (messageId: string) => void; /** 保存到笔记回调(仅对 AI 消息有效),传入消息内容 */ @@ -77,7 +81,7 @@ function getDocumentIcon(type: string) { return FileText; } -export function MessageBubble({ message, user, thinkingContent, isStreaming, error, images, searchImages, searchVideos, uploadedImages, uploadedDocuments, usedTools, pyodideStatus, onRegenerate, onSaveToNote, onLinkClick, conversationId, isHighlighted }: MessageBubbleProps) { +export function MessageBubble({ message, user, thinkingContent, isStreaming, error, images, searchImages, searchVideos, uploadedImages, uploadedDocuments, usedTools, pyodideStatus, generatedImages, isGeneratingImage, onRegenerate, onSaveToNote, onLinkClick, conversationId, isHighlighted }: MessageBubbleProps) { const isUser = message.role === 'user'; const [thinkingExpanded, setThinkingExpanded] = useState(false); const [copied, setCopied] = useState(false); @@ -296,8 +300,22 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err )} - {/* 主要内容 */} -
+ {/* 检查是否有需要显示在白色卡片内的内容 */} + {(() => { + const hasCardContent = + message.content || + isStreaming || + (searchImages && searchImages.length > 0) || + (searchVideos && searchVideos.length > 0) || + (message.toolResults && message.toolResults.length > 0) || + pyodideStatus || + (images && images.length > 0) || + isGeneratingImage; + + if (!hasCardContent) return null; + + return ( +
{/* 搜索到的图片(图片搜索工具结果)- 显示在最上面 */} {searchImages && searchImages.length > 0 && ( @@ -373,6 +391,37 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err ); })()} + {/* AI 生成的图片加载状态 */} + {isGeneratingImage && ( +
+
+ {/* 闪光动画背景 */} +
+ + {/* 加载内容 */} +
+ {/* 旋转加载器 */} +
+ + {/* 加载文字 */} +
+ 图片生成中 +
+ + + +
+
+ + {/* 进度条 */} +
+
+
+
+
+
+ )} + {/* 流式状态指示器 */} {isStreaming && message.content && ( @@ -424,7 +473,106 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
)} -
+
+ ); + })()} + + {/* AI 生成的图片(Gemini 等模型)- 单独显示,不包在白色卡片内 */} + {generatedImages && generatedImages.length > 0 && (() => { + // 将 GeneratedImageData 转换为 data URL + const generatedImageUrls = generatedImages.map( + (img) => `data:${img.mimeType};base64,${img.data}` + ); + + // 复制图片到剪贴板 + const handleCopyImage = async (imgUrl: string) => { + try { + const response = await fetch(imgUrl); + const blob = await response.blob(); + await navigator.clipboard.write([ + new ClipboardItem({ [blob.type]: blob }) + ]); + } catch (error) { + console.error('Failed to copy image:', error); + } + }; + + // 下载图片 + const handleDownloadImage = (imgUrl: string, index: number) => { + const link = document.createElement('a'); + link.href = imgUrl; + link.download = `generated-image-${index + 1}.png`; + link.click(); + }; + + return ( +
+ {generatedImageUrls.map((imgUrl, index) => ( +
+ {/* 图片 */} +
openLightbox(generatedImageUrls, index)} + > + {`生成的图片 +
+ + {/* 底部操作按钮栏 - 悬停时显示,横向排列 */} +
+ + + + {onRegenerate && ( + + + + )} + + + + + + +
+
+ ))} +
+ ); + })()}
{/* 图片 Lightbox */}