import { Plus, Search, History, Folder, CheckSquare, MoreHorizontal, LayoutGrid, PanelLeftClose, PanelLeftOpen, Clock, Edit2, Trash2, Share2, Star, ExternalLink, FolderInput } from 'lucide-react'; import { useState, useMemo, useRef, useEffect, memo } from 'react'; import type { ChatSession } from '../services/chatService'; import Modal from './Modal'; interface SidebarProps { isCollapsed: boolean; toggleSidebar: () => void; onNavigate: (view: 'home' | 'detail' | 'original') => void; activeView: 'home' | 'detail' | 'original'; onReset: () => void; sessions: ChatSession[]; currentSessionId: string | null; onSelectSession: (id: string) => void; onRenameSession: (id: string, newTitle: string) => void; onDeleteSession: (id: string) => void; } const formatTime = (timestamp: number) => { const date = new Date(timestamp); const now = new Date(); const diff = now.getTime() - date.getTime(); if (diff < 60 * 60 * 1000) { const minutes = Math.floor(diff / (60 * 1000)); return `${minutes}分钟前`; } else if (diff < 24 * 60 * 60 * 1000) { const hours = Math.floor(diff / (60 * 60 * 1000)); return `${hours}小时前`; } else { return `${date.getMonth() + 1}月${date.getDate()}日`; } }; export default memo(function Sidebar({ isCollapsed, toggleSidebar, activeView, onNavigate, onReset, sessions, currentSessionId, onSelectSession, onRenameSession, onDeleteSession }: SidebarProps) { const [tooltip, setTooltip] = useState<{ text: string; top: number; visible: boolean }>({ text: '', top: 0, visible: false }); // Menu State const [activeMenuId, setActiveMenuId] = useState(null); const [menuPosition, setMenuPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 }); const menuRef = useRef(null); // Modal State const [renameModal, setRenameModal] = useState<{ isOpen: boolean; sessionId: string; currentTitle: string }>({ isOpen: false, sessionId: '', currentTitle: '' }); const [deleteModal, setDeleteModal] = useState<{ isOpen: boolean; sessionId: string }>({ isOpen: false, sessionId: '' }); const [newTitleInput, setNewTitleInput] = useState(''); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(event.target as Node)) { setActiveMenuId(null); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const handleMenuClick = (e: React.MouseEvent, sessionId: string) => { e.stopPropagation(); e.preventDefault(); const rect = e.currentTarget.getBoundingClientRect(); // Position menu to the right of the button if possible, or below // Image shows it to the right or below. Let's align top-right of menu to bottom-right of button setMenuPosition({ top: rect.bottom + 5, left: rect.right - 180 }); setActiveMenuId(sessionId); }; const openRenameModal = (id: string, currentTitle: string) => { setNewTitleInput(currentTitle); setRenameModal({ isOpen: true, sessionId: id, currentTitle }); setActiveMenuId(null); }; const openDeleteModal = (id: string) => { setDeleteModal({ isOpen: true, sessionId: id }); setActiveMenuId(null); }; const handleRenameConfirm = () => { if (newTitleInput.trim()) { onRenameSession(renameModal.sessionId, newTitleInput.trim()); } setRenameModal({ isOpen: false, sessionId: '', currentTitle: '' }); }; const handleDeleteConfirm = () => { onDeleteSession(deleteModal.sessionId); setDeleteModal({ isOpen: false, sessionId: '' }); }; // Recent Activity Popover State const [recentPopover, setRecentPopover] = useState<{ top: number; visible: boolean }>({ top: 0, visible: false }); const popoverTimeoutRef = useRef | null>(null); const showRecentPopover = (e: React.MouseEvent) => { if (!isCollapsed) return; if (popoverTimeoutRef.current) clearTimeout(popoverTimeoutRef.current); const rect = e.currentTarget.getBoundingClientRect(); setRecentPopover({ top: rect.top, visible: true }); }; const hideRecentPopover = () => { popoverTimeoutRef.current = setTimeout(() => { setRecentPopover(prev => ({ ...prev, visible: false })); }, 300); }; const keepRecentPopover = () => { if (popoverTimeoutRef.current) clearTimeout(popoverTimeoutRef.current); }; useEffect(() => { return () => { if (popoverTimeoutRef.current) clearTimeout(popoverTimeoutRef.current); }; }, []); const showTooltip = (e: React.MouseEvent, text: string) => { if (!isCollapsed) return; const rect = e.currentTarget.getBoundingClientRect(); setTooltip({ text, top: rect.top + rect.height / 2, visible: true }); }; const hideTooltip = () => { setTooltip(prev => ({ ...prev, visible: false })); }; const menuItems = useMemo(() => [ { icon: Plus, label: '开启新对话', active: activeView === 'home', onClick: onReset }, { icon: Search, label: '搜索', active: false }, { icon: History, label: '历史', active: false }, { icon: Folder, label: '项目', active: false }, { icon: CheckSquare, label: '待办任务', active: false }, { icon: LayoutGrid, label: '知识库', active: false }, // { icon: FileText, label: '原始样式', active: activeView === 'original', onClick: () => onNavigate('original') }, ], [activeView, onReset, onNavigate]); return ( <> {/* Mobile Backdrop */} {!isCollapsed && (
)}
{/* Header */}
showTooltip(e, '打开侧栏')} onMouseLeave={hideTooltip} >
Logo {isCollapsed && (
)}
Manus-clone-cn
{/* Menu Items & Recent Activity Container */}
{/* Static Section: Menu Items + Divider */}
{menuItems.map((item, index) => ( ))} {/* Divider */} {/*
*/}
{/* Recent Activity Section - Scrollable */} {!isCollapsed ? (
最近活动
{sessions.map((session) => (
handleMenuClick(e, session.id)} >
))}
) : (
)}
{/* Footer */}
{/* Recent Activity Popover */}
最近活动
{sessions.map(session => ( ))} {sessions.length === 0 && (
暂无活动记录
)}
{/* Session Menu */} {activeMenuId && (
)} {/* Modals */} setRenameModal({ ...renameModal, isOpen: false })} title="重命名对话" primaryAction={{ label: '保存', onClick: handleRenameConfirm }} secondaryAction={{ label: '取消', onClick: () => setRenameModal({ ...renameModal, isOpen: false }) }} >
setNewTitleInput(e.target.value)} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800 dark:text-white" placeholder="输入新的对话标题" autoFocus onKeyDown={(e) => { if (e.key === 'Enter') handleRenameConfirm(); }} />
setDeleteModal({ ...deleteModal, isOpen: false })} title="删除对话" primaryAction={{ label: '删除', onClick: handleDeleteConfirm, danger: true }} secondaryAction={{ label: '取消', onClick: () => setDeleteModal({ ...deleteModal, isOpen: false }) }} >
确定要删除这个对话吗?此操作无法撤销。
{/* Tooltip */}
{tooltip.text} {/* Arrow */}
); });