// src/components/ChatBot/ChatInterfaceV2.js // 重新设计的聊天界面 - 更漂亮、支持Agent模式 import React, { useState, useRef, useEffect } from 'react'; import { Box, Flex, Input, IconButton, VStack, HStack, Text, Spinner, useColorModeValue, useToast, Divider, Badge, Button, Avatar, Heading, Progress, Fade, } from '@chakra-ui/react'; import { FiSend, FiRefreshCw, FiDownload, FiCpu, FiUser, FiZap } from 'react-icons/fi'; import { PlanCard } from './PlanCard'; import { StepResultCard } from './StepResultCard'; import { mcpService } from '../../services/mcpService'; import { logger } from '../../utils/logger'; /** * Agent消息类型 */ const MessageTypes = { USER: 'user', AGENT_THINKING: 'agent_thinking', AGENT_PLAN: 'agent_plan', AGENT_EXECUTING: 'agent_executing', AGENT_RESPONSE: 'agent_response', ERROR: 'error', }; /** * 聊天界面V2组件 - Agent模式 */ export const ChatInterfaceV2 = () => { const [messages, setMessages] = useState([ { id: 1, type: MessageTypes.AGENT_RESPONSE, content: '你好!我是AI投资研究助手。我会通过多步骤分析来帮助你深入了解金融市场。\n\n你可以问我:\n• 全面分析某只股票\n• 某个行业的投资机会\n• 今日市场热点\n• 某个概念板块的表现', timestamp: new Date().toISOString(), }, ]); const [inputValue, setInputValue] = useState(''); const [isProcessing, setIsProcessing] = useState(false); const [currentProgress, setCurrentProgress] = useState(0); const messagesEndRef = useRef(null); const inputRef = useRef(null); const toast = useToast(); // 颜色主题 const bgColor = useColorModeValue('gray.50', 'gray.900'); const chatBg = useColorModeValue('white', 'gray.800'); const inputBg = useColorModeValue('white', 'gray.700'); const userBubbleBg = useColorModeValue('blue.500', 'blue.600'); const agentBubbleBg = useColorModeValue('white', 'gray.700'); const borderColor = useColorModeValue('gray.200', 'gray.600'); // 自动滚动到底部 const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [messages]); // 添加消息 const addMessage = (message) => { setMessages((prev) => [...prev, { ...message, id: Date.now() }]); }; // 更新最后一条消息 const updateLastMessage = (updates) => { setMessages((prev) => { const newMessages = [...prev]; if (newMessages.length > 0) { newMessages[newMessages.length - 1] = { ...newMessages[newMessages.length - 1], ...updates, }; } return newMessages; }); }; // 发送消息(Agent模式 - 流式) 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); setCurrentProgress(0); // 用于存储步骤结果 let currentPlan = null; let stepResults = []; let executingMessageId = null; try { // 1. 显示思考状态 addMessage({ type: MessageTypes.AGENT_THINKING, content: '正在分析你的问题...', timestamp: new Date().toISOString(), }); setCurrentProgress(10); // 使用 EventSource 接收流式数据 const eventSource = new EventSource( `${mcpService.baseURL.replace('/mcp', '')}/mcp/agent/chat/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ 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, })), }), } ); // 由于 EventSource 不支持 POST,我们使用 fetch + ReadableStream const response = await fetch(`${mcpService.baseURL.replace('/mcp', '')}/mcp/agent/chat/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ 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, })), }), }); if (!response.ok) { throw new Error('Agent请求失败'); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; // 读取流式数据 while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n\n'); buffer = lines.pop(); // 保留不完整的行 for (const line of lines) { if (!line.trim()) continue; // 解析 SSE 消息 const eventMatch = line.match(/^event: (.+)$/m); const dataMatch = line.match(/^data: (.+)$/m); if (!eventMatch || !dataMatch) continue; const event = eventMatch[1]; const data = JSON.parse(dataMatch[1]); logger.info(`SSE Event: ${event}`, data); // 处理不同类型的事件 switch (event) { case 'status': if (data.stage === 'planning') { // 移除思考消息,显示规划中 setMessages(prev => prev.filter(m => m.type !== MessageTypes.AGENT_THINKING)); addMessage({ type: MessageTypes.AGENT_THINKING, content: data.message, timestamp: new Date().toISOString(), }); setCurrentProgress(20); } else if (data.stage === 'executing') { setCurrentProgress(30); } else if (data.stage === 'summarizing') { setCurrentProgress(90); } break; case 'plan': // 移除思考消息 setMessages(prev => prev.filter(m => m.type !== MessageTypes.AGENT_THINKING)); // 显示执行计划 currentPlan = data; addMessage({ type: MessageTypes.AGENT_PLAN, content: '已制定执行计划', plan: data, timestamp: new Date().toISOString(), }); setCurrentProgress(30); break; case 'step_start': // 如果还没有执行中消息,创建一个 if (!executingMessageId) { const executingMsg = { type: MessageTypes.AGENT_EXECUTING, content: '正在执行步骤...', plan: currentPlan, stepResults: [], timestamp: new Date().toISOString(), }; addMessage(executingMsg); executingMessageId = Date.now(); } break; case 'step_complete': // 添加步骤结果 stepResults.push({ step_index: data.step_index, tool: data.tool, status: data.status, result: data.result, error: data.error, execution_time: data.execution_time, arguments: data.arguments, }); // 更新执行中消息 setMessages(prev => prev.map(msg => msg.type === MessageTypes.AGENT_EXECUTING ? { ...msg, stepResults: [...stepResults] } : msg ) ); // 更新进度 if (currentPlan) { const progress = 30 + ((data.step_index + 1) / currentPlan.steps.length) * 60; setCurrentProgress(progress); } break; case 'summary': // 移除执行中消息 setMessages(prev => prev.filter(m => m.type !== MessageTypes.AGENT_EXECUTING)); // 显示最终结果 addMessage({ type: MessageTypes.AGENT_RESPONSE, content: data.content, plan: currentPlan, stepResults: stepResults, metadata: data.metadata, timestamp: new Date().toISOString(), }); setCurrentProgress(100); break; case 'error': throw new Error(data.message); case 'done': logger.info('Stream完成'); break; default: logger.warn('未知事件类型:', event); } } } } catch (error) { logger.error('Agent chat error', error); // 移除思考/执行中消息 setMessages(prev => prev.filter( m => m.type !== MessageTypes.AGENT_THINKING && m.type !== MessageTypes.AGENT_EXECUTING )); addMessage({ type: MessageTypes.ERROR, content: `处理失败:${error.message}`, timestamp: new Date().toISOString(), }); toast({ title: '处理失败', description: error.message, status: 'error', duration: 3000, isClosable: true, }); } finally { setIsProcessing(false); setCurrentProgress(0); inputRef.current?.focus(); } }; // 处理键盘事件 const handleKeyPress = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } }; // 清空对话 const handleClearChat = () => { setMessages([ { id: 1, type: MessageTypes.AGENT_RESPONSE, content: '对话已清空。有什么可以帮到你的?', timestamp: new Date().toISOString(), }, ]); }; // 导出对话 const handleExportChat = () => { const chatText = messages .filter(m => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE) .map((msg) => `[${msg.type === MessageTypes.USER ? '用户' : 'AI助手'}] ${msg.content}`) .join('\n\n'); const blob = new Blob([chatText], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `chat_${new Date().toISOString().slice(0, 10)}.txt`; a.click(); URL.revokeObjectURL(url); }; // 快捷问题 const quickQuestions = [ '全面分析贵州茅台这只股票', '今日涨停股票有哪些亮点', '新能源概念板块的投资机会', '半导体行业最新动态', ]; return ( {/* 头部 */} } /> AI投资研究助手 智能分析 多步骤深度研究 } size="sm" variant="ghost" aria-label="清空对话" onClick={handleClearChat} /> } size="sm" variant="ghost" aria-label="导出对话" onClick={handleExportChat} /> {/* 进度条 */} {isProcessing && ( )} {/* 消息列表 */} {messages.map((message) => ( ))}
{/* 快捷问题 */} {messages.length <= 2 && !isProcessing && ( 💡 试试这些问题: {quickQuestions.map((question, idx) => ( ))} )} {/* 输入框 */} setInputValue(e.target.value)} onKeyPress={handleKeyPress} placeholder="输入你的问题,我会进行深度分析..." bg={inputBg} border="1px" borderColor={borderColor} _focus={{ borderColor: 'blue.500', boxShadow: '0 0 0 1px #3182CE' }} mr={2} disabled={isProcessing} size="lg" /> : } colorScheme="blue" aria-label="发送" onClick={handleSendMessage} isLoading={isProcessing} disabled={!inputValue.trim() || isProcessing} size="lg" /> ); }; /** * 消息渲染器 */ const MessageRenderer = ({ message }) => { const userBubbleBg = useColorModeValue('blue.500', 'blue.600'); const agentBubbleBg = useColorModeValue('white', 'gray.700'); const borderColor = useColorModeValue('gray.200', 'gray.600'); switch (message.type) { case MessageTypes.USER: return ( {message.content} } /> ); case MessageTypes.AGENT_THINKING: return ( } /> {message.content} ); case MessageTypes.AGENT_PLAN: return ( } /> ); case MessageTypes.AGENT_EXECUTING: return ( } /> {message.stepResults?.map((result, idx) => ( ))} ); case MessageTypes.AGENT_RESPONSE: return ( } /> {/* 最终总结 */} {message.content} {/* 元数据 */} {message.metadata && ( 总步骤: {message.metadata.total_steps} ✓ {message.metadata.successful_steps} {message.metadata.failed_steps > 0 && ( ✗ {message.metadata.failed_steps} )} 耗时: {message.metadata.total_execution_time?.toFixed(1)}s )} {/* 执行详情(可选) */} {message.plan && message.stepResults && message.stepResults.length > 0 && ( 📊 执行详情(点击展开查看) {message.stepResults.map((result, idx) => ( ))} )} ); case MessageTypes.ERROR: return ( } /> {message.content} ); default: return null; } }; export default ChatInterfaceV2;