agent功能开发增加MCP后端
This commit is contained in:
376
src/components/ChatBot/ChatInterface.js
Normal file
376
src/components/ChatBot/ChatInterface.js
Normal file
@@ -0,0 +1,376 @@
|
||||
// src/components/ChatBot/ChatInterface.js
|
||||
// 聊天界面主组件
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Input,
|
||||
IconButton,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Spinner,
|
||||
useColorModeValue,
|
||||
useToast,
|
||||
Divider,
|
||||
Badge,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import { FiSend, FiRefreshCw, FiSettings, FiDownload } from 'react-icons/fi';
|
||||
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||
import MessageBubble from './MessageBubble';
|
||||
import { mcpService } from '../../services/mcpService';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
/**
|
||||
* 聊天界面组件
|
||||
*/
|
||||
export const ChatInterface = () => {
|
||||
const [messages, setMessages] = useState([
|
||||
{
|
||||
id: 1,
|
||||
content: '你好!我是AI投资助手,我可以帮你查询股票信息、新闻资讯、概念板块、涨停分析等。请问有什么可以帮到你的?',
|
||||
isUser: false,
|
||||
type: 'text',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
]);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [availableTools, setAvailableTools] = useState([]);
|
||||
|
||||
const messagesEndRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
const toast = useToast();
|
||||
|
||||
// 颜色主题
|
||||
const bgColor = useColorModeValue('white', 'gray.800');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
||||
const inputBg = useColorModeValue('gray.50', 'gray.700');
|
||||
|
||||
// 加载可用工具列表
|
||||
useEffect(() => {
|
||||
const loadTools = async () => {
|
||||
const result = await mcpService.listTools();
|
||||
if (result.success) {
|
||||
setAvailableTools(result.data);
|
||||
logger.info('ChatInterface', '已加载MCP工具', { count: result.data.length });
|
||||
}
|
||||
};
|
||||
loadTools();
|
||||
}, []);
|
||||
|
||||
// 自动滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
// 发送消息
|
||||
const handleSendMessage = async () => {
|
||||
if (!inputValue.trim() || isLoading) return;
|
||||
|
||||
const userMessage = {
|
||||
id: Date.now(),
|
||||
content: inputValue,
|
||||
isUser: true,
|
||||
type: 'text',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setInputValue('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// 调用MCP服务
|
||||
const response = await mcpService.chat(inputValue, messages);
|
||||
|
||||
let botMessage;
|
||||
if (response.success) {
|
||||
// 根据返回的数据类型构造消息
|
||||
const data = response.data;
|
||||
|
||||
if (typeof data === 'string') {
|
||||
botMessage = {
|
||||
id: Date.now() + 1,
|
||||
content: data,
|
||||
isUser: false,
|
||||
type: 'text',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
} else if (Array.isArray(data)) {
|
||||
// 数据列表
|
||||
botMessage = {
|
||||
id: Date.now() + 1,
|
||||
content: `找到 ${data.length} 条结果:`,
|
||||
isUser: false,
|
||||
type: 'data',
|
||||
data: data,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
} else if (typeof data === 'object') {
|
||||
// 对象数据
|
||||
botMessage = {
|
||||
id: Date.now() + 1,
|
||||
content: JSON.stringify(data, null, 2),
|
||||
isUser: false,
|
||||
type: 'markdown',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
} else {
|
||||
botMessage = {
|
||||
id: Date.now() + 1,
|
||||
content: '抱歉,我无法理解这个查询结果。',
|
||||
isUser: false,
|
||||
type: 'text',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
botMessage = {
|
||||
id: Date.now() + 1,
|
||||
content: `抱歉,查询失败:${response.error}`,
|
||||
isUser: false,
|
||||
type: 'text',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
setMessages((prev) => [...prev, botMessage]);
|
||||
} catch (error) {
|
||||
logger.error('ChatInterface', 'handleSendMessage', error);
|
||||
const errorMessage = {
|
||||
id: Date.now() + 1,
|
||||
content: `抱歉,发生了错误:${error.message}`,
|
||||
isUser: false,
|
||||
type: 'text',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
setMessages((prev) => [...prev, errorMessage]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理键盘事件
|
||||
const handleKeyPress = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
// 清空对话
|
||||
const handleClearChat = () => {
|
||||
setMessages([
|
||||
{
|
||||
id: 1,
|
||||
content: '对话已清空。有什么可以帮到你的?',
|
||||
isUser: false,
|
||||
type: 'text',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// 复制消息
|
||||
const handleCopyMessage = () => {
|
||||
toast({
|
||||
title: '已复制',
|
||||
status: 'success',
|
||||
duration: 2000,
|
||||
isClosable: true,
|
||||
});
|
||||
};
|
||||
|
||||
// 反馈
|
||||
const handleFeedback = (type) => {
|
||||
logger.info('ChatInterface', 'Feedback', { type });
|
||||
toast({
|
||||
title: type === 'positive' ? '感谢反馈!' : '我们会改进',
|
||||
status: 'info',
|
||||
duration: 2000,
|
||||
isClosable: true,
|
||||
});
|
||||
};
|
||||
|
||||
// 快捷问题
|
||||
const quickQuestions = [
|
||||
'查询贵州茅台的股票信息',
|
||||
'搜索人工智能相关新闻',
|
||||
'今日涨停股票有哪些',
|
||||
'新能源概念板块分析',
|
||||
];
|
||||
|
||||
const handleQuickQuestion = (question) => {
|
||||
setInputValue(question);
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
// 导出对话
|
||||
const handleExportChat = () => {
|
||||
const chatText = messages
|
||||
.map((msg) => `[${msg.isUser ? '用户' : '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);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex direction="column" h="100%" bg={bgColor}>
|
||||
{/* 头部工具栏 */}
|
||||
<Flex
|
||||
px={4}
|
||||
py={3}
|
||||
borderBottom="1px"
|
||||
borderColor={borderColor}
|
||||
align="center"
|
||||
justify="space-between"
|
||||
>
|
||||
<HStack>
|
||||
<Text fontWeight="bold" fontSize="lg">AI投资助手</Text>
|
||||
<Badge colorScheme="green">在线</Badge>
|
||||
{availableTools.length > 0 && (
|
||||
<Badge colorScheme="blue">{availableTools.length} 个工具</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
<HStack>
|
||||
<IconButton
|
||||
icon={<FiRefreshCw />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label="清空对话"
|
||||
onClick={handleClearChat}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<FiDownload />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label="导出对话"
|
||||
onClick={handleExportChat}
|
||||
/>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
icon={<FiSettings />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label="设置"
|
||||
/>
|
||||
<MenuList>
|
||||
<MenuItem>模型设置</MenuItem>
|
||||
<MenuItem>快捷指令</MenuItem>
|
||||
<MenuItem>历史记录</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
{/* 消息列表 */}
|
||||
<Box
|
||||
flex="1"
|
||||
overflowY="auto"
|
||||
px={4}
|
||||
py={4}
|
||||
css={{
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: 'transparent',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: '#CBD5E0',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<VStack spacing={0} align="stretch">
|
||||
{messages.map((message) => (
|
||||
<MessageBubble
|
||||
key={message.id}
|
||||
message={message}
|
||||
isUser={message.isUser}
|
||||
onCopy={handleCopyMessage}
|
||||
onFeedback={handleFeedback}
|
||||
/>
|
||||
))}
|
||||
{isLoading && (
|
||||
<Flex justify="flex-start" mb={4}>
|
||||
<Flex align="center" bg={inputBg} px={4} py={3} borderRadius="lg">
|
||||
<Spinner size="sm" mr={2} />
|
||||
<Text fontSize="sm">AI正在思考...</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 快捷问题(仅在消息较少时显示) */}
|
||||
{messages.length <= 2 && (
|
||||
<Box px={4} py={2}>
|
||||
<Text fontSize="xs" color="gray.500" mb={2}>快捷问题:</Text>
|
||||
<Flex wrap="wrap" gap={2}>
|
||||
{quickQuestions.map((question, idx) => (
|
||||
<Button
|
||||
key={idx}
|
||||
size="xs"
|
||||
variant="outline"
|
||||
onClick={() => handleQuickQuestion(question)}
|
||||
>
|
||||
{question}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 输入框 */}
|
||||
<Box px={4} py={3} borderTop="1px" borderColor={borderColor}>
|
||||
<Flex>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="输入消息... (Shift+Enter换行,Enter发送)"
|
||||
bg={inputBg}
|
||||
border="none"
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
mr={2}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<FiSend />}
|
||||
colorScheme="blue"
|
||||
aria-label="发送"
|
||||
onClick={handleSendMessage}
|
||||
isLoading={isLoading}
|
||||
disabled={!inputValue.trim()}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatInterface;
|
||||
Reference in New Issue
Block a user