// src/views/AgentChat/index.js // 超炫酷的 AI 投研助手 - Hero UI 深色模式版本 // 使用 Framer Motion 物理动画引擎 import React, { useState, useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Button, Card, CardHeader, CardBody, CardFooter, Input, Avatar, Chip, Divider, Spinner, Tooltip, Badge, Checkbox, CheckboxGroup, Tabs, Tab, ScrollShadow, Kbd, Accordion, AccordionItem, } from '@heroui/react'; import { useAuth } from '@contexts/AuthContext'; import { logger } from '@utils/logger'; import axios from 'axios'; import { useToast } from '@chakra-ui/react'; // 图标 - 使用 Lucide Icons import { Send, Plus, Search, MessageSquare, Trash2, MoreVertical, RefreshCw, Download, Cpu, User, Zap, Clock, Settings, ChevronLeft, ChevronRight, Activity, Code, Database, TrendingUp, FileText, BookOpen, Menu, X, Check, Circle, Maximize2, Minimize2, Copy, ThumbsUp, ThumbsDown, Sparkles, Brain, Rocket, Paperclip, Image as ImageIcon, File, Calendar, Globe, DollarSign, Newspaper, BarChart3, PieChart, LineChart, Briefcase, Users, } from 'lucide-react'; /** * Framer Motion 动画变体配置 */ const animations = { slideInLeft: { initial: { x: -320, opacity: 0 }, animate: { x: 0, opacity: 1, transition: { type: 'spring', stiffness: 300, damping: 30, }, }, exit: { x: -320, opacity: 0, transition: { duration: 0.2 }, }, }, slideInRight: { initial: { x: 320, opacity: 0 }, animate: { x: 0, opacity: 1, transition: { type: 'spring', stiffness: 300, damping: 30, }, }, exit: { x: 320, opacity: 0, transition: { duration: 0.2 }, }, }, fadeInUp: { initial: { opacity: 0, y: 20 }, animate: { opacity: 1, y: 0, transition: { type: 'spring', stiffness: 400, damping: 25, }, }, }, staggerItem: { initial: { opacity: 0, y: 10 }, animate: { opacity: 1, y: 0 }, }, staggerContainer: { animate: { transition: { staggerChildren: 0.05, }, }, }, pressScale: { whileTap: { scale: 0.95 }, whileHover: { scale: 1.05 }, }, }; /** * 消息类型 */ 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-thinking', name: 'Kimi K2 Thinking', description: '深度思考模型,适合复杂分析', icon: , color: 'secondary', }, { id: 'kimi-k2', name: 'Kimi K2', description: '快速响应模型,适合简单查询', icon: , color: 'primary', }, { id: 'deepmoney', name: 'DeepMoney', description: '金融专业模型', icon: , color: 'success', }, ]; /** * MCP 工具配置(完整列表) */ const MCP_TOOLS = [ // 新闻搜索类 { id: 'search_news', name: '全球新闻搜索', icon: , category: '新闻资讯', description: '搜索全球新闻,支持关键词和日期过滤' }, { id: 'search_china_news', name: '中国新闻搜索', icon: , category: '新闻资讯', description: 'KNN语义搜索中国新闻' }, { id: 'search_medical_news', name: '医疗健康新闻', icon: , category: '新闻资讯', description: '医药、医疗设备、生物技术新闻' }, // 概念板块类 { id: 'search_concepts', name: '概念板块搜索', icon: , category: '概念板块', description: '搜索股票概念板块及相关股票' }, { id: 'get_concept_details', name: '概念详情', icon: , category: '概念板块', description: '获取概念板块详细信息' }, { id: 'get_stock_concepts', name: '股票概念', icon: , category: '概念板块', description: '查询股票相关概念板块' }, { id: 'get_concept_statistics', name: '概念统计', icon: , category: '概念板块', description: '涨幅榜、跌幅榜、活跃榜等' }, // 涨停分析类 { id: 'search_limit_up_stocks', name: '涨停股票搜索', icon: , category: '涨停分析', description: '搜索涨停股票,支持多条件筛选' }, { id: 'get_daily_stock_analysis', name: '涨停日报', icon: , category: '涨停分析', description: '每日涨停股票分析报告' }, // 研报路演类 { id: 'search_research_reports', name: '研报搜索', icon: , category: '研报路演', description: '搜索研究报告,支持语义搜索' }, { id: 'search_roadshows', name: '路演活动', icon: , category: '研报路演', description: '上市公司路演、投资者交流活动' }, // 股票数据类 { id: 'get_stock_basic_info', name: '股票基本信息', icon: , category: '股票数据', description: '公司名称、行业、主营业务等' }, { id: 'get_stock_financial_index', name: '财务指标', icon: , category: '股票数据', description: 'EPS、ROE、营收增长率等' }, { id: 'get_stock_trade_data', name: '交易数据', icon: , category: '股票数据', description: '价格、成交量、涨跌幅等' }, { id: 'get_stock_balance_sheet', name: '资产负债表', icon: , category: '股票数据', description: '资产、负债、所有者权益' }, { id: 'get_stock_cashflow', name: '现金流量表', icon: , category: '股票数据', description: '经营、投资、筹资现金流' }, { id: 'search_stocks_by_criteria', name: '条件选股', icon: , category: '股票数据', description: '按行业、地区、市值筛选' }, { id: 'get_stock_comparison', name: '股票对比', icon: , category: '股票数据', description: '多只股票财务指标对比' }, // 用户数据类 { id: 'get_user_watchlist', name: '自选股列表', icon: , category: '用户数据', description: '用户关注的股票及行情' }, { id: 'get_user_following_events', name: '关注事件', icon: , category: '用户数据', description: '用户关注的重大事件' }, ]; // 按类别分组工具 const TOOL_CATEGORIES = { '新闻资讯': MCP_TOOLS.filter(t => t.category === '新闻资讯'), '概念板块': MCP_TOOLS.filter(t => t.category === '概念板块'), '涨停分析': MCP_TOOLS.filter(t => t.category === '涨停分析'), '研报路演': MCP_TOOLS.filter(t => t.category === '研报路演'), '股票数据': MCP_TOOLS.filter(t => t.category === '股票数据'), '用户数据': MCP_TOOLS.filter(t => t.category === '用户数据'), }; /** * Hero Agent Chat - 主组件(深色模式) */ const AgentChat = () => { const { user } = useAuth(); const toast = useToast(); // 会话管理 const [sessions, setSessions] = useState([]); const [currentSessionId, setCurrentSessionId] = useState(null); const [isLoadingSessions, setIsLoadingSessions] = useState(false); // 消息管理 const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(''); const [isProcessing, setIsProcessing] = useState(false); // UI 状态 const [searchQuery, setSearchQuery] = useState(''); const [selectedModel, setSelectedModel] = useState('kimi-k2-thinking'); const [selectedTools, setSelectedTools] = useState([ 'search_news', 'search_china_news', 'search_concepts', 'search_limit_up_stocks', 'search_research_reports', ]); const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(true); const [isRightSidebarOpen, setIsRightSidebarOpen] = useState(true); // 文件上传 const [uploadedFiles, setUploadedFiles] = useState([]); const fileInputRef = useRef(null); // Refs const messagesEndRef = useRef(null); const inputRef = useRef(null); // ==================== 启用深色模式 ==================== useEffect(() => { // 为 AgentChat 页面强制启用深色模式 document.documentElement.classList.add('dark'); return () => { // 组件卸载时不移除,让其他页面自己控制 // document.documentElement.classList.remove('dark'); }; }, []); // ==================== API 调用函数 ==================== const loadSessions = async () => { if (!user?.id) return; setIsLoadingSessions(true); try { const response = await axios.get('/mcp/agent/sessions', { params: { user_id: user.id, limit: 50 }, }); if (response.data.success) { setSessions(response.data.data); } } catch (error) { logger.error('加载会话列表失败', error); } 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); } } catch (error) { logger.error('加载会话历史失败', error); } }; const createNewSession = () => { setCurrentSessionId(null); setMessages([ { id: Date.now(), type: MessageTypes.AGENT_RESPONSE, content: `你好${user?.nickname || ''}!👋\n\n我是**价小前**,你的 AI 投研助手。\n\n**我能做什么?**\n• 📊 全面分析股票基本面和技术面\n• 🔥 追踪市场热点和涨停板块\n• 📈 研究行业趋势和投资机会\n• 📰 汇总最新财经新闻和研报\n\n直接输入你的问题开始探索!`, timestamp: new Date().toISOString(), }, ]); }; const switchSession = (sessionId) => { setCurrentSessionId(sessionId); loadSessionHistory(sessionId); }; const handleSendMessage = async () => { if (!inputValue.trim() || isProcessing) return; const userMessage = { type: MessageTypes.USER, content: inputValue, timestamp: new Date().toISOString(), files: uploadedFiles.length > 0 ? uploadedFiles : undefined, }; addMessage(userMessage); const userInput = inputValue; setInputValue(''); setUploadedFiles([]); setIsProcessing(true); try { addMessage({ type: MessageTypes.AGENT_THINKING, content: '正在分析你的问题...', timestamp: new Date().toISOString(), }); 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 || 'anonymous', user_nickname: user?.nickname || '匿名用户', user_avatar: user?.avatar || '', subscription_type: user?.subscription_type || 'free', session_id: currentSessionId, model: selectedModel, tools: selectedTools, files: uploadedFiles.length > 0 ? uploadedFiles : undefined, }); setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING)); if (response.data.success) { const data = response.data; if (data.session_id && !currentSessionId) { setCurrentSessionId(data.session_id); } if (data.plan) { addMessage({ type: MessageTypes.AGENT_PLAN, content: '已制定执行计划', plan: data.plan, timestamp: new Date().toISOString(), }); } if (data.steps && data.steps.length > 0) { addMessage({ type: MessageTypes.AGENT_EXECUTING, content: '正在执行步骤...', plan: data.plan, stepResults: data.steps, timestamp: new Date().toISOString(), }); } setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_EXECUTING)); addMessage({ type: MessageTypes.AGENT_RESPONSE, content: data.final_answer || data.message || '处理完成', plan: data.plan, stepResults: data.steps, metadata: data.metadata, timestamp: new Date().toISOString(), }); 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, }); } finally { setIsProcessing(false); } }; // 文件上传处理 const handleFileSelect = (event) => { const files = Array.from(event.target.files || []); const fileData = files.map(file => ({ name: file.name, size: file.size, type: file.type, // 实际上传时需要转换为 base64 或上传到服务器 url: URL.createObjectURL(file), })); setUploadedFiles(prev => [...prev, ...fileData]); }; const removeFile = (index) => { setUploadedFiles(prev => prev.filter((_, i) => i !== index)); }; const addMessage = (message) => { setMessages((prev) => [ ...prev, { id: Date.now() + Math.random(), ...message, }, ]); }; const handleKeyPress = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } }; useEffect(() => { loadSessions(); createNewSession(); }, [user]); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); // ==================== 按日期分组会话 ==================== const groupSessionsByDate = (sessions) => { const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const weekAgo = new Date(today); weekAgo.setDate(weekAgo.getDate() - 7); const groups = { today: [], yesterday: [], thisWeek: [], older: [], }; sessions.forEach(session => { const sessionDate = new Date(session.created_at || session.timestamp); const daysDiff = Math.floor((today - sessionDate) / (1000 * 60 * 60 * 24)); if (daysDiff === 0) { groups.today.push(session); } else if (daysDiff === 1) { groups.yesterday.push(session); } else if (daysDiff <= 7) { groups.thisWeek.push(session); } else { groups.older.push(session); } }); return groups; }; const sessionGroups = groupSessionsByDate(sessions); const filteredSessions = searchQuery ? sessions.filter((s) => s.title?.toLowerCase().includes(searchQuery.toLowerCase()) || s.session_id?.toLowerCase().includes(searchQuery.toLowerCase()) ) : sessions; const quickQuestions = [ { text: '今日涨停板块分析', emoji: '🔥' }, { text: '新能源概念机会', emoji: '⚡' }, { text: '半导体行业动态', emoji: '💾' }, { text: '本周热门研报', emoji: '📊' }, ]; return (
{/* 左侧栏 - 深色毛玻璃 */} {isLeftSidebarOpen && (
对话历史
} size="sm" variant="bordered" classNames={{ input: 'text-sm text-gray-100', inputWrapper: 'border-gray-700 bg-gray-800/50 hover:border-gray-600', }} />
{/* 按日期分组显示会话 */} {sessionGroups.today.length > 0 && (

今天

{sessionGroups.today.map((session) => ( switchSession(session.session_id)} /> ))}
)} {sessionGroups.yesterday.length > 0 && (

昨天

{sessionGroups.yesterday.map((session) => ( switchSession(session.session_id)} /> ))}
)} {sessionGroups.thisWeek.length > 0 && (

本周

{sessionGroups.thisWeek.map((session) => ( switchSession(session.session_id)} /> ))}
)} {sessionGroups.older.length > 0 && (

更早

{sessionGroups.older.map((session) => ( switchSession(session.session_id)} /> ))}
)} {isLoadingSessions && (
)} {sessions.length === 0 && !isLoadingSessions && (

还没有对话历史

开始一个新对话吧!

)}

{user?.nickname || '未登录'}

{user?.subscription_type || 'free'} 用户

)} {/* 中间主聊天区域 */}
{/* 顶部标题栏 - 深色 */}
{!isLeftSidebarOpen && ( )} } classNames={{ base: 'bg-gradient-to-br from-purple-500 to-pink-500', }} />

价小前投研 AI

} classNames={{ base: 'bg-green-500/20', content: 'text-green-400', }} > 智能分析 {AVAILABLE_MODELS.find((m) => m.id === selectedModel)?.name}
{!isRightSidebarOpen && ( )}
{/* 消息列表 */} {messages.map((message, index) => ( ))}
{/* 快捷问题 */} {messages.length <= 2 && !isProcessing && (

快速开始

{quickQuestions.map((question, idx) => ( ))}
)}
{/* 输入栏 - 深色 */}
{/* 已上传文件预览 */} {uploadedFiles.length > 0 && (
{uploadedFiles.map((file, idx) => ( removeFile(idx)} variant="flat" classNames={{ base: 'bg-gray-800 border border-gray-700', content: 'text-gray-300', }} > {file.name} ))}
)}
Enter 发送
Shift + Enter 换行
{/* 右侧栏 - 深色配置中心 */} {isRightSidebarOpen && (
配置中心
{/* 模型选择 */} 模型
} >
{AVAILABLE_MODELS.map((model) => ( setSelectedModel(model.id)} className={`transition-all ${ selectedModel === model.id ? 'bg-blue-500/20 border-2 border-blue-500' : 'bg-gray-800/50 border-2 border-gray-700 hover:border-gray-600' }`} >
{model.icon}

{model.name}

{model.description}

{selectedModel === model.id && ( )}
))}
{/* 工具选择 - 按分类显示 */} 工具
} >
{Object.entries(TOOL_CATEGORIES).map(([category, tools]) => ( {category} {tools.filter(t => selectedTools.includes(t.id)).length}/{tools.length}
} > {tools.map((tool) => (
{tool.icon}

{tool.name}

{tool.description}

))}
))}
{/* 统计信息 */} 统计 } >

对话数

{sessions.length}

消息数

{messages.length}

已选工具

{selectedTools.length}

)} ); }; export default AgentChat; /** * 会话卡片组件 */ const SessionCard = ({ session, isActive, onPress }) => { return (

{session.title || '新对话'}

{new Date(session.created_at || session.timestamp).toLocaleString('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', })}

{session.message_count && ( {session.message_count} )}
); }; /** * 消息渲染器 */ const MessageRenderer = ({ message, userAvatar }) => { switch (message.type) { case MessageTypes.USER: return (

{message.content}

{message.files && message.files.length > 0 && (
{message.files.map((file, idx) => ( {file.name} ))}
)}
} size="sm" classNames={{ base: 'bg-gradient-to-br from-blue-500 to-purple-600', }} />
); case MessageTypes.AGENT_THINKING: return (
} size="sm" classNames={{ base: 'bg-gradient-to-br from-purple-500 to-pink-500', }} />

{message.content}

); case MessageTypes.AGENT_RESPONSE: return (
} size="sm" classNames={{ base: 'bg-gradient-to-br from-purple-500 to-pink-500', }} />

{message.content}

{message.stepResults && message.stepResults.length > 0 && (
)}
{new Date(message.timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', })}
); case MessageTypes.ERROR: return (

{message.content}

); default: return null; } }; /** * 执行步骤显示组件 */ const ExecutionStepsDisplay = ({ steps, plan }) => { return ( 执行详情 {steps.length} 步骤 } >
{steps.map((result, idx) => (

步骤 {idx + 1}: {result.tool_name}

{result.status}

{result.execution_time?.toFixed(2)}s

{result.error &&

⚠️ {result.error}

}
))}
); };