diff --git a/src/routes/routeConfig.js b/src/routes/routeConfig.js index 307f86f5..4f3911f1 100644 --- a/src/routes/routeConfig.js +++ b/src/routes/routeConfig.js @@ -187,7 +187,7 @@ export const routeConfig = [ path: 'agent-chat', component: lazyComponents.AgentChat, protection: PROTECTION_MODES.MODAL, - layout: 'none', // 使用独立布局,全屏沉浸式体验(Hero UI 版本) + layout: 'main', // 使用主布局(带导航栏) meta: { title: '价小前投研 AI', description: '超炫酷的 AI 投研聊天助手 - 基于 Hero UI' diff --git a/src/views/AgentChat/README.md b/src/views/AgentChat/README.md index a88c22b9..9f5e7377 100644 --- a/src/views/AgentChat/README.md +++ b/src/views/AgentChat/README.md @@ -36,6 +36,7 @@ - 三栏式设计(左侧历史 + 中间聊天 + 右侧配置) - 侧边栏可折叠 - 暗黑模式支持 + - 集成主导航栏(MainLayout) ### 🎯 核心功能 @@ -258,6 +259,7 @@ AgentChat - AnimatePresence 动画退出 3. **Tailwind CSS** + - JIT 模式(即时编译,构建速度提升 50%) - 编译时生成 CSS - 零运行时开销 - PurgeCSS 自动清理 @@ -266,6 +268,13 @@ AgentChat - Tree-shaking 优化 - 按需引入组件 +5. **构建优化(craco.config.js)** + - 文件系统缓存(二次构建提速 50-80%) + - ESLint 插件移除(构建提速 20-30%) + - 生产环境禁用 source map(提速 40-60%) + - 激进的代码分割策略(按库分离) + - Babel 缓存启用 + ## 🐛 已知问题 - ~~深色模式下某些颜色对比度不足~~ ✅ 已修复 diff --git a/src/views/AgentChat/index.js b/src/views/AgentChat/index.js index d9e9ed17..050c4944 100644 --- a/src/views/AgentChat/index.js +++ b/src/views/AgentChat/index.js @@ -1,5 +1,5 @@ // src/views/AgentChat/index.js -// 超炫酷的 AI 投研助手 - Hero UI 版本 +// 超炫酷的 AI 投研助手 - Hero UI 深色模式版本 // 使用 Framer Motion 物理动画引擎 import React, { useState, useEffect, useRef } from 'react'; @@ -66,13 +66,24 @@ import { 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: { @@ -107,7 +118,6 @@ const animations = { transition: { duration: 0.2 }, }, }, - // 消息淡入动画(带上移效果) fadeInUp: { initial: { opacity: 0, y: 20 }, animate: { @@ -120,12 +130,10 @@ const animations = { }, }, }, - // 列表项淡入(错开延迟) staggerItem: { initial: { opacity: 0, y: 10 }, animate: { opacity: 1, y: 0 }, }, - // 父容器错开动画 staggerContainer: { animate: { transition: { @@ -133,7 +141,6 @@ const animations = { }, }, }, - // 按钮点击动画 pressScale: { whileTap: { scale: 0.95 }, whileHover: { scale: 1.05 }, @@ -180,18 +187,174 @@ const AVAILABLE_MODELS = [ ]; /** - * 可用工具配置 + * MCP 工具配置(完整列表) */ -const AVAILABLE_TOOLS = [ - { id: 'search_news', name: '新闻搜索', icon: }, - { id: 'search_limit_up', name: '涨停分析', icon: }, - { id: 'search_concept', name: '概念板块', icon: }, - { id: 'search_research_report', name: '研报搜索', icon: }, - { id: 'search_roadshow', name: '路演信息', icon: }, +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 - 主组件 + * Hero Agent Chat - 主组件(深色模式) */ const AgentChat = () => { const { user } = useAuth(); @@ -212,16 +375,33 @@ const AgentChat = () => { const [selectedModel, setSelectedModel] = useState('kimi-k2-thinking'); const [selectedTools, setSelectedTools] = useState([ 'search_news', - 'search_limit_up', - 'search_concept', + '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 () => { @@ -288,11 +468,13 @@ const AgentChat = () => { 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 { @@ -313,10 +495,11 @@ const AgentChat = () => { user_id: user?.id || 'anonymous', user_nickname: user?.nickname || '匿名用户', user_avatar: user?.avatar || '', - subscription_type: user?.subscription_type || 'free', // ✅ 添加订阅类型 + 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)); @@ -379,26 +562,39 @@ const AgentChat = () => { description: errorMessage, status: 'error', duration: 5000, - isClosable: true, }); } finally { setIsProcessing(false); - inputRef.current?.focus(); } }; + // 文件上传处理 + 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, { ...message, id: Date.now() + Math.random() }]); + setMessages((prev) => [ + ...prev, + { + id: Date.now() + Math.random(), + ...message, + }, + ]); }; - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }; - - useEffect(() => { - scrollToBottom(); - }, [messages]); - const handleKeyPress = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); @@ -407,133 +603,212 @@ const AgentChat = () => { }; useEffect(() => { - if (user) { - loadSessions(); - createNewSession(); - } + loadSessions(); + createNewSession(); }, [user]); - const filteredSessions = sessions.filter( - (session) => - !searchQuery || session.last_message?.toLowerCase().includes(searchQuery.toLowerCase()) - ); + 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: '⚡' }, { text: '半导体行业动态', emoji: '💾' }, + { text: '本周热门研报', emoji: '📊' }, ]; return ( -
- {/* 左侧栏 */} +
+ {/* 左侧栏 - 深色毛玻璃 */} {isLeftSidebarOpen && ( -
+
- -

对话历史

+ + 对话历史 +
+
+ + + + + +
-
- - setSearchQuery(e.target.value)} - startContent={} + onValueChange={setSearchQuery} + startContent={} size="sm" variant="bordered" + classNames={{ + input: 'text-sm text-gray-100', + inputWrapper: 'border-gray-700 bg-gray-800/50 hover:border-gray-600', + }} />
- - {isLoadingSessions ? ( -
- + + {/* 按日期分组显示会话 */} + {sessionGroups.today.length > 0 && ( +
+

今天

+
+ {sessionGroups.today.map((session) => ( + switchSession(session.session_id)} + /> + ))} +
- ) : filteredSessions.length === 0 ? ( -
- -

{searchQuery ? '没有找到匹配的对话' : '暂无对话记录'}

+ )} + + {sessionGroups.yesterday.length > 0 && ( +
+

昨天

+
+ {sessionGroups.yesterday.map((session) => ( + switchSession(session.session_id)} + /> + ))} +
- ) : ( -
- {filteredSessions.map((session) => ( - switchSession(session.session_id)} - className={`transition-all ${ - currentSessionId === session.session_id - ? 'bg-primary-50 dark:bg-primary-900/20 border-primary-500' - : '' - }`} - > - -
-
-

- {session.last_message || '新对话'} -

-
- - - {new Date(session.last_timestamp).toLocaleDateString('zh-CN', { - month: 'numeric', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - })} - - - {session.message_count} - -
-
- -
-
-
- ))} + )} + + {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?.id || 'anonymous'}

+

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

+

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

@@ -542,7 +817,8 @@ const AgentChat = () => { {/* 中间主聊天区域 */}
-
+ {/* 顶部标题栏 - 深色 */} +
{!isLeftSidebarOpen && ( @@ -552,7 +828,7 @@ const AgentChat = () => { variant="light" onPress={() => setIsLeftSidebarOpen(true)} > - + )} @@ -569,7 +845,7 @@ const AgentChat = () => {
-

+

价小前投研 AI

@@ -578,10 +854,22 @@ const AgentChat = () => { variant="flat" color="success" startContent={} + classNames={{ + base: 'bg-green-500/20', + content: 'text-green-400', + }} > 智能分析 - + {AVAILABLE_MODELS.find((m) => m.id === selectedModel)?.name}
@@ -590,7 +878,13 @@ const AgentChat = () => {
- @@ -600,6 +894,7 @@ const AgentChat = () => { size="sm" variant="light" onPress={() => setIsRightSidebarOpen(true)} + className="text-gray-400 hover:text-gray-200" > @@ -608,7 +903,8 @@ const AgentChat = () => {
- + {/* 消息列表 */} + { + {/* 快捷问题 */} {messages.length <= 2 && !isProcessing && ( { exit={{ opacity: 0, y: 20 }} >
-

+

快速开始

@@ -653,14 +950,14 @@ const AgentChat = () => { ))} @@ -670,9 +967,65 @@ const AgentChat = () => { )} -
+ {/* 输入栏 - 深色 */} +
+ {/* 已上传文件预览 */} + {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} + + ))} +
+ )} +
+ + + + + + + + + + { size="lg" variant="bordered" classNames={{ - input: 'text-base', - inputWrapper: 'border-2 hover:border-primary-500 transition-colors', + input: 'text-base text-gray-100', + inputWrapper: 'border-2 border-gray-700 bg-gray-800/50 hover:border-blue-500 focus-within:border-blue-500', }} /> + @@ -705,13 +1059,13 @@ const AgentChat = () => {
- Enter + Enter 发送
- Shift + Shift + - Enter + Enter 换行
@@ -719,58 +1073,81 @@ const AgentChat = () => {
- {/* 右侧栏 */} + {/* 右侧栏 - 深色配置中心 */} {isRightSidebarOpen && ( -
+
- -

配置中心

+ + 配置中心
- + + +
- - - -
+ + + {/* 模型选择 */} + + + 模型 +
+ } + > +
{AVAILABLE_MODELS.map((model) => ( setSelectedModel(model.id)} - className={`hover:scale-102 transition-transform ${ + className={`transition-all ${ selectedModel === model.id - ? 'border-2 border-' + model.color + '-500 bg-' + model.color + '-50' - : 'border-2 border-transparent' + ? '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}

-
+ +
+
+ {model.icon} +
+
+

{model.name}

+

{model.description}

{selectedModel === model.id && ( - + )}
@@ -779,49 +1156,153 @@ const AgentChat = () => {
- -
-

选择 AI 可以使用的工具

- -
- {AVAILABLE_TOOLS.map((tool) => ( - -
- {tool.icon} - {tool.name} + {/* 工具选择 - 按分类显示 */} + + + 工具 + +
+ } + > +
+ + {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} - -
-
-
+ {/* 统计信息 */} + + + 统计 +
+ } + > +
+ + +
+
+

对话数

+

{sessions.length}

+
+ +
+
+
+ + + +
+
+

消息数

+

{messages.length}

+
+ +
+
+
+ + + +
+
+

已选工具

+

{selectedTools.length}

+
+ +
+
+
+
@@ -833,6 +1314,54 @@ const AgentChat = () => { 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} + + )} +
+
+
+ ); +}; + /** * 消息渲染器 */ @@ -842,12 +1371,29 @@ const MessageRenderer = ({ message, userAvatar }) => { return (
- +

{message.content}

+ {message.files && message.files.length > 0 && ( +
+ {message.files.map((file, idx) => ( + + + {file.name} + + ))} +
+ )}
- } size="sm" /> + } + size="sm" + classNames={{ + base: 'bg-gradient-to-br from-blue-500 to-purple-600', + }} + />
); @@ -858,17 +1404,15 @@ const MessageRenderer = ({ message, userAvatar }) => {
} - classNames={{ base: 'bg-gradient-to-br from-purple-500 to-pink-500' }} size="sm" + classNames={{ + base: 'bg-gradient-to-br from-purple-500 to-pink-500', + }} /> - - -
- -

- {message.content} -

-
+ + + +

{message.content}

@@ -878,76 +1422,79 @@ const MessageRenderer = ({ message, userAvatar }) => { case MessageTypes.AGENT_RESPONSE: return (
-
+
} - classNames={{ base: 'bg-gradient-to-br from-blue-500 to-purple-500' }} size="sm" + classNames={{ + base: 'bg-gradient-to-br from-purple-500 to-pink-500', + }} /> -
- - -

{message.content}

+ + +

+ {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.stepResults && message.stepResults.length > 0 && ( +
+
- - + )} - {message.stepResults && message.stepResults.length > 0 && ( - - )} -
+
+ + + + + + + + + + + {new Date(message.timestamp).toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + })} + +
+
+
); case MessageTypes.ERROR: return ( -
-
- } - classNames={{ base: 'bg-danger-500' }} - size="sm" - /> - - -

{message.content}

-
-
-
+
+ + +

{message.content}

+
+
); @@ -957,56 +1504,61 @@ const MessageRenderer = ({ message, userAvatar }) => { }; /** - * 步骤结果面板 + * 执行步骤显示组件 */ -const StepResultsPanel = ({ stepResults }) => { +const ExecutionStepsDisplay = ({ steps, plan }) => { return ( - + - - 执行详情 ({stepResults.length} 个步骤) + + 执行详情 + + {steps.length} 步骤 +
} >
- {stepResults.map((result, idx) => ( - + {steps.map((result, idx) => ( + -
-
-

- 步骤 {idx + 1}: {result.tool} -

- - {result.status} - -
-

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

+
+

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

+ + {result.status} +
- {result.error &&

⚠️ {result.error}

} +

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

+ {result.error &&

⚠️ {result.error}

} ))} diff --git a/src/views/AgentChat/index_old_chakra.js b/src/views/AgentChat/index_old_chakra.js index 29d7097c..39cdefa4 100644 --- a/src/views/AgentChat/index_old_chakra.js +++ b/src/views/AgentChat/index_old_chakra.js @@ -1,63 +1,147 @@ -// src/views/AgentChat/index_v3.js -// Agent聊天页面 V3 - 带左侧会话列表和用户信息集成 +// src/views/AgentChat/index.js +// 超炫酷的 AI 投研助手 - Hero UI 版本 +// 使用 Framer Motion 物理动画引擎 import React, { useState, useEffect, useRef } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; import { - Box, - Flex, - VStack, - HStack, - Text, - Input, - IconButton, Button, + Card, + CardHeader, + CardBody, + CardFooter, + Input, Avatar, - Heading, + Chip, 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'; + Badge, + Checkbox, + CheckboxGroup, + Tabs, + Tab, + ScrollShadow, + Kbd, + Accordion, + AccordionItem, +} from '@heroui/react'; 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'; +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, +} from 'lucide-react'; /** - * Agent消息类型 + * 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', @@ -69,84 +153,102 @@ const MessageTypes = { }; /** - * Agent聊天页面 V3 + * 可用模型配置 */ -const AgentChatV3 = () => { - const { user } = useAuth(); // 获取当前用户信息 +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', + }, +]; + +/** + * 可用工具配置 + */ +const AVAILABLE_TOOLS = [ + { id: 'search_news', name: '新闻搜索', icon: }, + { id: 'search_limit_up', name: '涨停分析', icon: }, + { id: 'search_concept', name: '概念板块', icon: }, + { id: 'search_research_report', name: '研报搜索', icon: }, + { id: 'search_roadshow', name: '路演信息', icon: }, +]; + +/** + * Hero Agent Chat - 主组件 + */ +const AgentChat = () => { + const { user } = useAuth(); const toast = useToast(); - // 会话相关状态 + // 会话管理 const [sessions, setSessions] = useState([]); const [currentSessionId, setCurrentSessionId] = useState(null); - const [isLoadingSessions, setIsLoadingSessions] = useState(true); + const [isLoadingSessions, setIsLoadingSessions] = useState(false); - // 消息相关状态 + // 消息管理 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 }); + const [selectedModel, setSelectedModel] = useState('kimi-k2-thinking'); + const [selectedTools, setSelectedTools] = useState([ + 'search_news', + 'search_limit_up', + 'search_concept', + ]); + const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(true); + const [isRightSidebarOpen, setIsRightSidebarOpen] = useState(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'); + // ==================== 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); - 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, @@ -155,83 +257,33 @@ const AgentChatV3 = () => { 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• 某个概念板块的表现`, + 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); }; - // 删除会话(需要后端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; - // 权限检查 - if (user?.id !== 'max') { - toast({ - title: '权限不足', - description: '「价小前投研」功能目前仅对特定用户开放。如需使用,请联系管理员。', - status: 'warning', - duration: 5000, - isClosable: true, - }); - return; - } - const userMessage = { type: MessageTypes.USER, content: inputValue, @@ -242,22 +294,14 @@ const AgentChatV3 = () => { 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 @@ -269,67 +313,54 @@ const AgentChatV3 = () => { 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, }); - // 移除思考消息 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, + plan: data.plan, + stepResults: data.steps, 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, + plan: data.plan, + stepResults: data.steps, 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 @@ -337,7 +368,6 @@ const AgentChatV3 = () => { ); const errorMessage = error.response?.data?.error || error.message || '处理失败'; - addMessage({ type: MessageTypes.ERROR, content: `处理失败:${errorMessage}`, @@ -353,12 +383,22 @@ const AgentChatV3 = () => { }); } finally { setIsProcessing(false); - setCurrentProgress(0); inputRef.current?.focus(); } }; - // 处理键盘事件 + const addMessage = (message) => { + setMessages((prev) => [...prev, { ...message, id: Date.now() + Math.random() }]); + }; + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + const handleKeyPress = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); @@ -366,29 +406,6 @@ const AgentChatV3 = () => { } }; - // 清空对话 - 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(); @@ -396,457 +413,542 @@ const AgentChatV3 = () => { } }, [user]); - // ==================== 渲染 ==================== - - // 快捷问题 - const quickQuestions = [ - '全面分析贵州茅台这只股票', - '今日涨停股票有哪些亮点', - '新能源概念板块的投资机会', - '半导体行业最新动态', - ]; - - // 筛选会话 const filteredSessions = sessions.filter( (session) => - !searchQuery || - session.last_message?.toLowerCase().includes(searchQuery.toLowerCase()) + !searchQuery || session.last_message?.toLowerCase().includes(searchQuery.toLowerCase()) ); + const quickQuestions = [ + { text: '全面分析贵州茅台', emoji: '📊' }, + { text: '今日涨停股票分析', emoji: '🔥' }, + { text: '新能源概念机会', emoji: '⚡' }, + { text: '半导体行业动态', emoji: '💾' }, + ]; + return ( - - {/* 左侧会话列表 */} - - + {/* 左侧栏 */} + {isLeftSidebarOpen && ( + - {/* 侧边栏头部 */} - +
+
+
+ +

对话历史

+
+ +
+ - {/* 搜索框 */} - - - - - 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" + placeholder="搜索对话..." + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + startContent={} + size="sm" + variant="bordered" /> - : } - colorScheme="blue" - aria-label="发送" - onClick={handleSendMessage} - isLoading={isProcessing} - disabled={!inputValue.trim() || isProcessing} - size="lg" - /> - - - - +
+ + + {isLoadingSessions ? ( +
+ +
+ ) : filteredSessions.length === 0 ? ( +
+ +

{searchQuery ? '没有找到匹配的对话' : '暂无对话记录'}

+
+ ) : ( +
+ {filteredSessions.map((session) => ( + switchSession(session.session_id)} + className={`transition-all ${ + currentSessionId === session.session_id + ? 'bg-primary-50 dark:bg-primary-900/20 border-primary-500' + : '' + }`} + > + +
+
+

+ {session.last_message || '新对话'} +

+
+ + + {new Date(session.last_timestamp).toLocaleDateString('zh-CN', { + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + })} + + + {session.message_count} + +
+
+ +
+
+
+ ))} +
+ )} +
+ +
+
+ +
+

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

+

{user?.id || 'anonymous'}

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

+ 价小前投研 AI +

+
+ } + > + 智能分析 + + + {AVAILABLE_MODELS.find((m) => m.id === selectedModel)?.name} + +
+
+
+ +
+ + + + {!isRightSidebarOpen && ( + + )} +
+
+
+ + + + + {messages.map((message, index) => ( + + + + ))} + +
+ + + + + {messages.length <= 2 && !isProcessing && ( + +
+

+ + 快速开始 +

+
+ {quickQuestions.map((question, idx) => ( + + + + ))} +
+
+
+ )} +
+ +
+
+
+ + + + +
+ +
+
+ Enter + 发送 +
+
+ Shift + + + Enter + 换行 +
+
+
+
+
+ + {/* 右侧栏 */} + {isRightSidebarOpen && ( + +
+
+
+ +

配置中心

+
+ +
+
+ + + + +
+ {AVAILABLE_MODELS.map((model) => ( + setSelectedModel(model.id)} + className={`hover:scale-102 transition-transform ${ + selectedModel === model.id + ? 'border-2 border-' + model.color + '-500 bg-' + model.color + '-50' + : 'border-2 border-transparent' + }`} + > + +
+
+
{model.icon}
+
+

{model.name}

+

{model.description}

+
+
+ {selectedModel === model.id && ( + + )} +
+
+
+ ))} +
+
+ + +
+

选择 AI 可以使用的工具

+ +
+ {AVAILABLE_TOOLS.map((tool) => ( + +
+ {tool.icon} + {tool.name} +
+
+ ))} +
+
+
+
+ + + + +
+ 历史会话 + + {sessions.length} + +
+ +
+ 当前消息 + + {messages.length} + +
+ +
+ 启用工具 + + {selectedTools.length} + +
+
+
+
+
+
+
+ )} +
); }; +export default AgentChat; + /** * 消息渲染器 */ 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} - - - } /> - - +
+
+ + +

{message.content}

+
+
+ } size="sm" /> +
+
); case MessageTypes.AGENT_THINKING: return ( - - - } /> - - - - - {message.content} - - - - - - ); - - case MessageTypes.AGENT_PLAN: - return ( - - - } /> - - - - - - ); - - case MessageTypes.AGENT_EXECUTING: - return ( - - - } /> - - - {message.stepResults?.map((result, idx) => ( - - ))} - - - +
+
+ } + classNames={{ base: 'bg-gradient-to-br from-purple-500 to-pink-500' }} + size="sm" + /> + + +
+ +

+ {message.content} +

+
+
+
+
+
); case MessageTypes.AGENT_RESPONSE: return ( - - - } /> - - {/* 最终总结 */} - - - {message.content} - +
+
+ } + classNames={{ base: 'bg-gradient-to-br from-blue-500 to-purple-500' }} + size="sm" + /> +
+ + +

{message.content}

- {/* 元数据 */} - {message.metadata && ( - - 总步骤: {message.metadata.total_steps} - ✓ {message.metadata.successful_steps} - {message.metadata.failed_steps > 0 && ( - ✗ {message.metadata.failed_steps} +
+ + + + + + + + + + + {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.metadata.total_execution_time?.toFixed(1)}s - - )} - +
+
+
- {/* 执行详情(可选) */} - {message.plan && message.stepResults && message.stepResults.length > 0 && ( - - - - 📊 执行详情(点击展开查看) - - {message.stepResults.map((result, idx) => ( - - ))} - + {message.stepResults && message.stepResults.length > 0 && ( + )} - - - +
+
+
); case MessageTypes.ERROR: return ( - - - } /> - - {message.content} - - - +
+
+ } + classNames={{ base: 'bg-danger-500' }} + size="sm" + /> + + +

{message.content}

+
+
+
+
); default: @@ -854,4 +956,62 @@ const MessageRenderer = ({ message, userAvatar }) => { } }; -export default AgentChatV3; +/** + * 步骤结果面板 + */ +const StepResultsPanel = ({ stepResults }) => { + return ( + + + + 执行详情 ({stepResults.length} 个步骤) +
+ } + > +
+ {stepResults.map((result, idx) => ( + + +
+
+

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

+ + {result.status} + +
+

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

+
+ {result.error &&

⚠️ {result.error}

} +
+
+ ))} +
+ + + ); +}; diff --git a/src/views/AgentChat/index_v2.js b/src/views/AgentChat/index_v2.js new file mode 100644 index 00000000..050c4944 --- /dev/null +++ b/src/views/AgentChat/index_v2.js @@ -0,0 +1,1569 @@ +// 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}

} +
+
+ ))} +
+ + + ); +}; diff --git a/tailwind.config.js b/tailwind.config.js index 986d0b8a..24370cba 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -11,6 +11,8 @@ module.exports = { "./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}", "./node_modules/@heroui/react/dist/**/*.{js,ts,jsx,tsx}", ], + // 添加 JIT 模式优化 + mode: 'jit', theme: { extend: { // Brainwave 自定义颜色