AI问片关闭弹窗不中断
This commit is contained in:
@@ -8097,7 +8097,7 @@ function PlayPageClient() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* AI问片面板 */}
|
{/* AI问片面板 */}
|
||||||
{aiEnabled && showAIChat && detail && (
|
{aiEnabled && detail && (
|
||||||
<AIChatPanel
|
<AIChatPanel
|
||||||
isOpen={showAIChat}
|
isOpen={showAIChat}
|
||||||
onClose={() => setShowAIChat(false)}
|
onClose={() => setShowAIChat(false)}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Bot, Loader2, Send, Sparkles, Trash2,X } from 'lucide-react';
|
import { Bot, Loader2, Send, Sparkles, Trash2,X } from 'lucide-react';
|
||||||
import React, { useEffect,useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
@@ -19,6 +19,7 @@ interface AIChatPanelProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
context?: VideoContext;
|
context?: VideoContext;
|
||||||
welcomeMessage?: string;
|
welcomeMessage?: string;
|
||||||
|
onStreamingChange?: (isStreaming: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AIChatPanel({
|
export default function AIChatPanel({
|
||||||
@@ -26,14 +27,15 @@ export default function AIChatPanel({
|
|||||||
onClose,
|
onClose,
|
||||||
context,
|
context,
|
||||||
welcomeMessage = '你好!我是MoonTVPlus的AI影视助手,有什么可以帮你的吗?',
|
welcomeMessage = '你好!我是MoonTVPlus的AI影视助手,有什么可以帮你的吗?',
|
||||||
|
onStreamingChange,
|
||||||
}: AIChatPanelProps) {
|
}: AIChatPanelProps) {
|
||||||
// 生成sessionStorage的key,基于视频上下文
|
// 使用 useMemo 稳定 storage key,只在实际内容变化时才改变
|
||||||
const getStorageKey = () => {
|
const storageKey = useMemo(() => {
|
||||||
if (context?.title) {
|
if (context?.title) {
|
||||||
return `ai-chat-${context.title}-${context.year || ''}-${context.type || ''}`;
|
return `ai-chat-${context.title}-${context.year || ''}-${context.type || ''}`;
|
||||||
}
|
}
|
||||||
return 'ai-chat-general';
|
return 'ai-chat-general';
|
||||||
};
|
}, [context?.title, context?.year, context?.type]);
|
||||||
|
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||||
{ role: 'assistant', content: welcomeMessage },
|
{ role: 'assistant', content: welcomeMessage },
|
||||||
@@ -43,7 +45,9 @@ export default function AIChatPanel({
|
|||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const contextKeyRef = useRef<string>(getStorageKey());
|
const prevStorageKeyRef = useRef<string>(storageKey);
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
const hasLoadedRef = useRef(false);
|
||||||
|
|
||||||
// 自动滚动到底部
|
// 自动滚动到底部
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
@@ -58,7 +62,9 @@ export default function AIChatPanel({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
const storageKey = getStorageKey();
|
// 如果已经加载过当前 storageKey,跳过
|
||||||
|
if (hasLoadedRef.current) return;
|
||||||
|
|
||||||
const savedMessages = sessionStorage.getItem(storageKey);
|
const savedMessages = sessionStorage.getItem(storageKey);
|
||||||
|
|
||||||
if (savedMessages) {
|
if (savedMessages) {
|
||||||
@@ -71,32 +77,50 @@ export default function AIChatPanel({
|
|||||||
console.error('加载聊天记录失败:', error);
|
console.error('加载聊天记录失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, []); // 只在组件挂载时加载一次
|
|
||||||
|
// 标记为已加载
|
||||||
|
hasLoadedRef.current = true;
|
||||||
|
}, [storageKey]); // 当 storageKey 变化时重新加载
|
||||||
|
|
||||||
// 保存消息记录到sessionStorage
|
// 保存消息记录到sessionStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
const storageKey = getStorageKey();
|
|
||||||
try {
|
try {
|
||||||
sessionStorage.setItem(storageKey, JSON.stringify(messages));
|
sessionStorage.setItem(storageKey, JSON.stringify(messages));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存聊天记录失败:', error);
|
console.error('保存聊天记录失败:', error);
|
||||||
}
|
}
|
||||||
}, [messages, context]); // 消息变化时保存
|
}, [messages, storageKey]); // 消息变化时保存
|
||||||
|
|
||||||
// 检测VideoContext变化,清除旧的聊天记录
|
// 检测VideoContext变化,清除旧的聊天记录
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
const newKey = getStorageKey();
|
if (prevStorageKeyRef.current !== storageKey) {
|
||||||
if (contextKeyRef.current !== newKey) {
|
// 上下文变化了,取消正在进行的请求
|
||||||
// 上下文变化了,清除消息并重置为欢迎消息
|
if (abortControllerRef.current) {
|
||||||
|
console.log('视频上下文变化,取消正在进行的AI请求');
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
abortControllerRef.current = null;
|
||||||
|
setIsStreaming(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除消息并重置为欢迎消息
|
||||||
console.log('视频上下文变化,清除聊天记录');
|
console.log('视频上下文变化,清除聊天记录');
|
||||||
setMessages([{ role: 'assistant', content: welcomeMessage }]);
|
setMessages([{ role: 'assistant', content: welcomeMessage }]);
|
||||||
contextKeyRef.current = newKey;
|
|
||||||
|
// 重置加载标记,允许加载新视频的聊天记录
|
||||||
|
hasLoadedRef.current = false;
|
||||||
|
|
||||||
|
prevStorageKeyRef.current = storageKey;
|
||||||
}
|
}
|
||||||
}, [context, welcomeMessage]); // 监听context变化
|
}, [storageKey, welcomeMessage]); // 监听 storageKey 变化
|
||||||
|
|
||||||
|
// 通知父组件 streaming 状态变化
|
||||||
|
useEffect(() => {
|
||||||
|
onStreamingChange?.(isStreaming);
|
||||||
|
}, [isStreaming, onStreamingChange]);
|
||||||
|
|
||||||
// 自动聚焦输入框和防止背景滚动
|
// 自动聚焦输入框和防止背景滚动
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -144,6 +168,10 @@ export default function AIChatPanel({
|
|||||||
// 先添加一个空的助手消息用于流式更新或显示错误
|
// 先添加一个空的助手消息用于流式更新或显示错误
|
||||||
setMessages((prev) => [...prev, { role: 'assistant', content: '' }]);
|
setMessages((prev) => [...prev, { role: 'assistant', content: '' }]);
|
||||||
|
|
||||||
|
// 创建新的 AbortController
|
||||||
|
const abortController = new AbortController();
|
||||||
|
abortControllerRef.current = abortController;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/ai/chat', {
|
const response = await fetch('/api/ai/chat', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -155,6 +183,7 @@ export default function AIChatPanel({
|
|||||||
context,
|
context,
|
||||||
history: messages.filter((m) => m.role !== 'assistant' || m.content !== welcomeMessage),
|
history: messages.filter((m) => m.role !== 'assistant' || m.content !== welcomeMessage),
|
||||||
}),
|
}),
|
||||||
|
signal: abortController.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -231,6 +260,12 @@ export default function AIChatPanel({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 如果是主动取消的请求(切换视频或其他原因),不显示错误
|
||||||
|
if ((error as Error).name === 'AbortError') {
|
||||||
|
console.log('请求已取消');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.error('发送消息失败:', error);
|
console.error('发送消息失败:', error);
|
||||||
|
|
||||||
// 更新最后一条空消息为错误消息
|
// 更新最后一条空消息为错误消息
|
||||||
@@ -244,6 +279,7 @@ export default function AIChatPanel({
|
|||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsStreaming(false);
|
setIsStreaming(false);
|
||||||
|
abortControllerRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -259,7 +295,6 @@ export default function AIChatPanel({
|
|||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
// 清除sessionStorage
|
// 清除sessionStorage
|
||||||
const storageKey = getStorageKey();
|
|
||||||
sessionStorage.removeItem(storageKey);
|
sessionStorage.removeItem(storageKey);
|
||||||
|
|
||||||
// 重置消息为欢迎消息
|
// 重置消息为欢迎消息
|
||||||
@@ -268,14 +303,14 @@ export default function AIChatPanel({
|
|||||||
console.log('已清空聊天上下文');
|
console.log('已清空聊天上下文');
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
const modalContent = (
|
const modalContent = (
|
||||||
<div
|
<div
|
||||||
className='fixed inset-0 z-[1002] flex items-center justify-center bg-black/50 backdrop-blur-sm overflow-hidden'
|
className={`fixed inset-0 z-[1002] flex items-center justify-center bg-black/50 backdrop-blur-sm overflow-hidden transition-opacity duration-200 ${
|
||||||
|
isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||||
|
}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
// 点击遮罩层关闭弹窗
|
// 点击遮罩层关闭弹窗
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget && isOpen) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
|||||||
const [showMobileActions, setShowMobileActions] = useState(false);
|
const [showMobileActions, setShowMobileActions] = useState(false);
|
||||||
const [searchFavorited, setSearchFavorited] = useState<boolean | null>(null); // 搜索结果的收藏状态
|
const [searchFavorited, setSearchFavorited] = useState<boolean | null>(null); // 搜索结果的收藏状态
|
||||||
const [showAIChat, setShowAIChat] = useState(false);
|
const [showAIChat, setShowAIChat] = useState(false);
|
||||||
|
const [isAIStreaming, setIsAIStreaming] = useState(false);
|
||||||
const [aiEnabled, setAiEnabled] = useState(false);
|
const [aiEnabled, setAiEnabled] = useState(false);
|
||||||
const [aiDefaultMessageWithVideo, setAiDefaultMessageWithVideo] = useState('');
|
const [aiDefaultMessageWithVideo, setAiDefaultMessageWithVideo] = useState('');
|
||||||
const [showDetailPanel, setShowDetailPanel] = useState(false);
|
const [showDetailPanel, setShowDetailPanel] = useState(false);
|
||||||
@@ -1490,11 +1491,12 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* AI问片面板 */}
|
{/* AI问片面板 - 只在打开或正在流式响应时渲染 */}
|
||||||
{aiEnabled && showAIChat && (
|
{aiEnabled && (showAIChat || isAIStreaming) && (
|
||||||
<AIChatPanel
|
<AIChatPanel
|
||||||
isOpen={showAIChat}
|
isOpen={showAIChat}
|
||||||
onClose={() => setShowAIChat(false)}
|
onClose={() => setShowAIChat(false)}
|
||||||
|
onStreamingChange={setIsAIStreaming}
|
||||||
context={{
|
context={{
|
||||||
title: actualTitle,
|
title: actualTitle,
|
||||||
year: actualYear,
|
year: actualYear,
|
||||||
|
|||||||
Reference in New Issue
Block a user