/* eslint-disable @typescript-eslint/no-explicit-any */ 'use client'; import { Bot, Loader2, Send, Sparkles, Trash2,X } from 'lucide-react'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { VideoContext } from '@/lib/ai-orchestrator'; interface ChatMessage { role: 'user' | 'assistant'; content: string; } interface AIChatPanelProps { isOpen: boolean; onClose: () => void; context?: VideoContext; welcomeMessage?: string; onStreamingChange?: (isStreaming: boolean) => void; } export default function AIChatPanel({ isOpen, onClose, context, welcomeMessage = '你好!我是MoonTVPlus的AI影视助手,有什么可以帮你的吗?', onStreamingChange, }: AIChatPanelProps) { // 使用 useMemo 稳定 storage key,只在实际内容变化时才改变 const storageKey = useMemo(() => { if (context?.title) { return `ai-chat-${context.title}-${context.year || ''}-${context.type || ''}`; } return 'ai-chat-general'; }, [context?.title, context?.year, context?.type]); const [messages, setMessages] = useState([ { role: 'assistant', content: welcomeMessage }, ]); const [input, setInput] = useState(''); const [isStreaming, setIsStreaming] = useState(false); const [isMobile, setIsMobile] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); const prevStorageKeyRef = useRef(storageKey); const abortControllerRef = useRef(null); const hasLoadedRef = useRef(false); // 自动滚动到底部 const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [messages]); // 从sessionStorage加载消息记录 useEffect(() => { if (typeof window === 'undefined') return; // 如果已经加载过当前 storageKey,跳过 if (hasLoadedRef.current) return; const savedMessages = sessionStorage.getItem(storageKey); if (savedMessages) { try { const parsed = JSON.parse(savedMessages); if (Array.isArray(parsed) && parsed.length > 0) { setMessages(parsed); } } catch (error) { console.error('加载聊天记录失败:', error); } } // 标记为已加载 hasLoadedRef.current = true; }, [storageKey]); // 当 storageKey 变化时重新加载 // 保存消息记录到sessionStorage useEffect(() => { if (typeof window === 'undefined') return; try { sessionStorage.setItem(storageKey, JSON.stringify(messages)); } catch (error) { console.error('保存聊天记录失败:', error); } }, [messages, storageKey]); // 消息变化时保存 // 检测VideoContext变化,清除旧的聊天记录 useEffect(() => { if (typeof window === 'undefined') return; if (prevStorageKeyRef.current !== storageKey) { // 上下文变化了,取消正在进行的请求 if (abortControllerRef.current) { console.log('视频上下文变化,取消正在进行的AI请求'); abortControllerRef.current.abort(); abortControllerRef.current = null; setIsStreaming(false); } // 清除消息并重置为欢迎消息 console.log('视频上下文变化,清除聊天记录'); setMessages([{ role: 'assistant', content: welcomeMessage }]); // 重置加载标记,允许加载新视频的聊天记录 hasLoadedRef.current = false; prevStorageKeyRef.current = storageKey; } }, [storageKey, welcomeMessage]); // 监听 storageKey 变化 // 通知父组件 streaming 状态变化 useEffect(() => { onStreamingChange?.(isStreaming); }, [isStreaming, onStreamingChange]); // 自动聚焦输入框和防止背景滚动 useEffect(() => { if (isOpen) { // 检测是否为移动设备 const checkMobile = () => { setIsMobile(window.innerWidth < 768); }; checkMobile(); // 只在非移动设备上聚焦输入框 if (inputRef.current && window.innerWidth >= 768) { inputRef.current.focus(); } // 防止背景滚动 const originalOverflow = document.body.style.overflow; const originalPaddingRight = document.body.style.paddingRight; // 获取滚动条宽度 const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; document.body.style.overflow = 'hidden'; document.body.style.paddingRight = `${scrollbarWidth}px`; return () => { document.body.style.overflow = originalOverflow; document.body.style.paddingRight = originalPaddingRight; }; } }, [isOpen]); const handleSendMessage = async () => { if (!input.trim() || isStreaming) return; const userMessage = input.trim(); setInput(''); // 添加用户消息 setMessages((prev) => [...prev, { role: 'user', content: userMessage }]); // 开始流式响应 setIsStreaming(true); // 先添加一个空的助手消息用于流式更新或显示错误 setMessages((prev) => [...prev, { role: 'assistant', content: '' }]); // 创建新的 AbortController const abortController = new AbortController(); abortControllerRef.current = abortController; try { const response = await fetch('/api/ai/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message: userMessage, context, history: messages.filter((m) => m.role !== 'assistant' || m.content !== welcomeMessage), }), signal: abortController.signal, }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); const errorMsg = errorData.error || errorData.details || `请求失败 (${response.status})`; throw new Error(errorMsg); } // 检查响应类型:流式(text/event-stream)或非流式(application/json) const contentType = response.headers.get('content-type'); if (contentType?.includes('text/event-stream')) { // 处理流式响应 const reader = response.body?.getReader(); const decoder = new TextDecoder(); if (!reader) { throw new Error('无法读取响应流'); } let assistantMessage = ''; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n').filter((line) => line.trim() !== ''); for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') { break; } try { const json = JSON.parse(data); const text = json.text || ''; if (text) { assistantMessage += text; // 更新最后一条消息 setMessages((prev) => { const newMessages = [...prev]; newMessages[newMessages.length - 1] = { role: 'assistant', content: assistantMessage, }; return newMessages; }); } } catch (e) { console.error('解析SSE数据失败:', e); } } } } } else { // 处理非流式响应 const data = await response.json(); const content = data.content || ''; // 更新最后一条消息为完整响应 setMessages((prev) => { const newMessages = [...prev]; newMessages[newMessages.length - 1] = { role: 'assistant', content: content, }; return newMessages; }); } } catch (error) { // 如果是主动取消的请求(切换视频或其他原因),不显示错误 if ((error as Error).name === 'AbortError') { console.log('请求已取消'); return; } console.error('发送消息失败:', error); // 更新最后一条空消息为错误消息 setMessages((prev) => { const newMessages = [...prev]; newMessages[newMessages.length - 1] = { role: 'assistant', content: `❌ 抱歉,出现了错误:\n\n${(error as Error).message}\n\n请检查:\n- AI服务配置是否正确\n- API密钥是否有效\n- 网络连接是否正常`, }; return newMessages; }); } finally { setIsStreaming(false); abortControllerRef.current = null; } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } }; // 清空聊天上下文 const handleClearContext = () => { if (typeof window === 'undefined') return; // 清除sessionStorage sessionStorage.removeItem(storageKey); // 重置消息为欢迎消息 setMessages([{ role: 'assistant', content: welcomeMessage }]); console.log('已清空聊天上下文'); }; const modalContent = (
{ // 点击遮罩层关闭弹窗 if (e.target === e.currentTarget && isOpen) { onClose(); } }} >
{/* 头部 */}

AI影视助手

{context?.title && (

正在讨论: {context.title} {context.year && ` (${context.year})`}

)}
{/* 消息列表 */}
{messages.map((message, index) => (
{/* 头像 */}
{message.role === 'user' ? ( U ) : ( )}
{/* 消息内容 */}
{message.role === 'user' ? (

{message.content}

) : (
{message.content}
)}
))} {/* 加载指示器 */} {isStreaming && (
AI正在思考...
)}
{/* 输入区域 */}