feat: 拆分LeftSidebar 组件为ts组件

This commit is contained in:
zdl
2025-11-25 15:25:52 +08:00
parent b9eddbe752
commit a3cb5e928e
7 changed files with 757 additions and 314 deletions

View File

@@ -0,0 +1,261 @@
# LeftSidebar 组件架构说明
## 📁 目录结构
```
LeftSidebar/
├── index.tsx # 左侧栏主组件200 行)- 组合层
├── types.ts # TypeScript 类型定义
├── SessionList.tsx # 会话列表组件150 行)
├── SessionCard.js # 会话卡片组件(保留原有)
├── SessionSearchBar.tsx # 搜索框组件60 行)
└── UserInfoCard.tsx # 用户信息卡片80 行)
```
## 🎯 重构目标
将原来 315 行的单文件组件拆分为多个职责明确的子组件,提高代码可维护性和可测试性。
## 🏗️ 组件职责
### 1. `index.tsx` - 主组件(约 200 行)
**职责**
- 管理本地状态(搜索关键词)
- 数据处理(搜索过滤、日期分组)
- 布局组合(渲染标题栏、搜索框、会话列表、用户信息)
- 处理侧边栏动画
**Props**
```typescript
interface LeftSidebarProps {
isOpen: boolean; // 侧边栏是否展开
onClose: () => void; // 关闭回调
sessions: Session[]; // 会话列表
currentSessionId: string | null; // 当前会话 ID
onSessionSwitch: (id: string) => void; // 切换会话
onNewSession: () => void; // 新建会话
isLoadingSessions: boolean; // 加载状态
user: UserInfo | null | undefined; // 用户信息
}
```
---
### 2. `SessionList.tsx` - 会话列表(约 150 行)
**职责**
- 按日期分组渲染会话(今天、昨天、本周、更早)
- 处理加载状态和空状态
- 管理会话卡片的入场动画
**Props**
```typescript
interface SessionListProps {
sessionGroups: SessionGroups; // 分组后的会话
currentSessionId: string | null; // 当前会话 ID
onSessionSwitch: (id: string) => void; // 切换会话
isLoadingSessions: boolean; // 加载状态
totalSessions: number; // 会话总数
}
```
**特性**
- "今天"分组的会话有渐进入场动画
- 其他分组无动画(性能优化)
- 空状态显示提示文案和图标
---
### 3. `SessionSearchBar.tsx` - 搜索框(约 60 行)
**职责**
- 提供搜索输入框
- 显示搜索图标
- 处理输入变化事件
**Props**
```typescript
interface SessionSearchBarProps {
value: string; // 搜索关键词
onChange: (value: string) => void; // 变化回调
placeholder?: string; // 占位符
}
```
**设计**
- 毛玻璃效果背景
- 聚焦时紫色发光边框
- 左侧搜索图标
---
### 4. `UserInfoCard.tsx` - 用户信息卡片(约 80 行)
**职责**
- 展示用户头像和昵称
- 展示用户订阅类型徽章
- 处理未登录状态
**Props**
```typescript
interface UserInfoCardProps {
user: UserInfo | null | undefined; // 用户信息
}
```
**设计**
- 头像使用渐变色背景和发光效果
- 订阅类型使用渐变色徽章
- 文本溢出时自动截断
---
### 5. `SessionCard.js` - 会话卡片(保留原有)
保留原有的 JavaScript 实现,作为原子组件被 SessionList 调用。
**未来可选优化**:迁移为 TypeScript。
---
### 6. `types.ts` - 类型定义
**导出类型**
```typescript
// 会话数据结构
interface Session {
session_id: string;
title?: string;
created_at?: string;
timestamp?: string;
message_count?: number;
updated_at?: string;
}
// 按日期分组的会话
interface SessionGroups {
today: Session[];
yesterday: Session[];
thisWeek: Session[];
older: Session[];
}
// 用户信息
interface UserInfo {
avatar?: string;
nickname?: string;
subscription_type?: string;
}
```
---
## 🔄 数据流
```
LeftSidebar (index.tsx)
├─ 接收 sessions 数组
├─ 管理 searchQuery 状态
├─ 过滤和分组数据
├─→ SessionSearchBar
│ └─ 更新 searchQuery
├─→ SessionList
│ ├─ 接收 sessionGroups
│ └─→ SessionCard循环渲染
│ └─ 触发 onSessionSwitch
└─→ UserInfoCard
└─ 展示用户信息
```
---
## 📦 依赖关系
- **外部依赖**
- `@chakra-ui/react` - UI 组件库
- `framer-motion` - 动画库
- `lucide-react` - 图标库
- **内部依赖**
- `../../constants/animations` - 动画配置
- `../../utils/sessionUtils` - 会话分组工具函数
---
## 🎨 设计特性
1. **毛玻璃效果**
- `backdropFilter: blur(20px) saturate(180%)`
- 半透明背景 `rgba(17, 24, 39, 0.8)`
2. **渐变色**
- 标题:蓝色到紫色渐变
- 订阅徽章:蓝色到紫色渐变
- 头像背景:蓝色到紫色渐变
3. **交互动画**
- 按钮悬停:缩放 1.1x
- 按钮点击:缩放 0.9x
- 会话卡片悬停:缩放 1.02x + 上移 4px
4. **发光效果**
- 头像发光:`0 0 12px rgba(139, 92, 246, 0.4)`
- 聚焦发光:`0 0 12px rgba(139, 92, 246, 0.3)`
---
## ✅ 重构优势
1. **可维护性提升**
- 单文件从 315 行拆分为多个 60-200 行的小文件
- 每个组件职责单一,易于理解和修改
2. **可测试性提升**
- 每个子组件可独立测试
- 纯展示组件SessionCard、UserInfoCard易于编写单元测试
3. **可复用性提升**
- SessionSearchBar 可在其他地方复用
- UserInfoCard 可在其他侧边栏复用
4. **类型安全**
- 使用 TypeScript 提供完整类型检查
- 统一的类型定义文件types.ts
5. **性能优化**
- 拆分后的组件可独立优化(如 React.memo
- 减少不必要的重新渲染
---
## 🚀 未来优化方向
1. **SessionCard 迁移为 TypeScript**
2. **添加单元测试**
3. **使用 React.memo 优化渲染性能**
4. **添加虚拟滚动(如会话超过 100 个)**
5. **支持拖拽排序会话**
6. **支持会话分组(自定义文件夹)**
---
## 📝 使用示例
```typescript
import LeftSidebar from '@views/AgentChat/components/LeftSidebar';
<LeftSidebar
isOpen={isLeftSidebarOpen}
onClose={() => setIsLeftSidebarOpen(false)}
sessions={sessions}
currentSessionId={currentSessionId}
onSessionSwitch={switchSession}
onNewSession={createNewSession}
isLoadingSessions={isLoadingSessions}
user={user}
/>
```

View File

@@ -0,0 +1,126 @@
// src/views/AgentChat/components/LeftSidebar/SessionList.tsx
// 会话列表组件 - 按日期分组显示会话
import React from 'react';
import { motion } from 'framer-motion';
import { Box, Text, VStack, Flex, Spinner } from '@chakra-ui/react';
import { MessageSquare } from 'lucide-react';
import SessionCard from './SessionCard';
import type { Session, SessionGroups } from './types';
/**
* SessionList 组件的 Props 类型
*/
interface SessionListProps {
/** 按日期分组的会话对象 */
sessionGroups: SessionGroups;
/** 当前选中的会话 ID */
currentSessionId: string | null;
/** 切换会话回调 */
onSessionSwitch: (sessionId: string) => void;
/** 会话加载中状态 */
isLoadingSessions: boolean;
/** 会话总数(用于判断是否为空) */
totalSessions: number;
}
/**
* SessionList - 会话列表组件
*
* 职责:
* 1. 按日期分组显示会话(今天、昨天、本周、更早)
* 2. 处理加载状态和空状态
* 3. 渲染会话卡片列表
*/
const SessionList: React.FC<SessionListProps> = ({
sessionGroups,
currentSessionId,
onSessionSwitch,
isLoadingSessions,
totalSessions,
}) => {
/**
* 渲染会话分组
* @param label - 分组标签(如"今天"、"昨天"
* @param sessions - 会话数组
* @param withAnimation - 是否应用入场动画(今天的会话有动画)
*/
const renderSessionGroup = (
label: string,
sessions: Session[],
withAnimation: boolean = false
): React.ReactNode => {
if (sessions.length === 0) return null;
return (
<Box mb={4}>
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}>
{label}
</Text>
<VStack spacing={2} align="stretch">
{sessions.map((session, idx) => {
const sessionCard = (
<SessionCard
session={session}
isActive={currentSessionId === session.session_id}
onPress={() => onSessionSwitch(session.session_id)}
/>
);
// 今天的会话添加渐进入场动画
if (withAnimation) {
return (
<motion.div
key={session.session_id}
custom={idx}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.05 }}
>
{sessionCard}
</motion.div>
);
}
// 其他分组不添加动画
return <div key={session.session_id}>{sessionCard}</div>;
})}
</VStack>
</Box>
);
};
return (
<Box flex={1} p={3} overflowY="auto">
{/* 按日期分组显示会话 */}
{renderSessionGroup('今天', sessionGroups.today, true)}
{renderSessionGroup('昨天', sessionGroups.yesterday)}
{renderSessionGroup('本周', sessionGroups.thisWeek)}
{renderSessionGroup('更早', sessionGroups.older)}
{/* 加载状态 */}
{isLoadingSessions && (
<Flex justify="center" p={4}>
<Spinner
size="md"
color="purple.500"
emptyColor="gray.700"
thickness="3px"
speed="0.65s"
/>
</Flex>
)}
{/* 空状态 */}
{totalSessions === 0 && !isLoadingSessions && (
<VStack textAlign="center" py={8} color="gray.500" fontSize="sm" spacing={2}>
<MessageSquare className="w-8 h-8" style={{ opacity: 0.5, margin: '0 auto' }} />
<Text></Text>
<Text fontSize="xs"></Text>
</VStack>
)}
</Box>
);
};
export default SessionList;

View File

@@ -0,0 +1,72 @@
// src/views/AgentChat/components/LeftSidebar/SessionSearchBar.tsx
// 会话搜索框组件
import React from 'react';
import { Box, Input } from '@chakra-ui/react';
import { Search } from 'lucide-react';
/**
* SessionSearchBar 组件的 Props 类型
*/
interface SessionSearchBarProps {
/** 搜索关键词 */
value: string;
/** 搜索关键词变化回调 */
onChange: (value: string) => void;
/** 占位符文本 */
placeholder?: string;
}
/**
* SessionSearchBar - 会话搜索框组件
*
* 职责:
* 1. 提供搜索输入框
* 2. 显示搜索图标
* 3. 处理输入变化事件
*
* 设计:
* - 毛玻璃效果背景
* - 聚焦时紫色发光边框
* - 左侧搜索图标
*/
const SessionSearchBar: React.FC<SessionSearchBarProps> = ({
value,
onChange,
placeholder = '搜索对话...',
}) => {
return (
<Box position="relative">
{/* 搜索图标 */}
<Box position="absolute" left={3} top="50%" transform="translateY(-50%)" zIndex={1}>
<Search className="w-4 h-4" color="#9CA3AF" />
</Box>
{/* 搜索输入框 */}
<Input
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
size="sm"
variant="outline"
pl={10}
bg="rgba(255, 255, 255, 0.05)"
backdropFilter="blur(10px)"
border="1px solid"
borderColor="rgba(255, 255, 255, 0.1)"
color="white"
_placeholder={{ color: 'gray.500' }}
_hover={{
borderColor: 'rgba(255, 255, 255, 0.2)',
}}
_focus={{
borderColor: 'purple.400',
boxShadow: '0 0 0 1px var(--chakra-colors-purple-400), 0 0 12px rgba(139, 92, 246, 0.3)',
bg: 'rgba(255, 255, 255, 0.08)',
}}
/>
</Box>
);
};
export default SessionSearchBar;

View File

@@ -0,0 +1,68 @@
// src/views/AgentChat/components/LeftSidebar/UserInfoCard.tsx
// 用户信息卡片组件
import React from 'react';
import { Box, HStack, Avatar, Text, Badge } from '@chakra-ui/react';
import type { UserInfo } from './types';
/**
* UserInfoCard 组件的 Props 类型
*/
interface UserInfoCardProps {
/** 用户信息 */
user: UserInfo | null | undefined;
}
/**
* UserInfoCard - 用户信息卡片组件
*
* 职责:
* 1. 展示用户头像和昵称
* 2. 展示用户订阅类型徽章
* 3. 处理未登录状态
*
* 设计:
* - 头像使用渐变色背景和发光效果
* - 订阅类型使用渐变色徽章
* - 文本溢出时自动截断
*/
const UserInfoCard: React.FC<UserInfoCardProps> = ({ user }) => {
return (
<Box p={4} borderTop="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
<HStack spacing={3}>
{/* 用户头像 */}
<Avatar
src={user?.avatar}
name={user?.nickname}
size="sm"
bgGradient="linear(to-br, blue.500, purple.600)"
boxShadow="0 0 12px rgba(139, 92, 246, 0.4)"
/>
{/* 用户信息 */}
<Box flex={1} minW={0}>
{/* 用户昵称 */}
<Text fontSize="sm" fontWeight="medium" color="gray.100" noOfLines={1}>
{user?.nickname || '未登录'}
</Text>
{/* 订阅类型徽章 */}
<Badge
bgGradient="linear(to-r, blue.500, purple.500)"
color="white"
px={2}
py={0.5}
borderRadius="full"
fontSize="xs"
fontWeight="semibold"
textTransform="none"
>
{user?.subscription_type || 'free'}
</Badge>
</Box>
</HStack>
</Box>
);
};
export default UserInfoCard;

View File

@@ -1,314 +0,0 @@
// src/views/AgentChat/components/LeftSidebar/index.js
// 左侧栏组件 - 对话历史列表
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Box,
Text,
Input,
Avatar,
Badge,
Spinner,
Tooltip,
IconButton,
HStack,
VStack,
Flex,
} from '@chakra-ui/react';
import { MessageSquare, Plus, Search, ChevronLeft } from 'lucide-react';
import { animations } from '../../constants/animations';
import { groupSessionsByDate } from '../../utils/sessionUtils';
import SessionCard from './SessionCard';
/**
* LeftSidebar - 左侧栏组件
*
* @param {Object} props
* @param {boolean} props.isOpen - 侧边栏是否展开
* @param {Function} props.onClose - 关闭侧边栏回调
* @param {Array} props.sessions - 会话列表
* @param {string|null} props.currentSessionId - 当前选中的会话 ID
* @param {Function} props.onSessionSwitch - 切换会话回调
* @param {Function} props.onNewSession - 新建会话回调
* @param {boolean} props.isLoadingSessions - 会话加载中状态
* @param {Object} props.user - 用户信息
* @returns {JSX.Element|null}
*/
const LeftSidebar = ({
isOpen,
onClose,
sessions,
currentSessionId,
onSessionSwitch,
onNewSession,
isLoadingSessions,
user,
}) => {
const [searchQuery, setSearchQuery] = useState('');
// 按日期分组会话
const sessionGroups = groupSessionsByDate(sessions);
// 搜索过滤
const filteredSessions = searchQuery
? sessions.filter(
(s) =>
s.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.session_id?.toLowerCase().includes(searchQuery.toLowerCase())
)
: sessions;
return (
<AnimatePresence>
{isOpen && (
<motion.div
style={{ width: '320px', display: 'flex', flexDirection: 'column' }}
initial="initial"
animate="animate"
exit="exit"
variants={animations.slideInLeft}
>
<Box
w="320px"
h="100%"
display="flex"
flexDirection="column"
bg="rgba(17, 24, 39, 0.8)"
backdropFilter="blur(20px) saturate(180%)"
borderRight="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" mb={3}>
<HStack spacing={2}>
<MessageSquare className="w-5 h-5" color="#60A5FA" />
<Text
fontWeight="semibold"
bgGradient="linear(to-r, blue.300, purple.300)"
bgClip="text"
fontSize="md"
>
对话历史
</Text>
</HStack>
<HStack spacing={2}>
<Tooltip label="新建对话">
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
<IconButton
size="sm"
variant="ghost"
icon={<Plus className="w-4 h-4" />}
onClick={onNewSession}
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(59, 130, 246, 0.2)',
borderColor: 'blue.400',
color: 'blue.300',
boxShadow: '0 0 12px rgba(59, 130, 246, 0.3)',
}}
/>
</motion.div>
</Tooltip>
<Tooltip label="收起侧边栏">
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
<IconButton
size="sm"
variant="ghost"
icon={<ChevronLeft 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>
</HStack>
{/* 搜索框 */}
<Box position="relative">
<Box position="absolute" left={3} top="50%" transform="translateY(-50%)" zIndex={1}>
<Search className="w-4 h-4" color="#9CA3AF" />
</Box>
<Input
placeholder="搜索对话..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
size="sm"
variant="outline"
pl={10}
bg="rgba(255, 255, 255, 0.05)"
backdropFilter="blur(10px)"
border="1px solid"
borderColor="rgba(255, 255, 255, 0.1)"
color="white"
_placeholder={{ color: 'gray.500' }}
_hover={{
borderColor: 'rgba(255, 255, 255, 0.2)',
}}
_focus={{
borderColor: 'purple.400',
boxShadow:
'0 0 0 1px var(--chakra-colors-purple-400), 0 0 12px rgba(139, 92, 246, 0.3)',
bg: 'rgba(255, 255, 255, 0.08)',
}}
/>
</Box>
</Box>
{/* 会话列表 */}
<Box flex={1} p={3} overflowY="auto">
{/* 按日期分组显示会话 */}
{sessionGroups.today.length > 0 && (
<Box mb={4}>
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}>
今天
</Text>
<VStack spacing={2} align="stretch">
{sessionGroups.today.map((session, idx) => (
<motion.div
key={session.session_id}
custom={idx}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.05 }}
>
<SessionCard
session={session}
isActive={currentSessionId === session.session_id}
onPress={() => onSessionSwitch(session.session_id)}
/>
</motion.div>
))}
</VStack>
</Box>
)}
{sessionGroups.yesterday.length > 0 && (
<Box mb={4}>
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}>
昨天
</Text>
<VStack spacing={2} align="stretch">
{sessionGroups.yesterday.map((session) => (
<SessionCard
key={session.session_id}
session={session}
isActive={currentSessionId === session.session_id}
onPress={() => onSessionSwitch(session.session_id)}
/>
))}
</VStack>
</Box>
)}
{sessionGroups.thisWeek.length > 0 && (
<Box mb={4}>
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}>
本周
</Text>
<VStack spacing={2} align="stretch">
{sessionGroups.thisWeek.map((session) => (
<SessionCard
key={session.session_id}
session={session}
isActive={currentSessionId === session.session_id}
onPress={() => onSessionSwitch(session.session_id)}
/>
))}
</VStack>
</Box>
)}
{sessionGroups.older.length > 0 && (
<Box mb={4}>
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}>
更早
</Text>
<VStack spacing={2} align="stretch">
{sessionGroups.older.map((session) => (
<SessionCard
key={session.session_id}
session={session}
isActive={currentSessionId === session.session_id}
onPress={() => onSessionSwitch(session.session_id)}
/>
))}
</VStack>
</Box>
)}
{/* 加载状态 */}
{isLoadingSessions && (
<Flex justify="center" p={4}>
<Spinner
size="md"
color="purple.500"
emptyColor="gray.700"
thickness="3px"
speed="0.65s"
/>
</Flex>
)}
{/* 空状态 */}
{sessions.length === 0 && !isLoadingSessions && (
<VStack textAlign="center" py={8} color="gray.500" fontSize="sm" spacing={2}>
<MessageSquare className="w-8 h-8" style={{ opacity: 0.5, margin: '0 auto' }} />
<Text>还没有对话历史</Text>
<Text fontSize="xs">开始一个新对话吧</Text>
</VStack>
)}
</Box>
{/* 用户信息卡片 */}
<Box p={4} borderTop="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
<HStack spacing={3}>
<Avatar
src={user?.avatar}
name={user?.nickname}
size="sm"
bgGradient="linear(to-br, blue.500, purple.600)"
boxShadow="0 0 12px rgba(139, 92, 246, 0.4)"
/>
<Box flex={1} minW={0}>
<Text fontSize="sm" fontWeight="medium" color="gray.100" noOfLines={1}>
{user?.nickname || '未登录'}
</Text>
<Badge
bgGradient="linear(to-r, blue.500, purple.500)"
color="white"
px={2}
py={0.5}
borderRadius="full"
fontSize="xs"
fontWeight="semibold"
textTransform="none"
>
{user?.subscription_type || 'free'}
</Badge>
</Box>
</HStack>
</Box>
</Box>
</motion.div>
)}
</AnimatePresence>
);
};
export default LeftSidebar;

View File

@@ -0,0 +1,197 @@
// src/views/AgentChat/components/LeftSidebar/index.tsx
// 左侧栏组件 - 对话历史列表(重构版本)
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Box,
Text,
IconButton,
HStack,
Tooltip,
} from '@chakra-ui/react';
import { MessageSquare, Plus, ChevronLeft } from 'lucide-react';
import { animations } from '../../constants/animations';
import { groupSessionsByDate } from '../../utils/sessionUtils';
import SessionSearchBar from './SessionSearchBar';
import SessionList from './SessionList';
import UserInfoCard from './UserInfoCard';
import type { Session, UserInfo } from './types';
/**
* LeftSidebar 组件的 Props 类型
*/
interface LeftSidebarProps {
/** 侧边栏是否展开 */
isOpen: boolean;
/** 关闭侧边栏回调 */
onClose: () => void;
/** 会话列表 */
sessions: Session[];
/** 当前选中的会话 ID */
currentSessionId: string | null;
/** 切换会话回调 */
onSessionSwitch: (sessionId: string) => void;
/** 新建会话回调 */
onNewSession: () => void;
/** 会话加载中状态 */
isLoadingSessions: boolean;
/** 用户信息 */
user: UserInfo | null | undefined;
}
/**
* LeftSidebar - 左侧栏组件(重构版本)
*
* 架构改进:
* - 将会话列表逻辑提取到 SessionList 组件150 行)
* - 将用户信息卡片提取到 UserInfoCard 组件80 行)
* - 将搜索框提取到 SessionSearchBar 组件60 行)
* - 主组件只负责状态管理和布局组合200 行)
*
* 职责:
* 1. 管理搜索状态
* 2. 过滤和分组会话数据
* 3. 组合渲染子组件
* 4. 处理侧边栏动画
*/
const LeftSidebar: React.FC<LeftSidebarProps> = ({
isOpen,
onClose,
sessions,
currentSessionId,
onSessionSwitch,
onNewSession,
isLoadingSessions,
user,
}) => {
// ==================== 本地状态 ====================
const [searchQuery, setSearchQuery] = useState<string>('');
// ==================== 数据处理 ====================
// 搜索过滤
const filteredSessions = searchQuery
? sessions.filter(
(s) =>
s.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.session_id?.toLowerCase().includes(searchQuery.toLowerCase())
)
: sessions;
// 按日期分组会话
const sessionGroups = groupSessionsByDate(filteredSessions);
// ==================== 渲染组件 ====================
return (
<AnimatePresence>
{isOpen && (
<motion.div
style={{ width: '320px', display: 'flex', flexDirection: 'column' }}
initial="initial"
animate="animate"
exit="exit"
variants={animations.slideInLeft}
>
<Box
w="320px"
h="100%"
display="flex"
flexDirection="column"
bg="rgba(17, 24, 39, 0.8)"
backdropFilter="blur(20px) saturate(180%)"
borderRight="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" mb={3}>
{/* 左侧:标题 */}
<HStack spacing={2}>
<MessageSquare className="w-5 h-5" color="#60A5FA" />
<Text
fontWeight="semibold"
bgGradient="linear(to-r, blue.300, purple.300)"
bgClip="text"
fontSize="md"
>
</Text>
</HStack>
{/* 右侧:新建对话 + 收起按钮 */}
<HStack spacing={2}>
{/* 新建对话按钮 */}
<Tooltip label="新建对话">
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
<IconButton
size="sm"
variant="ghost"
icon={<Plus className="w-4 h-4" />}
onClick={onNewSession}
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(59, 130, 246, 0.2)',
borderColor: 'blue.400',
color: 'blue.300',
boxShadow: '0 0 12px rgba(59, 130, 246, 0.3)',
}}
/>
</motion.div>
</Tooltip>
{/* 收起侧边栏按钮 */}
<Tooltip label="收起侧边栏">
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
<IconButton
size="sm"
variant="ghost"
icon={<ChevronLeft 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>
</HStack>
{/* 搜索框组件 */}
<SessionSearchBar value={searchQuery} onChange={setSearchQuery} />
</Box>
{/* ==================== 会话列表组件 ==================== */}
<SessionList
sessionGroups={sessionGroups}
currentSessionId={currentSessionId}
onSessionSwitch={onSessionSwitch}
isLoadingSessions={isLoadingSessions}
totalSessions={sessions.length}
/>
{/* ==================== 用户信息卡片组件 ==================== */}
<UserInfoCard user={user} />
</Box>
</motion.div>
)}
</AnimatePresence>
);
};
export default LeftSidebar;

View File

@@ -0,0 +1,33 @@
// src/views/AgentChat/components/LeftSidebar/types.ts
// LeftSidebar 组件的 TypeScript 类型定义
/**
* 会话数据结构
*/
export interface Session {
session_id: string;
title?: string;
created_at?: string;
timestamp?: string;
message_count?: number;
updated_at?: string;
}
/**
* 按日期分组的会话数据
*/
export interface SessionGroups {
today: Session[];
yesterday: Session[];
thisWeek: Session[];
older: Session[];
}
/**
* 用户信息
*/
export interface UserInfo {
avatar?: string;
nickname?: string;
subscription_type?: string;
}