diff --git a/CLAUDE.md b/CLAUDE.md index 893b0cca..0d161d2f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,10 @@ **前端** - **核心框架**: React 18.3.1 - **类型系统**: TypeScript 5.9.3(渐进式接入中,支持 JS/TS 混合开发) -- **UI 组件库**: Chakra UI 2.10.9(主要) + Ant Design 5.27.4(表格/表单) +- **UI 组件库**: + - Chakra UI 2.10.9(主要,全局使用) + - Ant Design 5.27.4(表格/表单) + - **HeroUI 3.0.0-beta**(AgentChat 专用,2025-11-22 升级) - **状态管理**: Redux Toolkit 2.9.2 - **路由**: React Router v6.30.1 配合 React.lazy() 实现代码分割 - **构建系统**: CRACO 7.1.0 + 激进的 webpack 5 优化 @@ -58,7 +61,8 @@ - **开发工具**: MSW (Mock Service Worker) 用于 API mocking - **虚拟化**: @tanstack/react-virtual 3.13.12(性能优化) - **其他**: Draft.js(富文本编辑)、React Markdown、React Quill -- Use HeroUI v3 documentation from https://v3.heroui.com/llms.txt + +**注意**: HeroUI v3 文档参考 https://v3.heroui.com/llms.txt,详细升级说明见 [HEROUI_V3_UPGRADE_GUIDE.md](./HEROUI_V3_UPGRADE_GUIDE.md) **后端** - Flask + SQLAlchemy ORM diff --git a/package.json b/package.json index 6930f97a..e7a29807 100755 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "@fullcalendar/daygrid": "^5.9.0", "@fullcalendar/interaction": "^5.9.0", "@fullcalendar/react": "^5.9.0", - "@heroui/react": "^2.8.5", + "@heroui/react": "beta", + "@heroui/styles": "beta", "@reduxjs/toolkit": "^2.9.2", "@splidejs/react-splide": "^0.7.12", "@tanstack/react-virtual": "^3.13.12", diff --git a/src/index.js b/src/index.js index 3cae50cb..5c4d0027 100755 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,10 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter as Router } from 'react-router-dom'; + +// 导入 HeroUI v3 样式(必须在最前面导入,包含 Tailwind CSS) +import './styles/heroui.css'; + // 导入 Brainwave 样式(空文件,保留以避免错误) import './styles/brainwave.css'; diff --git a/src/providers/AppProviders.js b/src/providers/AppProviders.js index 5f7c4cea..976f53c4 100644 --- a/src/providers/AppProviders.js +++ b/src/providers/AppProviders.js @@ -3,7 +3,6 @@ import React from 'react'; import { ChakraProvider } from '@chakra-ui/react'; -import { HeroUIProvider } from '@heroui/react'; import { Provider as ReduxProvider } from 'react-redux'; // Redux Store @@ -23,11 +22,11 @@ import { NotificationProvider } from '../contexts/NotificationContext'; * Provider 层级顺序 (从外到内): * 1. ReduxProvider - 状态管理层 * 2. ChakraProvider - UI 框架层(主要) - * 3. HeroUIProvider - Hero UI 框架层(AgentChat 专用) - * 4. NotificationProvider - 通知系统 - * 5. AuthProvider - 认证系统 + * 3. NotificationProvider - 通知系统 + * 4. AuthProvider - 认证系统 * * 注意: + * - HeroUI v3 不再需要 HeroUIProvider,样式通过 CSS 导入加载 (src/styles/heroui.css) * - AuthModal 已迁移到 Redux (authModalSlice + useAuthModal Hook) * - ErrorBoundary 在各 Layout 层实现,不在全局层,以实现精细化错误隔离 * - MainLayout: PageTransitionWrapper 包含 ErrorBoundary (页面错误不影响导航栏) @@ -49,13 +48,11 @@ export function AppProviders({ children }) { } }} > - - - - {children} - - - + + + {children} + + ); diff --git a/src/styles/heroui.css b/src/styles/heroui.css new file mode 100644 index 00000000..bf9ed064 --- /dev/null +++ b/src/styles/heroui.css @@ -0,0 +1,19 @@ +/* HeroUI v3 样式导入 */ +/* 文档: https://v3.heroui.com/docs/quick-start */ + +/* + * 重要: 必须先导入 tailwindcss,再导入 @heroui/styles + * HeroUI v3 不再使用 Tailwind 插件,而是通过 CSS 层叠方式加载样式 + */ +@import "tailwindcss"; +@import "@heroui/styles"; + +/* + * AgentChat 页面的自定义样式 + * 这些样式仅在使用 HeroUI 组件的页面生效 + */ + +/* 确保深色模式正常工作 */ +.dark { + color-scheme: dark; +} diff --git a/src/views/AgentChat/index_old_chakra.js b/src/views/AgentChat/index_old_chakra.js deleted file mode 100644 index 39cdefa4..00000000 --- a/src/views/AgentChat/index_old_chakra.js +++ /dev/null @@ -1,1017 +0,0 @@ -// 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, -} 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', - }, -]; - -/** - * 可用工具配置 - */ -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(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_limit_up', - 'search_concept', - ]); - const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(true); - const [isRightSidebarOpen, setIsRightSidebarOpen] = useState(true); - - // Refs - const messagesEndRef = useRef(null); - const inputRef = useRef(null); - - // ==================== 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(), - }; - - addMessage(userMessage); - const userInput = inputValue; - setInputValue(''); - 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, - }); - - 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, - isClosable: true, - }); - } finally { - setIsProcessing(false); - 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(); - handleSendMessage(); - } - }; - - useEffect(() => { - if (user) { - loadSessions(); - createNewSession(); - } - }, [user]); - - const filteredSessions = sessions.filter( - (session) => - !searchQuery || session.last_message?.toLowerCase().includes(searchQuery.toLowerCase()) - ); - - const quickQuestions = [ - { text: '全面分析贵州茅台', emoji: '📊' }, - { text: '今日涨停股票分析', emoji: '🔥' }, - { text: '新能源概念机会', emoji: '⚡' }, - { text: '半导体行业动态', emoji: '💾' }, - ]; - - return ( -
- {/* 左侧栏 */} - {isLeftSidebarOpen && ( - -
-
-
- -

对话历史

-
- -
- - - - setSearchQuery(e.target.value)} - startContent={} - size="sm" - variant="bordered" - /> -
- - - {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 }) => { - switch (message.type) { - case MessageTypes.USER: - return ( -
-
- - -

{message.content}

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

- {message.content} -

-
-
-
-
-
- ); - - case MessageTypes.AGENT_RESPONSE: - return ( -
-
- } - 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.total_execution_time?.toFixed(1)}s -
- )} -
-
-
- - {message.stepResults && message.stepResults.length > 0 && ( - - )} -
-
-
- ); - - case MessageTypes.ERROR: - return ( -
-
- } - classNames={{ base: 'bg-danger-500' }} - size="sm" - /> - - -

{message.content}

-
-
-
-
- ); - - default: - return null; - } -}; - -/** - * 步骤结果面板 - */ -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 deleted file mode 100644 index 050c4944..00000000 --- a/src/views/AgentChat/index_v2.js +++ /dev/null @@ -1,1569 +0,0 @@ -// 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 bd5e5b5a..2a05091a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,11 +1,12 @@ /** @type {import('tailwindcss').Config} */ -const { heroui } = require("@heroui/react"); module.exports = { // 只扫描 AgentChat 页面(唯一使用 Hero UI 的地方) + // 使用精确路径,避免误匹配 node_modules content: [ - "./src/views/AgentChat/**/*.{js,jsx}", + "./src/views/AgentChat/index.js", "./src/providers/AppProviders.js", // HeroUIProvider + // HeroUI v3 不再需要扫描 node_modules,样式通过 CSS 导入加载 ], darkMode: "class", @@ -14,5 +15,6 @@ module.exports = { extend: {}, }, - plugins: [heroui()], + // HeroUI v3 不再需要 plugins,样式通过 @import "@heroui/styles" 加载 + plugins: [], }