From 0aa050b95f75a09f7f8b38abc20b03220f926647 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Fri, 21 Nov 2025 14:26:26 +0800 Subject: [PATCH] update pay function --- src/views/AgentChat/index.js | 807 ++++++++++--- src/views/AgentChat/index_v3_backup.js | 1039 ++++++++++++++++ src/views/AgentChat/index_v4.js | 1519 ++++++++++++++++++++++++ 3 files changed, 3202 insertions(+), 163 deletions(-) create mode 100644 src/views/AgentChat/index_v3_backup.js create mode 100644 src/views/AgentChat/index_v4.js diff --git a/src/views/AgentChat/index.js b/src/views/AgentChat/index.js index d74c3ce2..d2ff7e2d 100644 --- a/src/views/AgentChat/index.js +++ b/src/views/AgentChat/index.js @@ -1,5 +1,5 @@ -// src/views/AgentChat/index_v3.js -// Agent聊天页面 V3 - 带左侧会话列表和用户信息集成 +// src/views/AgentChat/index_v4.js +// Agent聊天页面 V4 - 黑金毛玻璃设计,带模型选择和工具选择 import React, { useState, useEffect, useRef } from 'react'; import { @@ -16,25 +16,27 @@ import { Divider, Spinner, Badge, - useColorModeValue, useToast, Progress, Fade, Collapse, - useDisclosure, InputGroup, InputLeftElement, Menu, MenuButton, MenuList, MenuItem, - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalBody, - ModalCloseButton, Tooltip, + Select, + Checkbox, + CheckboxGroup, + Stack, + Accordion, + AccordionItem, + AccordionButton, + AccordionPanel, + AccordionIcon, + useDisclosure, } from '@chakra-ui/react'; import { FiSend, @@ -49,6 +51,10 @@ import { FiUser, FiZap, FiClock, + FiSettings, + FiCheckCircle, + FiChevronRight, + FiTool, } from 'react-icons/fi'; import { useAuth } from '@contexts/AuthContext'; import { PlanCard } from '@components/ChatBot/PlanCard'; @@ -70,10 +76,101 @@ const MessageTypes = { }; /** - * Agent聊天页面 V3 + * 可用模型配置 */ -const AgentChatV3 = () => { - const { user } = useAuth(); // 获取当前用户信息 +const AVAILABLE_MODELS = [ + { + id: 'kimi-k2', + name: 'Kimi K2', + description: '快速响应,适合日常对话', + icon: '🚀', + provider: 'Moonshot', + }, + { + id: 'kimi-k2-thinking', + name: 'Kimi K2 Thinking', + description: '深度思考,提供详细推理过程', + icon: '🧠', + provider: 'Moonshot', + recommended: true, + }, + { + id: 'glm-4.6', + name: 'GLM 4.6', + description: '智谱AI最新模型,性能强大', + icon: '⚡', + provider: 'ZhipuAI', + }, + { + id: 'deepmoney', + name: 'DeepMoney', + description: '金融专业模型,擅长财经分析', + icon: '💰', + provider: 'Custom', + }, + { + id: 'gemini-3', + name: 'Gemini 3', + description: 'Google最新多模态模型', + icon: '✨', + provider: 'Google', + }, +]; + +/** + * MCP工具分类配置 + */ +const MCP_TOOL_CATEGORIES = [ + { + name: '新闻搜索', + icon: '📰', + tools: [ + { id: 'search_news', name: '全球新闻搜索', description: '搜索国际新闻、行业动态' }, + { id: 'search_china_news', name: '中国新闻搜索', description: 'KNN语义搜索中国新闻' }, + { id: 'search_medical_news', name: '医疗新闻搜索', description: '医药、医疗设备、生物技术' }, + ], + }, + { + name: '股票分析', + icon: '📈', + tools: [ + { id: 'search_limit_up_stocks', name: '涨停股票搜索', description: '搜索涨停股票及原因分析' }, + { id: 'get_stock_analysis', name: '个股分析', description: '获取股票深度分析报告' }, + { id: 'get_stock_concepts', name: '股票概念查询', description: '查询股票相关概念板块' }, + ], + }, + { + name: '概念板块', + icon: '🏢', + tools: [ + { id: 'search_concepts', name: '概念搜索', description: '搜索股票概念板块' }, + { id: 'get_concept_details', name: '概念详情', description: '获取概念板块详细信息' }, + { id: 'get_concept_statistics', name: '概念统计', description: '涨幅榜、活跃榜、连涨榜' }, + ], + }, + { + name: '公司信息', + icon: '🏭', + tools: [ + { id: 'search_roadshows', name: '路演搜索', description: '搜索上市公司路演活动' }, + { id: 'get_company_info', name: '公司信息', description: '获取公司基本面信息' }, + ], + }, + { + name: '数据分析', + icon: '📊', + tools: [ + { id: 'query_database', name: '数据库查询', description: 'SQL查询金融数据' }, + { id: 'get_market_overview', name: '市场概况', description: '获取市场整体行情' }, + ], + }, +]; + +/** + * Agent聊天页面 V4 - 黑金毛玻璃设计 + */ +const AgentChatV4 = () => { + const { user } = useAuth(); const toast = useToast(); // 会话相关状态 @@ -87,28 +184,36 @@ const AgentChatV3 = () => { const [isProcessing, setIsProcessing] = useState(false); const [currentProgress, setCurrentProgress] = useState(0); + // 模型和工具配置状态 + const [selectedModel, setSelectedModel] = useState('kimi-k2-thinking'); + const [selectedTools, setSelectedTools] = useState(() => { + // 默认全选所有工具 + const allToolIds = MCP_TOOL_CATEGORIES.flatMap(cat => cat.tools.map(t => t.id)); + return allToolIds; + }); + // UI 状态 const [searchQuery, setSearchQuery] = useState(''); const { isOpen: isSidebarOpen, onToggle: toggleSidebar } = useDisclosure({ defaultIsOpen: true }); + const { isOpen: isRightPanelOpen, onToggle: toggleRightPanel } = 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 glassBg = 'rgba(20, 20, 20, 0.7)'; + const glassHoverBg = 'rgba(30, 30, 30, 0.8)'; + const goldAccent = '#FFD700'; + const goldGradient = 'linear-gradient(135deg, #FFD700 0%, #FFA500 100%)'; + const darkBg = '#0A0A0A'; + const borderGold = 'rgba(255, 215, 0, 0.3)'; + const textGold = '#FFD700'; + const textWhite = '#FFFFFF'; + const textGray = '#CCCCCC'; // ==================== 会话管理函数 ==================== - // 加载会话列表 const loadSessions = async () => { if (!user?.id) return; @@ -135,7 +240,6 @@ const AgentChatV3 = () => { } }; - // 加载会话历史 const loadSessionHistory = async (sessionId) => { if (!sessionId) return; @@ -146,8 +250,6 @@ const AgentChatV3 = () => { 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, @@ -171,7 +273,6 @@ const AgentChatV3 = () => { } }; - // 创建新会话 const createNewSession = () => { setCurrentSessionId(null); setMessages([ @@ -184,15 +285,12 @@ const AgentChatV3 = () => { ]); }; - // 切换会话 const switchSession = (sessionId) => { setCurrentSessionId(sessionId); loadSessionHistory(sessionId); }; - // 删除会话(需要后端API支持) const deleteSession = async (sessionId) => { - // TODO: 实现删除会话的后端API toast({ title: '删除会话', description: '此功能尚未实现', @@ -203,7 +301,6 @@ const AgentChatV3 = () => { // ==================== 消息处理函数 ==================== - // 自动滚动到底部 const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; @@ -212,24 +309,19 @@ const AgentChatV3 = () => { scrollToBottom(); }, [messages]); - // 添加消息 const addMessage = (message) => { setMessages((prev) => [...prev, { ...message, id: Date.now() }]); }; - // 发送消息(流式 SSE 版本) const handleSendMessage = async () => { if (!inputValue.trim() || isProcessing) return; - // 权限检查 - 只允许 max 用户访问(与传导链分析权限保持一致) const hasAccess = user?.subscription_type === 'max'; if (!hasAccess) { logger.warn('AgentChat', '权限检查失败', { userId: user?.id, - username: user?.username, subscription_type: user?.subscription_type, - userObject: user }); toast({ @@ -242,12 +334,6 @@ const AgentChatV3 = () => { return; } - logger.info('AgentChat', '权限检查通过', { - userId: user?.id, - username: user?.username, - subscription_type: user?.subscription_type - }); - const userMessage = { type: MessageTypes.USER, content: inputValue, @@ -265,7 +351,6 @@ const AgentChatV3 = () => { let executingMessageId = null; try { - // 1. 显示思考状态 addMessage({ type: MessageTypes.AGENT_THINKING, content: '正在分析你的问题...', @@ -274,7 +359,6 @@ const AgentChatV3 = () => { setCurrentProgress(10); - // 2. 使用 EventSource 接收 SSE 流式数据 const requestBody = { message: userInput, conversation_history: messages @@ -288,9 +372,10 @@ const AgentChatV3 = () => { user_avatar: user?.avatar || '', subscription_type: user?.subscription_type || 'free', session_id: currentSessionId, + model: selectedModel, // 传递选中的模型 + tools: selectedTools, // 传递选中的工具 }; - // 使用 fetch API 进行 SSE 请求 const response = await fetch('/mcp/agent/chat/stream', { method: 'POST', headers: { @@ -307,20 +392,18 @@ const AgentChatV3 = () => { const decoder = new TextDecoder(); let buffer = ''; - // 流式状态变量 let thinkingMessageId = null; let thinkingContent = ''; let summaryMessageId = null; let summaryContent = ''; - // 读取流式数据 while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); - buffer = lines.pop(); // 保留不完整的行 + buffer = lines.pop(); let currentEvent = null; @@ -328,7 +411,6 @@ const AgentChatV3 = () => { if (!line.trim() || line.startsWith(':')) continue; if (line.startsWith('event:')) { - // 提取事件类型 currentEvent = line.substring(6).trim(); continue; } @@ -337,9 +419,7 @@ const AgentChatV3 = () => { try { const data = JSON.parse(line.substring(5).trim()); - // 根据事件类型处理数据 if (currentEvent === 'thinking') { - // Kimi 流式思考过程 if (!thinkingMessageId) { thinkingMessageId = Date.now(); thinkingContent = ''; @@ -352,7 +432,6 @@ const AgentChatV3 = () => { }); } thinkingContent += data.content; - // 实时更新思考内容 setMessages((prev) => prev.map((m) => m.id === thinkingMessageId @@ -360,11 +439,7 @@ const AgentChatV3 = () => { : m ) ); - } else if (currentEvent === 'reasoning') { - // Kimi 推理过程(可选显示) - logger.info('Kimi reasoning:', data.content); } else if (currentEvent === 'plan') { - // 收到执行计划 currentPlan = data; thinkingMessageId = null; thinkingContent = ''; @@ -377,7 +452,6 @@ const AgentChatV3 = () => { }); setCurrentProgress(30); } else if (currentEvent === 'step_complete') { - // 收到步骤完成事件 const stepResult = { step_index: data.step_index, tool: data.tool, @@ -388,7 +462,6 @@ const AgentChatV3 = () => { }; stepResults.push(stepResult); - // 更新执行中的消息 setMessages((prev) => prev.map((m) => m.id === executingMessageId @@ -397,11 +470,9 @@ const AgentChatV3 = () => { ) ); - // 更新进度 const progress = 40 + (stepResults.length / (currentPlan?.steps?.length || 5)) * 40; setCurrentProgress(Math.min(progress, 80)); } else if (currentEvent === 'summary_chunk') { - // 流式总结内容 if (!summaryMessageId) { summaryMessageId = Date.now(); summaryContent = ''; @@ -414,13 +485,12 @@ const AgentChatV3 = () => { content: '', plan: currentPlan, stepResults: stepResults, - isStreaming: true, // 标记为流式输出中 + isStreaming: true, timestamp: new Date().toISOString(), }); setCurrentProgress(85); } summaryContent += data.content; - // 实时更新总结内容 setMessages((prev) => prev.map((m) => m.id === summaryMessageId @@ -429,23 +499,20 @@ const AgentChatV3 = () => { ) ); } else if (currentEvent === 'summary') { - // 收到完整总结(包含元数据) if (summaryMessageId) { - // 更新已有消息的元数据和内容,并标记流式输出完成 setMessages((prev) => prev.map((m) => m.id === summaryMessageId ? { ...m, - content: data.content || summaryContent, // ✅ 使用后端返回的完整内容,如果没有则使用累积内容 + content: data.content || summaryContent, metadata: data.metadata, - isStreaming: false, // ✅ 标记流式输出完成 + isStreaming: false, } : m ) ); } else { - // 如果没有流式片段,直接显示完整总结 setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING && m.type !== MessageTypes.AGENT_EXECUTING) ); @@ -455,13 +522,12 @@ const AgentChatV3 = () => { plan: currentPlan, stepResults: stepResults, metadata: data.metadata, - isStreaming: false, // 非流式,直接标记完成 + isStreaming: false, timestamp: new Date().toISOString(), }); } setCurrentProgress(100); } else if (currentEvent === 'status') { - // 状态更新 if (data.stage === 'planning') { setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING)); addMessage({ @@ -499,13 +565,11 @@ const AgentChatV3 = () => { } } - // 重新加载会话列表 loadSessions(); } catch (error) { logger.error('Agent chat error', error); - // 移除思考/执行中消息 setMessages((prev) => prev.filter( (m) => m.type !== MessageTypes.AGENT_THINKING && m.type !== MessageTypes.AGENT_EXECUTING @@ -534,7 +598,6 @@ const AgentChatV3 = () => { } }; - // 处理键盘事件 const handleKeyPress = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); @@ -542,12 +605,10 @@ const AgentChatV3 = () => { } }; - // 清空对话 const handleClearChat = () => { createNewSession(); }; - // 导出对话 const handleExportChat = () => { const chatText = messages .filter((m) => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE) @@ -563,6 +624,35 @@ const AgentChatV3 = () => { URL.revokeObjectURL(url); }; + // ==================== 工具选择处理 ==================== + + const handleToolToggle = (toolId, isChecked) => { + if (isChecked) { + setSelectedTools((prev) => [...prev, toolId]); + } else { + setSelectedTools((prev) => prev.filter((id) => id !== toolId)); + } + }; + + const handleCategoryToggle = (categoryTools, isAllSelected) => { + const toolIds = categoryTools.map((t) => t.id); + if (isAllSelected) { + // 全部取消选中 + setSelectedTools((prev) => prev.filter((id) => !toolIds.includes(id))); + } else { + // 全部选中 + setSelectedTools((prev) => { + const newTools = [...prev]; + toolIds.forEach((id) => { + if (!newTools.includes(id)) { + newTools.push(id); + } + }); + return newTools; + }); + } + }; + // ==================== 初始化 ==================== useEffect(() => { @@ -574,7 +664,6 @@ const AgentChatV3 = () => { // ==================== 渲染 ==================== - // 快捷问题 const quickQuestions = [ '全面分析贵州茅台这只股票', '今日涨停股票有哪些亮点', @@ -582,7 +671,6 @@ const AgentChatV3 = () => { '半导体行业最新动态', ]; - // 筛选会话 const filteredSessions = sessions.filter( (session) => !searchQuery || @@ -590,39 +678,85 @@ const AgentChatV3 = () => { ); return ( - + + {/* 背景装饰 - 黄金光晕效果 */} + + + {/* 左侧会话列表 */} {/* 侧边栏头部 */} - + - {/* 搜索框 */} - + setSearchQuery(e.target.value)} + bg="rgba(255, 255, 255, 0.05)" + border="1px solid" + borderColor={borderGold} + color={textWhite} + _placeholder={{ color: textGray }} + _hover={{ borderColor: goldAccent }} + _focus={{ borderColor: goldAccent, boxShadow: `0 0 0 1px ${goldAccent}` }} /> @@ -636,19 +770,19 @@ const AgentChatV3 = () => { css={{ '&::-webkit-scrollbar': { width: '6px' }, '&::-webkit-scrollbar-thumb': { - background: '#CBD5E0', + background: borderGold, borderRadius: '3px', }, }} > {isLoadingSessions ? ( - + ) : filteredSessions.length === 0 ? ( - - + + {searchQuery ? '没有找到匹配的对话' : '暂无对话记录'} @@ -658,18 +792,19 @@ const AgentChatV3 = () => { key={session.session_id} p={3} cursor="pointer" - bg={currentSessionId === session.session_id ? activeBg : 'transparent'} - _hover={{ bg: hoverBg }} - borderBottom="1px" - borderColor={borderColor} + bg={currentSessionId === session.session_id ? 'rgba(255, 215, 0, 0.1)' : 'transparent'} + _hover={{ bg: 'rgba(255, 215, 0, 0.05)' }} + borderBottom="1px solid" + borderColor="rgba(255, 215, 0, 0.1)" onClick={() => switchSession(session.session_id)} + transition="all 0.2s" > - + {session.last_message || '新对话'} - + {new Date(session.last_timestamp).toLocaleDateString('zh-CN', { @@ -679,7 +814,13 @@ const AgentChatV3 = () => { minute: 'numeric', })} - + {session.message_count} 条 @@ -691,12 +832,16 @@ const AgentChatV3 = () => { icon={} size="xs" variant="ghost" + color={textGray} + _hover={{ color: goldAccent }} onClick={(e) => e.stopPropagation()} /> - + } - color="red.500" + color="red.400" + bg="transparent" + _hover={{ bg: 'rgba(255, 0, 0, 0.1)' }} onClick={(e) => { e.stopPropagation(); deleteSession(session.session_id); @@ -713,16 +858,28 @@ const AgentChatV3 = () => { {/* 用户信息 */} - + - + - + {user?.nickname || '未登录'} - - {user?.id || 'anonymous'} - + + MAX 订阅 + @@ -730,30 +887,57 @@ const AgentChatV3 = () => { {/* 主聊天区域 */} - + {/* 聊天头部 */} - + } size="sm" variant="ghost" + color={goldAccent} + _hover={{ bg: 'rgba(255, 215, 0, 0.1)' }} aria-label="切换侧边栏" onClick={toggleSidebar} /> - } /> + + + - 价小前投研 + 价小前投研 - + - 智能分析 + AI 深度分析 - - 多步骤深度研究 + + {AVAILABLE_MODELS.find(m => m.id === selectedModel)?.name || '智能模型'} @@ -764,6 +948,8 @@ const AgentChatV3 = () => { icon={} size="sm" variant="ghost" + color={textGray} + _hover={{ color: goldAccent, bg: 'rgba(255, 215, 0, 0.1)' }} aria-label="清空对话" onClick={handleClearChat} /> @@ -771,9 +957,20 @@ const AgentChatV3 = () => { icon={} size="sm" variant="ghost" + color={textGray} + _hover={{ color: goldAccent, bg: 'rgba(255, 215, 0, 0.1)' }} aria-label="导出对话" onClick={handleExportChat} /> + } + size="sm" + variant="ghost" + color={goldAccent} + _hover={{ bg: 'rgba(255, 215, 0, 0.1)' }} + aria-label="设置" + onClick={toggleRightPanel} + /> @@ -782,9 +979,14 @@ const AgentChatV3 = () => { div': { + background: goldGradient, + }, + }} isAnimated /> )} @@ -799,7 +1001,7 @@ const AgentChatV3 = () => { css={{ '&::-webkit-scrollbar': { width: '8px' }, '&::-webkit-scrollbar-thumb': { - background: '#CBD5E0', + background: borderGold, borderRadius: '4px', }, }} @@ -816,8 +1018,8 @@ const AgentChatV3 = () => { {/* 快捷问题 */} {messages.length <= 2 && !isProcessing && ( - - + + 💡 试试这些问题: @@ -826,8 +1028,15 @@ const AgentChatV3 = () => { key={idx} size="sm" variant="outline" - colorScheme="blue" + borderColor={borderGold} + color={textGold} fontSize="xs" + _hover={{ + bg: 'rgba(255, 215, 0, 0.1)', + borderColor: goldAccent, + transform: 'translateY(-2px)', + }} + transition="all 0.2s" onClick={() => { setInputValue(question); inputRef.current?.focus(); @@ -841,7 +1050,7 @@ const AgentChatV3 = () => { )} {/* 输入框 */} - + { onChange={(e) => setInputValue(e.target.value)} onKeyPress={handleKeyPress} placeholder="输入你的问题,我会进行深度分析..." - bg={inputBg} - border="1px" - borderColor={borderColor} - _focus={{ borderColor: 'blue.500', boxShadow: '0 0 0 1px #3182CE' }} + bg="rgba(255, 255, 255, 0.05)" + border="1px solid" + borderColor={borderGold} + color={textWhite} + _placeholder={{ color: textGray }} + _focus={{ borderColor: goldAccent, boxShadow: `0 0 0 1px ${goldAccent}` }} mr={2} disabled={isProcessing} size="lg" /> - : } - colorScheme="blue" - aria-label="发送" + + + {/* 右侧配置面板 */} + + + + {/* 模型选择 */} + + + + + 选择模型 + + + + {AVAILABLE_MODELS.map((model) => ( + setSelectedModel(model.id)} + transition="all 0.2s" + _hover={{ + bg: 'rgba(255, 215, 0, 0.1)', + borderColor: goldAccent, + transform: 'translateX(4px)', + }} + position="relative" + > + {model.recommended && ( + + 推荐 + + )} + + {model.icon} + + + {model.name} + + + {model.description} + + + {selectedModel === model.id && ( + + )} + + + ))} + + + + + + {/* 工具选择 */} + + + + + + MCP 工具 + + + + {selectedTools.length} 个已选 + + + + + {MCP_TOOL_CATEGORIES.map((category, catIdx) => { + const categoryToolIds = category.tools.map((t) => t.id); + const selectedInCategory = categoryToolIds.filter((id) => selectedTools.includes(id)); + const isAllSelected = selectedInCategory.length === categoryToolIds.length; + const isSomeSelected = selectedInCategory.length > 0 && !isAllSelected; + + return ( + + + + {category.icon} + + {category.name} + + + {selectedInCategory.length}/{category.tools.length} + + + + + + + {/* 全选按钮 */} + + + {category.tools.map((tool) => ( + handleToolToggle(tool.id, e.target.checked)} + colorScheme="yellow" + size="sm" + sx={{ + '.chakra-checkbox__control': { + borderColor: borderGold, + bg: 'rgba(255, 255, 255, 0.05)', + _checked: { + bg: goldGradient, + borderColor: goldAccent, + }, + }, + }} + > + + + {tool.name} + + + {tool.description} + + + + ))} + + + + ); + })} + + + + + ); }; /** - * 消息渲染器 + * 消息渲染器(黑金毛玻璃风格) */ const MessageRenderer = ({ message, userAvatar }) => { - const userBubbleBg = useColorModeValue('blue.500', 'blue.600'); - const agentBubbleBg = useColorModeValue('white', 'gray.700'); - const borderColor = useColorModeValue('gray.200', 'gray.600'); + const glassBg = 'rgba(20, 20, 20, 0.7)'; + const goldAccent = '#FFD700'; + const goldGradient = 'linear-gradient(135deg, #FFD700 0%, #FFA500 100%)'; + const darkBg = '#0A0A0A'; + const borderGold = 'rgba(255, 215, 0, 0.3)'; + const textWhite = '#FFFFFF'; + const textGray = '#CCCCCC'; switch (message.type) { case MessageTypes.USER: @@ -887,18 +1301,25 @@ const MessageRenderer = ({ message, userAvatar }) => { - + {message.content} - } /> + } + border="2px solid" + borderColor={goldAccent} + /> ); @@ -907,19 +1328,32 @@ const MessageRenderer = ({ message, userAvatar }) => { return ( - } /> + + + - - + + {message.content} @@ -932,7 +1366,19 @@ const MessageRenderer = ({ message, userAvatar }) => { return ( - } /> + + + @@ -944,7 +1390,19 @@ const MessageRenderer = ({ message, userAvatar }) => { return ( - } /> + + + {message.stepResults?.map((result, idx) => ( @@ -959,34 +1417,44 @@ const MessageRenderer = ({ message, userAvatar }) => { return ( - } /> + + + - {/* 最终总结(支持 Markdown + ECharts) */} + {/* 最终总结 */} - {/* 流式输出中显示纯文本,完成后才渲染 Markdown + 图表 */} {message.isStreaming ? ( - + {message.content} ) : ( )} - {/* 元数据 */} {message.metadata && ( - + 总步骤: {message.metadata.total_steps} - ✓ {message.metadata.successful_steps} + ✓ {message.metadata.successful_steps} {message.metadata.failed_steps > 0 && ( - ✗ {message.metadata.failed_steps} + ✗ {message.metadata.failed_steps} )} 耗时: {message.metadata.total_execution_time?.toFixed(1)}s @@ -996,8 +1464,8 @@ const MessageRenderer = ({ message, userAvatar }) => { {/* 执行详情(可选) */} {message.plan && message.stepResults && message.stepResults.length > 0 && ( - - + + 📊 执行详情(点击展开查看) {message.stepResults.map((result, idx) => ( @@ -1014,15 +1482,28 @@ const MessageRenderer = ({ message, userAvatar }) => { return ( - } /> + + + {message.content} @@ -1035,4 +1516,4 @@ const MessageRenderer = ({ message, userAvatar }) => { } }; -export default AgentChatV3; +export default AgentChatV4; diff --git a/src/views/AgentChat/index_v3_backup.js b/src/views/AgentChat/index_v3_backup.js new file mode 100644 index 00000000..40812bd7 --- /dev/null +++ b/src/views/AgentChat/index_v3_backup.js @@ -0,0 +1,1039 @@ +// src/views/AgentChat/index.js +// Agent聊天页面 - 黑金毛玻璃设计,V4版本(带模型选择和工具选择) +// 导出 V4 版本作为默认组件 + +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 { MarkdownWithCharts } from '@components/ChatBot/MarkdownWithCharts'; +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() }]); + }; + + // 发送消息(流式 SSE 版本) + const handleSendMessage = async () => { + if (!inputValue.trim() || isProcessing) return; + + // 权限检查 - 只允许 max 用户访问(与传导链分析权限保持一致) + const hasAccess = user?.subscription_type === 'max'; + + if (!hasAccess) { + logger.warn('AgentChat', '权限检查失败', { + userId: user?.id, + username: user?.username, + subscription_type: user?.subscription_type, + userObject: user + }); + + toast({ + title: '订阅升级', + description: '「价小前投研」功能需要 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 = []; + let executingMessageId = null; + + try { + // 1. 显示思考状态 + addMessage({ + type: MessageTypes.AGENT_THINKING, + content: '正在分析你的问题...', + timestamp: new Date().toISOString(), + }); + + setCurrentProgress(10); + + // 2. 使用 EventSource 接收 SSE 流式数据 + const requestBody = { + 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 || '', + subscription_type: user?.subscription_type || 'free', + session_id: currentSessionId, + }; + + // 使用 fetch API 进行 SSE 请求 + const response = await fetch('/mcp/agent/chat/stream', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + // 流式状态变量 + let thinkingMessageId = null; + let thinkingContent = ''; + let summaryMessageId = null; + let summaryContent = ''; + + // 读取流式数据 + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop(); // 保留不完整的行 + + let currentEvent = null; + + for (const line of lines) { + if (!line.trim() || line.startsWith(':')) continue; + + if (line.startsWith('event:')) { + // 提取事件类型 + currentEvent = line.substring(6).trim(); + continue; + } + + if (line.startsWith('data:')) { + try { + const data = JSON.parse(line.substring(5).trim()); + + // 根据事件类型处理数据 + if (currentEvent === 'thinking') { + // Kimi 流式思考过程 + if (!thinkingMessageId) { + thinkingMessageId = Date.now(); + thinkingContent = ''; + setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING)); + addMessage({ + id: thinkingMessageId, + type: MessageTypes.AGENT_THINKING, + content: '', + timestamp: new Date().toISOString(), + }); + } + thinkingContent += data.content; + // 实时更新思考内容 + setMessages((prev) => + prev.map((m) => + m.id === thinkingMessageId + ? { ...m, content: thinkingContent } + : m + ) + ); + } else if (currentEvent === 'reasoning') { + // Kimi 推理过程(可选显示) + logger.info('Kimi reasoning:', data.content); + } else if (currentEvent === 'plan') { + // 收到执行计划 + currentPlan = data; + thinkingMessageId = null; + thinkingContent = ''; + setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING)); + addMessage({ + type: MessageTypes.AGENT_PLAN, + content: '已制定执行计划', + plan: data, + timestamp: new Date().toISOString(), + }); + setCurrentProgress(30); + } else if (currentEvent === 'step_complete') { + // 收到步骤完成事件 + const stepResult = { + step_index: data.step_index, + tool: data.tool, + status: data.status, + result: data.result, + error: data.error, + execution_time: data.execution_time, + }; + stepResults.push(stepResult); + + // 更新执行中的消息 + setMessages((prev) => + prev.map((m) => + m.id === executingMessageId + ? { ...m, stepResults: [...stepResults] } + : m + ) + ); + + // 更新进度 + const progress = 40 + (stepResults.length / (currentPlan?.steps?.length || 5)) * 40; + setCurrentProgress(Math.min(progress, 80)); + } else if (currentEvent === 'summary_chunk') { + // 流式总结内容 + if (!summaryMessageId) { + summaryMessageId = Date.now(); + summaryContent = ''; + setMessages((prev) => + prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING && m.type !== MessageTypes.AGENT_EXECUTING) + ); + addMessage({ + id: summaryMessageId, + type: MessageTypes.AGENT_RESPONSE, + content: '', + plan: currentPlan, + stepResults: stepResults, + isStreaming: true, // 标记为流式输出中 + timestamp: new Date().toISOString(), + }); + setCurrentProgress(85); + } + summaryContent += data.content; + // 实时更新总结内容 + setMessages((prev) => + prev.map((m) => + m.id === summaryMessageId + ? { ...m, content: summaryContent } + : m + ) + ); + } else if (currentEvent === 'summary') { + // 收到完整总结(包含元数据) + if (summaryMessageId) { + // 更新已有消息的元数据和内容,并标记流式输出完成 + setMessages((prev) => + prev.map((m) => + m.id === summaryMessageId + ? { + ...m, + content: data.content || summaryContent, // ✅ 使用后端返回的完整内容,如果没有则使用累积内容 + metadata: data.metadata, + isStreaming: false, // ✅ 标记流式输出完成 + } + : m + ) + ); + } else { + // 如果没有流式片段,直接显示完整总结 + setMessages((prev) => + prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING && m.type !== MessageTypes.AGENT_EXECUTING) + ); + addMessage({ + type: MessageTypes.AGENT_RESPONSE, + content: data.content, + plan: currentPlan, + stepResults: stepResults, + metadata: data.metadata, + isStreaming: false, // 非流式,直接标记完成 + timestamp: new Date().toISOString(), + }); + } + setCurrentProgress(100); + } else if (currentEvent === 'status') { + // 状态更新 + if (data.stage === 'planning') { + setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING)); + addMessage({ + type: MessageTypes.AGENT_THINKING, + content: data.message, + timestamp: new Date().toISOString(), + }); + setCurrentProgress(10); + } else if (data.stage === 'executing') { + const msgId = Date.now(); + executingMessageId = msgId; + addMessage({ + id: msgId, + type: MessageTypes.AGENT_EXECUTING, + content: data.message, + plan: currentPlan, + stepResults: [], + timestamp: new Date().toISOString(), + }); + setCurrentProgress(40); + } else if (data.stage === 'summarizing') { + setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_EXECUTING)); + addMessage({ + type: MessageTypes.AGENT_THINKING, + content: data.message, + timestamp: new Date().toISOString(), + }); + setCurrentProgress(80); + } + } + } catch (e) { + logger.error('解析 SSE 数据失败', e); + } + } + } + } + + // 重新加载会话列表 + 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?.detail || 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 ( + + + } /> + + {/* 最终总结(支持 Markdown + ECharts) */} + + {/* 流式输出中显示纯文本,完成后才渲染 Markdown + 图表 */} + {message.isStreaming ? ( + + {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; diff --git a/src/views/AgentChat/index_v4.js b/src/views/AgentChat/index_v4.js new file mode 100644 index 00000000..d2ff7e2d --- /dev/null +++ b/src/views/AgentChat/index_v4.js @@ -0,0 +1,1519 @@ +// src/views/AgentChat/index_v4.js +// Agent聊天页面 V4 - 黑金毛玻璃设计,带模型选择和工具选择 + +import React, { useState, useEffect, useRef } from 'react'; +import { + Box, + Flex, + VStack, + HStack, + Text, + Input, + IconButton, + Button, + Avatar, + Heading, + Divider, + Spinner, + Badge, + useToast, + Progress, + Fade, + Collapse, + InputGroup, + InputLeftElement, + Menu, + MenuButton, + MenuList, + MenuItem, + Tooltip, + Select, + Checkbox, + CheckboxGroup, + Stack, + Accordion, + AccordionItem, + AccordionButton, + AccordionPanel, + AccordionIcon, + useDisclosure, +} from '@chakra-ui/react'; +import { + FiSend, + FiSearch, + FiPlus, + FiMessageSquare, + FiTrash2, + FiMoreVertical, + FiRefreshCw, + FiDownload, + FiCpu, + FiUser, + FiZap, + FiClock, + FiSettings, + FiCheckCircle, + FiChevronRight, + FiTool, +} from 'react-icons/fi'; +import { useAuth } from '@contexts/AuthContext'; +import { PlanCard } from '@components/ChatBot/PlanCard'; +import { StepResultCard } from '@components/ChatBot/StepResultCard'; +import { MarkdownWithCharts } from '@components/ChatBot/MarkdownWithCharts'; +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', +}; + +/** + * 可用模型配置 + */ +const AVAILABLE_MODELS = [ + { + id: 'kimi-k2', + name: 'Kimi K2', + description: '快速响应,适合日常对话', + icon: '🚀', + provider: 'Moonshot', + }, + { + id: 'kimi-k2-thinking', + name: 'Kimi K2 Thinking', + description: '深度思考,提供详细推理过程', + icon: '🧠', + provider: 'Moonshot', + recommended: true, + }, + { + id: 'glm-4.6', + name: 'GLM 4.6', + description: '智谱AI最新模型,性能强大', + icon: '⚡', + provider: 'ZhipuAI', + }, + { + id: 'deepmoney', + name: 'DeepMoney', + description: '金融专业模型,擅长财经分析', + icon: '💰', + provider: 'Custom', + }, + { + id: 'gemini-3', + name: 'Gemini 3', + description: 'Google最新多模态模型', + icon: '✨', + provider: 'Google', + }, +]; + +/** + * MCP工具分类配置 + */ +const MCP_TOOL_CATEGORIES = [ + { + name: '新闻搜索', + icon: '📰', + tools: [ + { id: 'search_news', name: '全球新闻搜索', description: '搜索国际新闻、行业动态' }, + { id: 'search_china_news', name: '中国新闻搜索', description: 'KNN语义搜索中国新闻' }, + { id: 'search_medical_news', name: '医疗新闻搜索', description: '医药、医疗设备、生物技术' }, + ], + }, + { + name: '股票分析', + icon: '📈', + tools: [ + { id: 'search_limit_up_stocks', name: '涨停股票搜索', description: '搜索涨停股票及原因分析' }, + { id: 'get_stock_analysis', name: '个股分析', description: '获取股票深度分析报告' }, + { id: 'get_stock_concepts', name: '股票概念查询', description: '查询股票相关概念板块' }, + ], + }, + { + name: '概念板块', + icon: '🏢', + tools: [ + { id: 'search_concepts', name: '概念搜索', description: '搜索股票概念板块' }, + { id: 'get_concept_details', name: '概念详情', description: '获取概念板块详细信息' }, + { id: 'get_concept_statistics', name: '概念统计', description: '涨幅榜、活跃榜、连涨榜' }, + ], + }, + { + name: '公司信息', + icon: '🏭', + tools: [ + { id: 'search_roadshows', name: '路演搜索', description: '搜索上市公司路演活动' }, + { id: 'get_company_info', name: '公司信息', description: '获取公司基本面信息' }, + ], + }, + { + name: '数据分析', + icon: '📊', + tools: [ + { id: 'query_database', name: '数据库查询', description: 'SQL查询金融数据' }, + { id: 'get_market_overview', name: '市场概况', description: '获取市场整体行情' }, + ], + }, +]; + +/** + * Agent聊天页面 V4 - 黑金毛玻璃设计 + */ +const AgentChatV4 = () => { + 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); + + // 模型和工具配置状态 + const [selectedModel, setSelectedModel] = useState('kimi-k2-thinking'); + const [selectedTools, setSelectedTools] = useState(() => { + // 默认全选所有工具 + const allToolIds = MCP_TOOL_CATEGORIES.flatMap(cat => cat.tools.map(t => t.id)); + return allToolIds; + }); + + // UI 状态 + const [searchQuery, setSearchQuery] = useState(''); + const { isOpen: isSidebarOpen, onToggle: toggleSidebar } = useDisclosure({ defaultIsOpen: true }); + const { isOpen: isRightPanelOpen, onToggle: toggleRightPanel } = useDisclosure({ defaultIsOpen: true }); + + // Refs + const messagesEndRef = useRef(null); + const inputRef = useRef(null); + + // 毛玻璃黑金配色主题 + const glassBg = 'rgba(20, 20, 20, 0.7)'; + const glassHoverBg = 'rgba(30, 30, 30, 0.8)'; + const goldAccent = '#FFD700'; + const goldGradient = 'linear-gradient(135deg, #FFD700 0%, #FFA500 100%)'; + const darkBg = '#0A0A0A'; + const borderGold = 'rgba(255, 215, 0, 0.3)'; + const textGold = '#FFD700'; + const textWhite = '#FFFFFF'; + const textGray = '#CCCCCC'; + + // ==================== 会话管理函数 ==================== + + 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); + }; + + const deleteSession = async (sessionId) => { + 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; + + const hasAccess = user?.subscription_type === 'max'; + + if (!hasAccess) { + logger.warn('AgentChat', '权限检查失败', { + userId: user?.id, + subscription_type: user?.subscription_type, + }); + + toast({ + title: '订阅升级', + description: '「价小前投研」功能需要 Max 订阅。请前往设置页面升级您的订阅。', + status: 'warning', + duration: 5000, + isClosable: true, + }); + return; + } + + 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 = []; + let executingMessageId = null; + + try { + addMessage({ + type: MessageTypes.AGENT_THINKING, + content: '正在分析你的问题...', + timestamp: new Date().toISOString(), + }); + + setCurrentProgress(10); + + const requestBody = { + 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 || '', + subscription_type: user?.subscription_type || 'free', + session_id: currentSessionId, + model: selectedModel, // 传递选中的模型 + tools: selectedTools, // 传递选中的工具 + }; + + const response = await fetch('/mcp/agent/chat/stream', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + let thinkingMessageId = null; + let thinkingContent = ''; + let summaryMessageId = null; + let summaryContent = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop(); + + let currentEvent = null; + + for (const line of lines) { + if (!line.trim() || line.startsWith(':')) continue; + + if (line.startsWith('event:')) { + currentEvent = line.substring(6).trim(); + continue; + } + + if (line.startsWith('data:')) { + try { + const data = JSON.parse(line.substring(5).trim()); + + if (currentEvent === 'thinking') { + if (!thinkingMessageId) { + thinkingMessageId = Date.now(); + thinkingContent = ''; + setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING)); + addMessage({ + id: thinkingMessageId, + type: MessageTypes.AGENT_THINKING, + content: '', + timestamp: new Date().toISOString(), + }); + } + thinkingContent += data.content; + setMessages((prev) => + prev.map((m) => + m.id === thinkingMessageId + ? { ...m, content: thinkingContent } + : m + ) + ); + } else if (currentEvent === 'plan') { + currentPlan = data; + thinkingMessageId = null; + thinkingContent = ''; + setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING)); + addMessage({ + type: MessageTypes.AGENT_PLAN, + content: '已制定执行计划', + plan: data, + timestamp: new Date().toISOString(), + }); + setCurrentProgress(30); + } else if (currentEvent === 'step_complete') { + const stepResult = { + step_index: data.step_index, + tool: data.tool, + status: data.status, + result: data.result, + error: data.error, + execution_time: data.execution_time, + }; + stepResults.push(stepResult); + + setMessages((prev) => + prev.map((m) => + m.id === executingMessageId + ? { ...m, stepResults: [...stepResults] } + : m + ) + ); + + const progress = 40 + (stepResults.length / (currentPlan?.steps?.length || 5)) * 40; + setCurrentProgress(Math.min(progress, 80)); + } else if (currentEvent === 'summary_chunk') { + if (!summaryMessageId) { + summaryMessageId = Date.now(); + summaryContent = ''; + setMessages((prev) => + prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING && m.type !== MessageTypes.AGENT_EXECUTING) + ); + addMessage({ + id: summaryMessageId, + type: MessageTypes.AGENT_RESPONSE, + content: '', + plan: currentPlan, + stepResults: stepResults, + isStreaming: true, + timestamp: new Date().toISOString(), + }); + setCurrentProgress(85); + } + summaryContent += data.content; + setMessages((prev) => + prev.map((m) => + m.id === summaryMessageId + ? { ...m, content: summaryContent } + : m + ) + ); + } else if (currentEvent === 'summary') { + if (summaryMessageId) { + setMessages((prev) => + prev.map((m) => + m.id === summaryMessageId + ? { + ...m, + content: data.content || summaryContent, + metadata: data.metadata, + isStreaming: false, + } + : m + ) + ); + } else { + setMessages((prev) => + prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING && m.type !== MessageTypes.AGENT_EXECUTING) + ); + addMessage({ + type: MessageTypes.AGENT_RESPONSE, + content: data.content, + plan: currentPlan, + stepResults: stepResults, + metadata: data.metadata, + isStreaming: false, + timestamp: new Date().toISOString(), + }); + } + setCurrentProgress(100); + } else if (currentEvent === 'status') { + if (data.stage === 'planning') { + setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING)); + addMessage({ + type: MessageTypes.AGENT_THINKING, + content: data.message, + timestamp: new Date().toISOString(), + }); + setCurrentProgress(10); + } else if (data.stage === 'executing') { + const msgId = Date.now(); + executingMessageId = msgId; + addMessage({ + id: msgId, + type: MessageTypes.AGENT_EXECUTING, + content: data.message, + plan: currentPlan, + stepResults: [], + timestamp: new Date().toISOString(), + }); + setCurrentProgress(40); + } else if (data.stage === 'summarizing') { + setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_EXECUTING)); + addMessage({ + type: MessageTypes.AGENT_THINKING, + content: data.message, + timestamp: new Date().toISOString(), + }); + setCurrentProgress(80); + } + } + } catch (e) { + logger.error('解析 SSE 数据失败', e); + } + } + } + } + + 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?.detail || 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); + }; + + // ==================== 工具选择处理 ==================== + + const handleToolToggle = (toolId, isChecked) => { + if (isChecked) { + setSelectedTools((prev) => [...prev, toolId]); + } else { + setSelectedTools((prev) => prev.filter((id) => id !== toolId)); + } + }; + + const handleCategoryToggle = (categoryTools, isAllSelected) => { + const toolIds = categoryTools.map((t) => t.id); + if (isAllSelected) { + // 全部取消选中 + setSelectedTools((prev) => prev.filter((id) => !toolIds.includes(id))); + } else { + // 全部选中 + setSelectedTools((prev) => { + const newTools = [...prev]; + toolIds.forEach((id) => { + if (!newTools.includes(id)) { + newTools.push(id); + } + }); + return newTools; + }); + } + }; + + // ==================== 初始化 ==================== + + 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)} + bg="rgba(255, 255, 255, 0.05)" + border="1px solid" + borderColor={borderGold} + color={textWhite} + _placeholder={{ color: textGray }} + _hover={{ borderColor: goldAccent }} + _focus={{ borderColor: goldAccent, boxShadow: `0 0 0 1px ${goldAccent}` }} + /> + + + + {/* 会话列表 */} + + {isLoadingSessions ? ( + + + + ) : filteredSessions.length === 0 ? ( + + + + {searchQuery ? '没有找到匹配的对话' : '暂无对话记录'} + + + ) : ( + filteredSessions.map((session) => ( + switchSession(session.session_id)} + transition="all 0.2s" + > + + + + {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" + color={textGray} + _hover={{ color: goldAccent }} + onClick={(e) => e.stopPropagation()} + /> + + } + color="red.400" + bg="transparent" + _hover={{ bg: 'rgba(255, 0, 0, 0.1)' }} + onClick={(e) => { + e.stopPropagation(); + deleteSession(session.session_id); + }} + > + 删除对话 + + + + + + )) + )} + + + {/* 用户信息 */} + + + + + + {user?.nickname || '未登录'} + + + MAX 订阅 + + + + + + + + {/* 主聊天区域 */} + + {/* 聊天头部 */} + + + + } + size="sm" + variant="ghost" + color={goldAccent} + _hover={{ bg: 'rgba(255, 215, 0, 0.1)' }} + aria-label="切换侧边栏" + onClick={toggleSidebar} + /> + + + + + 价小前投研 + + + + + AI 深度分析 + + + + {AVAILABLE_MODELS.find(m => m.id === selectedModel)?.name || '智能模型'} + + + + + + + } + size="sm" + variant="ghost" + color={textGray} + _hover={{ color: goldAccent, bg: 'rgba(255, 215, 0, 0.1)' }} + aria-label="清空对话" + onClick={handleClearChat} + /> + } + size="sm" + variant="ghost" + color={textGray} + _hover={{ color: goldAccent, bg: 'rgba(255, 215, 0, 0.1)' }} + aria-label="导出对话" + onClick={handleExportChat} + /> + } + size="sm" + variant="ghost" + color={goldAccent} + _hover={{ bg: 'rgba(255, 215, 0, 0.1)' }} + aria-label="设置" + onClick={toggleRightPanel} + /> + + + + {/* 进度条 */} + {isProcessing && ( + div': { + background: goldGradient, + }, + }} + isAnimated + /> + )} + + + {/* 消息列表 */} + + + {messages.map((message) => ( + + + + ))} +
+ + + + {/* 快捷问题 */} + {messages.length <= 2 && !isProcessing && ( + + + 💡 试试这些问题: + + + {quickQuestions.map((question, idx) => ( + + ))} + + + )} + + {/* 输入框 */} + + + setInputValue(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="输入你的问题,我会进行深度分析..." + bg="rgba(255, 255, 255, 0.05)" + border="1px solid" + borderColor={borderGold} + color={textWhite} + _placeholder={{ color: textGray }} + _focus={{ borderColor: goldAccent, boxShadow: `0 0 0 1px ${goldAccent}` }} + mr={2} + disabled={isProcessing} + size="lg" + /> + + + + + + {/* 右侧配置面板 */} + + + + {/* 模型选择 */} + + + + + 选择模型 + + + + {AVAILABLE_MODELS.map((model) => ( + setSelectedModel(model.id)} + transition="all 0.2s" + _hover={{ + bg: 'rgba(255, 215, 0, 0.1)', + borderColor: goldAccent, + transform: 'translateX(4px)', + }} + position="relative" + > + {model.recommended && ( + + 推荐 + + )} + + {model.icon} + + + {model.name} + + + {model.description} + + + {selectedModel === model.id && ( + + )} + + + ))} + + + + + + {/* 工具选择 */} + + + + + + MCP 工具 + + + + {selectedTools.length} 个已选 + + + + + {MCP_TOOL_CATEGORIES.map((category, catIdx) => { + const categoryToolIds = category.tools.map((t) => t.id); + const selectedInCategory = categoryToolIds.filter((id) => selectedTools.includes(id)); + const isAllSelected = selectedInCategory.length === categoryToolIds.length; + const isSomeSelected = selectedInCategory.length > 0 && !isAllSelected; + + return ( + + + + {category.icon} + + {category.name} + + + {selectedInCategory.length}/{category.tools.length} + + + + + + + {/* 全选按钮 */} + + + {category.tools.map((tool) => ( + handleToolToggle(tool.id, e.target.checked)} + colorScheme="yellow" + size="sm" + sx={{ + '.chakra-checkbox__control': { + borderColor: borderGold, + bg: 'rgba(255, 255, 255, 0.05)', + _checked: { + bg: goldGradient, + borderColor: goldAccent, + }, + }, + }} + > + + + {tool.name} + + + {tool.description} + + + + ))} + + + + ); + })} + + + + + + + ); +}; + +/** + * 消息渲染器(黑金毛玻璃风格) + */ +const MessageRenderer = ({ message, userAvatar }) => { + const glassBg = 'rgba(20, 20, 20, 0.7)'; + const goldAccent = '#FFD700'; + const goldGradient = 'linear-gradient(135deg, #FFD700 0%, #FFA500 100%)'; + const darkBg = '#0A0A0A'; + const borderGold = 'rgba(255, 215, 0, 0.3)'; + const textWhite = '#FFFFFF'; + const textGray = '#CCCCCC'; + + switch (message.type) { + case MessageTypes.USER: + return ( + + + + + {message.content} + + + } + border="2px solid" + borderColor={goldAccent} + /> + + + ); + + 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.isStreaming ? ( + + {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 AgentChatV4;