feat(页面): 聊天页面集成视频展示和链接预览

- MessageBubble.tsx 消息气泡组件:
  - 新增 searchVideos 属性接收视频搜索结果
  - 新增 onLinkClick 属性支持链接点击回调
  - 在消息内容中渲染 SearchVideosGrid 组件
  - 将 onLinkClick 传递给 MarkdownRenderer
- chat/[id]/page.tsx 聊天页面:
  - 添加 LinkPreviewModal 链接预览弹窗状态管理
  - 新增 handleLinkClick 处理链接点击打开预览
  - MessageBubble 传递 searchVideos 和 onLinkClick
  - 渲染 LinkPreviewModal 组件
This commit is contained in:
gaoziman 2025-12-22 22:01:19 +08:00
parent e0b82b6257
commit be03aebb09
2 changed files with 32 additions and 1 deletions

View File

@ -10,6 +10,7 @@ import { MessageBubble } from '@/components/features/MessageBubble';
import { ChatHeaderInfo } from '@/components/features/ChatHeader';
import { SaveToNoteModal } from '@/components/features/SaveToNoteModal';
import { PromptOptimizer } from '@/components/features/PromptOptimizer';
import { LinkPreviewModal } from '@/components/features/LinkPreviewModal';
import { cn } from '@/lib/utils';
import { useConversation, useConversations } from '@/hooks/useConversations';
import { useStreamChat, type ChatMessage } from '@/hooks/useStreamChat';
@ -47,6 +48,10 @@ export default function ChatPage({ params }: PageProps) {
const [noteModalOpen, setNoteModalOpen] = useState(false);
const [noteContent, setNoteContent] = useState('');
// 链接预览状态
const [linkPreviewOpen, setLinkPreviewOpen] = useState(false);
const [linkPreviewUrl, setLinkPreviewUrl] = useState<string | null>(null);
// 获取数据
const { conversation, loading: conversationLoading, error: conversationError } = useConversation(chatId);
const { createConversation, updateConversation, deleteConversation } = useConversations();
@ -342,6 +347,12 @@ export default function ChatPage({ params }: PageProps) {
}
};
// 处理链接点击 - 在预览弹窗中打开
const handleLinkClick = (url: string) => {
setLinkPreviewUrl(url);
setLinkPreviewOpen(true);
};
// 转换模型格式
const modelOptions = models.map((m) => ({
id: m.modelId,
@ -568,12 +579,14 @@ export default function ChatPage({ params }: PageProps) {
error={message.error}
images={message.images}
searchImages={message.searchImages}
searchVideos={message.searchVideos}
uploadedImages={message.uploadedImages}
uploadedDocuments={message.uploadedDocuments}
usedTools={message.usedTools}
pyodideStatus={message.pyodideStatus}
onRegenerate={message.role === 'assistant' && !isStreaming ? handleRegenerate : undefined}
onSaveToNote={message.role === 'assistant' && !isStreaming ? handleSaveToNote : undefined}
onLinkClick={handleLinkClick}
conversationId={chatId}
/>
))
@ -624,6 +637,13 @@ export default function ChatPage({ params }: PageProps) {
conversationId={chatId}
/>
{/* 链接预览弹窗 */}
<LinkPreviewModal
url={linkPreviewUrl}
isOpen={linkPreviewOpen}
onClose={() => setLinkPreviewOpen(false)}
/>
{/* 提示词优化工具浮动按钮 */}
<PromptOptimizer onUsePrompt={setOptimizedPrompt} />
</div>

View File

@ -8,6 +8,7 @@ import { Tooltip } from '@/components/ui/Tooltip';
import { MarkdownRenderer } from '@/components/markdown/MarkdownRenderer';
import { CodeExecutionResult, PyodideLoading } from '@/components/features/CodeExecutionResult';
import { SearchImagesGrid, type SearchImageItem } from '@/components/features/SearchImagesGrid';
import { SearchVideosGrid, type SearchVideoItem } from '@/components/features/SearchVideosGrid';
import { ImageLightbox } from '@/components/ui/ImageLightbox';
import { DocumentPreview, type DocumentData } from '@/components/ui/DocumentPreview';
import { cn } from '@/lib/utils';
@ -33,6 +34,8 @@ interface MessageBubbleProps {
images?: string[];
/** 搜索到的图片(来自图片搜索工具) */
searchImages?: SearchImageItem[];
/** 搜索到的视频(来自视频搜索工具) */
searchVideos?: SearchVideoItem[];
/** 用户上传的图片Base64 或 URL */
uploadedImages?: string[];
/** 用户上传的文档 */
@ -49,6 +52,8 @@ interface MessageBubbleProps {
onRegenerate?: (messageId: string) => void;
/** 保存到笔记回调(仅对 AI 消息有效),传入消息内容 */
onSaveToNote?: (content: string) => void;
/** 链接点击回调,用于在预览窗口中打开链接 */
onLinkClick?: (url: string) => void;
/** 对话 ID用于关联笔记来源 */
conversationId?: string;
}
@ -70,7 +75,7 @@ function getDocumentIcon(type: string) {
return FileText;
}
export function MessageBubble({ message, user, thinkingContent, isStreaming, error, images, searchImages, uploadedImages, uploadedDocuments, usedTools, pyodideStatus, onRegenerate, onSaveToNote, conversationId }: MessageBubbleProps) {
export function MessageBubble({ message, user, thinkingContent, isStreaming, error, images, searchImages, searchVideos, uploadedImages, uploadedDocuments, usedTools, pyodideStatus, onRegenerate, onSaveToNote, onLinkClick, conversationId }: MessageBubbleProps) {
const isUser = message.role === 'user';
const [thinkingExpanded, setThinkingExpanded] = useState(false);
const [copied, setCopied] = useState(false);
@ -284,11 +289,17 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
<SearchImagesGrid images={searchImages} className="mt-0 mb-4" />
)}
{/* 搜索到的视频(视频搜索工具结果)- 显示在图片下方 */}
{searchVideos && searchVideos.length > 0 && (
<SearchVideosGrid videos={searchVideos} className="mt-0 mb-4" />
)}
<div className="text-[var(--color-text-primary)] leading-[1.75]">
{message.content ? (
<MarkdownRenderer
content={message.content}
onImageLinkClick={handleImageLinkClick}
onLinkClick={onLinkClick}
/>
) : isStreaming ? (
<div className="flex items-center gap-2 text-[var(--color-text-tertiary)]">