NotebooksView.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import React from 'react'
  2. import { knowledgeGroupService } from '../../services/knowledgeGroupService'
  3. import { KnowledgeGroup, UpdateGroupData, CreateGroupData } from '../../types'
  4. import { Plus, Book, MoreVertical, Library, MessageSquare, Trash2, Edit2, FolderInput } from 'lucide-react'
  5. import { motion, AnimatePresence } from 'framer-motion'
  6. import { NotebookDetailView } from './NotebookDetailView'
  7. import { CreateNotebookDrawer } from '../CreateNotebookDrawer'
  8. import { EditNotebookDrawer } from '../EditNotebookDrawer'
  9. import { ImportFolderDrawer } from '../ImportFolderDrawer'
  10. import { useLanguage } from '../../contexts/LanguageContext'
  11. import { useToast } from '../../contexts/ToastContext'
  12. import { useConfirm } from '../../contexts/ConfirmContext'
  13. interface NotebooksViewProps {
  14. authToken: string
  15. onChatWithContext: (context: { selectedGroups?: string[], selectedFiles?: string[] }) => void
  16. isAdmin?: boolean
  17. }
  18. export const NotebooksView: React.FC<NotebooksViewProps> = ({ authToken, onChatWithContext, isAdmin = false }) => {
  19. const { t } = useLanguage()
  20. const { showError } = useToast()
  21. const { confirm } = useConfirm()
  22. const [notebooks, setNotebooks] = React.useState<KnowledgeGroup[]>([])
  23. const [isLoading, setIsLoading] = React.useState(true)
  24. const [selectedNotebook, setSelectedNotebook] = React.useState<KnowledgeGroup | null>(null)
  25. const [isCreateDrawerOpen, setIsCreateDrawerOpen] = React.useState(false)
  26. const [isImportDrawerOpen, setIsImportDrawerOpen] = React.useState(false)
  27. const [editingNotebook, setEditingNotebook] = React.useState<KnowledgeGroup | null>(null)
  28. const fetchNotebooks = async () => {
  29. try {
  30. const groups = await knowledgeGroupService.getGroups()
  31. setNotebooks(groups)
  32. } catch (error) {
  33. console.error(error)
  34. } finally {
  35. setIsLoading(false)
  36. }
  37. }
  38. React.useEffect(() => {
  39. fetchNotebooks()
  40. }, [authToken, selectedNotebook])
  41. const handleCreateNotebook = async (data: CreateGroupData) => {
  42. try {
  43. setIsLoading(true)
  44. await knowledgeGroupService.createGroup(data)
  45. await fetchNotebooks()
  46. setIsCreateDrawerOpen(false)
  47. } catch (error) {
  48. console.error(error)
  49. showError(t('createFailed'))
  50. } finally {
  51. setIsLoading(false)
  52. }
  53. }
  54. const handleUpdateNotebook = async (id: string, data: UpdateGroupData) => {
  55. await knowledgeGroupService.updateGroup(id, data)
  56. await fetchNotebooks()
  57. }
  58. const handleDeleteNotebook = async (e: React.MouseEvent, id: string, name: string) => {
  59. e.stopPropagation()
  60. if (!(await confirm(t('confirmDeleteNotebook').replace('$1', name)))) return
  61. try {
  62. setIsLoading(true)
  63. await knowledgeGroupService.deleteGroup(id)
  64. setNotebooks(prev => prev.filter(n => n.id !== id))
  65. } catch (error) {
  66. console.error(error)
  67. showError(t('deleteFailed'))
  68. } finally {
  69. setIsLoading(false)
  70. }
  71. }
  72. if (selectedNotebook) {
  73. return (
  74. <NotebookDetailView
  75. authToken={authToken}
  76. notebook={selectedNotebook}
  77. onBack={() => setSelectedNotebook(null)}
  78. onChatWithContext={onChatWithContext}
  79. isAdmin={!!isAdmin}
  80. />
  81. )
  82. }
  83. return (
  84. <div className="flex flex-col h-full bg-transparent overflow-hidden">
  85. <div className="px-8 pt-8 pb-6 flex items-start justify-between shrink-0">
  86. <div>
  87. <h1 className="text-2xl font-bold text-slate-900 leading-tight">{t('navKnowledgeGroups')}</h1>
  88. <p className="text-[15px] text-slate-500 mt-1">{t('notebooksDesc')}</p>
  89. </div>
  90. <div className="flex items-center gap-3">
  91. {isAdmin && (
  92. <button
  93. onClick={() => setIsImportDrawerOpen(true)}
  94. className="flex items-center gap-2 px-4 py-2.5 bg-white border border-slate-200 text-slate-700 text-sm font-semibold rounded-lg hover:bg-slate-50 hover:border-slate-300 transition-all active:scale-95 shadow-sm"
  95. >
  96. <FolderInput size={18} className="text-blue-600" />
  97. <span>Import Folder</span>
  98. </button>
  99. )}
  100. {isAdmin && (
  101. <button
  102. onClick={() => setIsCreateDrawerOpen(true)}
  103. className="flex items-center gap-2 px-5 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg shadow-sm shadow-blue-100 transition-all font-semibold text-sm active:scale-95"
  104. >
  105. <Plus size={18} />
  106. <span>New Group</span>
  107. </button>
  108. )}
  109. </div>
  110. </div>
  111. <div className="px-8 pb-8 flex-1 overflow-y-auto">
  112. {isLoading ? (
  113. <div className="flex h-64 items-center justify-center">
  114. <div className="w-8 h-8 border-2 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
  115. </div>
  116. ) : notebooks.length === 0 ? (
  117. <div className="flex flex-col items-center justify-center py-32 border-2 border-dashed border-slate-200 rounded-2xl bg-white/50 text-center">
  118. <Library className="w-12 h-12 text-slate-200 mx-auto mb-4" />
  119. <h3 className="text-slate-900 font-bold">No Knowledge Groups</h3>
  120. <p className="text-slate-500 text-sm mt-1">Create a group to start organizing your files.</p>
  121. {isAdmin && (
  122. <button
  123. onClick={() => setIsCreateDrawerOpen(true)}
  124. className="mt-6 px-5 py-2 bg-blue-600 text-white rounded-lg font-semibold text-sm hover:bg-blue-700 transition-all shadow-sm"
  125. >
  126. {t('createNotebook')}
  127. </button>
  128. )}
  129. </div>
  130. ) : (
  131. <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 max-w-7xl mx-auto">
  132. <AnimatePresence>
  133. {notebooks.map((notebook, index) => (
  134. <motion.div
  135. key={notebook.id}
  136. layout
  137. initial={{ opacity: 0, y: 10 }}
  138. animate={{ opacity: 1, y: 0 }}
  139. onClick={() => setSelectedNotebook(notebook)}
  140. className="bg-white rounded-xl border border-slate-200/80 p-5 shadow-sm hover:shadow-md transition-all group relative overflow-hidden flex flex-col h-64 cursor-pointer"
  141. >
  142. <div className="absolute top-4 right-4 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity" onClick={e => e.stopPropagation()}>
  143. <button
  144. onClick={(e) => {
  145. e.stopPropagation()
  146. onChatWithContext({ selectedGroups: [notebook.id] })
  147. }}
  148. className="p-1.5 text-slate-400 hover:text-blue-600 rounded-md"
  149. >
  150. <MessageSquare size={16} />
  151. </button>
  152. {isAdmin && (
  153. <button
  154. onClick={(e) => {
  155. e.stopPropagation()
  156. setEditingNotebook(notebook)
  157. }}
  158. className="p-1.5 text-slate-400 hover:text-blue-600 rounded-md"
  159. >
  160. <Edit2 size={16} />
  161. </button>
  162. )}
  163. {isAdmin && (
  164. <button
  165. onClick={(e) => handleDeleteNotebook(e, notebook.id, notebook.name)}
  166. className="p-1.5 text-slate-400 hover:text-red-500 rounded-md"
  167. >
  168. <Trash2 size={16} />
  169. </button>
  170. )}
  171. </div>
  172. <div className="flex-1">
  173. <div className="w-11 h-11 bg-blue-50 rounded-lg text-blue-600 shadow-sm border border-blue-100/30 flex items-center justify-center mb-4 transition-transform group-hover:scale-105">
  174. <Book size={20} />
  175. </div>
  176. <h3 className="font-bold text-slate-900 text-[16px] mb-2 leading-tight group-hover:text-blue-600 transition-colors truncate">
  177. {notebook.name}
  178. </h3>
  179. <p className="text-[13px] text-slate-500 leading-relaxed line-clamp-3 italic opacity-85">
  180. {notebook.description || "No description provided."}
  181. </p>
  182. </div>
  183. <div className="mt-auto pt-4 border-t border-slate-50 flex items-center justify-between">
  184. <div className="flex items-center gap-2 px-2.5 py-1 bg-slate-50 border border-slate-100 rounded-md">
  185. <span className="text-[11px] font-bold text-slate-700">{notebook.fileCount || 0}</span>
  186. <span className="text-[11px] font-semibold text-slate-400 uppercase tracking-tight">Files</span>
  187. </div>
  188. <span className="text-[11px] font-medium text-slate-300">
  189. {notebook.updatedAt ? new Date(notebook.updatedAt).toLocaleDateString() : ''}
  190. </span>
  191. </div>
  192. </motion.div>
  193. ))}
  194. </AnimatePresence>
  195. </div>
  196. )}
  197. </div>
  198. {isCreateDrawerOpen && (
  199. <CreateNotebookDrawer
  200. isOpen={isCreateDrawerOpen}
  201. onClose={() => setIsCreateDrawerOpen(false)}
  202. onCreate={handleCreateNotebook}
  203. />
  204. )}
  205. {editingNotebook && (
  206. <EditNotebookDrawer
  207. isOpen={!!editingNotebook}
  208. onClose={() => setEditingNotebook(null)}
  209. notebook={editingNotebook}
  210. onUpdate={handleUpdateNotebook}
  211. />
  212. )}
  213. <ImportFolderDrawer
  214. isOpen={isImportDrawerOpen}
  215. onClose={() => setIsImportDrawerOpen(false)}
  216. authToken={authToken}
  217. onImportSuccess={fetchNotebooks}
  218. />
  219. </div>
  220. )
  221. }