SidebarRail.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import React, { useState } from 'react'
  2. import { MessageSquare, Book, Mic, Settings, LogOut, Database, ChevronRight, ChevronLeft, Menu, Globe, DownloadCloud, User } from 'lucide-react'
  3. import { Logo } from '../Logo'
  4. import { useLanguage } from '../../contexts/LanguageContext'
  5. import { UserInfoDisplay } from '../UserInfoDisplay'
  6. export type ViewType = 'chat' | 'knowledge' | 'notebooks' | 'settings';
  7. interface SidebarRailProps {
  8. currentView: ViewType;
  9. onViewChange: (view: ViewType) => void;
  10. onLogout?: () => void;
  11. onOpenSettings?: () => void;
  12. currentUser?: any;
  13. }
  14. export const SidebarRail: React.FC<SidebarRailProps> = ({ currentView, onViewChange, onLogout, currentUser }) => {
  15. const [isExpanded, setIsExpanded] = useState(false)
  16. const { language, setLanguage, t } = useLanguage()
  17. const handleLanguageCycle = () => {
  18. const langs = ['zh', 'en', 'ja'] as const
  19. const currentIndex = langs.indexOf(language as any)
  20. const nextIndex = (currentIndex + 1) % langs.length
  21. setLanguage(langs[nextIndex])
  22. }
  23. const navItems = [
  24. { id: 'chat' as ViewType, icon: MessageSquare, label: t('navChat') },
  25. { id: 'knowledge' as ViewType, icon: Database, label: t('navKnowledge') },
  26. { id: 'notebooks' as ViewType, icon: Book, label: t('navKnowledgeGroups') },
  27. ];
  28. return (
  29. <div className={`
  30. ${isExpanded ? 'w-64' : 'w-20'}
  31. bg-slate-950 flex flex-col py-6 shrink-0 z-50 transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] border-r border-slate-800
  32. `}>
  33. {/* Header / Logo */}
  34. <div className={`mb-8 flex items-center ${isExpanded ? 'px-6 justify-between' : 'justify-center'}`}>
  35. <div className="flex items-center justify-center shrink-0 cursor-default">
  36. <Logo size={isExpanded ? 32 : 40} withText={isExpanded} />
  37. </div>
  38. {isExpanded && (
  39. <button
  40. onClick={() => setIsExpanded(false)}
  41. className="text-slate-500 hover:text-slate-300 transition-colors p-1 rounded-full hover:bg-slate-800/50 ml-auto"
  42. >
  43. <ChevronLeft size={18} />
  44. </button>
  45. )}
  46. </div>
  47. {/* Current User Display - only when expanded */}
  48. {isExpanded && currentUser && (
  49. <div className="px-2 mb-4">
  50. <UserInfoDisplay currentUser={currentUser} />
  51. </div>
  52. )}
  53. {/* ナビゲーション項目 */}
  54. <div className="flex-1 flex flex-col space-y-2 w-full px-3">
  55. {navItems.map((item) => {
  56. // 現在のルートに基づいてアクティブなタブを決定
  57. const isActive = currentView === item.id;
  58. return (
  59. <button
  60. key={item.id}
  61. onClick={() => onViewChange(item.id)}
  62. draggable="false"
  63. className={`
  64. py-3 px-3 rounded-xl transition-all duration-300 group relative flex items-center outline-none
  65. ${isExpanded ? 'justify-start' : 'justify-center'}
  66. ${isActive
  67. ? 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white shadow-md shadow-blue-500/25'
  68. : 'text-slate-400 hover:bg-white/5 hover:text-slate-200'
  69. }
  70. `}
  71. title={!isExpanded ? item.label : undefined}
  72. >
  73. <item.icon size={20} strokeWidth={isActive ? 2.5 : 2} className={`shrink-0 transition-transform duration-300 ${isActive ? 'scale-110' : 'group-hover:scale-110'}`} />
  74. <span className={`
  75. ml-3 font-medium whitespace-nowrap overflow-hidden transition-all duration-300
  76. ${isExpanded ? 'opacity-100 w-auto translate-x-0' : 'opacity-0 w-0 -translate-x-4 absolute'}
  77. `}>
  78. {item.label}
  79. </span>
  80. {/* Tooltip (only when collapsed) */}
  81. {!isExpanded && (
  82. <span className="
  83. absolute left-full ml-4 bg-slate-900 text-slate-200 text-xs px-2.5 py-1.5 rounded-md
  84. opacity-0 group-hover:opacity-100 transition-all duration-200 pointer-events-none whitespace-nowrap z-50
  85. top-1/2 -translate-y-1/2 shadow-xl border border-slate-800 translate-x-[-8px] group-hover:translate-x-0
  86. ">
  87. {item.label}
  88. </span>
  89. )}
  90. </button>
  91. )
  92. })}
  93. </div>
  94. {/* Footer / Settings / Toggle */}
  95. <div className="flex flex-col space-y-2 w-full px-3 mt-auto">
  96. {/* Toggle Button (When collapsed, show here to expand) */}
  97. {!isExpanded && (
  98. <button
  99. onClick={() => setIsExpanded(true)}
  100. className="p-3 rounded-xl text-slate-500 hover:bg-white/5 hover:text-slate-300 transition-all flex justify-center mb-2"
  101. title={t('expandMenu')}
  102. >
  103. <ChevronRight size={20} />
  104. </button>
  105. )}
  106. {/* Settings Button */}
  107. <button
  108. onClick={() => onViewChange('settings')}
  109. className={`
  110. py-3 px-3 rounded-xl transition-all duration-300 flex items-center group relative outline-none
  111. ${isExpanded ? 'justify-start' : 'justify-center'}
  112. ${currentView === 'settings'
  113. ? 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white shadow-md shadow-blue-500/25'
  114. : 'text-slate-400 hover:bg-white/5 hover:text-slate-200'
  115. }
  116. `}
  117. title={!isExpanded ? t('settings') : undefined}
  118. >
  119. <Settings size={20} className={`shrink-0 transition-transform duration-300 ${currentView === 'settings' ? 'rotate-90' : 'group-hover:rotate-45'}`} />
  120. <span className={`
  121. ml-3 font-medium whitespace-nowrap overflow-hidden transition-all duration-300
  122. ${isExpanded ? 'opacity-100 w-auto' : 'opacity-0 w-0 absolute'}
  123. `}>
  124. {t('settings')}
  125. </span>
  126. {!isExpanded && (
  127. <span className="
  128. absolute left-full ml-4 bg-slate-900 text-slate-200 text-xs px-2.5 py-1.5 rounded-md
  129. opacity-0 group-hover:opacity-100 transition-all duration-200 pointer-events-none whitespace-nowrap z-50
  130. top-1/2 -translate-y-1/2 shadow-xl border border-slate-800 translate-x-[-8px] group-hover:translate-x-0
  131. ">
  132. {t('settings')}
  133. </span>
  134. )}
  135. </button>
  136. {/* Language Switcher */}
  137. <button
  138. onClick={handleLanguageCycle}
  139. className={`
  140. py-3 px-3 rounded-xl transition-all duration-300 flex items-center group relative outline-none
  141. ${isExpanded ? 'justify-start' : 'justify-center'}
  142. text-slate-400 hover:bg-white/5 hover:text-slate-200
  143. `}
  144. title={!isExpanded ? t('switchLanguage') : undefined}
  145. >
  146. <Globe size={20} className="shrink-0 group-hover:scale-110 transition-transform duration-300" />
  147. <span className={`
  148. ml-3 font-medium whitespace-nowrap overflow-hidden transition-all duration-300
  149. ${isExpanded ? 'opacity-100 w-auto' : 'opacity-0 w-0 absolute'}
  150. `}>
  151. {language === 'ja' ? t('langJa') : language === 'en' ? t('langEn') : t('langZh')}
  152. </span>
  153. {!isExpanded && (
  154. <span className="
  155. absolute left-full ml-4 bg-slate-900 text-slate-200 text-xs px-2.5 py-1.5 rounded-md
  156. opacity-0 group-hover:opacity-100 transition-all duration-200 pointer-events-none whitespace-nowrap z-50
  157. top-1/2 -translate-y-1/2 shadow-xl border border-slate-800 translate-x-[-8px] group-hover:translate-x-0
  158. ">
  159. {t('switchLanguage')}
  160. </span>
  161. )}
  162. </button>
  163. {/* Logout Button */}
  164. <button
  165. onClick={onLogout}
  166. className={`
  167. py-3 px-3 rounded-xl transition-all duration-300 flex items-center group relative outline-none
  168. ${isExpanded ? 'justify-start' : 'justify-center'}
  169. text-slate-400 hover:bg-red-500/10 hover:text-red-400
  170. `}
  171. title={!isExpanded ? t('logout') : undefined}
  172. >
  173. <LogOut size={20} className="shrink-0 group-hover:translate-x-1 transition-transform duration-300" />
  174. <span className={`
  175. ml-3 font-medium whitespace-nowrap overflow-hidden transition-all duration-300
  176. ${isExpanded ? 'opacity-100 w-auto' : 'opacity-0 w-0 absolute'}
  177. `}>
  178. {t('logout')}
  179. </span>
  180. {!isExpanded && (
  181. <span className="
  182. absolute left-full ml-4 bg-slate-900 text-slate-200 text-xs px-2.5 py-1.5 rounded-md
  183. opacity-0 group-hover:opacity-100 transition-all duration-200 pointer-events-none whitespace-nowrap z-50
  184. top-1/2 -translate-y-1/2 shadow-xl border border-slate-800 translate-x-[-8px] group-hover:translate-x-0
  185. ">
  186. {t('logout')}
  187. </span>
  188. )}
  189. </button>
  190. </div>
  191. </div>
  192. )
  193. }