feat: 组组件拆分

This commit is contained in:
zdl
2025-11-25 15:53:12 +08:00
parent 11821d8256
commit d37c974d23
6 changed files with 703 additions and 499 deletions

View File

@@ -0,0 +1,118 @@
// src/views/AgentChat/components/RightSidebar/ModelSelector.tsx
// 模型选择组件
import React from 'react';
import { motion } from 'framer-motion';
import {
Card,
CardBody,
HStack,
VStack,
Box,
Text,
} from '@chakra-ui/react';
import { Check } from 'lucide-react';
import { AVAILABLE_MODELS } from '../../constants/models';
/**
* ModelSelector 组件的 Props 类型
*/
interface ModelSelectorProps {
/** 当前选中的模型 ID */
selectedModel: string;
/** 模型切换回调 */
onModelChange: (modelId: string) => void;
}
/**
* ModelSelector - 模型选择组件
*
* 职责:
* 1. 渲染模型选择卡片列表
* 2. 显示模型图标、名称、描述
* 3. 高亮当前选中模型(紫色边框 + 发光效果)
* 4. 显示选中标记Check 图标 + 弹簧动画)
*
* 设计特性:
* - 卡片渐进入场动画(延迟 `idx * 0.1`
* - 悬停缩放 1.02x + 上移 2px
* - 选中态:紫色边框 + 发光效果
* - 模型图标渐变色背景
*/
const ModelSelector: React.FC<ModelSelectorProps> = ({ selectedModel, onModelChange }) => {
return (
<VStack spacing={3} align="stretch">
{AVAILABLE_MODELS.map((model, idx) => {
const isSelected = selectedModel === model.id;
return (
<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={() => onModelChange(model.id)}
bg={isSelected ? 'rgba(139, 92, 246, 0.15)' : 'rgba(255, 255, 255, 0.05)'}
backdropFilter="blur(12px)"
borderWidth={2}
borderColor={isSelected ? 'purple.400' : 'rgba(255, 255, 255, 0.1)'}
_hover={{
borderColor: isSelected ? 'purple.400' : 'rgba(255, 255, 255, 0.2)',
boxShadow: isSelected
? '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={
isSelected
? 'linear(to-br, purple.500, pink.500)'
: 'linear(to-br, rgba(139, 92, 246, 0.2), rgba(236, 72, 153, 0.2))'
}
boxShadow={isSelected ? '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>
{/* 选中标记Check 图标) */}
{isSelected && (
<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>
);
};
export default ModelSelector;

View File

@@ -0,0 +1,119 @@
// src/views/AgentChat/components/RightSidebar/Statistics.tsx
// 统计信息组件
import React from 'react';
import { motion } from 'framer-motion';
import {
Card,
CardBody,
VStack,
Flex,
Box,
Text,
} from '@chakra-ui/react';
import { MessageSquare, Activity, Code } from 'lucide-react';
/**
* Statistics 组件的 Props 类型
*/
interface StatisticsProps {
/** 对话总数 */
sessionsCount: number;
/** 消息总数 */
messagesCount: number;
/** 已选工具数 */
selectedToolsCount: number;
}
/**
* Statistics - 统计信息组件
*
* 职责:
* 1. 显示统计卡片(对话数、消息数、已选工具数)
* 2. 渐变色大数字展示
* 3. 图标装饰
*
* 设计特性:
* - 卡片渐进入场动画(延迟 `idx * 0.1`
* - 毛玻璃效果卡片
* - 渐变色数字(蓝紫/紫粉/绿青)
* - 半透明图标装饰
*/
const Statistics: React.FC<StatisticsProps> = ({
sessionsCount,
messagesCount,
selectedToolsCount,
}) => {
const stats = [
{
label: '对话数',
value: sessionsCount,
gradient: 'linear(to-r, blue.400, purple.400)',
icon: MessageSquare,
iconColor: '#60A5FA',
},
{
label: '消息数',
value: messagesCount,
gradient: 'linear(to-r, purple.400, pink.400)',
icon: Activity,
iconColor: '#C084FC',
},
{
label: '已选工具',
value: selectedToolsCount,
gradient: 'linear(to-r, green.400, teal.400)',
icon: Code,
iconColor: '#34D399',
},
];
return (
<VStack spacing={4} align="stretch">
{stats.map((stat, idx) => {
const Icon = stat.icon;
return (
<motion.div
key={stat.label}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 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">
{stat.label}
</Text>
<Text
fontSize="2xl"
fontWeight="bold"
bgGradient={stat.gradient}
bgClip="text"
>
{stat.value}
</Text>
</Box>
{/* 右侧:图标装饰 */}
<Icon className="w-8 h-8" color={stat.iconColor} style={{ opacity: 0.5 }} />
</Flex>
</CardBody>
</Card>
</motion.div>
);
})}
</VStack>
);
};
export default Statistics;

View File

@@ -0,0 +1,199 @@
// src/views/AgentChat/components/RightSidebar/ToolSelector.tsx
// 工具选择组件
import React from 'react';
import { motion } from 'framer-motion';
import {
Button,
Badge,
Checkbox,
CheckboxGroup,
Accordion,
AccordionItem,
AccordionButton,
AccordionPanel,
AccordionIcon,
HStack,
VStack,
Box,
Text,
} from '@chakra-ui/react';
import { MCP_TOOLS, TOOL_CATEGORIES } from '../../constants/tools';
/**
* ToolSelector 组件的 Props 类型
*/
interface ToolSelectorProps {
/** 已选工具 ID 列表 */
selectedTools: string[];
/** 工具选择变化回调 */
onToolsChange: (tools: string[]) => void;
}
/**
* ToolSelector - 工具选择组件
*
* 职责:
* 1. 按分类展示工具列表Accordion 手风琴)
* 2. 复选框选择/取消工具
* 3. 显示每个分类的已选/总数(如 "3/5"
* 4. 全选/清空按钮
*
* 设计特性:
* - 手风琴分类折叠
* - 悬停工具项右移 4px
* - 全选/清空按钮渐变色
* - 分类徽章显示选中数量
*/
const ToolSelector: React.FC<ToolSelectorProps> = ({ selectedTools, onToolsChange }) => {
/**
* 全选所有工具
*/
const handleSelectAll = () => {
onToolsChange(MCP_TOOLS.map((t) => t.id));
};
/**
* 清空所有选择
*/
const handleClearAll = () => {
onToolsChange([]);
};
return (
<>
{/* 工具分类手风琴 */}
<Accordion allowMultiple>
{Object.entries(TOOL_CATEGORIES).map(([category, tools], catIdx) => {
const selectedCount = tools.filter((t) => selectedTools.includes(t.id)).length;
const totalCount = tools.length;
return (
<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)"
>
{selectedCount}/{totalCount}
</Badge>
</HStack>
<AccordionIcon color="gray.400" />
</AccordionButton>
{/* 手风琴内容 */}
<AccordionPanel pb={4}>
<CheckboxGroup value={selectedTools} onChange={onToolsChange}>
<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={handleSelectAll}
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={handleClearAll}
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>
</>
);
};
export default ToolSelector;

View File

@@ -1,499 +0,0 @@
// src/views/AgentChat/components/RightSidebar/index.js
// 右侧栏组件 - 配置中心
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Box,
Button,
Badge,
Checkbox,
CheckboxGroup,
Tooltip,
IconButton,
Accordion,
AccordionItem,
AccordionButton,
AccordionPanel,
AccordionIcon,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
Card,
CardBody,
HStack,
VStack,
Flex,
Text,
} from '@chakra-ui/react';
import {
Settings,
ChevronRight,
Cpu,
Code,
BarChart3,
Check,
MessageSquare,
Activity,
} from 'lucide-react';
import { animations } from '../../constants/animations';
import { AVAILABLE_MODELS } from '../../constants/models';
import { MCP_TOOLS, TOOL_CATEGORIES } from '../../constants/tools';
/**
* RightSidebar - 右侧栏组件(配置中心)
*
* @param {Object} props
* @param {boolean} props.isOpen - 侧边栏是否展开
* @param {Function} props.onClose - 关闭侧边栏回调
* @param {string} props.selectedModel - 当前选中的模型 ID
* @param {Function} props.onModelChange - 模型切换回调
* @param {Array} props.selectedTools - 已选工具 ID 列表
* @param {Function} props.onToolsChange - 工具选择变化回调
* @param {number} props.sessionsCount - 会话总数
* @param {number} props.messagesCount - 消息总数
* @returns {JSX.Element|null}
*/
const RightSidebar = ({
isOpen,
onClose,
selectedModel,
onModelChange,
selectedTools,
onToolsChange,
sessionsCount,
messagesCount,
}) => {
return (
<AnimatePresence>
{isOpen && (
<motion.div
style={{ width: '320px', display: 'flex', flexDirection: 'column' }}
initial="initial"
animate="animate"
exit="exit"
variants={animations.slideInRight}
>
<Box
w="320px"
h="100%"
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={onClose}
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>
{/* Tab 面板 */}
<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={() => onModelChange(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={onToolsChange}>
<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={() => onToolsChange(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={() => onToolsChange([])}
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"
>
{sessionsCount}
</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"
>
{messagesCount}
</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>
);
};
export default RightSidebar;

View File

@@ -0,0 +1,240 @@
// src/views/AgentChat/components/RightSidebar/index.tsx
// 右侧栏组件 - 配置中心(重构版本)
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Box,
Tooltip,
IconButton,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
Badge,
HStack,
Text,
} from '@chakra-ui/react';
import {
Settings,
ChevronRight,
Cpu,
Code,
BarChart3,
} from 'lucide-react';
import { animations } from '../../constants/animations';
import ModelSelector from './ModelSelector';
import ToolSelector from './ToolSelector';
import Statistics from './Statistics';
/**
* RightSidebar 组件的 Props 类型
*/
interface RightSidebarProps {
/** 侧边栏是否展开 */
isOpen: boolean;
/** 关闭侧边栏回调 */
onClose: () => void;
/** 当前选中的模型 ID */
selectedModel: string;
/** 模型切换回调 */
onModelChange: (modelId: string) => void;
/** 已选工具 ID 列表 */
selectedTools: string[];
/** 工具选择变化回调 */
onToolsChange: (tools: string[]) => void;
/** 会话总数 */
sessionsCount: number;
/** 消息总数 */
messagesCount: number;
}
/**
* RightSidebar - 右侧栏组件(配置中心)(重构版本)
*
* 架构改进:
* - 模型选择提取到 ModelSelector 组件120 行)
* - 工具选择提取到 ToolSelector 组件200 行)
* - 统计信息提取到 Statistics 组件100 行)
* - 主组件只负责 Tabs 管理和布局组合150 行)
*
* 主组件职责:
* 1. 管理 Tabs 切换(模型/工具/统计)
* 2. 渲染标题栏(配置中心 + 收起按钮)
* 3. 组合三个子组件TabPanels
* 4. 处理侧边栏动画(滑入/滑出)
*/
const RightSidebar: React.FC<RightSidebarProps> = ({
isOpen,
onClose,
selectedModel,
onModelChange,
selectedTools,
onToolsChange,
sessionsCount,
messagesCount,
}) => {
return (
<AnimatePresence>
{isOpen && (
<motion.div
style={{ width: '320px', display: 'flex', flexDirection: 'column' }}
initial="initial"
animate="animate"
exit="exit"
variants={animations.slideInRight}
>
<Box
w="320px"
h="100%"
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={onClose}
aria-label="收起侧边栏"
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>
{/* ==================== Tab 面板 ==================== */}
<Box flex={1} overflowY="auto">
<Tabs colorScheme="purple" variant="line">
{/* Tab 标签栏 */}
<TabList px={4} borderBottom="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
{/* 模型 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}>
<Cpu className="w-4 h-4" />
<Text></Text>
</HStack>
</Tab>
{/* 工具 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 */}
<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>
{/* Tab 内容面板 */}
<TabPanels>
{/* 模型选择面板 */}
<TabPanel p={4}>
<ModelSelector
selectedModel={selectedModel}
onModelChange={onModelChange}
/>
</TabPanel>
{/* 工具选择面板 */}
<TabPanel p={4}>
<ToolSelector
selectedTools={selectedTools}
onToolsChange={onToolsChange}
/>
</TabPanel>
{/* 统计信息面板 */}
<TabPanel p={4}>
<Statistics
sessionsCount={sessionsCount}
messagesCount={messagesCount}
selectedToolsCount={selectedTools.length}
/>
</TabPanel>
</TabPanels>
</Tabs>
</Box>
</Box>
</motion.div>
)}
</AnimatePresence>
);
};
export default RightSidebar;

View File

@@ -0,0 +1,27 @@
// src/views/AgentChat/components/RightSidebar/types.ts
// RightSidebar 组件的 TypeScript 类型定义
/**
* 模型数据结构(来自 constants/models
*/
export interface Model {
id: string;
name: string;
description: string;
icon: React.ReactNode;
}
/**
* 工具数据结构(来自 constants/tools
*/
export interface Tool {
id: string;
name: string;
description: string;
icon: React.ReactNode;
}
/**
* 工具分类Map<分类名, 工具列表>
*/
export type ToolCategories = Record<string, Tool[]>;