feat: LeftSidebar (~280 行) - 对话历史列表 + 用户信息卡片
This commit is contained in:
76
src/views/AgentChat/components/LeftSidebar/SessionCard.js
Normal file
76
src/views/AgentChat/components/LeftSidebar/SessionCard.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// src/views/AgentChat/components/LeftSidebar/SessionCard.js
|
||||
// 会话卡片组件
|
||||
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Card, CardBody, Flex, Box, Text, Badge } from '@chakra-ui/react';
|
||||
|
||||
/**
|
||||
* SessionCard - 会话卡片组件
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Object} props.session - 会话数据
|
||||
* @param {boolean} props.isActive - 是否为当前选中的会话
|
||||
* @param {Function} props.onPress - 点击回调函数
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SessionCard = ({ session, isActive, onPress }) => {
|
||||
return (
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02, y: -4 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Card
|
||||
cursor="pointer"
|
||||
onClick={onPress}
|
||||
bg={isActive ? 'rgba(139, 92, 246, 0.15)' : 'rgba(255, 255, 255, 0.05)'}
|
||||
backdropFilter="blur(12px)"
|
||||
borderWidth={1}
|
||||
borderColor={isActive ? 'purple.400' : 'rgba(255, 255, 255, 0.1)'}
|
||||
_hover={{
|
||||
bg: isActive ? 'rgba(139, 92, 246, 0.2)' : 'rgba(255, 255, 255, 0.08)',
|
||||
borderColor: isActive ? 'purple.400' : 'rgba(255, 255, 255, 0.2)',
|
||||
boxShadow: isActive
|
||||
? '0 12px 24px rgba(139, 92, 246, 0.4)'
|
||||
: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
transition="all 0.3s"
|
||||
>
|
||||
<CardBody p={3}>
|
||||
<Flex align="start" justify="space-between" gap={2}>
|
||||
<Box flex={1} minW={0}>
|
||||
<Text fontSize="sm" fontWeight="medium" color="gray.100" noOfLines={1}>
|
||||
{session.title || '新对话'}
|
||||
</Text>
|
||||
<Text fontSize="xs" color="gray.500" mt={1}>
|
||||
{new Date(session.created_at || session.timestamp).toLocaleString('zh-CN', {
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
{session.message_count && (
|
||||
<Badge
|
||||
bgGradient={
|
||||
isActive
|
||||
? 'linear(to-r, blue.500, purple.500)'
|
||||
: 'linear(to-r, gray.600, gray.700)'
|
||||
}
|
||||
color={isActive ? 'white' : 'gray.400'}
|
||||
variant="subtle"
|
||||
boxShadow={isActive ? '0 2px 8px rgba(139, 92, 246, 0.3)' : 'none'}
|
||||
>
|
||||
{session.message_count}
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionCard;
|
||||
313
src/views/AgentChat/components/LeftSidebar/index.js
Normal file
313
src/views/AgentChat/components/LeftSidebar/index.js
Normal file
@@ -0,0 +1,313 @@
|
||||
// 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"
|
||||
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;
|
||||
Reference in New Issue
Block a user