// src/views/AgentChat/index_v3.js // Agent聊天页面 V3 - 带左侧会话列表和用户信息集成 import React, { useState, useEffect, useRef } from 'react'; import { Box, Flex, VStack, HStack, Text, Input, IconButton, Button, Avatar, Heading, Divider, Spinner, Badge, useColorModeValue, useToast, Progress, Fade, Collapse, useDisclosure, InputGroup, InputLeftElement, Menu, MenuButton, MenuList, MenuItem, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, Tooltip, } from '@chakra-ui/react'; import { FiSend, FiSearch, FiPlus, FiMessageSquare, FiTrash2, FiMoreVertical, FiRefreshCw, FiDownload, FiCpu, FiUser, FiZap, FiClock, } from 'react-icons/fi'; import { useAuth } from '@contexts/AuthContext'; import { PlanCard } from '@components/ChatBot/PlanCard'; import { StepResultCard } from '@components/ChatBot/StepResultCard'; import { logger } from '@utils/logger'; import axios from 'axios'; /** * Agent消息类型 */ const MessageTypes = { USER: 'user', AGENT_THINKING: 'agent_thinking', AGENT_PLAN: 'agent_plan', AGENT_EXECUTING: 'agent_executing', AGENT_RESPONSE: 'agent_response', ERROR: 'error', }; /** * Agent聊天页面 V3 */ const AgentChatV3 = () => { const { user } = useAuth(); // 获取当前用户信息 const toast = useToast(); // 会话相关状态 const [sessions, setSessions] = useState([]); const [currentSessionId, setCurrentSessionId] = useState(null); const [isLoadingSessions, setIsLoadingSessions] = useState(true); // 消息相关状态 const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(''); const [isProcessing, setIsProcessing] = useState(false); const [currentProgress, setCurrentProgress] = useState(0); // UI 状态 const [searchQuery, setSearchQuery] = useState(''); const { isOpen: isSidebarOpen, onToggle: toggleSidebar } = useDisclosure({ defaultIsOpen: true }); // Refs const messagesEndRef = useRef(null); const inputRef = useRef(null); // 颜色主题 const bgColor = useColorModeValue('gray.50', 'gray.900'); const sidebarBg = useColorModeValue('white', 'gray.800'); const chatBg = useColorModeValue('white', 'gray.800'); const inputBg = useColorModeValue('white', 'gray.700'); const borderColor = useColorModeValue('gray.200', 'gray.600'); const hoverBg = useColorModeValue('gray.100', 'gray.700'); const activeBg = useColorModeValue('blue.50', 'blue.900'); const userBubbleBg = useColorModeValue('blue.500', 'blue.600'); const agentBubbleBg = useColorModeValue('white', 'gray.700'); // ==================== 会话管理函数 ==================== // 加载会话列表 const loadSessions = async () => { if (!user?.id) return; setIsLoadingSessions(true); try { const response = await axios.get('/mcp/agent/sessions', { params: { user_id: String(user.id), limit: 50 }, }); if (response.data.success) { setSessions(response.data.data); logger.info('会话列表加载成功', response.data.data); } } catch (error) { logger.error('加载会话列表失败', error); toast({ title: '加载失败', description: '无法加载会话列表', status: 'error', duration: 3000, }); } finally { setIsLoadingSessions(false); } }; // 加载会话历史 const loadSessionHistory = async (sessionId) => { if (!sessionId) return; try { const response = await axios.get(`/mcp/agent/history/${sessionId}`, { params: { limit: 100 }, }); if (response.data.success) { const history = response.data.data; // 将历史记录转换为消息格式 const formattedMessages = history.map((msg, idx) => ({ id: `${sessionId}-${idx}`, type: msg.message_type === 'user' ? MessageTypes.USER : MessageTypes.AGENT_RESPONSE, content: msg.message, plan: msg.plan ? JSON.parse(msg.plan) : null, stepResults: msg.steps ? JSON.parse(msg.steps) : null, timestamp: msg.timestamp, })); setMessages(formattedMessages); logger.info('会话历史加载成功', formattedMessages); } } catch (error) { logger.error('加载会话历史失败', error); toast({ title: '加载失败', description: '无法加载会话历史', status: 'error', duration: 3000, }); } }; // 创建新会话 const createNewSession = () => { setCurrentSessionId(null); setMessages([ { id: Date.now(), type: MessageTypes.AGENT_RESPONSE, content: `你好${user?.nickname || ''}!我是价小前,北京价值前沿科技公司的AI投研助手。\n\n我会通过多步骤分析来帮助你深入了解金融市场。\n\n你可以问我:\n• 全面分析某只股票\n• 某个行业的投资机会\n• 今日市场热点\n• 某个概念板块的表现`, timestamp: new Date().toISOString(), }, ]); }; // 切换会话 const switchSession = (sessionId) => { setCurrentSessionId(sessionId); loadSessionHistory(sessionId); }; // 删除会话(需要后端API支持) const deleteSession = async (sessionId) => { // TODO: 实现删除会话的后端API toast({ title: '删除会话', description: '此功能尚未实现', status: 'info', duration: 2000, }); }; // ==================== 消息处理函数 ==================== // 自动滚动到底部 const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [messages]); // 添加消息 const addMessage = (message) => { setMessages((prev) => [...prev, { ...message, id: Date.now() }]); }; // 发送消息 const handleSendMessage = async () => { if (!inputValue.trim() || isProcessing) return; // 权限检查 - 检查订阅类型(与Community等页面保持一致) const hasAccess = user?.subscription_type === 'max' || user?.subscription_type === 'pro'; if (!hasAccess) { logger.warn('AgentChat', '权限检查失败', { userId: user?.id, username: user?.username, subscription_type: user?.subscription_type, userObject: user }); toast({ title: '订阅升级', description: '「价小前投研」功能需要 Pro 或 Max 订阅。请前往设置页面升级您的订阅。', status: 'warning', duration: 5000, isClosable: true, }); return; } logger.info('AgentChat', '权限检查通过', { userId: user?.id, username: user?.username, subscription_type: user?.subscription_type }); const userMessage = { type: MessageTypes.USER, content: inputValue, timestamp: new Date().toISOString(), }; addMessage(userMessage); const userInput = inputValue; setInputValue(''); setIsProcessing(true); setCurrentProgress(0); let currentPlan = null; let stepResults = []; try { // 1. 显示思考状态 addMessage({ type: MessageTypes.AGENT_THINKING, content: '正在分析你的问题...', timestamp: new Date().toISOString(), }); setCurrentProgress(10); // 2. 调用后端API(非流式) const response = await axios.post('/mcp/agent/chat', { message: userInput, conversation_history: messages .filter((m) => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE) .map((m) => ({ isUser: m.type === MessageTypes.USER, content: m.content, })), user_id: user?.id ? String(user.id) : 'anonymous', user_nickname: user?.nickname || user?.username || '匿名用户', user_avatar: user?.avatar || '', session_id: currentSessionId, }); // 移除思考消息 setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING)); if (response.data.success) { const data = response.data; // 更新 session_id(如果是新会话) if (data.session_id && !currentSessionId) { setCurrentSessionId(data.session_id); } // 显示执行计划 if (data.plan) { currentPlan = data.plan; addMessage({ type: MessageTypes.AGENT_PLAN, content: '已制定执行计划', plan: data.plan, timestamp: new Date().toISOString(), }); setCurrentProgress(30); } // 显示执行步骤 if (data.steps && data.steps.length > 0) { stepResults = data.steps; addMessage({ type: MessageTypes.AGENT_EXECUTING, content: '正在执行步骤...', plan: currentPlan, stepResults: stepResults, timestamp: new Date().toISOString(), }); setCurrentProgress(70); } // 移除执行中消息 setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_EXECUTING)); // 显示最终结果 addMessage({ type: MessageTypes.AGENT_RESPONSE, content: data.final_answer || data.message || '处理完成', plan: currentPlan, stepResults: stepResults, metadata: data.metadata, timestamp: new Date().toISOString(), }); setCurrentProgress(100); // 重新加载会话列表 loadSessions(); } } catch (error) { logger.error('Agent chat error', error); // 移除思考/执行中消息 setMessages((prev) => prev.filter( (m) => m.type !== MessageTypes.AGENT_THINKING && m.type !== MessageTypes.AGENT_EXECUTING ) ); const errorMessage = error.response?.data?.error || error.message || '处理失败'; addMessage({ type: MessageTypes.ERROR, content: `处理失败:${errorMessage}`, timestamp: new Date().toISOString(), }); toast({ title: '处理失败', description: errorMessage, status: 'error', duration: 5000, isClosable: true, }); } finally { setIsProcessing(false); setCurrentProgress(0); inputRef.current?.focus(); } }; // 处理键盘事件 const handleKeyPress = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } }; // 清空对话 const handleClearChat = () => { createNewSession(); }; // 导出对话 const handleExportChat = () => { const chatText = messages .filter((m) => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE) .map((msg) => `[${msg.type === MessageTypes.USER ? '用户' : '价小前'}] ${msg.content}`) .join('\n\n'); const blob = new Blob([chatText], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `chat_${new Date().toISOString().slice(0, 10)}.txt`; a.click(); URL.revokeObjectURL(url); }; // ==================== 初始化 ==================== useEffect(() => { if (user) { loadSessions(); createNewSession(); } }, [user]); // ==================== 渲染 ==================== // 快捷问题 const quickQuestions = [ '全面分析贵州茅台这只股票', '今日涨停股票有哪些亮点', '新能源概念板块的投资机会', '半导体行业最新动态', ]; // 筛选会话 const filteredSessions = sessions.filter( (session) => !searchQuery || session.last_message?.toLowerCase().includes(searchQuery.toLowerCase()) ); return ( {/* 左侧会话列表 */} {/* 侧边栏头部 */} {/* 搜索框 */} setSearchQuery(e.target.value)} /> {/* 会话列表 */} {isLoadingSessions ? ( ) : filteredSessions.length === 0 ? ( {searchQuery ? '没有找到匹配的对话' : '暂无对话记录'} ) : ( filteredSessions.map((session) => ( switchSession(session.session_id)} > {session.last_message || '新对话'} {new Date(session.last_timestamp).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', })} {session.message_count} 条 } size="xs" variant="ghost" onClick={(e) => e.stopPropagation()} /> } color="red.500" onClick={(e) => { e.stopPropagation(); deleteSession(session.session_id); }} > 删除对话 )) )} {/* 用户信息 */} {user?.nickname || '未登录'} {user?.id || 'anonymous'} {/* 主聊天区域 */} {/* 聊天头部 */} } size="sm" variant="ghost" aria-label="切换侧边栏" onClick={toggleSidebar} /> } /> 价小前投研 智能分析 多步骤深度研究 } size="sm" variant="ghost" aria-label="清空对话" onClick={handleClearChat} /> } size="sm" variant="ghost" aria-label="导出对话" onClick={handleExportChat} /> {/* 进度条 */} {isProcessing && ( )} {/* 消息列表 */} {messages.map((message) => ( ))}
{/* 快捷问题 */} {messages.length <= 2 && !isProcessing && ( 💡 试试这些问题: {quickQuestions.map((question, idx) => ( ))} )} {/* 输入框 */} setInputValue(e.target.value)} onKeyPress={handleKeyPress} placeholder="输入你的问题,我会进行深度分析..." bg={inputBg} border="1px" borderColor={borderColor} _focus={{ borderColor: 'blue.500', boxShadow: '0 0 0 1px #3182CE' }} mr={2} disabled={isProcessing} size="lg" /> : } colorScheme="blue" aria-label="发送" onClick={handleSendMessage} isLoading={isProcessing} disabled={!inputValue.trim() || isProcessing} size="lg" /> ); }; /** * 消息渲染器 */ const MessageRenderer = ({ message, userAvatar }) => { const userBubbleBg = useColorModeValue('blue.500', 'blue.600'); const agentBubbleBg = useColorModeValue('white', 'gray.700'); const borderColor = useColorModeValue('gray.200', 'gray.600'); switch (message.type) { case MessageTypes.USER: return ( {message.content} } /> ); case MessageTypes.AGENT_THINKING: return ( } /> {message.content} ); case MessageTypes.AGENT_PLAN: return ( } /> ); case MessageTypes.AGENT_EXECUTING: return ( } /> {message.stepResults?.map((result, idx) => ( ))} ); case MessageTypes.AGENT_RESPONSE: return ( } /> {/* 最终总结 */} {message.content} {/* 元数据 */} {message.metadata && ( 总步骤: {message.metadata.total_steps} ✓ {message.metadata.successful_steps} {message.metadata.failed_steps > 0 && ( ✗ {message.metadata.failed_steps} )} 耗时: {message.metadata.total_execution_time?.toFixed(1)}s )} {/* 执行详情(可选) */} {message.plan && message.stepResults && message.stepResults.length > 0 && ( 📊 执行详情(点击展开查看) {message.stepResults.map((result, idx) => ( ))} )} ); case MessageTypes.ERROR: return ( } /> {message.content} ); default: return null; } }; export default AgentChatV3;