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 { ChatHeaderInfo } from '@/components/features/ChatHeader';
|
||||||
import { SaveToNoteModal } from '@/components/features/SaveToNoteModal';
|
import { SaveToNoteModal } from '@/components/features/SaveToNoteModal';
|
||||||
import { PromptOptimizer } from '@/components/features/PromptOptimizer';
|
import { PromptOptimizer } from '@/components/features/PromptOptimizer';
|
||||||
|
import { LinkPreviewModal } from '@/components/features/LinkPreviewModal';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useConversation, useConversations } from '@/hooks/useConversations';
|
import { useConversation, useConversations } from '@/hooks/useConversations';
|
||||||
import { useStreamChat, type ChatMessage } from '@/hooks/useStreamChat';
|
import { useStreamChat, type ChatMessage } from '@/hooks/useStreamChat';
|
||||||
@ -47,6 +48,10 @@ export default function ChatPage({ params }: PageProps) {
|
|||||||
const [noteModalOpen, setNoteModalOpen] = useState(false);
|
const [noteModalOpen, setNoteModalOpen] = useState(false);
|
||||||
const [noteContent, setNoteContent] = useState('');
|
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 { conversation, loading: conversationLoading, error: conversationError } = useConversation(chatId);
|
||||||
const { createConversation, updateConversation, deleteConversation } = useConversations();
|
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) => ({
|
const modelOptions = models.map((m) => ({
|
||||||
id: m.modelId,
|
id: m.modelId,
|
||||||
@ -568,12 +579,14 @@ export default function ChatPage({ params }: PageProps) {
|
|||||||
error={message.error}
|
error={message.error}
|
||||||
images={message.images}
|
images={message.images}
|
||||||
searchImages={message.searchImages}
|
searchImages={message.searchImages}
|
||||||
|
searchVideos={message.searchVideos}
|
||||||
uploadedImages={message.uploadedImages}
|
uploadedImages={message.uploadedImages}
|
||||||
uploadedDocuments={message.uploadedDocuments}
|
uploadedDocuments={message.uploadedDocuments}
|
||||||
usedTools={message.usedTools}
|
usedTools={message.usedTools}
|
||||||
pyodideStatus={message.pyodideStatus}
|
pyodideStatus={message.pyodideStatus}
|
||||||
onRegenerate={message.role === 'assistant' && !isStreaming ? handleRegenerate : undefined}
|
onRegenerate={message.role === 'assistant' && !isStreaming ? handleRegenerate : undefined}
|
||||||
onSaveToNote={message.role === 'assistant' && !isStreaming ? handleSaveToNote : undefined}
|
onSaveToNote={message.role === 'assistant' && !isStreaming ? handleSaveToNote : undefined}
|
||||||
|
onLinkClick={handleLinkClick}
|
||||||
conversationId={chatId}
|
conversationId={chatId}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@ -624,6 +637,13 @@ export default function ChatPage({ params }: PageProps) {
|
|||||||
conversationId={chatId}
|
conversationId={chatId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 链接预览弹窗 */}
|
||||||
|
<LinkPreviewModal
|
||||||
|
url={linkPreviewUrl}
|
||||||
|
isOpen={linkPreviewOpen}
|
||||||
|
onClose={() => setLinkPreviewOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 提示词优化工具浮动按钮 */}
|
{/* 提示词优化工具浮动按钮 */}
|
||||||
<PromptOptimizer onUsePrompt={setOptimizedPrompt} />
|
<PromptOptimizer onUsePrompt={setOptimizedPrompt} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { Tooltip } from '@/components/ui/Tooltip';
|
|||||||
import { MarkdownRenderer } from '@/components/markdown/MarkdownRenderer';
|
import { MarkdownRenderer } from '@/components/markdown/MarkdownRenderer';
|
||||||
import { CodeExecutionResult, PyodideLoading } from '@/components/features/CodeExecutionResult';
|
import { CodeExecutionResult, PyodideLoading } from '@/components/features/CodeExecutionResult';
|
||||||
import { SearchImagesGrid, type SearchImageItem } from '@/components/features/SearchImagesGrid';
|
import { SearchImagesGrid, type SearchImageItem } from '@/components/features/SearchImagesGrid';
|
||||||
|
import { SearchVideosGrid, type SearchVideoItem } from '@/components/features/SearchVideosGrid';
|
||||||
import { ImageLightbox } from '@/components/ui/ImageLightbox';
|
import { ImageLightbox } from '@/components/ui/ImageLightbox';
|
||||||
import { DocumentPreview, type DocumentData } from '@/components/ui/DocumentPreview';
|
import { DocumentPreview, type DocumentData } from '@/components/ui/DocumentPreview';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@ -33,6 +34,8 @@ interface MessageBubbleProps {
|
|||||||
images?: string[];
|
images?: string[];
|
||||||
/** 搜索到的图片(来自图片搜索工具) */
|
/** 搜索到的图片(来自图片搜索工具) */
|
||||||
searchImages?: SearchImageItem[];
|
searchImages?: SearchImageItem[];
|
||||||
|
/** 搜索到的视频(来自视频搜索工具) */
|
||||||
|
searchVideos?: SearchVideoItem[];
|
||||||
/** 用户上传的图片(Base64 或 URL) */
|
/** 用户上传的图片(Base64 或 URL) */
|
||||||
uploadedImages?: string[];
|
uploadedImages?: string[];
|
||||||
/** 用户上传的文档 */
|
/** 用户上传的文档 */
|
||||||
@ -49,6 +52,8 @@ interface MessageBubbleProps {
|
|||||||
onRegenerate?: (messageId: string) => void;
|
onRegenerate?: (messageId: string) => void;
|
||||||
/** 保存到笔记回调(仅对 AI 消息有效),传入消息内容 */
|
/** 保存到笔记回调(仅对 AI 消息有效),传入消息内容 */
|
||||||
onSaveToNote?: (content: string) => void;
|
onSaveToNote?: (content: string) => void;
|
||||||
|
/** 链接点击回调,用于在预览窗口中打开链接 */
|
||||||
|
onLinkClick?: (url: string) => void;
|
||||||
/** 对话 ID(用于关联笔记来源) */
|
/** 对话 ID(用于关联笔记来源) */
|
||||||
conversationId?: string;
|
conversationId?: string;
|
||||||
}
|
}
|
||||||
@ -70,7 +75,7 @@ function getDocumentIcon(type: string) {
|
|||||||
return FileText;
|
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 isUser = message.role === 'user';
|
||||||
const [thinkingExpanded, setThinkingExpanded] = useState(false);
|
const [thinkingExpanded, setThinkingExpanded] = useState(false);
|
||||||
const [copied, setCopied] = 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" />
|
<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]">
|
<div className="text-[var(--color-text-primary)] leading-[1.75]">
|
||||||
{message.content ? (
|
{message.content ? (
|
||||||
<MarkdownRenderer
|
<MarkdownRenderer
|
||||||
content={message.content}
|
content={message.content}
|
||||||
onImageLinkClick={handleImageLinkClick}
|
onImageLinkClick={handleImageLinkClick}
|
||||||
|
onLinkClick={onLinkClick}
|
||||||
/>
|
/>
|
||||||
) : isStreaming ? (
|
) : isStreaming ? (
|
||||||
<div className="flex items-center gap-2 text-[var(--color-text-tertiary)]">
|
<div className="flex items-center gap-2 text-[var(--color-text-tertiary)]">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user