// File: frontend/app/chat/page.tsx (更新会话逻辑) // Description: AI 聊天界面,实现助手关联会话、临时新对话和发送时创建会话 'use client'; import React, { useState, useRef, useEffect, useCallback } from 'react'; import { SendHorizontal, Loader2, PanelRightOpen, PanelRightClose, UserPlus, Settings2 } from 'lucide-react'; import { sendChatMessage } from '@/lib/api'; // 假设 sendChatMessage 只需 message // --- 数据接口定义 --- interface Message { id: string; text: string; sender: 'user' | 'ai'; isError?: boolean; } interface ChatSession { id: string; // 唯一 ID,例如 'session-uuid-123' 或 'temp-new-chat' title: string; createdAt: Date; assistantId: string; // 关联的助手 ID isTemporary?: boolean; // 标记是否为临时会话 } interface Assistant { id: string; name: string; description: string; avatar?: string; systemPrompt: string; model: string; temperature: number; } // --- Helper Function --- // 查找助手的最新会话 (非临时) const findLastSession = (sessions: ChatSession[], assistantId: string): ChatSession | undefined => { return sessions .filter(s => s.assistantId === assistantId && !s.isTemporary) .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0]; }; export default function ChatPage() { // --- State Variables --- const [inputMessage, setInputMessage] = useState(''); const [messages, setMessages] = useState([]); // 初始为空,由选中助手/会话决定 const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [isSessionPanelOpen, setIsSessionPanelOpen] = useState(true); // --- Mock Data & State --- const [assistants, setAssistants] = useState([ { id: 'asst-default', name: '默认助手', description: '通用聊天助手', avatar: '🤖', systemPrompt: '你是一个乐于助人的 AI 助手。', model: 'gpt-3.5-turbo', temperature: 0.7 }, { id: 'asst-coder', name: '代码助手', description: '帮助编写和解释代码', avatar: '💻', systemPrompt: '你是一个专业的代码助手...', model: 'gpt-4', temperature: 0.5 }, { id: 'asst-writer', name: '写作助手', description: '协助进行创意写作', avatar: '✍️', systemPrompt: '你是一位富有创意的写作伙伴...', model: 'gemini-pro', temperature: 0.9 }, ]); const [currentAssistantId, setCurrentAssistantId] = useState('asst-default'); // 所有会话存储在一起,通过 assistantId 过滤 const [allSessions, setAllSessions] = useState([ {id: 'session-1', title: '默认助手的讨论', createdAt: new Date(Date.now() - 3600000), assistantId: 'asst-default'}, {id: 'session-2', title: '代码调试记录', createdAt: new Date(Date.now() - 7200000), assistantId: 'asst-coder'}, {id: 'session-3', title: '默认助手的学习 LangChain', createdAt: new Date(), assistantId: 'asst-default'}, ]); // 当前激活的会话 ID,可能是真实 ID 或 'temp-new-chat' const [currentSessionId, setCurrentSessionId] = useState(null); // --- Refs --- const messagesEndRef = useRef(null); // --- Effects --- // 自动滚动 useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); // 根据当前助手 ID 过滤出会话列表 const currentAssistantSessions = React.useMemo(() => { return allSessions.filter(s => s.assistantId === currentAssistantId); }, [allSessions, currentAssistantId]); // 当助手切换时,决定加载哪个会话 useEffect(() => { const lastSession = findLastSession(allSessions, currentAssistantId); if (lastSession) { setCurrentSessionId(lastSession.id); // TODO: 在实际应用中,这里需要调用 API 加载 lastSession.id 的历史消息 console.log(`加载助手 ${currentAssistantId} 的最后一个会话: ${lastSession.id}`); // 模拟加载消息 const selectedAssistant = assistants.find(a => a.id === currentAssistantId); setMessages([ { id: `init-${lastSession.id}-1`, text: `继续与 ${selectedAssistant?.name || '助手'} 的对话: ${lastSession.title}`, sender: 'ai' }, // ... 应该加载真实历史消息 ]); } else { // 没有历史会话,进入临时新对话状态 setCurrentSessionId('temp-new-chat'); console.log(`助手 ${currentAssistantId} 没有历史会话,创建临时新对话`); const selectedAssistant = assistants.find(a => a.id === currentAssistantId); setMessages([ { id: `init-temp-${currentAssistantId}`, text: `开始与 ${selectedAssistant?.name || '助手'} 的新对话吧!`, sender: 'ai' }, ]); // 不将会话添加到 allSessions 列表 } }, [currentAssistantId, allSessions, assistants]); // 依赖助手 ID 和所有会话 // --- Event Handlers --- const handleSendMessage = async (e?: React.FormEvent) => { e?.preventDefault(); const trimmedMessage = inputMessage.trim(); if (!trimmedMessage || isLoading) return; setError(null); setIsLoading(true); // 先设置加载状态 const userMessage: Message = { id: Date.now().toString(), text: trimmedMessage, sender: 'user', }; let targetSessionId = currentSessionId; let newSessionCreated = false; // 检查是否是临时新对话的第一条消息 if (currentSessionId === 'temp-new-chat') { console.log("发送临时新对话的第一条消息,准备创建会话..."); try { // --- 模拟后端创建会话并生成标题 --- // TODO: 调用后端 API (e.g., POST /api/v1/sessions) // const { newSessionId, newTitle } = await createSessionOnBackend(currentAssistantId, trimmedMessage); await new Promise(resolve => setTimeout(resolve, 500)); // 模拟网络延迟 const newSessionId = `session-${Date.now()}`; // 模拟生成的新 ID const newTitle = `关于 "${trimmedMessage.substring(0, 15)}..." 的讨论`; // 模拟 AI 生成的标题 // --- 模拟结束 --- const newSession: ChatSession = { id: newSessionId, title: newTitle, createdAt: new Date(), assistantId: currentAssistantId, isTemporary: false, // 不再是临时的 }; // 将新创建的会话添加到全局列表,并设为当前会话 setAllSessions(prev => [...prev, newSession]); setCurrentSessionId(newSessionId); targetSessionId = newSessionId; // 更新目标会话 ID newSessionCreated = true; // 标记已创建 console.log(`新会话创建成功: ID=${newSessionId}, Title=${newTitle}`); // 更新消息列表(先添加用户消息) // 注意:因为 setMessages 是异步的,这里先更新,后续 AI 回复再更新 setMessages(prev => [...prev, userMessage]); } catch (creationError: any) { console.error("创建新会话失败:", creationError); setError(`无法创建新会话: ${creationError.message}`); setIsLoading(false); return; // 创建失败则停止后续操作 } } else { // 不是新会话,直接将用户消息添加到当前消息列表 setMessages(prev => [...prev, userMessage]); } // 清空输入框 setInputMessage(''); // 发送消息到后端 (无论是否新创建会话,都需要发送) try { console.log(`发送消息到会话: ${targetSessionId}, 助手: ${currentAssistantId}`); // TODO: 调用后端 API 时需要传递 targetSessionId 和 currentAssistantId // const aiReply = await sendChatMessage(trimmedMessage, targetSessionId, currentAssistantId); const aiReply = await sendChatMessage(trimmedMessage); // 暂时保持不变 const aiMessage: Message = { id: Date.now().toString() + '_ai', text: aiReply, sender: 'ai', }; // 使用函数式更新,确保基于最新的消息列表添加回复 setMessages((prevMessages) => [...prevMessages, aiMessage]); } catch (sendError: any) { console.error("发送消息失败:", sendError); const errorMessageText = sendError.message || 'An unknown error occurred.'; setError(errorMessageText); // 更新错误状态 const errorMessage: Message = { id: Date.now().toString() + '_err', text: `错误: ${errorMessageText}`, sender: 'ai', isError: true, }; setMessages((prevMessages) => [...prevMessages, errorMessage]); } finally { setIsLoading(false); // 无论成功失败,结束加载 } }; const handleInputChange = (e: React.ChangeEvent) => { setInputMessage(e.target.value); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey && !isLoading) { e.preventDefault(); handleSendMessage(); } }; const toggleSessionPanel = () => { setIsSessionPanelOpen(!isSessionPanelOpen); }; // 点击助手列表项的处理函数 const handleSelectAssistant = (assistantId: string) => { if (assistantId !== currentAssistantId) { setCurrentAssistantId(assistantId); // 后续逻辑由 useEffect 处理 } } // 点击会话列表项的处理函数 const handleSelectSession = (sessionId: string) => { if (sessionId !== currentSessionId) { setCurrentSessionId(sessionId); // TODO: 调用 API 加载该会话的历史消息 console.log(`切换到会话: ${sessionId}`); // 模拟加载消息 const session = allSessions.find(s => s.id === sessionId); const assistant = assistants.find(a => a.id === session?.assistantId); setMessages([ { id: `init-${sessionId}-1`, text: `继续与 ${assistant?.name || '助手'} 的对话: ${session?.title || ''}`, sender: 'ai' }, // ... 加载真实历史消息 ]); } } // 点击新建话题按钮 const handleNewTopic = () => { if (currentSessionId !== 'temp-new-chat') { setCurrentSessionId('temp-new-chat'); const selectedAssistant = assistants.find(a => a.id === currentAssistantId); setMessages([ { id: `init-temp-${currentAssistantId}`, text: `开始与 ${selectedAssistant?.name || '助手'} 的新对话吧!`, sender: 'ai' }, ]); console.log("手动创建临时新对话"); } } // --- JSX Rendering --- return ( // 最外层 Flex 容器
{/* 使用 gap 添加间距 */} {/* 左侧助手面板 */} {/* 中间主聊天区域 */}
{/* 聊天窗口标题 - 显示当前助手和切换会话按钮 */}
{assistants.find(a => a.id === currentAssistantId)?.avatar || '👤'}

{assistants.find(a => a.id === currentAssistantId)?.name || '助手'}

{/* TODO: 显示模型、温度等信息 */} {/* {assistants.find(a => a.id === currentAssistantId)?.model} */}
{/* 消息显示区域 */}
{messages.map((message) => (

{message.text}

))} {isLoading && (
AI 正在思考...
)}
{/* 消息输入区域 */}
{/* 右侧会话管理面板 */}
); }