NotebooksView.tsx 10 KB

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