NotebooksView.tsx 10 KB

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