Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | import { useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Users, Bot, Zap, Layers, Package, Search, Sparkles, ChevronRight, X } from 'lucide-react'; import { CloneManager } from './CloneManager'; import { TeamList } from './TeamList'; import { useConfigStore } from '../store/configStore'; import { containerVariants, defaultTransition } from '../lib/animations'; export type MainViewType = 'chat' | 'automation' | 'team' | 'swarm' | 'skills'; interface SidebarProps { onOpenSettings?: () => void; onMainViewChange?: (view: MainViewType) => void; selectedTeamId?: string; onSelectTeam?: (teamId: string) => void; onNewChat?: () => void; } type Tab = 'chat' | 'clones' | 'automation' | 'team' | 'swarm' | 'skills'; // 导航项配置 - WorkBuddy 风格 const NAV_ITEMS: { key: Tab; label: string; icon: React.ComponentType<{ className?: string }>; mainView?: MainViewType; }[] = [ { key: 'clones', label: '分身', icon: Bot }, { key: 'automation', label: '自动化', icon: Zap, mainView: 'automation' }, { key: 'skills', label: '技能', icon: Package, mainView: 'skills' }, { key: 'team', label: '团队', icon: Users, mainView: 'team' }, { key: 'swarm', label: '协作', icon: Layers, mainView: 'swarm' }, ]; export function Sidebar({ onOpenSettings, onMainViewChange, selectedTeamId, onSelectTeam, onNewChat }: SidebarProps) { const [activeTab, setActiveTab] = useState<Tab>('clones'); const [searchQuery, setSearchQuery] = useState(''); const userName = useConfigStore((state) => state.quickConfig?.userName) || '用户7141'; const handleNavClick = (key: Tab, mainView?: MainViewType) => { setActiveTab(key); if (mainView && onMainViewChange) { onMainViewChange(mainView); } else if (onMainViewChange) { onMainViewChange('chat'); } }; const handleSelectTeam = (teamId: string) => { onSelectTeam?.(teamId); setActiveTab('team'); onMainViewChange?.('team'); }; return ( <aside className="w-64 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700 flex flex-col flex-shrink-0"> {/* 搜索框 */} <div className="p-3 border-b border-gray-100 dark:border-gray-800"> <div className="relative"> <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-4 h-4" /> <input type="text" placeholder="搜索..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="w-full pl-9 pr-8 py-2 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-all text-gray-700 dark:text-gray-300 placeholder-gray-400" /> {searchQuery && ( <button onClick={() => setSearchQuery('')} className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded text-gray-400 transition-colors" > <X className="w-3 h-3" /> </button> )} </div> </div> {/* 新对话按钮 */} <div className="px-3 py-2"> <button onClick={() => { setActiveTab('clones'); onNewChat?.(); }} className="w-full flex items-center gap-3 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg text-gray-700 dark:text-gray-300 transition-colors group" > <Sparkles className="w-5 h-5 text-gray-500" /> <span className="font-medium">新对话</span> </button> </div> {/* 导航项 */} <nav className="px-3 space-y-0.5"> {NAV_ITEMS.map(({ key, label, icon: Icon, mainView }) => ( <button key={key} onClick={() => handleNavClick(key, mainView)} className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${ activeTab === key ? 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-medium' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800/50 hover:text-gray-900 dark:hover:text-gray-200' }`} > <Icon className={`w-5 h-5 ${activeTab === key ? 'text-gray-700 dark:text-gray-300' : 'text-gray-400'}`} /> <span>{label}</span> {activeTab === key && ( <ChevronRight className="w-4 h-4 ml-auto text-gray-400" /> )} </button> ))} </nav> {/* 分隔线 */} <div className="my-3 mx-3 border-t border-gray-100 dark:border-gray-800" /> {/* 内容区域 - 只显示分身、团队、协作的内容,自动化和技能在主内容区显示 */} <div className="flex-1 overflow-hidden"> <AnimatePresence mode="wait"> <motion.div key={activeTab} variants={containerVariants} initial="hidden" animate="visible" exit="exit" transition={defaultTransition} className="h-full overflow-y-auto" > {activeTab === 'clones' && <CloneManager />} {/* skills、automation 和 swarm 不在侧边栏显示内容,由主内容区显示 */} {activeTab === 'team' && ( <TeamList selectedTeamId={selectedTeamId} onSelectTeam={handleSelectTeam} /> )} </motion.div> </AnimatePresence> </div> {/* 底部用户栏 */} <div className="p-3 border-t border-gray-200 dark:border-gray-700"> <button onClick={onOpenSettings} aria-label="打开设置" title="设置" className="flex items-center gap-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 p-2 rounded-lg transition-colors" > <div className="w-8 h-8 bg-gray-600 rounded-full flex items-center justify-center text-white font-bold shadow-sm"> {userName?.charAt(0) || '用'} </div> <span className="flex-1 text-left text-sm font-medium text-gray-700 dark:text-gray-300 truncate"> {userName} </span> <ChevronRight className="w-4 h-4 text-gray-400" /> </button> </div> </aside> ); } |