858 lines
26 KiB
JavaScript
858 lines
26 KiB
JavaScript
// 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;
|