@lobehub/lobehub 2.0.0-next.54 → 2.0.0-next.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/common.json +1 -0
  4. package/locales/ar/file.json +85 -2
  5. package/locales/bg-BG/common.json +1 -0
  6. package/locales/bg-BG/file.json +85 -2
  7. package/locales/de-DE/common.json +1 -0
  8. package/locales/de-DE/file.json +85 -2
  9. package/locales/en-US/common.json +1 -0
  10. package/locales/en-US/file.json +85 -2
  11. package/locales/es-ES/common.json +1 -0
  12. package/locales/es-ES/file.json +85 -2
  13. package/locales/fa-IR/common.json +1 -0
  14. package/locales/fa-IR/file.json +85 -2
  15. package/locales/fr-FR/common.json +1 -0
  16. package/locales/fr-FR/file.json +85 -2
  17. package/locales/it-IT/common.json +1 -0
  18. package/locales/it-IT/file.json +85 -2
  19. package/locales/ja-JP/common.json +1 -0
  20. package/locales/ja-JP/file.json +85 -2
  21. package/locales/ko-KR/common.json +1 -0
  22. package/locales/ko-KR/file.json +85 -2
  23. package/locales/nl-NL/common.json +1 -0
  24. package/locales/nl-NL/file.json +85 -2
  25. package/locales/pl-PL/common.json +1 -0
  26. package/locales/pl-PL/file.json +85 -2
  27. package/locales/pt-BR/common.json +1 -0
  28. package/locales/pt-BR/file.json +85 -2
  29. package/locales/ru-RU/common.json +1 -0
  30. package/locales/ru-RU/file.json +85 -2
  31. package/locales/tr-TR/common.json +1 -0
  32. package/locales/tr-TR/file.json +85 -2
  33. package/locales/vi-VN/common.json +1 -0
  34. package/locales/vi-VN/file.json +85 -2
  35. package/locales/zh-CN/common.json +1 -0
  36. package/locales/zh-CN/file.json +85 -2
  37. package/locales/zh-TW/common.json +1 -0
  38. package/locales/zh-TW/file.json +85 -2
  39. package/package.json +1 -1
  40. package/packages/database/src/models/__tests__/file.test.ts +94 -29
  41. package/packages/database/src/models/file.ts +15 -4
  42. package/packages/database/src/repositories/knowledge/index.test.ts +300 -0
  43. package/packages/database/src/repositories/knowledge/index.ts +420 -0
  44. package/packages/model-bank/src/aiModels/aihubmix.ts +1 -0
  45. package/packages/model-bank/src/aiModels/google.ts +9 -5
  46. package/packages/model-bank/src/aiModels/openai.ts +2 -35
  47. package/packages/model-bank/src/aiModels/openrouter.ts +1 -0
  48. package/packages/model-bank/src/aiModels/vertexai.ts +2 -0
  49. package/packages/model-bank/src/types/aiModel.ts +15 -2
  50. package/packages/model-runtime/src/core/usageConverters/index.ts +1 -0
  51. package/packages/model-runtime/src/core/usageConverters/utils/resolveImageSinglePrice.ts +34 -0
  52. package/packages/types/src/document/index.ts +14 -2
  53. package/packages/types/src/files/index.ts +2 -0
  54. package/packages/types/src/files/list.ts +10 -0
  55. package/packages/types/src/llm.ts +1 -1
  56. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect/ImageModelItem.tsx +93 -0
  57. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/{ModelSelect.tsx → ModelSelect/index.tsx} +17 -2
  58. package/src/app/[variants]/(main)/knowledge/KnowledgeRouter.tsx +2 -1
  59. package/src/app/[variants]/(main)/knowledge/components/KnowledgeBaseItem/index.tsx +0 -2
  60. package/src/app/[variants]/(main)/knowledge/hooks/useFileCategory.ts +6 -3
  61. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/index.tsx +2 -2
  62. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/menu/{MenuItems.tsx → CategoryMenu.tsx} +3 -3
  63. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/menu/Menu.tsx +2 -2
  64. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/index.tsx +40 -18
  65. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/layout/Container.tsx +1 -1
  66. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/CategoryMenu.tsx +148 -0
  67. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/KnowledgeBase.tsx +20 -7
  68. package/src/components/FileIcon/index.tsx +3 -1
  69. package/src/features/ChatInput/ActionBar/Knowledge/index.tsx +2 -2
  70. package/src/features/FileSidePanel/index.tsx +1 -1
  71. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItem.tsx +80 -0
  72. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItemWrapper.tsx +27 -0
  73. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/List.tsx +104 -23
  74. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/MasonrySkeleton.tsx +62 -0
  75. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/index.tsx +3 -2
  76. package/src/features/KnowledgeBaseModal/CreateNew/CreateForm.tsx +1 -1
  77. package/src/features/KnowledgeManager/DocumentExplorer/DocumentActions.tsx +111 -0
  78. package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditor.tsx +723 -0
  79. package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditorPlaceholder.tsx +169 -0
  80. package/src/features/KnowledgeManager/DocumentExplorer/DocumentListItem.tsx +148 -0
  81. package/src/features/KnowledgeManager/DocumentExplorer/DocumentListSkeleton.tsx +39 -0
  82. package/src/features/KnowledgeManager/DocumentExplorer/NoteEditorModal.tsx +348 -0
  83. package/src/features/KnowledgeManager/DocumentExplorer/RenamePopover.tsx +163 -0
  84. package/src/features/KnowledgeManager/DocumentExplorer/index.tsx +318 -0
  85. package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/index.tsx +48 -9
  86. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/DefaultFileItem.tsx +149 -0
  87. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/ImageFileItem.tsx +245 -0
  88. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/MarkdownFileItem.tsx +232 -0
  89. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/NoteFileItem.tsx +230 -0
  90. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/index.tsx +398 -0
  91. package/src/features/KnowledgeManager/FileExplorer/ToolBar/ViewSwitcher.tsx +45 -0
  92. package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/index.tsx +55 -16
  93. package/src/features/KnowledgeManager/Header/AddButton.tsx +118 -0
  94. package/src/features/KnowledgeManager/Header/NewNoteButton.tsx +33 -0
  95. package/src/features/{FileManager → KnowledgeManager}/Header/index.tsx +3 -9
  96. package/src/features/KnowledgeManager/Home/RecentDocumentCard.tsx +116 -0
  97. package/src/features/KnowledgeManager/Home/RecentDocuments.tsx +77 -0
  98. package/src/features/KnowledgeManager/Home/RecentFileCard.tsx +121 -0
  99. package/src/features/KnowledgeManager/Home/RecentFiles.tsx +73 -0
  100. package/src/features/KnowledgeManager/Home/RecentFilesSkeleton.tsx +81 -0
  101. package/src/features/KnowledgeManager/Home/UploadEntries.tsx +208 -0
  102. package/src/features/KnowledgeManager/Home/index.tsx +221 -0
  103. package/src/features/KnowledgeManager/index.tsx +75 -0
  104. package/src/features/Portal/FilePreview/Body/index.tsx +1 -1
  105. package/src/features/Portal/FilePreview/Header.tsx +1 -1
  106. package/src/locales/default/common.ts +1 -0
  107. package/src/locales/default/file.ts +87 -2
  108. package/src/server/routers/lambda/__tests__/file.test.ts +85 -6
  109. package/src/server/routers/lambda/document.ts +57 -0
  110. package/src/server/routers/lambda/file.ts +72 -0
  111. package/src/server/routers/lambda/knowledge.ts +94 -0
  112. package/src/server/services/document/index.ts +103 -0
  113. package/src/services/document/index.ts +44 -0
  114. package/src/services/file/index.ts +5 -3
  115. package/src/store/aiInfra/slices/aiProvider/__tests__/action.test.ts +125 -229
  116. package/src/store/aiInfra/slices/aiProvider/action.ts +113 -33
  117. package/src/store/file/initialState.ts +6 -1
  118. package/src/store/file/slices/chat/action.ts +3 -3
  119. package/src/store/file/slices/document/action.ts +359 -0
  120. package/src/store/file/slices/document/index.ts +3 -0
  121. package/src/store/file/slices/document/initialState.ts +22 -0
  122. package/src/store/file/slices/document/selectors.ts +25 -0
  123. package/src/store/file/slices/fileManager/action.test.ts +16 -9
  124. package/src/store/file/slices/fileManager/action.ts +11 -11
  125. package/src/store/file/store.ts +3 -0
  126. package/src/store/global/initialState.ts +3 -1
  127. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/FileMenu.tsx +0 -75
  128. package/src/features/FileManager/FileList/MasonryFileItem/index.tsx +0 -582
  129. package/src/features/FileManager/index.tsx +0 -36
  130. /package/src/features/{FileManager/FileList/ToolBar → KnowledgeBaseModal/AssignKnowledgeBase}/ViewSwitcher.tsx +0 -0
  131. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/ChunkItem.tsx +0 -0
  132. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/index.tsx +0 -0
  133. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Content.tsx +0 -0
  134. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Loading/index.tsx +0 -0
  135. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/Item.tsx +0 -0
  136. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/index.tsx +0 -0
  137. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/index.tsx +0 -0
  138. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/EmptyStatus.tsx +0 -0
  139. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/ChunkTag.tsx +0 -0
  140. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/DropdownMenu.tsx +0 -0
  141. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileSkeleton.tsx +0 -0
  142. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonryFileItem/MasonryItemWrapper.tsx +0 -0
  143. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonrySkeleton.tsx +0 -0
  144. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/Config.tsx +0 -0
  145. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/MultiSelectActions.tsx +0 -0
  146. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/index.tsx +0 -0
  147. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/useCheckTaskStatus.ts +0 -0
  148. /package/src/features/{FileManager → KnowledgeManager}/Header/FilesSearchBar.tsx +0 -0
  149. /package/src/features/{FileManager → KnowledgeManager}/Header/TogglePanelButton.tsx +0 -0
  150. /package/src/features/{FileManager → KnowledgeManager}/Header/UploadFileButton.tsx +0 -0
  151. /package/src/features/{FileManager → KnowledgeManager}/UploadDock/Item.tsx +0 -0
  152. /package/src/features/{FileManager → KnowledgeManager}/UploadDock/index.tsx +0 -0
@@ -1,11 +1,16 @@
1
1
  import { ImageFileState, initialImageFileState } from './slices/chat';
2
2
  import { FileChunkState, initialFileChunkState } from './slices/chunk';
3
+ import { DocumentState, initialDocumentState } from './slices/document';
3
4
  import { FileManagerState, initialFileManagerState } from './slices/fileManager';
4
5
 
5
- export type FilesStoreState = ImageFileState & FileManagerState & FileChunkState;
6
+ export type FilesStoreState = ImageFileState &
7
+ DocumentState &
8
+ FileManagerState &
9
+ FileChunkState;
6
10
 
7
11
  export const initialState: FilesStoreState = {
8
12
  ...initialImageFileState,
13
+ ...initialDocumentState,
9
14
  ...initialFileManagerState,
10
15
  ...initialFileChunkState,
11
16
  };
@@ -23,14 +23,12 @@ const n = setNamespace('chat');
23
23
  export interface FileAction {
24
24
  clearChatUploadFileList: () => void;
25
25
  dispatchChatUploadFileList: (payload: UploadFileListDispatch) => void;
26
-
27
26
  removeChatUploadFile: (id: string) => Promise<void>;
28
27
  startAsyncTask: (
29
28
  fileId: string,
30
29
  runner: (id: string) => Promise<string>,
31
30
  onFileItemChange: (fileItem: FileListItem) => void,
32
31
  ) => Promise<void>;
33
-
34
32
  uploadChatFiles: (files: File[]) => Promise<void>;
35
33
  }
36
34
 
@@ -43,12 +41,14 @@ export const createFileSlice: StateCreator<
43
41
  clearChatUploadFileList: () => {
44
42
  set({ chatUploadFileList: [] }, false, n('clearChatUploadFileList'));
45
43
  },
44
+
46
45
  dispatchChatUploadFileList: (payload) => {
47
46
  const nextValue = uploadFileListReducer(get().chatUploadFileList, payload);
48
47
  if (nextValue === get().chatUploadFileList) return;
49
48
 
50
49
  set({ chatUploadFileList: nextValue }, false, `dispatchChatFileList/${payload.type}`);
51
50
  },
51
+
52
52
  removeChatUploadFile: async (id) => {
53
53
  const { dispatchChatUploadFileList } = get();
54
54
 
@@ -68,7 +68,7 @@ export const createFileSlice: StateCreator<
68
68
  let fileItem: FileListItem | undefined = undefined;
69
69
 
70
70
  try {
71
- fileItem = await fileService.getFileItem(id);
71
+ fileItem = await fileService.getKnowledgeItem(id);
72
72
  } catch (e) {
73
73
  console.error('getFileItem Error:', e);
74
74
  continue;
@@ -0,0 +1,359 @@
1
+ import { StateCreator } from 'zustand/vanilla';
2
+
3
+ import { documentService } from '@/services/document';
4
+ import { DocumentSourceType, LobeDocument } from '@/types/document';
5
+ import { setNamespace } from '@/utils/storeDebug';
6
+
7
+ import { FileStore } from '../../store';
8
+
9
+ const n = setNamespace('document');
10
+
11
+ const ALLOWED_DOCUMENT_SOURCE_TYPES = new Set(['editor', 'file', 'api']);
12
+ const ALLOWED_DOCUMENT_FILE_TYPES = new Set(['custom/document', 'application/pdf']);
13
+ const EDITOR_DOCUMENT_FILE_TYPE = 'custom/document';
14
+
15
+ /**
16
+ * Check if a document should be displayed in the document list
17
+ */
18
+ const isAllowedDocument = (document: { fileType: string; sourceType: string }) => {
19
+ return (
20
+ ALLOWED_DOCUMENT_SOURCE_TYPES.has(document.sourceType) &&
21
+ ALLOWED_DOCUMENT_FILE_TYPES.has(document.fileType)
22
+ );
23
+ };
24
+
25
+ export interface DocumentAction {
26
+ /**
27
+ * Create a new document with markdown content (not optimistic, waits for server response)
28
+ * Returns the created document
29
+ */
30
+ createDocument: (params: {
31
+ content: string;
32
+ knowledgeBaseId?: string;
33
+ title: string;
34
+ }) => Promise<{ [key: string]: any; id: string }>;
35
+ /**
36
+ * Create a new optimistic document immediately in local map
37
+ * Returns the temporary ID for the new document
38
+ */
39
+ createOptimisticDocument: (title?: string) => string;
40
+ /**
41
+ * Duplicate an existing document
42
+ * Returns the created document
43
+ */
44
+ duplicateDocument: (documentId: string) => Promise<{ [key: string]: any; id: string }>;
45
+ /**
46
+ * Fetch all documents from the server
47
+ */
48
+ fetchDocuments: () => Promise<void>;
49
+ /**
50
+ * Get documents from local optimistic map merged with server data
51
+ */
52
+ getOptimisticDocuments: () => LobeDocument[];
53
+ /**
54
+ * Remove a document (deletes from documents table)
55
+ */
56
+ removeDocument: (documentId: string) => Promise<void>;
57
+ /**
58
+ * Remove a temp document from local map
59
+ */
60
+ removeTempDocument: (tempId: string) => void;
61
+ /**
62
+ * Replace a temp document with real document data (for smooth UX when creating documents)
63
+ */
64
+ replaceTempDocumentWithReal: (tempId: string, realDocument: LobeDocument) => void;
65
+ /**
66
+ * Optimistically update document in local map and queue for DB sync
67
+ */
68
+ updateDocumentOptimistically: (
69
+ documentId: string,
70
+ updates: Partial<LobeDocument>,
71
+ ) => Promise<void>;
72
+ }
73
+
74
+ export const createDocumentSlice: StateCreator<
75
+ FileStore,
76
+ [['zustand/devtools', never]],
77
+ [],
78
+ DocumentAction
79
+ > = (set, get) => ({
80
+ createDocument: async ({ title, content, knowledgeBaseId }) => {
81
+ const now = Date.now();
82
+
83
+ // Create document with markdown content, leave editorData as empty JSON object
84
+ const newDoc = await documentService.createDocument({
85
+ content,
86
+ editorData: '{}', // Empty JSON object instead of empty string
87
+ fileType: EDITOR_DOCUMENT_FILE_TYPE,
88
+ knowledgeBaseId,
89
+ metadata: {
90
+ createdAt: now,
91
+ },
92
+ title,
93
+ });
94
+
95
+ // Don't refresh documents here - the caller will handle replacing the temp document
96
+ // with the real one via replaceTempDocumentWithReal, which provides a smooth UX
97
+ // without triggering the loading skeleton
98
+
99
+ return newDoc;
100
+ },
101
+
102
+ createOptimisticDocument: (title = 'Untitled') => {
103
+ const { localDocumentMap } = get();
104
+
105
+ // Generate temporary ID with prefix to identify optimistic documents
106
+ const tempId = `temp-document-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
107
+ const now = new Date();
108
+
109
+ const newDocument: LobeDocument = {
110
+ content: null,
111
+ createdAt: now,
112
+ editorData: null,
113
+ fileType: EDITOR_DOCUMENT_FILE_TYPE,
114
+ filename: title,
115
+ id: tempId,
116
+ metadata: {},
117
+ source: 'document',
118
+ sourceType: DocumentSourceType.EDITOR,
119
+ title: title,
120
+ totalCharCount: 0,
121
+ totalLineCount: 0,
122
+ updatedAt: now,
123
+ };
124
+
125
+ // Add to local map
126
+ const newMap = new Map(localDocumentMap);
127
+ newMap.set(tempId, newDocument);
128
+ set({ localDocumentMap: newMap }, false, n('createOptimisticDocument'));
129
+
130
+ return tempId;
131
+ },
132
+
133
+ duplicateDocument: async (documentId) => {
134
+ // Fetch the source document
135
+ const sourceDoc = await documentService.getDocumentById(documentId);
136
+
137
+ if (!sourceDoc) {
138
+ throw new Error(`Document with ID ${documentId} not found`);
139
+ }
140
+
141
+ // Create a new document with copied properties
142
+ const newDoc = await documentService.createDocument({
143
+ content: sourceDoc.content || '',
144
+ editorData: sourceDoc.editorData
145
+ ? typeof sourceDoc.editorData === 'string'
146
+ ? sourceDoc.editorData
147
+ : JSON.stringify(sourceDoc.editorData)
148
+ : '{}',
149
+ fileType: sourceDoc.fileType,
150
+ metadata: {
151
+ ...sourceDoc.metadata,
152
+ createdAt: Date.now(),
153
+ duplicatedFrom: documentId,
154
+ },
155
+ title: `${sourceDoc.title} (Copy)`,
156
+ });
157
+
158
+ // Add the new document to local map immediately for instant UI update
159
+ const { localDocumentMap } = get();
160
+ const newMap = new Map(localDocumentMap);
161
+ const editorDoc: LobeDocument = {
162
+ content: newDoc.content || null,
163
+ createdAt: newDoc.createdAt ? new Date(newDoc.createdAt) : new Date(),
164
+ editorData:
165
+ typeof newDoc.editorData === 'string'
166
+ ? JSON.parse(newDoc.editorData)
167
+ : newDoc.editorData || null,
168
+ fileType: newDoc.fileType,
169
+ filename: newDoc.title || newDoc.filename || '',
170
+ id: newDoc.id,
171
+ metadata: newDoc.metadata || {},
172
+ source: 'document',
173
+ sourceType: DocumentSourceType.EDITOR,
174
+ title: newDoc.title || '',
175
+ totalCharCount: newDoc.content?.length || 0,
176
+ totalLineCount: 0,
177
+ updatedAt: newDoc.updatedAt ? new Date(newDoc.updatedAt) : new Date(),
178
+ };
179
+ newMap.set(newDoc.id, editorDoc);
180
+ set({ localDocumentMap: newMap }, false, n('duplicateDocument'));
181
+
182
+ // Don't refresh documents here - we've already added it to the local map
183
+ // This prevents the loading skeleton from appearing
184
+
185
+ return newDoc;
186
+ },
187
+
188
+ fetchDocuments: async () => {
189
+ set({ isDocumentListLoading: true }, false, n('fetchDocuments/start'));
190
+
191
+ try {
192
+ const documentItems = await documentService.queryDocuments();
193
+ const documents = documentItems.filter(isAllowedDocument).map((doc) => ({
194
+ ...doc,
195
+ filename: doc.filename ?? doc.title ?? 'Untitled',
196
+ })) as LobeDocument[];
197
+ set({ documents, isDocumentListLoading: false }, false, n('fetchDocuments/success'));
198
+
199
+ // Sync with local map: remove temp documents that now exist on server
200
+ const { localDocumentMap } = get();
201
+ const newMap = new Map(localDocumentMap);
202
+
203
+ for (const [id] of localDocumentMap.entries()) {
204
+ if (id.startsWith('temp-document-')) {
205
+ newMap.delete(id);
206
+ }
207
+ }
208
+
209
+ set({ localDocumentMap: newMap }, false, n('fetchDocuments/syncLocalMap'));
210
+ } catch (error) {
211
+ console.error('Failed to fetch documents:', error);
212
+ set({ isDocumentListLoading: false }, false, n('fetchDocuments/error'));
213
+ throw error;
214
+ }
215
+ },
216
+
217
+ getOptimisticDocuments: () => {
218
+ const { localDocumentMap, documents } = get();
219
+
220
+ // Track which documents we've added
221
+ const addedIds = new Set<string>();
222
+
223
+ // Create result array - start with server documents
224
+ const result: LobeDocument[] = documents.map((document) => {
225
+ addedIds.add(document.id);
226
+ // Check if we have a local optimistic update for this document
227
+ const localUpdate = localDocumentMap.get(document.id);
228
+ // If local update exists and is newer, use it; otherwise use server version
229
+ if (localUpdate && new Date(localUpdate.updatedAt) >= new Date(document.updatedAt)) {
230
+ return localUpdate;
231
+ }
232
+ return document;
233
+ });
234
+
235
+ // Add any optimistic documents that aren't in server list yet (e.g., newly created temp documents)
236
+ for (const [id, document] of localDocumentMap.entries()) {
237
+ if (!addedIds.has(id)) {
238
+ result.unshift(document); // Add new documents to the beginning
239
+ }
240
+ }
241
+
242
+ return result;
243
+ },
244
+
245
+ removeDocument: async (documentId) => {
246
+ // Remove from local optimistic map first (optimistic update)
247
+ const { localDocumentMap, documents } = get();
248
+ const newMap = new Map(localDocumentMap);
249
+ newMap.delete(documentId);
250
+
251
+ // Also remove from documents array to update the list immediately
252
+ const newDocuments = documents.filter((doc) => doc.id !== documentId);
253
+
254
+ set(
255
+ { documents: newDocuments, localDocumentMap: newMap },
256
+ false,
257
+ n('removeDocument/optimistic'),
258
+ );
259
+
260
+ try {
261
+ // Delete from documents table
262
+ await documentService.deleteDocument(documentId);
263
+ // No need to call fetchDocuments() - optimistic update is enough
264
+ } catch (error) {
265
+ console.error('Failed to delete document:', error);
266
+ // Restore the document in local map and documents array on error
267
+ const restoredMap = new Map(localDocumentMap);
268
+ set({ documents, localDocumentMap: restoredMap }, false, n('removeDocument/restore'));
269
+ throw error;
270
+ }
271
+ },
272
+
273
+ removeTempDocument: (tempId) => {
274
+ const { localDocumentMap } = get();
275
+ const newMap = new Map(localDocumentMap);
276
+ newMap.delete(tempId);
277
+ set({ localDocumentMap: newMap }, false, n('removeTempDocument'));
278
+ },
279
+
280
+ replaceTempDocumentWithReal: (tempId, realDocument) => {
281
+ const { localDocumentMap } = get();
282
+ const newMap = new Map(localDocumentMap);
283
+
284
+ // Remove temp document
285
+ newMap.delete(tempId);
286
+
287
+ // Add real document with same position
288
+ newMap.set(realDocument.id, realDocument);
289
+
290
+ set({ localDocumentMap: newMap }, false, n('replaceTempDocumentWithReal'));
291
+ },
292
+
293
+ updateDocumentOptimistically: async (documentId, updates) => {
294
+ const { localDocumentMap, documents } = get();
295
+
296
+ // Find the document either in local map or documents state
297
+ let existingDocument = localDocumentMap.get(documentId);
298
+ if (!existingDocument) {
299
+ existingDocument = documents.find((doc) => doc.id === documentId);
300
+ }
301
+
302
+ if (!existingDocument) {
303
+ console.warn('[updateDocumentOptimistically] Document not found:', documentId);
304
+ return;
305
+ }
306
+
307
+ // Create updated document with new timestamp
308
+ // Merge metadata if both exist, otherwise use the update's metadata or preserve existing
309
+ const mergedMetadata =
310
+ updates.metadata !== undefined
311
+ ? { ...existingDocument.metadata, ...updates.metadata }
312
+ : existingDocument.metadata;
313
+
314
+ // Clean up undefined values from metadata
315
+ const cleanedMetadata = mergedMetadata
316
+ ? Object.fromEntries(Object.entries(mergedMetadata).filter(([, v]) => v !== undefined))
317
+ : {};
318
+
319
+ const updatedDocument: LobeDocument = {
320
+ ...existingDocument,
321
+ ...updates,
322
+ metadata: cleanedMetadata,
323
+ title: updates.title || existingDocument.title,
324
+ updatedAt: new Date(),
325
+ };
326
+
327
+ // Update local map immediately for optimistic UI
328
+ const newMap = new Map(localDocumentMap);
329
+ newMap.set(documentId, updatedDocument);
330
+ set({ localDocumentMap: newMap }, false, n('updateDocumentOptimistically'));
331
+
332
+ // Queue background sync to DB
333
+ try {
334
+ await documentService.updateDocument({
335
+ content: updatedDocument.content || '',
336
+ editorData:
337
+ typeof updatedDocument.editorData === 'string'
338
+ ? updatedDocument.editorData
339
+ : JSON.stringify(updatedDocument.editorData || {}),
340
+ id: documentId,
341
+ metadata: updatedDocument.metadata || {},
342
+ title: updatedDocument.title || updatedDocument.filename,
343
+ });
344
+
345
+ // After successful sync, refresh file list to get server state
346
+ // This will eventually sync back to the map via syncDocumentMapWithServer
347
+ } catch (error) {
348
+ console.error('[updateDocumentOptimistically] Failed to sync to DB:', error);
349
+ // On error, revert the optimistic update
350
+ const revertMap = new Map(localDocumentMap);
351
+ if (existingDocument) {
352
+ revertMap.set(documentId, existingDocument);
353
+ } else {
354
+ revertMap.delete(documentId);
355
+ }
356
+ set({ localDocumentMap: revertMap }, false, n('revertOptimisticUpdate'));
357
+ }
358
+ },
359
+ });
@@ -0,0 +1,3 @@
1
+ export * from './action';
2
+ export * from './initialState';
3
+ export * from './selectors';
@@ -0,0 +1,22 @@
1
+ import { LobeDocument } from '@/types/document';
2
+
3
+ export interface DocumentState {
4
+ /**
5
+ * Server documents fetched from document service
6
+ */
7
+ documents: LobeDocument[];
8
+ /**
9
+ * Loading state for document fetching
10
+ */
11
+ isDocumentListLoading: boolean;
12
+ /**
13
+ * Local optimistic document map for immediate UI updates
14
+ */
15
+ localDocumentMap: Map<string, LobeDocument>;
16
+ }
17
+
18
+ export const initialDocumentState: DocumentState = {
19
+ documents: [],
20
+ isDocumentListLoading: false,
21
+ localDocumentMap: new Map(),
22
+ };
@@ -0,0 +1,25 @@
1
+ import { FilesStoreState } from '../../initialState';
2
+
3
+ const getDocumentById = (documentId: string | undefined) => (s: FilesStoreState) => {
4
+ if (!documentId) return undefined;
5
+
6
+ // First check local optimistic map
7
+ const localDocument = s.localDocumentMap.get(documentId);
8
+
9
+ // Then check server documents
10
+ const serverDocument = s.documents.find((doc) => doc.id === documentId);
11
+
12
+ // If both exist, prefer the local update if it's newer
13
+ if (localDocument && serverDocument) {
14
+ return new Date(localDocument.updatedAt) >= new Date(serverDocument.updatedAt)
15
+ ? localDocument
16
+ : serverDocument;
17
+ }
18
+
19
+ // Return whichever exists, or undefined if neither exists
20
+ return localDocument || serverDocument;
21
+ };
22
+
23
+ export const documentSelectors = {
24
+ getDocumentById,
25
+ };
@@ -61,6 +61,7 @@ vi.mock('@/libs/trpc/client', () => ({
61
61
  file: {
62
62
  getFileItemById: { query: vi.fn() },
63
63
  getFiles: { query: vi.fn() },
64
+ getKnowledgeItems: { query: vi.fn() },
64
65
  removeFileAsyncTask: { mutate: vi.fn() },
65
66
  },
66
67
  },
@@ -610,7 +611,7 @@ describe('FileManagerActions', () => {
610
611
  await result.current.refreshFileList();
611
612
  });
612
613
 
613
- expect(mutate).toHaveBeenCalledWith(['useFetchFileManage', params]);
614
+ expect(mutate).toHaveBeenCalledWith(['useFetchKnowledgeItems', params]);
614
615
  });
615
616
 
616
617
  it('should call mutate with undefined params', async () => {
@@ -620,7 +621,7 @@ describe('FileManagerActions', () => {
620
621
  await result.current.refreshFileList();
621
622
  });
622
623
 
623
- expect(mutate).toHaveBeenCalledWith(['useFetchFileManage', undefined]);
624
+ expect(mutate).toHaveBeenCalledWith(['useFetchKnowledgeItems', undefined]);
624
625
  });
625
626
  });
626
627
 
@@ -802,7 +803,7 @@ describe('FileManagerActions', () => {
802
803
  it('should not fetch when id is undefined', () => {
803
804
  const { result } = renderHook(() => useStore());
804
805
 
805
- renderHook(() => result.current.useFetchFileItem(undefined));
806
+ renderHook(() => result.current.useFetchKnowledgeItem(undefined));
806
807
 
807
808
  expect(lambdaClient.file.getFileItemById.query).not.toHaveBeenCalled();
808
809
  });
@@ -820,13 +821,16 @@ describe('FileManagerActions', () => {
820
821
  id: 'file-1',
821
822
  name: 'test.txt',
822
823
  size: 100,
824
+ sourceType: 'file',
823
825
  updatedAt: new Date(),
824
826
  url: 'http://example.com/test.txt',
825
827
  };
826
828
 
827
829
  vi.mocked(lambdaClient.file.getFileItemById.query).mockResolvedValue(mockFile);
828
830
 
829
- const { result: swrResult } = renderHook(() => result.current.useFetchFileItem('file-1'));
831
+ const { result: swrResult } = renderHook(() =>
832
+ result.current.useFetchKnowledgeItem('file-1'),
833
+ );
830
834
 
831
835
  await waitFor(() => {
832
836
  expect(swrResult.current.data).toEqual(mockFile);
@@ -834,7 +838,7 @@ describe('FileManagerActions', () => {
834
838
  });
835
839
  });
836
840
 
837
- describe('useFetchFileManage', () => {
841
+ describe('useFetchKnowledgeItems', () => {
838
842
  it('should fetch file list with params', async () => {
839
843
  const { result } = renderHook(() => useStore());
840
844
 
@@ -849,6 +853,7 @@ describe('FileManagerActions', () => {
849
853
  id: 'file-1',
850
854
  name: 'test1.txt',
851
855
  size: 100,
856
+ sourceType: 'file',
852
857
  updatedAt: new Date(),
853
858
  url: 'http://example.com/test1.txt',
854
859
  },
@@ -862,15 +867,16 @@ describe('FileManagerActions', () => {
862
867
  id: 'file-2',
863
868
  name: 'test2.txt',
864
869
  size: 200,
870
+ sourceType: 'file',
865
871
  updatedAt: new Date(),
866
872
  url: 'http://example.com/test2.txt',
867
873
  },
868
874
  ];
869
875
 
870
- vi.mocked(lambdaClient.file.getFiles.query).mockResolvedValue(mockFiles);
876
+ vi.mocked(lambdaClient.file.getKnowledgeItems.query).mockResolvedValue(mockFiles);
871
877
 
872
878
  const params = { category: 'all' as any };
873
- const { result: swrResult } = renderHook(() => result.current.useFetchFileManage(params));
879
+ const { result: swrResult } = renderHook(() => result.current.useFetchKnowledgeItems(params));
874
880
 
875
881
  await waitFor(() => {
876
882
  expect(swrResult.current.data).toEqual(mockFiles);
@@ -891,15 +897,16 @@ describe('FileManagerActions', () => {
891
897
  id: 'file-1',
892
898
  name: 'test.txt',
893
899
  size: 100,
900
+ sourceType: 'file',
894
901
  updatedAt: new Date(),
895
902
  url: 'http://example.com/test.txt',
896
903
  },
897
904
  ];
898
905
 
899
- vi.mocked(lambdaClient.file.getFiles.query).mockResolvedValue(mockFiles);
906
+ vi.mocked(lambdaClient.file.getKnowledgeItems.query).mockResolvedValue(mockFiles);
900
907
 
901
908
  const params = { category: 'all' as any };
902
- renderHook(() => result.current.useFetchFileManage(params));
909
+ renderHook(() => result.current.useFetchKnowledgeItems(params));
903
910
 
904
911
  await waitFor(() => {
905
912
  expect(result.current.fileList).toEqual(mockFiles);
@@ -4,7 +4,7 @@ import { StateCreator } from 'zustand/vanilla';
4
4
 
5
5
  import { FILE_UPLOAD_BLACKLIST, MAX_UPLOAD_FILE_COUNT } from '@/const/file';
6
6
  import { useClientDataSWR } from '@/libs/swr';
7
- import { fileService , FileService } from '@/services/file';
7
+ import { FileService, fileService } from '@/services/file';
8
8
  import { ragService } from '@/services/rag';
9
9
  import {
10
10
  UploadFileListDispatch,
@@ -35,11 +35,11 @@ export interface FileManageAction {
35
35
  toggleEmbeddingIds: (ids: string[], loading?: boolean) => void;
36
36
  toggleParsingIds: (ids: string[], loading?: boolean) => void;
37
37
 
38
- useFetchFileItem: (id?: string) => SWRResponse<FileListItem | undefined>;
39
- useFetchFileManage: (params: QueryFileListParams) => SWRResponse<FileListItem[]>;
38
+ useFetchKnowledgeItem: (id?: string) => SWRResponse<FileListItem | undefined>;
39
+ useFetchKnowledgeItems: (params: QueryFileListParams) => SWRResponse<FileListItem[]>;
40
40
  }
41
41
 
42
- const FETCH_FILE_LIST_KEY = 'useFetchFileManage';
42
+ const FETCH_ALL_KNOWLEDGE_KEY = 'useFetchKnowledgeItems';
43
43
 
44
44
  export const createFileManageSlice: StateCreator<
45
45
  FileStore,
@@ -171,7 +171,7 @@ export const createFileManageSlice: StateCreator<
171
171
  get().toggleParsingIds([id], false);
172
172
  },
173
173
  refreshFileList: async () => {
174
- await mutate([FETCH_FILE_LIST_KEY, get().queryListParams]);
174
+ await mutate([FETCH_ALL_KNOWLEDGE_KEY, get().queryListParams]);
175
175
  },
176
176
  removeAllFiles: async () => {
177
177
  await fileService.removeAllFiles();
@@ -220,15 +220,15 @@ export const createFileManageSlice: StateCreator<
220
220
  });
221
221
  },
222
222
 
223
- useFetchFileItem: (id) =>
224
- useClientDataSWR<FileListItem | undefined>(!id ? null : ['useFetchFileItem', id], () =>
225
- serverFileService.getFileItem(id!),
223
+ useFetchKnowledgeItem: (id) =>
224
+ useClientDataSWR<FileListItem | undefined>(!id ? null : ['useFetchKnowledgeItem', id], () =>
225
+ serverFileService.getKnowledgeItem(id!),
226
226
  ),
227
227
 
228
- useFetchFileManage: (params) =>
228
+ useFetchKnowledgeItems: (params) =>
229
229
  useClientDataSWR<FileListItem[]>(
230
- [FETCH_FILE_LIST_KEY, params],
231
- () => serverFileService.getFiles(params),
230
+ [FETCH_ALL_KNOWLEDGE_KEY, params],
231
+ () => serverFileService.getKnowledgeItems(params),
232
232
  {
233
233
  onSuccess: (data) => {
234
234
  set({ fileList: data, queryListParams: params });
@@ -6,6 +6,7 @@ import { createDevtools } from '../middleware/createDevtools';
6
6
  import { FilesStoreState, initialState } from './initialState';
7
7
  import { FileAction, createFileSlice } from './slices/chat';
8
8
  import { FileChunkAction, createFileChunkSlice } from './slices/chunk';
9
+ import { DocumentAction, createDocumentSlice } from './slices/document';
9
10
  import { FileManageAction, createFileManageSlice } from './slices/fileManager';
10
11
  import { TTSFileAction, createTTSFileSlice } from './slices/tts';
11
12
  import { FileUploadAction, createFileUploadSlice } from './slices/upload/action';
@@ -14,6 +15,7 @@ import { FileUploadAction, createFileUploadSlice } from './slices/upload/action'
14
15
 
15
16
  export type FileStore = FilesStoreState &
16
17
  FileAction &
18
+ DocumentAction &
17
19
  TTSFileAction &
18
20
  FileManageAction &
19
21
  FileChunkAction &
@@ -22,6 +24,7 @@ export type FileStore = FilesStoreState &
22
24
  const createStore: StateCreator<FileStore, [['zustand/devtools', never]]> = (...parameters) => ({
23
25
  ...initialState,
24
26
  ...createFileSlice(...parameters),
27
+ ...createDocumentSlice(...parameters),
25
28
  ...createFileManageSlice(...parameters),
26
29
  ...createTTSFileSlice(...parameters),
27
30
  ...createFileChunkSlice(...parameters),
@@ -9,7 +9,7 @@ import { AsyncLocalStorage } from '@/utils/localStorage';
9
9
  export enum SidebarTabKey {
10
10
  Chat = 'chat',
11
11
  Discover = 'discover',
12
- Files = 'files',
12
+ Files = 'knowledge',
13
13
  Image = 'image',
14
14
  Me = 'me',
15
15
  Setting = 'settings',
@@ -72,6 +72,7 @@ export interface SystemStatus {
72
72
  */
73
73
  isEnablePglite?: boolean;
74
74
  isShowCredit?: boolean;
75
+ knowledgeBaseModalViewMode?: 'list' | 'masonry';
75
76
  language?: LocaleMode;
76
77
  /**
77
78
  * 记住用户最后选择的图像生成模型
@@ -142,6 +143,7 @@ export const INITIAL_STATUS = {
142
143
  hideThreadLimitAlert: false,
143
144
  imagePanelWidth: 320,
144
145
  imageTopicPanelWidth: 80,
146
+ knowledgeBaseModalViewMode: 'list' as const,
145
147
  mobileShowTopic: false,
146
148
  noWideScreen: true,
147
149
  portalWidth: 400,