agent功能开发增加MCP后端
This commit is contained in:
@@ -1,53 +1,857 @@
|
|||||||
// src/views/AgentChat/index.js
|
// src/views/AgentChat/index_v3.js
|
||||||
// Agent聊天页面
|
// Agent聊天页面 V3 - 带左侧会话列表和用户信息集成
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Container,
|
Flex,
|
||||||
Heading,
|
|
||||||
Text,
|
|
||||||
VStack,
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Input,
|
||||||
|
IconButton,
|
||||||
|
Button,
|
||||||
|
Avatar,
|
||||||
|
Heading,
|
||||||
|
Divider,
|
||||||
|
Spinner,
|
||||||
|
Badge,
|
||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
|
useToast,
|
||||||
|
Progress,
|
||||||
|
Fade,
|
||||||
|
Collapse,
|
||||||
|
useDisclosure,
|
||||||
|
InputGroup,
|
||||||
|
InputLeftElement,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuList,
|
||||||
|
MenuItem,
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
Tooltip,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { ChatInterfaceV2 } from '../../components/ChatBot';
|
import {
|
||||||
|
FiSend,
|
||||||
|
FiSearch,
|
||||||
|
FiPlus,
|
||||||
|
FiMessageSquare,
|
||||||
|
FiTrash2,
|
||||||
|
FiMoreVertical,
|
||||||
|
FiRefreshCw,
|
||||||
|
FiDownload,
|
||||||
|
FiCpu,
|
||||||
|
FiUser,
|
||||||
|
FiZap,
|
||||||
|
FiClock,
|
||||||
|
} from 'react-icons/fi';
|
||||||
|
import { useAuth } from '@contexts/AuthContext';
|
||||||
|
import { PlanCard } from '@components/ChatBot/PlanCard';
|
||||||
|
import { StepResultCard } from '@components/ChatBot/StepResultCard';
|
||||||
|
import { logger } from '@utils/logger';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Agent聊天页面
|
* Agent消息类型
|
||||||
* 提供基于MCP的AI助手对话功能
|
|
||||||
*/
|
*/
|
||||||
const AgentChat = () => {
|
const MessageTypes = {
|
||||||
|
USER: 'user',
|
||||||
|
AGENT_THINKING: 'agent_thinking',
|
||||||
|
AGENT_PLAN: 'agent_plan',
|
||||||
|
AGENT_EXECUTING: 'agent_executing',
|
||||||
|
AGENT_RESPONSE: 'agent_response',
|
||||||
|
ERROR: 'error',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent聊天页面 V3
|
||||||
|
*/
|
||||||
|
const AgentChatV3 = () => {
|
||||||
|
const { user } = useAuth(); // 获取当前用户信息
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
// 会话相关状态
|
||||||
|
const [sessions, setSessions] = useState([]);
|
||||||
|
const [currentSessionId, setCurrentSessionId] = useState(null);
|
||||||
|
const [isLoadingSessions, setIsLoadingSessions] = useState(true);
|
||||||
|
|
||||||
|
// 消息相关状态
|
||||||
|
const [messages, setMessages] = useState([]);
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [currentProgress, setCurrentProgress] = useState(0);
|
||||||
|
|
||||||
|
// UI 状态
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const { isOpen: isSidebarOpen, onToggle: toggleSidebar } = useDisclosure({ defaultIsOpen: true });
|
||||||
|
|
||||||
|
// Refs
|
||||||
|
const messagesEndRef = useRef(null);
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
|
// 颜色主题
|
||||||
const bgColor = useColorModeValue('gray.50', 'gray.900');
|
const bgColor = useColorModeValue('gray.50', 'gray.900');
|
||||||
const cardBg = useColorModeValue('white', 'gray.800');
|
const sidebarBg = useColorModeValue('white', 'gray.800');
|
||||||
|
const chatBg = useColorModeValue('white', 'gray.800');
|
||||||
|
const inputBg = useColorModeValue('white', 'gray.700');
|
||||||
|
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
||||||
|
const hoverBg = useColorModeValue('gray.100', 'gray.700');
|
||||||
|
const activeBg = useColorModeValue('blue.50', 'blue.900');
|
||||||
|
const userBubbleBg = useColorModeValue('blue.500', 'blue.600');
|
||||||
|
const agentBubbleBg = useColorModeValue('white', 'gray.700');
|
||||||
|
|
||||||
|
// ==================== 会话管理函数 ====================
|
||||||
|
|
||||||
|
// 加载会话列表
|
||||||
|
const loadSessions = async () => {
|
||||||
|
if (!user?.id) return;
|
||||||
|
|
||||||
|
setIsLoadingSessions(true);
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/mcp/agent/sessions', {
|
||||||
|
params: { user_id: user.id, limit: 50 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
setSessions(response.data.data);
|
||||||
|
logger.info('会话列表加载成功', response.data.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('加载会话列表失败', error);
|
||||||
|
toast({
|
||||||
|
title: '加载失败',
|
||||||
|
description: '无法加载会话列表',
|
||||||
|
status: 'error',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoadingSessions(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载会话历史
|
||||||
|
const loadSessionHistory = async (sessionId) => {
|
||||||
|
if (!sessionId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/mcp/agent/history/${sessionId}`, {
|
||||||
|
params: { limit: 100 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
const history = response.data.data;
|
||||||
|
|
||||||
|
// 将历史记录转换为消息格式
|
||||||
|
const formattedMessages = history.map((msg, idx) => ({
|
||||||
|
id: `${sessionId}-${idx}`,
|
||||||
|
type: msg.message_type === 'user' ? MessageTypes.USER : MessageTypes.AGENT_RESPONSE,
|
||||||
|
content: msg.message,
|
||||||
|
plan: msg.plan ? JSON.parse(msg.plan) : null,
|
||||||
|
stepResults: msg.steps ? JSON.parse(msg.steps) : null,
|
||||||
|
timestamp: msg.timestamp,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setMessages(formattedMessages);
|
||||||
|
logger.info('会话历史加载成功', formattedMessages);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('加载会话历史失败', error);
|
||||||
|
toast({
|
||||||
|
title: '加载失败',
|
||||||
|
description: '无法加载会话历史',
|
||||||
|
status: 'error',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建新会话
|
||||||
|
const createNewSession = () => {
|
||||||
|
setCurrentSessionId(null);
|
||||||
|
setMessages([
|
||||||
|
{
|
||||||
|
id: Date.now(),
|
||||||
|
type: MessageTypes.AGENT_RESPONSE,
|
||||||
|
content: `你好${user?.nickname || ''}!我是价小前,北京价值前沿科技公司的AI投研助手。\n\n我会通过多步骤分析来帮助你深入了解金融市场。\n\n你可以问我:\n• 全面分析某只股票\n• 某个行业的投资机会\n• 今日市场热点\n• 某个概念板块的表现`,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换会话
|
||||||
|
const switchSession = (sessionId) => {
|
||||||
|
setCurrentSessionId(sessionId);
|
||||||
|
loadSessionHistory(sessionId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除会话(需要后端API支持)
|
||||||
|
const deleteSession = async (sessionId) => {
|
||||||
|
// TODO: 实现删除会话的后端API
|
||||||
|
toast({
|
||||||
|
title: '删除会话',
|
||||||
|
description: '此功能尚未实现',
|
||||||
|
status: 'info',
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 消息处理函数 ====================
|
||||||
|
|
||||||
|
// 自动滚动到底部
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// 添加消息
|
||||||
|
const addMessage = (message) => {
|
||||||
|
setMessages((prev) => [...prev, { ...message, id: Date.now() }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
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,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
addMessage(userMessage);
|
||||||
|
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
|
||||||
|
.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 || '',
|
||||||
|
session_id: currentSessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移除思考消息
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
setCurrentProgress(0);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理键盘事件
|
||||||
|
const handleKeyPress = (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSendMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清空对话
|
||||||
|
const handleClearChat = () => {
|
||||||
|
createNewSession();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出对话
|
||||||
|
const handleExportChat = () => {
|
||||||
|
const chatText = messages
|
||||||
|
.filter((m) => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE)
|
||||||
|
.map((msg) => `[${msg.type === MessageTypes.USER ? '用户' : '价小前'}] ${msg.content}`)
|
||||||
|
.join('\n\n');
|
||||||
|
|
||||||
|
const blob = new Blob([chatText], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `chat_${new Date().toISOString().slice(0, 10)}.txt`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 初始化 ====================
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
loadSessions();
|
||||||
|
createNewSession();
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
// ==================== 渲染 ====================
|
||||||
|
|
||||||
|
// 快捷问题
|
||||||
|
const quickQuestions = [
|
||||||
|
'全面分析贵州茅台这只股票',
|
||||||
|
'今日涨停股票有哪些亮点',
|
||||||
|
'新能源概念板块的投资机会',
|
||||||
|
'半导体行业最新动态',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 筛选会话
|
||||||
|
const filteredSessions = sessions.filter(
|
||||||
|
(session) =>
|
||||||
|
!searchQuery ||
|
||||||
|
session.last_message?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box minH="calc(100vh - 200px)" bg={bgColor} py={8}>
|
<Flex h="calc(100vh - 80px)" bg={bgColor}>
|
||||||
<Container maxW="container.xl" h="100%">
|
{/* 左侧会话列表 */}
|
||||||
<VStack spacing={6} align="stretch" h="100%">
|
<Collapse in={isSidebarOpen} animateOpacity>
|
||||||
{/* 页面标题 */}
|
<Box
|
||||||
<Box>
|
w="300px"
|
||||||
<Heading size="lg" mb={2}>AI投资助手</Heading>
|
bg={sidebarBg}
|
||||||
<Text color="gray.600" fontSize="sm">
|
borderRight="1px"
|
||||||
基于MCP协议的智能投资顾问,支持股票查询、新闻搜索、概念分析等多种功能
|
borderColor={borderColor}
|
||||||
</Text>
|
h="100%"
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
{/* 侧边栏头部 */}
|
||||||
|
<Box p={4} borderBottom="1px" borderColor={borderColor}>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FiPlus />}
|
||||||
|
colorScheme="blue"
|
||||||
|
w="100%"
|
||||||
|
onClick={createNewSession}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
新建对话
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 搜索框 */}
|
||||||
|
<InputGroup mt={3} size="sm">
|
||||||
|
<InputLeftElement pointerEvents="none">
|
||||||
|
<FiSearch color="gray.300" />
|
||||||
|
</InputLeftElement>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索对话..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 聊天界面 */}
|
{/* 会话列表 */}
|
||||||
<Box
|
<VStack
|
||||||
flex="1"
|
flex="1"
|
||||||
bg={cardBg}
|
overflowY="auto"
|
||||||
borderRadius="xl"
|
spacing={0}
|
||||||
boxShadow="xl"
|
align="stretch"
|
||||||
overflow="hidden"
|
css={{
|
||||||
h="calc(100vh - 300px)"
|
'&::-webkit-scrollbar': { width: '6px' },
|
||||||
minH="600px"
|
'&::-webkit-scrollbar-thumb': {
|
||||||
|
background: '#CBD5E0',
|
||||||
|
borderRadius: '3px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ChatInterfaceV2 />
|
{isLoadingSessions ? (
|
||||||
|
<Flex justify="center" align="center" h="200px">
|
||||||
|
<Spinner />
|
||||||
|
</Flex>
|
||||||
|
) : filteredSessions.length === 0 ? (
|
||||||
|
<Flex justify="center" align="center" h="200px" direction="column">
|
||||||
|
<FiMessageSquare size={32} color="gray" />
|
||||||
|
<Text mt={2} fontSize="sm" color="gray.500">
|
||||||
|
{searchQuery ? '没有找到匹配的对话' : '暂无对话记录'}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
filteredSessions.map((session) => (
|
||||||
|
<Box
|
||||||
|
key={session.session_id}
|
||||||
|
p={3}
|
||||||
|
cursor="pointer"
|
||||||
|
bg={currentSessionId === session.session_id ? activeBg : 'transparent'}
|
||||||
|
_hover={{ bg: hoverBg }}
|
||||||
|
borderBottom="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
onClick={() => switchSession(session.session_id)}
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="start">
|
||||||
|
<VStack align="start" spacing={1} flex="1">
|
||||||
|
<Text fontSize="sm" fontWeight="medium" noOfLines={2}>
|
||||||
|
{session.last_message || '新对话'}
|
||||||
|
</Text>
|
||||||
|
<HStack spacing={2} fontSize="xs" color="gray.500">
|
||||||
|
<FiClock size={12} />
|
||||||
|
<Text>
|
||||||
|
{new Date(session.last_timestamp).toLocaleDateString('zh-CN', {
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
<Badge colorScheme="blue" fontSize="xx-small">
|
||||||
|
{session.message_count} 条
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<Menu>
|
||||||
|
<MenuButton
|
||||||
|
as={IconButton}
|
||||||
|
icon={<FiMoreVertical />}
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem
|
||||||
|
icon={<FiTrash2 />}
|
||||||
|
color="red.500"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteSession(session.session_id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除对话
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* 用户信息 */}
|
||||||
|
<Box p={4} borderTop="1px" borderColor={borderColor}>
|
||||||
|
<HStack spacing={3}>
|
||||||
|
<Avatar size="sm" name={user?.nickname} src={user?.avatar} />
|
||||||
|
<VStack align="start" spacing={0} flex="1">
|
||||||
|
<Text fontSize="sm" fontWeight="medium">
|
||||||
|
{user?.nickname || '未登录'}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
{user?.id || 'anonymous'}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
</VStack>
|
</Box>
|
||||||
</Container>
|
</Collapse>
|
||||||
</Box>
|
|
||||||
|
{/* 主聊天区域 */}
|
||||||
|
<Flex flex="1" direction="column" h="100%">
|
||||||
|
{/* 聊天头部 */}
|
||||||
|
<Box bg={chatBg} borderBottom="1px" borderColor={borderColor} px={6} py={4}>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<HStack spacing={4}>
|
||||||
|
<IconButton
|
||||||
|
icon={<FiMessageSquare />}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label="切换侧边栏"
|
||||||
|
onClick={toggleSidebar}
|
||||||
|
/>
|
||||||
|
<Avatar size="md" bg="blue.500" icon={<FiCpu fontSize="1.5rem" />} />
|
||||||
|
<VStack align="start" spacing={0}>
|
||||||
|
<Heading size="md">价小前投研</Heading>
|
||||||
|
<HStack>
|
||||||
|
<Badge colorScheme="green" fontSize="xs">
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<FiZap size={10} />
|
||||||
|
<span>智能分析</span>
|
||||||
|
</HStack>
|
||||||
|
</Badge>
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
多步骤深度研究
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack>
|
||||||
|
<IconButton
|
||||||
|
icon={<FiRefreshCw />}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label="清空对话"
|
||||||
|
onClick={handleClearChat}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<FiDownload />}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label="导出对话"
|
||||||
|
onClick={handleExportChat}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 进度条 */}
|
||||||
|
{isProcessing && (
|
||||||
|
<Progress
|
||||||
|
value={currentProgress}
|
||||||
|
size="xs"
|
||||||
|
colorScheme="blue"
|
||||||
|
mt={3}
|
||||||
|
borderRadius="full"
|
||||||
|
isAnimated
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 消息列表 */}
|
||||||
|
<Box
|
||||||
|
flex="1"
|
||||||
|
overflowY="auto"
|
||||||
|
px={6}
|
||||||
|
py={4}
|
||||||
|
css={{
|
||||||
|
'&::-webkit-scrollbar': { width: '8px' },
|
||||||
|
'&::-webkit-scrollbar-thumb': {
|
||||||
|
background: '#CBD5E0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VStack spacing={4} align="stretch">
|
||||||
|
{messages.map((message) => (
|
||||||
|
<Fade in key={message.id}>
|
||||||
|
<MessageRenderer message={message} userAvatar={user?.avatar} />
|
||||||
|
</Fade>
|
||||||
|
))}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 快捷问题 */}
|
||||||
|
{messages.length <= 2 && !isProcessing && (
|
||||||
|
<Box px={6} py={3} bg={chatBg} borderTop="1px" borderColor={borderColor}>
|
||||||
|
<Text fontSize="xs" color="gray.500" mb={2}>
|
||||||
|
💡 试试这些问题:
|
||||||
|
</Text>
|
||||||
|
<Flex wrap="wrap" gap={2}>
|
||||||
|
{quickQuestions.map((question, idx) => (
|
||||||
|
<Button
|
||||||
|
key={idx}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme="blue"
|
||||||
|
fontSize="xs"
|
||||||
|
onClick={() => {
|
||||||
|
setInputValue(question);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{question}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 输入框 */}
|
||||||
|
<Box px={6} py={4} bg={chatBg} borderTop="1px" borderColor={borderColor}>
|
||||||
|
<Flex>
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
placeholder="输入你的问题,我会进行深度分析..."
|
||||||
|
bg={inputBg}
|
||||||
|
border="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
_focus={{ borderColor: 'blue.500', boxShadow: '0 0 0 1px #3182CE' }}
|
||||||
|
mr={2}
|
||||||
|
disabled={isProcessing}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={isProcessing ? <Spinner size="sm" /> : <FiSend />}
|
||||||
|
colorScheme="blue"
|
||||||
|
aria-label="发送"
|
||||||
|
onClick={handleSendMessage}
|
||||||
|
isLoading={isProcessing}
|
||||||
|
disabled={!inputValue.trim() || isProcessing}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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 (
|
||||||
|
<Flex justify="flex-end">
|
||||||
|
<HStack align="flex-start" maxW="75%">
|
||||||
|
<Box
|
||||||
|
bg={userBubbleBg}
|
||||||
|
color="white"
|
||||||
|
px={4}
|
||||||
|
py={3}
|
||||||
|
borderRadius="lg"
|
||||||
|
boxShadow="md"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm" whiteSpace="pre-wrap">
|
||||||
|
{message.content}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Avatar size="sm" src={userAvatar} icon={<FiUser fontSize="1rem" />} />
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
case MessageTypes.AGENT_THINKING:
|
||||||
|
return (
|
||||||
|
<Flex justify="flex-start">
|
||||||
|
<HStack align="flex-start" maxW="75%">
|
||||||
|
<Avatar size="sm" bg="purple.500" icon={<FiCpu fontSize="1rem" />} />
|
||||||
|
<Box
|
||||||
|
bg={agentBubbleBg}
|
||||||
|
px={4}
|
||||||
|
py={3}
|
||||||
|
borderRadius="lg"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
boxShadow="sm"
|
||||||
|
>
|
||||||
|
<HStack>
|
||||||
|
<Spinner size="sm" color="purple.500" />
|
||||||
|
<Text fontSize="sm" color="purple.600">
|
||||||
|
{message.content}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
case MessageTypes.AGENT_PLAN:
|
||||||
|
return (
|
||||||
|
<Flex justify="flex-start">
|
||||||
|
<HStack align="flex-start" maxW="85%">
|
||||||
|
<Avatar size="sm" bg="blue.500" icon={<FiCpu fontSize="1rem" />} />
|
||||||
|
<VStack align="stretch" flex="1">
|
||||||
|
<PlanCard plan={message.plan} stepResults={[]} />
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
case MessageTypes.AGENT_EXECUTING:
|
||||||
|
return (
|
||||||
|
<Flex justify="flex-start">
|
||||||
|
<HStack align="flex-start" maxW="85%">
|
||||||
|
<Avatar size="sm" bg="orange.500" icon={<FiCpu fontSize="1rem" />} />
|
||||||
|
<VStack align="stretch" flex="1" spacing={3}>
|
||||||
|
<PlanCard plan={message.plan} stepResults={message.stepResults} />
|
||||||
|
{message.stepResults?.map((result, idx) => (
|
||||||
|
<StepResultCard key={idx} stepResult={result} />
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
case MessageTypes.AGENT_RESPONSE:
|
||||||
|
return (
|
||||||
|
<Flex justify="flex-start">
|
||||||
|
<HStack align="flex-start" maxW="85%">
|
||||||
|
<Avatar size="sm" bg="green.500" icon={<FiCpu fontSize="1rem" />} />
|
||||||
|
<VStack align="stretch" flex="1" spacing={3}>
|
||||||
|
{/* 最终总结 */}
|
||||||
|
<Box
|
||||||
|
bg={agentBubbleBg}
|
||||||
|
px={4}
|
||||||
|
py={3}
|
||||||
|
borderRadius="lg"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
boxShadow="md"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm" whiteSpace="pre-wrap">
|
||||||
|
{message.content}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* 元数据 */}
|
||||||
|
{message.metadata && (
|
||||||
|
<HStack mt={3} spacing={4} fontSize="xs" color="gray.500">
|
||||||
|
<Text>总步骤: {message.metadata.total_steps}</Text>
|
||||||
|
<Text>✓ {message.metadata.successful_steps}</Text>
|
||||||
|
{message.metadata.failed_steps > 0 && (
|
||||||
|
<Text>✗ {message.metadata.failed_steps}</Text>
|
||||||
|
)}
|
||||||
|
<Text>耗时: {message.metadata.total_execution_time?.toFixed(1)}s</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 执行详情(可选) */}
|
||||||
|
{message.plan && message.stepResults && message.stepResults.length > 0 && (
|
||||||
|
<VStack align="stretch" spacing={2}>
|
||||||
|
<Divider />
|
||||||
|
<Text fontSize="xs" fontWeight="bold" color="gray.500">
|
||||||
|
📊 执行详情(点击展开查看)
|
||||||
|
</Text>
|
||||||
|
{message.stepResults.map((result, idx) => (
|
||||||
|
<StepResultCard key={idx} stepResult={result} />
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
case MessageTypes.ERROR:
|
||||||
|
return (
|
||||||
|
<Flex justify="flex-start">
|
||||||
|
<HStack align="flex-start" maxW="75%">
|
||||||
|
<Avatar size="sm" bg="red.500" icon={<FiCpu fontSize="1rem" />} />
|
||||||
|
<Box
|
||||||
|
bg="red.50"
|
||||||
|
color="red.700"
|
||||||
|
px={4}
|
||||||
|
py={3}
|
||||||
|
borderRadius="lg"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor="red.200"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm">{message.content}</Text>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AgentChatV3;
|
||||||
|
|||||||
53
src/views/AgentChat/index_backup.js
Normal file
53
src/views/AgentChat/index_backup.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// src/views/AgentChat/index.js
|
||||||
|
// Agent聊天页面
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
useColorModeValue,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { ChatInterfaceV2 } from '../../components/ChatBot';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent聊天页面
|
||||||
|
* 提供基于MCP的AI助手对话功能
|
||||||
|
*/
|
||||||
|
const AgentChat = () => {
|
||||||
|
const bgColor = useColorModeValue('gray.50', 'gray.900');
|
||||||
|
const cardBg = useColorModeValue('white', 'gray.800');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box minH="calc(100vh - 200px)" bg={bgColor} py={8}>
|
||||||
|
<Container maxW="container.xl" h="100%">
|
||||||
|
<VStack spacing={6} align="stretch" h="100%">
|
||||||
|
{/* 页面标题 */}
|
||||||
|
<Box>
|
||||||
|
<Heading size="lg" mb={2}>AI投资助手</Heading>
|
||||||
|
<Text color="gray.600" fontSize="sm">
|
||||||
|
基于MCP协议的智能投资顾问,支持股票查询、新闻搜索、概念分析等多种功能
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 聊天界面 */}
|
||||||
|
<Box
|
||||||
|
flex="1"
|
||||||
|
bg={cardBg}
|
||||||
|
borderRadius="xl"
|
||||||
|
boxShadow="xl"
|
||||||
|
overflow="hidden"
|
||||||
|
h="calc(100vh - 300px)"
|
||||||
|
minH="600px"
|
||||||
|
>
|
||||||
|
<ChatInterfaceV2 />
|
||||||
|
</Box>
|
||||||
|
</VStack>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AgentChat;
|
||||||
857
src/views/AgentChat/index_v3.js
Normal file
857
src/views/AgentChat/index_v3.js
Normal file
@@ -0,0 +1,857 @@
|
|||||||
|
// src/views/AgentChat/index_v3.js
|
||||||
|
// Agent聊天页面 V3 - 带左侧会话列表和用户信息集成
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Input,
|
||||||
|
IconButton,
|
||||||
|
Button,
|
||||||
|
Avatar,
|
||||||
|
Heading,
|
||||||
|
Divider,
|
||||||
|
Spinner,
|
||||||
|
Badge,
|
||||||
|
useColorModeValue,
|
||||||
|
useToast,
|
||||||
|
Progress,
|
||||||
|
Fade,
|
||||||
|
Collapse,
|
||||||
|
useDisclosure,
|
||||||
|
InputGroup,
|
||||||
|
InputLeftElement,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuList,
|
||||||
|
MenuItem,
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
Tooltip,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import {
|
||||||
|
FiSend,
|
||||||
|
FiSearch,
|
||||||
|
FiPlus,
|
||||||
|
FiMessageSquare,
|
||||||
|
FiTrash2,
|
||||||
|
FiMoreVertical,
|
||||||
|
FiRefreshCw,
|
||||||
|
FiDownload,
|
||||||
|
FiCpu,
|
||||||
|
FiUser,
|
||||||
|
FiZap,
|
||||||
|
FiClock,
|
||||||
|
} from 'react-icons/fi';
|
||||||
|
import { useAuth } from '@contexts/AuthContext';
|
||||||
|
import { PlanCard } from '@components/ChatBot/PlanCard';
|
||||||
|
import { StepResultCard } from '@components/ChatBot/StepResultCard';
|
||||||
|
import { logger } from '@utils/logger';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent消息类型
|
||||||
|
*/
|
||||||
|
const MessageTypes = {
|
||||||
|
USER: 'user',
|
||||||
|
AGENT_THINKING: 'agent_thinking',
|
||||||
|
AGENT_PLAN: 'agent_plan',
|
||||||
|
AGENT_EXECUTING: 'agent_executing',
|
||||||
|
AGENT_RESPONSE: 'agent_response',
|
||||||
|
ERROR: 'error',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent聊天页面 V3
|
||||||
|
*/
|
||||||
|
const AgentChatV3 = () => {
|
||||||
|
const { user } = useAuth(); // 获取当前用户信息
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
// 会话相关状态
|
||||||
|
const [sessions, setSessions] = useState([]);
|
||||||
|
const [currentSessionId, setCurrentSessionId] = useState(null);
|
||||||
|
const [isLoadingSessions, setIsLoadingSessions] = useState(true);
|
||||||
|
|
||||||
|
// 消息相关状态
|
||||||
|
const [messages, setMessages] = useState([]);
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [currentProgress, setCurrentProgress] = useState(0);
|
||||||
|
|
||||||
|
// UI 状态
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const { isOpen: isSidebarOpen, onToggle: toggleSidebar } = useDisclosure({ defaultIsOpen: true });
|
||||||
|
|
||||||
|
// Refs
|
||||||
|
const messagesEndRef = useRef(null);
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
|
// 颜色主题
|
||||||
|
const bgColor = useColorModeValue('gray.50', 'gray.900');
|
||||||
|
const sidebarBg = useColorModeValue('white', 'gray.800');
|
||||||
|
const chatBg = useColorModeValue('white', 'gray.800');
|
||||||
|
const inputBg = useColorModeValue('white', 'gray.700');
|
||||||
|
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
||||||
|
const hoverBg = useColorModeValue('gray.100', 'gray.700');
|
||||||
|
const activeBg = useColorModeValue('blue.50', 'blue.900');
|
||||||
|
const userBubbleBg = useColorModeValue('blue.500', 'blue.600');
|
||||||
|
const agentBubbleBg = useColorModeValue('white', 'gray.700');
|
||||||
|
|
||||||
|
// ==================== 会话管理函数 ====================
|
||||||
|
|
||||||
|
// 加载会话列表
|
||||||
|
const loadSessions = async () => {
|
||||||
|
if (!user?.id) return;
|
||||||
|
|
||||||
|
setIsLoadingSessions(true);
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/mcp/agent/sessions', {
|
||||||
|
params: { user_id: user.id, limit: 50 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
setSessions(response.data.data);
|
||||||
|
logger.info('会话列表加载成功', response.data.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('加载会话列表失败', error);
|
||||||
|
toast({
|
||||||
|
title: '加载失败',
|
||||||
|
description: '无法加载会话列表',
|
||||||
|
status: 'error',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoadingSessions(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载会话历史
|
||||||
|
const loadSessionHistory = async (sessionId) => {
|
||||||
|
if (!sessionId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/mcp/agent/history/${sessionId}`, {
|
||||||
|
params: { limit: 100 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
const history = response.data.data;
|
||||||
|
|
||||||
|
// 将历史记录转换为消息格式
|
||||||
|
const formattedMessages = history.map((msg, idx) => ({
|
||||||
|
id: `${sessionId}-${idx}`,
|
||||||
|
type: msg.message_type === 'user' ? MessageTypes.USER : MessageTypes.AGENT_RESPONSE,
|
||||||
|
content: msg.message,
|
||||||
|
plan: msg.plan ? JSON.parse(msg.plan) : null,
|
||||||
|
stepResults: msg.steps ? JSON.parse(msg.steps) : null,
|
||||||
|
timestamp: msg.timestamp,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setMessages(formattedMessages);
|
||||||
|
logger.info('会话历史加载成功', formattedMessages);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('加载会话历史失败', error);
|
||||||
|
toast({
|
||||||
|
title: '加载失败',
|
||||||
|
description: '无法加载会话历史',
|
||||||
|
status: 'error',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建新会话
|
||||||
|
const createNewSession = () => {
|
||||||
|
setCurrentSessionId(null);
|
||||||
|
setMessages([
|
||||||
|
{
|
||||||
|
id: Date.now(),
|
||||||
|
type: MessageTypes.AGENT_RESPONSE,
|
||||||
|
content: `你好${user?.nickname || ''}!我是价小前,北京价值前沿科技公司的AI投研助手。\n\n我会通过多步骤分析来帮助你深入了解金融市场。\n\n你可以问我:\n• 全面分析某只股票\n• 某个行业的投资机会\n• 今日市场热点\n• 某个概念板块的表现`,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换会话
|
||||||
|
const switchSession = (sessionId) => {
|
||||||
|
setCurrentSessionId(sessionId);
|
||||||
|
loadSessionHistory(sessionId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除会话(需要后端API支持)
|
||||||
|
const deleteSession = async (sessionId) => {
|
||||||
|
// TODO: 实现删除会话的后端API
|
||||||
|
toast({
|
||||||
|
title: '删除会话',
|
||||||
|
description: '此功能尚未实现',
|
||||||
|
status: 'info',
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 消息处理函数 ====================
|
||||||
|
|
||||||
|
// 自动滚动到底部
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// 添加消息
|
||||||
|
const addMessage = (message) => {
|
||||||
|
setMessages((prev) => [...prev, { ...message, id: Date.now() }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
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,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
addMessage(userMessage);
|
||||||
|
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
|
||||||
|
.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 || '',
|
||||||
|
session_id: currentSessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移除思考消息
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
setCurrentProgress(0);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理键盘事件
|
||||||
|
const handleKeyPress = (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSendMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清空对话
|
||||||
|
const handleClearChat = () => {
|
||||||
|
createNewSession();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出对话
|
||||||
|
const handleExportChat = () => {
|
||||||
|
const chatText = messages
|
||||||
|
.filter((m) => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE)
|
||||||
|
.map((msg) => `[${msg.type === MessageTypes.USER ? '用户' : '价小前'}] ${msg.content}`)
|
||||||
|
.join('\n\n');
|
||||||
|
|
||||||
|
const blob = new Blob([chatText], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `chat_${new Date().toISOString().slice(0, 10)}.txt`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 初始化 ====================
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
loadSessions();
|
||||||
|
createNewSession();
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
// ==================== 渲染 ====================
|
||||||
|
|
||||||
|
// 快捷问题
|
||||||
|
const quickQuestions = [
|
||||||
|
'全面分析贵州茅台这只股票',
|
||||||
|
'今日涨停股票有哪些亮点',
|
||||||
|
'新能源概念板块的投资机会',
|
||||||
|
'半导体行业最新动态',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 筛选会话
|
||||||
|
const filteredSessions = sessions.filter(
|
||||||
|
(session) =>
|
||||||
|
!searchQuery ||
|
||||||
|
session.last_message?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex h="calc(100vh - 80px)" bg={bgColor}>
|
||||||
|
{/* 左侧会话列表 */}
|
||||||
|
<Collapse in={isSidebarOpen} animateOpacity>
|
||||||
|
<Box
|
||||||
|
w="300px"
|
||||||
|
bg={sidebarBg}
|
||||||
|
borderRight="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
h="100%"
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
{/* 侧边栏头部 */}
|
||||||
|
<Box p={4} borderBottom="1px" borderColor={borderColor}>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FiPlus />}
|
||||||
|
colorScheme="blue"
|
||||||
|
w="100%"
|
||||||
|
onClick={createNewSession}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
新建对话
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 搜索框 */}
|
||||||
|
<InputGroup mt={3} size="sm">
|
||||||
|
<InputLeftElement pointerEvents="none">
|
||||||
|
<FiSearch color="gray.300" />
|
||||||
|
</InputLeftElement>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索对话..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 会话列表 */}
|
||||||
|
<VStack
|
||||||
|
flex="1"
|
||||||
|
overflowY="auto"
|
||||||
|
spacing={0}
|
||||||
|
align="stretch"
|
||||||
|
css={{
|
||||||
|
'&::-webkit-scrollbar': { width: '6px' },
|
||||||
|
'&::-webkit-scrollbar-thumb': {
|
||||||
|
background: '#CBD5E0',
|
||||||
|
borderRadius: '3px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLoadingSessions ? (
|
||||||
|
<Flex justify="center" align="center" h="200px">
|
||||||
|
<Spinner />
|
||||||
|
</Flex>
|
||||||
|
) : filteredSessions.length === 0 ? (
|
||||||
|
<Flex justify="center" align="center" h="200px" direction="column">
|
||||||
|
<FiMessageSquare size={32} color="gray" />
|
||||||
|
<Text mt={2} fontSize="sm" color="gray.500">
|
||||||
|
{searchQuery ? '没有找到匹配的对话' : '暂无对话记录'}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
filteredSessions.map((session) => (
|
||||||
|
<Box
|
||||||
|
key={session.session_id}
|
||||||
|
p={3}
|
||||||
|
cursor="pointer"
|
||||||
|
bg={currentSessionId === session.session_id ? activeBg : 'transparent'}
|
||||||
|
_hover={{ bg: hoverBg }}
|
||||||
|
borderBottom="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
onClick={() => switchSession(session.session_id)}
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="start">
|
||||||
|
<VStack align="start" spacing={1} flex="1">
|
||||||
|
<Text fontSize="sm" fontWeight="medium" noOfLines={2}>
|
||||||
|
{session.last_message || '新对话'}
|
||||||
|
</Text>
|
||||||
|
<HStack spacing={2} fontSize="xs" color="gray.500">
|
||||||
|
<FiClock size={12} />
|
||||||
|
<Text>
|
||||||
|
{new Date(session.last_timestamp).toLocaleDateString('zh-CN', {
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
<Badge colorScheme="blue" fontSize="xx-small">
|
||||||
|
{session.message_count} 条
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<Menu>
|
||||||
|
<MenuButton
|
||||||
|
as={IconButton}
|
||||||
|
icon={<FiMoreVertical />}
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem
|
||||||
|
icon={<FiTrash2 />}
|
||||||
|
color="red.500"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteSession(session.session_id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除对话
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* 用户信息 */}
|
||||||
|
<Box p={4} borderTop="1px" borderColor={borderColor}>
|
||||||
|
<HStack spacing={3}>
|
||||||
|
<Avatar size="sm" name={user?.nickname} src={user?.avatar} />
|
||||||
|
<VStack align="start" spacing={0} flex="1">
|
||||||
|
<Text fontSize="sm" fontWeight="medium">
|
||||||
|
{user?.nickname || '未登录'}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
{user?.id || 'anonymous'}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
{/* 主聊天区域 */}
|
||||||
|
<Flex flex="1" direction="column" h="100%">
|
||||||
|
{/* 聊天头部 */}
|
||||||
|
<Box bg={chatBg} borderBottom="1px" borderColor={borderColor} px={6} py={4}>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<HStack spacing={4}>
|
||||||
|
<IconButton
|
||||||
|
icon={<FiMessageSquare />}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label="切换侧边栏"
|
||||||
|
onClick={toggleSidebar}
|
||||||
|
/>
|
||||||
|
<Avatar size="md" bg="blue.500" icon={<FiCpu fontSize="1.5rem" />} />
|
||||||
|
<VStack align="start" spacing={0}>
|
||||||
|
<Heading size="md">价小前投研</Heading>
|
||||||
|
<HStack>
|
||||||
|
<Badge colorScheme="green" fontSize="xs">
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<FiZap size={10} />
|
||||||
|
<span>智能分析</span>
|
||||||
|
</HStack>
|
||||||
|
</Badge>
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
多步骤深度研究
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack>
|
||||||
|
<IconButton
|
||||||
|
icon={<FiRefreshCw />}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label="清空对话"
|
||||||
|
onClick={handleClearChat}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<FiDownload />}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label="导出对话"
|
||||||
|
onClick={handleExportChat}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 进度条 */}
|
||||||
|
{isProcessing && (
|
||||||
|
<Progress
|
||||||
|
value={currentProgress}
|
||||||
|
size="xs"
|
||||||
|
colorScheme="blue"
|
||||||
|
mt={3}
|
||||||
|
borderRadius="full"
|
||||||
|
isAnimated
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 消息列表 */}
|
||||||
|
<Box
|
||||||
|
flex="1"
|
||||||
|
overflowY="auto"
|
||||||
|
px={6}
|
||||||
|
py={4}
|
||||||
|
css={{
|
||||||
|
'&::-webkit-scrollbar': { width: '8px' },
|
||||||
|
'&::-webkit-scrollbar-thumb': {
|
||||||
|
background: '#CBD5E0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VStack spacing={4} align="stretch">
|
||||||
|
{messages.map((message) => (
|
||||||
|
<Fade in key={message.id}>
|
||||||
|
<MessageRenderer message={message} userAvatar={user?.avatar} />
|
||||||
|
</Fade>
|
||||||
|
))}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 快捷问题 */}
|
||||||
|
{messages.length <= 2 && !isProcessing && (
|
||||||
|
<Box px={6} py={3} bg={chatBg} borderTop="1px" borderColor={borderColor}>
|
||||||
|
<Text fontSize="xs" color="gray.500" mb={2}>
|
||||||
|
💡 试试这些问题:
|
||||||
|
</Text>
|
||||||
|
<Flex wrap="wrap" gap={2}>
|
||||||
|
{quickQuestions.map((question, idx) => (
|
||||||
|
<Button
|
||||||
|
key={idx}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme="blue"
|
||||||
|
fontSize="xs"
|
||||||
|
onClick={() => {
|
||||||
|
setInputValue(question);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{question}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 输入框 */}
|
||||||
|
<Box px={6} py={4} bg={chatBg} borderTop="1px" borderColor={borderColor}>
|
||||||
|
<Flex>
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
placeholder="输入你的问题,我会进行深度分析..."
|
||||||
|
bg={inputBg}
|
||||||
|
border="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
_focus={{ borderColor: 'blue.500', boxShadow: '0 0 0 1px #3182CE' }}
|
||||||
|
mr={2}
|
||||||
|
disabled={isProcessing}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={isProcessing ? <Spinner size="sm" /> : <FiSend />}
|
||||||
|
colorScheme="blue"
|
||||||
|
aria-label="发送"
|
||||||
|
onClick={handleSendMessage}
|
||||||
|
isLoading={isProcessing}
|
||||||
|
disabled={!inputValue.trim() || isProcessing}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息渲染器
|
||||||
|
*/
|
||||||
|
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 (
|
||||||
|
<Flex justify="flex-end">
|
||||||
|
<HStack align="flex-start" maxW="75%">
|
||||||
|
<Box
|
||||||
|
bg={userBubbleBg}
|
||||||
|
color="white"
|
||||||
|
px={4}
|
||||||
|
py={3}
|
||||||
|
borderRadius="lg"
|
||||||
|
boxShadow="md"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm" whiteSpace="pre-wrap">
|
||||||
|
{message.content}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Avatar size="sm" src={userAvatar} icon={<FiUser fontSize="1rem" />} />
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
case MessageTypes.AGENT_THINKING:
|
||||||
|
return (
|
||||||
|
<Flex justify="flex-start">
|
||||||
|
<HStack align="flex-start" maxW="75%">
|
||||||
|
<Avatar size="sm" bg="purple.500" icon={<FiCpu fontSize="1rem" />} />
|
||||||
|
<Box
|
||||||
|
bg={agentBubbleBg}
|
||||||
|
px={4}
|
||||||
|
py={3}
|
||||||
|
borderRadius="lg"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
boxShadow="sm"
|
||||||
|
>
|
||||||
|
<HStack>
|
||||||
|
<Spinner size="sm" color="purple.500" />
|
||||||
|
<Text fontSize="sm" color="purple.600">
|
||||||
|
{message.content}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
case MessageTypes.AGENT_PLAN:
|
||||||
|
return (
|
||||||
|
<Flex justify="flex-start">
|
||||||
|
<HStack align="flex-start" maxW="85%">
|
||||||
|
<Avatar size="sm" bg="blue.500" icon={<FiCpu fontSize="1rem" />} />
|
||||||
|
<VStack align="stretch" flex="1">
|
||||||
|
<PlanCard plan={message.plan} stepResults={[]} />
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
case MessageTypes.AGENT_EXECUTING:
|
||||||
|
return (
|
||||||
|
<Flex justify="flex-start">
|
||||||
|
<HStack align="flex-start" maxW="85%">
|
||||||
|
<Avatar size="sm" bg="orange.500" icon={<FiCpu fontSize="1rem" />} />
|
||||||
|
<VStack align="stretch" flex="1" spacing={3}>
|
||||||
|
<PlanCard plan={message.plan} stepResults={message.stepResults} />
|
||||||
|
{message.stepResults?.map((result, idx) => (
|
||||||
|
<StepResultCard key={idx} stepResult={result} />
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
case MessageTypes.AGENT_RESPONSE:
|
||||||
|
return (
|
||||||
|
<Flex justify="flex-start">
|
||||||
|
<HStack align="flex-start" maxW="85%">
|
||||||
|
<Avatar size="sm" bg="green.500" icon={<FiCpu fontSize="1rem" />} />
|
||||||
|
<VStack align="stretch" flex="1" spacing={3}>
|
||||||
|
{/* 最终总结 */}
|
||||||
|
<Box
|
||||||
|
bg={agentBubbleBg}
|
||||||
|
px={4}
|
||||||
|
py={3}
|
||||||
|
borderRadius="lg"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
boxShadow="md"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm" whiteSpace="pre-wrap">
|
||||||
|
{message.content}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* 元数据 */}
|
||||||
|
{message.metadata && (
|
||||||
|
<HStack mt={3} spacing={4} fontSize="xs" color="gray.500">
|
||||||
|
<Text>总步骤: {message.metadata.total_steps}</Text>
|
||||||
|
<Text>✓ {message.metadata.successful_steps}</Text>
|
||||||
|
{message.metadata.failed_steps > 0 && (
|
||||||
|
<Text>✗ {message.metadata.failed_steps}</Text>
|
||||||
|
)}
|
||||||
|
<Text>耗时: {message.metadata.total_execution_time?.toFixed(1)}s</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 执行详情(可选) */}
|
||||||
|
{message.plan && message.stepResults && message.stepResults.length > 0 && (
|
||||||
|
<VStack align="stretch" spacing={2}>
|
||||||
|
<Divider />
|
||||||
|
<Text fontSize="xs" fontWeight="bold" color="gray.500">
|
||||||
|
📊 执行详情(点击展开查看)
|
||||||
|
</Text>
|
||||||
|
{message.stepResults.map((result, idx) => (
|
||||||
|
<StepResultCard key={idx} stepResult={result} />
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
case MessageTypes.ERROR:
|
||||||
|
return (
|
||||||
|
<Flex justify="flex-start">
|
||||||
|
<HStack align="flex-start" maxW="75%">
|
||||||
|
<Avatar size="sm" bg="red.500" icon={<FiCpu fontSize="1rem" />} />
|
||||||
|
<Box
|
||||||
|
bg="red.50"
|
||||||
|
color="red.700"
|
||||||
|
px={4}
|
||||||
|
py={3}
|
||||||
|
borderRadius="lg"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor="red.200"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm">{message.content}</Text>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AgentChatV3;
|
||||||
Reference in New Issue
Block a user