feat: 简化主组件 index.js - 使用组件组合方式重构
This commit is contained in:
File diff suppressed because it is too large
Load Diff
1529
src/views/AgentChat/index.js.bak
Normal file
1529
src/views/AgentChat/index.js.bak
Normal file
File diff suppressed because it is too large
Load Diff
1173
src/views/AgentChat/index.js.bak2
Normal file
1173
src/views/AgentChat/index.js.bak2
Normal file
File diff suppressed because it is too large
Load Diff
816
src/views/AgentChat/index.js.bak3
Normal file
816
src/views/AgentChat/index.js.bak3
Normal file
@@ -0,0 +1,816 @@
|
|||||||
|
// src/views/AgentChat/index.js
|
||||||
|
// 超炫酷的 AI 投研助手 - HeroUI v3 现代深色主题版本
|
||||||
|
// 使用 Framer Motion 物理动画引擎 + 毛玻璃效果
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
Divider,
|
||||||
|
Spinner,
|
||||||
|
Tooltip,
|
||||||
|
Checkbox,
|
||||||
|
CheckboxGroup,
|
||||||
|
Kbd,
|
||||||
|
Accordion,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionButton,
|
||||||
|
AccordionPanel,
|
||||||
|
AccordionIcon,
|
||||||
|
Tabs,
|
||||||
|
TabList,
|
||||||
|
TabPanels,
|
||||||
|
Tab,
|
||||||
|
TabPanel,
|
||||||
|
useToast,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Flex,
|
||||||
|
IconButton,
|
||||||
|
useColorMode,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
Tag,
|
||||||
|
TagLabel,
|
||||||
|
TagCloseButton,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useAuth } from '@contexts/AuthContext';
|
||||||
|
import { logger } from '@utils/logger';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// 图标 - 使用 Lucide Icons
|
||||||
|
import {
|
||||||
|
Send,
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
MessageSquare,
|
||||||
|
Trash2,
|
||||||
|
MoreVertical,
|
||||||
|
RefreshCw,
|
||||||
|
Download,
|
||||||
|
Cpu,
|
||||||
|
User,
|
||||||
|
Zap,
|
||||||
|
Clock,
|
||||||
|
Settings,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Activity,
|
||||||
|
Code,
|
||||||
|
Database,
|
||||||
|
TrendingUp,
|
||||||
|
FileText,
|
||||||
|
BookOpen,
|
||||||
|
Menu,
|
||||||
|
X,
|
||||||
|
Check,
|
||||||
|
Circle,
|
||||||
|
Maximize2,
|
||||||
|
Minimize2,
|
||||||
|
Copy,
|
||||||
|
ThumbsUp,
|
||||||
|
ThumbsDown,
|
||||||
|
Sparkles,
|
||||||
|
Brain,
|
||||||
|
Rocket,
|
||||||
|
Paperclip,
|
||||||
|
Image as ImageIcon,
|
||||||
|
File,
|
||||||
|
Calendar,
|
||||||
|
Globe,
|
||||||
|
DollarSign,
|
||||||
|
Newspaper,
|
||||||
|
BarChart3,
|
||||||
|
PieChart,
|
||||||
|
LineChart,
|
||||||
|
Briefcase,
|
||||||
|
Users,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
// 常量配置 - 从 TypeScript 模块导入
|
||||||
|
import { MessageTypes } from './constants/messageTypes';
|
||||||
|
import { DEFAULT_MODEL_ID } from './constants/models';
|
||||||
|
import { DEFAULT_SELECTED_TOOLS } from './constants/tools';
|
||||||
|
|
||||||
|
// 拆分后的子组件
|
||||||
|
import BackgroundEffects from './components/BackgroundEffects';
|
||||||
|
import LeftSidebar from './components/LeftSidebar';
|
||||||
|
import ChatArea from './components/ChatArea';
|
||||||
|
import RightSidebar from './components/RightSidebar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent Chat - 主组件(HeroUI v3 深色主题)
|
||||||
|
*
|
||||||
|
* 注意:所有常量配置已提取到 constants/ 目录:
|
||||||
|
* - animations: constants/animations.ts
|
||||||
|
* - MessageTypes: constants/messageTypes.ts
|
||||||
|
* - AVAILABLE_MODELS: constants/models.ts
|
||||||
|
* - MCP_TOOLS, TOOL_CATEGORIES: constants/tools.ts
|
||||||
|
* - quickQuestions: constants/quickQuestions.ts
|
||||||
|
*/
|
||||||
|
const AgentChat = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const toast = useToast();
|
||||||
|
const { setColorMode } = useColorMode();
|
||||||
|
|
||||||
|
// 会话管理
|
||||||
|
const [sessions, setSessions] = useState([]);
|
||||||
|
const [currentSessionId, setCurrentSessionId] = useState(null);
|
||||||
|
const [isLoadingSessions, setIsLoadingSessions] = useState(false);
|
||||||
|
|
||||||
|
// 消息管理
|
||||||
|
const [messages, setMessages] = useState([]);
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
|
||||||
|
// UI 状态
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [selectedModel, setSelectedModel] = useState(DEFAULT_MODEL_ID);
|
||||||
|
const [selectedTools, setSelectedTools] = useState(DEFAULT_SELECTED_TOOLS);
|
||||||
|
const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(true);
|
||||||
|
const [isRightSidebarOpen, setIsRightSidebarOpen] = useState(true);
|
||||||
|
|
||||||
|
// 文件上传
|
||||||
|
const [uploadedFiles, setUploadedFiles] = useState([]);
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
|
// Refs
|
||||||
|
const messagesEndRef = useRef(null);
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
|
// ==================== 启用深色模式 ====================
|
||||||
|
useEffect(() => {
|
||||||
|
// 为 AgentChat 页面强制启用深色模式
|
||||||
|
setColorMode('dark');
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// 组件卸载时不移除,让其他页面自己控制
|
||||||
|
// document.documentElement.classList.remove('dark');
|
||||||
|
};
|
||||||
|
}, [setColorMode]);
|
||||||
|
|
||||||
|
// ==================== API 调用函数 ====================
|
||||||
|
|
||||||
|
const loadSessions = async () => {
|
||||||
|
if (!user?.id) return;
|
||||||
|
setIsLoadingSessions(true);
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/mcp/agent/sessions', {
|
||||||
|
params: { user_id: user.id, limit: 50 },
|
||||||
|
});
|
||||||
|
if (response.data.success) {
|
||||||
|
setSessions(response.data.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('加载会话列表失败', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingSessions(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadSessionHistory = async (sessionId) => {
|
||||||
|
if (!sessionId) return;
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/mcp/agent/history/${sessionId}`, {
|
||||||
|
params: { limit: 100 },
|
||||||
|
});
|
||||||
|
if (response.data.success) {
|
||||||
|
const history = response.data.data;
|
||||||
|
const formattedMessages = history.map((msg, idx) => ({
|
||||||
|
id: `${sessionId}-${idx}`,
|
||||||
|
type: msg.message_type === 'user' ? MessageTypes.USER : MessageTypes.AGENT_RESPONSE,
|
||||||
|
content: msg.message,
|
||||||
|
plan: msg.plan ? JSON.parse(msg.plan) : null,
|
||||||
|
stepResults: msg.steps ? JSON.parse(msg.steps) : null,
|
||||||
|
timestamp: msg.timestamp,
|
||||||
|
}));
|
||||||
|
setMessages(formattedMessages);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('加载会话历史失败', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createNewSession = () => {
|
||||||
|
setCurrentSessionId(null);
|
||||||
|
setMessages([
|
||||||
|
{
|
||||||
|
id: Date.now(),
|
||||||
|
type: MessageTypes.AGENT_RESPONSE,
|
||||||
|
content: `你好${user?.nickname || ''}!👋\n\n我是**价小前**,你的 AI 投研助手。\n\n**我能做什么?**\n• 📊 全面分析股票基本面和技术面\n• 🔥 追踪市场热点和涨停板块\n• 📈 研究行业趋势和投资机会\n• 📰 汇总最新财经新闻和研报\n\n直接输入你的问题开始探索!`,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchSession = (sessionId) => {
|
||||||
|
setCurrentSessionId(sessionId);
|
||||||
|
loadSessionHistory(sessionId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendMessage = async () => {
|
||||||
|
if (!inputValue.trim() || isProcessing) return;
|
||||||
|
|
||||||
|
const userMessage = {
|
||||||
|
type: MessageTypes.USER,
|
||||||
|
content: inputValue,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
files: uploadedFiles.length > 0 ? uploadedFiles : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
addMessage(userMessage);
|
||||||
|
const userInput = inputValue;
|
||||||
|
setInputValue('');
|
||||||
|
setUploadedFiles([]);
|
||||||
|
setIsProcessing(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
addMessage({
|
||||||
|
type: MessageTypes.AGENT_THINKING,
|
||||||
|
content: '正在分析你的问题...',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await axios.post('/mcp/agent/chat', {
|
||||||
|
message: userInput,
|
||||||
|
conversation_history: messages
|
||||||
|
.filter((m) => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE)
|
||||||
|
.map((m) => ({
|
||||||
|
isUser: m.type === MessageTypes.USER,
|
||||||
|
content: m.content,
|
||||||
|
})),
|
||||||
|
user_id: user?.id || 'anonymous',
|
||||||
|
user_nickname: user?.nickname || '匿名用户',
|
||||||
|
user_avatar: user?.avatar || '',
|
||||||
|
subscription_type: user?.subscription_type || 'free',
|
||||||
|
session_id: currentSessionId,
|
||||||
|
model: selectedModel,
|
||||||
|
tools: selectedTools,
|
||||||
|
files: uploadedFiles.length > 0 ? uploadedFiles : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING));
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
const data = response.data;
|
||||||
|
if (data.session_id && !currentSessionId) {
|
||||||
|
setCurrentSessionId(data.session_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.plan) {
|
||||||
|
addMessage({
|
||||||
|
type: MessageTypes.AGENT_PLAN,
|
||||||
|
content: '已制定执行计划',
|
||||||
|
plan: data.plan,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.steps && data.steps.length > 0) {
|
||||||
|
addMessage({
|
||||||
|
type: MessageTypes.AGENT_EXECUTING,
|
||||||
|
content: '正在执行步骤...',
|
||||||
|
plan: data.plan,
|
||||||
|
stepResults: data.steps,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_EXECUTING));
|
||||||
|
|
||||||
|
addMessage({
|
||||||
|
type: MessageTypes.AGENT_RESPONSE,
|
||||||
|
content: data.final_answer || data.message || '处理完成',
|
||||||
|
plan: data.plan,
|
||||||
|
stepResults: data.steps,
|
||||||
|
metadata: data.metadata,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
loadSessions();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Agent chat error', error);
|
||||||
|
setMessages((prev) =>
|
||||||
|
prev.filter(
|
||||||
|
(m) => m.type !== MessageTypes.AGENT_THINKING && m.type !== MessageTypes.AGENT_EXECUTING
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorMessage = error.response?.data?.error || error.message || '处理失败';
|
||||||
|
addMessage({
|
||||||
|
type: MessageTypes.ERROR,
|
||||||
|
content: `处理失败:${errorMessage}`,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: '处理失败',
|
||||||
|
description: errorMessage,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 文件上传处理
|
||||||
|
const handleFileSelect = (event) => {
|
||||||
|
const files = Array.from(event.target.files || []);
|
||||||
|
const fileData = files.map(file => ({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
// 实际上传时需要转换为 base64 或上传到服务器
|
||||||
|
url: URL.createObjectURL(file),
|
||||||
|
}));
|
||||||
|
setUploadedFiles(prev => [...prev, ...fileData]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFile = (index) => {
|
||||||
|
setUploadedFiles(prev => prev.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMessage = (message) => {
|
||||||
|
setMessages((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
id: Date.now() + Math.random(),
|
||||||
|
...message,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSendMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadSessions();
|
||||||
|
createNewSession();
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flex={1} bg="gray.900">
|
||||||
|
<Flex h="100%" overflow="hidden" position="relative">
|
||||||
|
{/* 背景渐变装饰 */}
|
||||||
|
<BackgroundEffects />
|
||||||
|
|
||||||
|
{/* 左侧栏 */}
|
||||||
|
<LeftSidebar
|
||||||
|
isOpen={isLeftSidebarOpen}
|
||||||
|
onClose={() => setIsLeftSidebarOpen(false)}
|
||||||
|
sessions={sessions}
|
||||||
|
currentSessionId={currentSessionId}
|
||||||
|
onSessionSwitch={switchSession}
|
||||||
|
onNewSession={createNewSession}
|
||||||
|
isLoadingSessions={isLoadingSessions}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 中间聊天区 */}
|
||||||
|
<ChatArea
|
||||||
|
messages={messages}
|
||||||
|
inputValue={inputValue}
|
||||||
|
onInputChange={setInputValue}
|
||||||
|
isProcessing={isProcessing}
|
||||||
|
onSendMessage={handleSendMessage}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
uploadedFiles={uploadedFiles}
|
||||||
|
onFileSelect={handleFileSelect}
|
||||||
|
onFileRemove={removeFile}
|
||||||
|
selectedModel={selectedModel}
|
||||||
|
isLeftSidebarOpen={isLeftSidebarOpen}
|
||||||
|
isRightSidebarOpen={isRightSidebarOpen}
|
||||||
|
onToggleLeftSidebar={() => setIsLeftSidebarOpen(true)}
|
||||||
|
onToggleRightSidebar={() => setIsRightSidebarOpen(true)}
|
||||||
|
onNewSession={createNewSession}
|
||||||
|
userAvatar={user?.avatar}
|
||||||
|
messagesEndRef={messagesEndRef}
|
||||||
|
inputRef={inputRef}
|
||||||
|
fileInputRef={fileInputRef}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 右侧栏 - 深色配置中心 */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{isRightSidebarOpen && (
|
||||||
|
<motion.div
|
||||||
|
style={{ width: '320px', display: 'flex', flexDirection: 'column' }}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
variants={animations.slideInRight}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
w="320px"
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
bg="rgba(17, 24, 39, 0.8)"
|
||||||
|
backdropFilter="blur(20px) saturate(180%)"
|
||||||
|
borderLeft="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
boxShadow="-4px 0 24px rgba(0, 0, 0, 0.3)"
|
||||||
|
>
|
||||||
|
<Box p={4} borderBottom="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Settings className="w-5 h-5" color="#C084FC" />
|
||||||
|
<Text
|
||||||
|
fontWeight="semibold"
|
||||||
|
bgGradient="linear(to-r, purple.300, pink.300)"
|
||||||
|
bgClip="text"
|
||||||
|
fontSize="md"
|
||||||
|
>
|
||||||
|
配置中心
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<Tooltip label="收起侧边栏">
|
||||||
|
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
icon={<ChevronRight className="w-4 h-4" />}
|
||||||
|
onClick={() => setIsRightSidebarOpen(false)}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.300"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
_hover={{
|
||||||
|
bg: "rgba(255, 255, 255, 0.1)",
|
||||||
|
borderColor: "purple.400",
|
||||||
|
color: "white"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</Tooltip>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box flex={1} overflowY="auto">
|
||||||
|
<Tabs colorScheme="purple" variant="line">
|
||||||
|
<TabList px={4} borderBottom="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
|
||||||
|
<Tab
|
||||||
|
color="gray.400"
|
||||||
|
_selected={{
|
||||||
|
color: "purple.400",
|
||||||
|
borderColor: "purple.500",
|
||||||
|
boxShadow: "0 2px 8px rgba(139, 92, 246, 0.3)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Cpu className="w-4 h-4" />
|
||||||
|
<Text>模型</Text>
|
||||||
|
</HStack>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
color="gray.400"
|
||||||
|
_selected={{
|
||||||
|
color: "purple.400",
|
||||||
|
borderColor: "purple.500",
|
||||||
|
boxShadow: "0 2px 8px rgba(139, 92, 246, 0.3)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Code className="w-4 h-4" />
|
||||||
|
<Text>工具</Text>
|
||||||
|
{selectedTools.length > 0 && (
|
||||||
|
<Badge
|
||||||
|
bgGradient="linear(to-r, blue.500, purple.500)"
|
||||||
|
color="white"
|
||||||
|
borderRadius="full"
|
||||||
|
fontSize="xs"
|
||||||
|
px={2}
|
||||||
|
py={0.5}
|
||||||
|
boxShadow="0 2px 8px rgba(139, 92, 246, 0.3)"
|
||||||
|
>
|
||||||
|
{selectedTools.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
color="gray.400"
|
||||||
|
_selected={{
|
||||||
|
color: "purple.400",
|
||||||
|
borderColor: "purple.500",
|
||||||
|
boxShadow: "0 2px 8px rgba(139, 92, 246, 0.3)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<BarChart3 className="w-4 h-4" />
|
||||||
|
<Text>统计</Text>
|
||||||
|
</HStack>
|
||||||
|
</Tab>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<TabPanels>
|
||||||
|
{/* 模型选择 */}
|
||||||
|
<TabPanel p={4}>
|
||||||
|
<VStack spacing={3} align="stretch">
|
||||||
|
{AVAILABLE_MODELS.map((model, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={model.id}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
whileHover={{ scale: 1.02, y: -2 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() => setSelectedModel(model.id)}
|
||||||
|
bg={selectedModel === model.id
|
||||||
|
? 'rgba(139, 92, 246, 0.15)'
|
||||||
|
: 'rgba(255, 255, 255, 0.05)'}
|
||||||
|
backdropFilter="blur(12px)"
|
||||||
|
borderWidth={2}
|
||||||
|
borderColor={selectedModel === model.id
|
||||||
|
? 'purple.400'
|
||||||
|
: 'rgba(255, 255, 255, 0.1)'}
|
||||||
|
_hover={{
|
||||||
|
borderColor: selectedModel === model.id
|
||||||
|
? 'purple.400'
|
||||||
|
: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
boxShadow: selectedModel === model.id
|
||||||
|
? "0 8px 20px rgba(139, 92, 246, 0.4)"
|
||||||
|
: "0 4px 12px rgba(0, 0, 0, 0.3)"
|
||||||
|
}}
|
||||||
|
transition="all 0.3s"
|
||||||
|
>
|
||||||
|
<CardBody p={3}>
|
||||||
|
<HStack align="start" spacing={3}>
|
||||||
|
<Box
|
||||||
|
p={2}
|
||||||
|
borderRadius="lg"
|
||||||
|
bgGradient={selectedModel === model.id
|
||||||
|
? "linear(to-br, purple.500, pink.500)"
|
||||||
|
: "linear(to-br, rgba(139, 92, 246, 0.2), rgba(236, 72, 153, 0.2))"}
|
||||||
|
boxShadow={selectedModel === model.id
|
||||||
|
? "0 4px 12px rgba(139, 92, 246, 0.4)"
|
||||||
|
: "none"}
|
||||||
|
>
|
||||||
|
{model.icon}
|
||||||
|
</Box>
|
||||||
|
<Box flex={1}>
|
||||||
|
<Text fontWeight="semibold" fontSize="sm" color="gray.100">
|
||||||
|
{model.name}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" color="gray.400" mt={1}>
|
||||||
|
{model.description}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
{selectedModel === model.id && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||||
|
>
|
||||||
|
<Check className="w-5 h-5" color="#A78BFA" />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* 工具选择 */}
|
||||||
|
<TabPanel p={4}>
|
||||||
|
<Accordion allowMultiple>
|
||||||
|
{Object.entries(TOOL_CATEGORIES).map(([category, tools], catIdx) => (
|
||||||
|
<motion.div
|
||||||
|
key={category}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: catIdx * 0.05 }}
|
||||||
|
>
|
||||||
|
<AccordionItem
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
borderRadius="lg"
|
||||||
|
mb={2}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
backdropFilter="blur(12px)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.2)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AccordionButton>
|
||||||
|
<HStack flex={1} justify="space-between" pr={2}>
|
||||||
|
<Text color="gray.100" fontSize="sm">{category}</Text>
|
||||||
|
<Badge
|
||||||
|
bgGradient="linear(to-r, blue.500, purple.500)"
|
||||||
|
color="white"
|
||||||
|
variant="subtle"
|
||||||
|
boxShadow="0 2px 8px rgba(139, 92, 246, 0.3)"
|
||||||
|
>
|
||||||
|
{tools.filter(t => selectedTools.includes(t.id)).length}/{tools.length}
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
<AccordionIcon color="gray.400" />
|
||||||
|
</AccordionButton>
|
||||||
|
<AccordionPanel pb={4}>
|
||||||
|
<CheckboxGroup
|
||||||
|
value={selectedTools}
|
||||||
|
onChange={setSelectedTools}
|
||||||
|
>
|
||||||
|
<VStack align="stretch" spacing={2}>
|
||||||
|
{tools.map((tool) => (
|
||||||
|
<motion.div
|
||||||
|
key={tool.id}
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
transition={{ type: "spring", stiffness: 300 }}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
value={tool.id}
|
||||||
|
colorScheme="purple"
|
||||||
|
p={2}
|
||||||
|
borderRadius="lg"
|
||||||
|
bg="rgba(255, 255, 255, 0.02)"
|
||||||
|
_hover={{ bg: 'rgba(255, 255, 255, 0.05)' }}
|
||||||
|
transition="background 0.2s"
|
||||||
|
>
|
||||||
|
<HStack spacing={2} align="start">
|
||||||
|
<Box color="purple.400" mt={0.5}>{tool.icon}</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="sm" color="gray.200">{tool.name}</Text>
|
||||||
|
<Text fontSize="xs" color="gray.500">{tool.description}</Text>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
</Checkbox>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</CheckboxGroup>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<HStack mt={4} spacing={2}>
|
||||||
|
<motion.div flex={1} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
w="full"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setSelectedTools(MCP_TOOLS.map(t => t.id))}
|
||||||
|
bgGradient="linear(to-r, blue.500, purple.500)"
|
||||||
|
color="white"
|
||||||
|
_hover={{
|
||||||
|
bgGradient: "linear(to-r, blue.600, purple.600)",
|
||||||
|
boxShadow: "0 4px 12px rgba(139, 92, 246, 0.4)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
全选
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div flex={1} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
w="full"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setSelectedTools([])}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.300"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
_hover={{
|
||||||
|
bg: "rgba(255, 255, 255, 0.1)",
|
||||||
|
borderColor: "rgba(255, 255, 255, 0.2)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</HStack>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* 统计信息 */}
|
||||||
|
<TabPanel p={4}>
|
||||||
|
<VStack spacing={4} align="stretch">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0 }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
backdropFilter="blur(12px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)"
|
||||||
|
>
|
||||||
|
<CardBody p={4}>
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="xs" color="gray.400">对话数</Text>
|
||||||
|
<Text
|
||||||
|
fontSize="2xl"
|
||||||
|
fontWeight="bold"
|
||||||
|
bgGradient="linear(to-r, blue.400, purple.400)"
|
||||||
|
bgClip="text"
|
||||||
|
>
|
||||||
|
{sessions.length}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<MessageSquare className="w-8 h-8" color="#60A5FA" style={{ opacity: 0.5 }} />
|
||||||
|
</Flex>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
backdropFilter="blur(12px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)"
|
||||||
|
>
|
||||||
|
<CardBody p={4}>
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="xs" color="gray.400">消息数</Text>
|
||||||
|
<Text
|
||||||
|
fontSize="2xl"
|
||||||
|
fontWeight="bold"
|
||||||
|
bgGradient="linear(to-r, purple.400, pink.400)"
|
||||||
|
bgClip="text"
|
||||||
|
>
|
||||||
|
{messages.length}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Activity className="w-8 h-8" color="#C084FC" style={{ opacity: 0.5 }} />
|
||||||
|
</Flex>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
backdropFilter="blur(12px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)"
|
||||||
|
>
|
||||||
|
<CardBody p={4}>
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="xs" color="gray.400">已选工具</Text>
|
||||||
|
<Text
|
||||||
|
fontSize="2xl"
|
||||||
|
fontWeight="bold"
|
||||||
|
bgGradient="linear(to-r, green.400, teal.400)"
|
||||||
|
bgClip="text"
|
||||||
|
>
|
||||||
|
{selectedTools.length}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Code className="w-8 h-8" color="#34D399" style={{ opacity: 0.5 }} />
|
||||||
|
</Flex>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</VStack>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AgentChat;
|
||||||
Reference in New Issue
Block a user