feat(页面): 聊天页面集成视频展示和链接预览
- MessageBubble.tsx 消息气泡组件: - 新增 searchVideos 属性接收视频搜索结果 - 新增 onLinkClick 属性支持链接点击回调 - 在消息内容中渲染 SearchVideosGrid 组件 - 将 onLinkClick 传递给 MarkdownRenderer - chat/[id]/page.tsx 聊天页面: - 添加 LinkPreviewModal 链接预览弹窗状态管理 - 新增 handleLinkClick 处理链接点击打开预览 - MessageBubble 传递 searchVideos 和 onLinkClick - 渲染 LinkPreviewModal 组件
This commit is contained in:
parent
e0b82b6257
commit
be03aebb09
@ -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>
|
||||
|
||||
@ -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)]">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user