@lobehub/lobehub 2.0.0-next.255 → 2.0.0-next.256

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 (140) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/en-US/plugin.json +1 -0
  4. package/locales/zh-CN/plugin.json +1 -0
  5. package/package.json +1 -1
  6. package/packages/builtin-tool-notebook/src/client/Placeholder/CreateDocument.tsx +6 -6
  7. package/packages/builtin-tool-notebook/src/client/Render/CreateDocument/DocumentCard.tsx +23 -10
  8. package/packages/builtin-tool-notebook/src/client/Streaming/CreateDocument/index.tsx +1 -1
  9. package/packages/database/src/models/__tests__/agent.test.ts +91 -4
  10. package/packages/database/src/models/agent.ts +15 -7
  11. package/packages/editor-runtime/src/EditorRuntime.ts +1 -1
  12. package/packages/editor-runtime/src/__tests__/EditorRuntime.real.test.ts +65 -4
  13. package/packages/editor-runtime/src/__tests__/__snapshots__/EditorRuntime.real.test.ts.snap +108 -17
  14. package/packages/editor-runtime/src/__tests__/fixtures/remove-then-add.json +636 -0
  15. package/packages/editor-runtime/src/__tests__/fixtures/remove.json +1 -0
  16. package/packages/types/src/agent/agentConfig.ts +8 -8
  17. package/src/app/[variants]/(main)/chat/features/Portal/_layout/Mobile.tsx +2 -1
  18. package/src/app/[variants]/(main)/group/features/Portal/_layout/Mobile.tsx +2 -1
  19. package/src/app/[variants]/(main)/home/_layout/hooks/useCreateMenuItems.tsx +2 -2
  20. package/src/app/[variants]/(main)/page/{features/PageTitle → PageTitle}/index.tsx +0 -1
  21. package/src/app/[variants]/(main)/page/[id]/index.tsx +43 -1
  22. package/src/app/[variants]/(main)/page/_layout/Body/AllPagesDrawer/Content.tsx +15 -15
  23. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/Editing.tsx +3 -3
  24. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/index.tsx +7 -12
  25. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/useDropdownMenu.tsx +5 -5
  26. package/src/app/[variants]/(main)/page/_layout/Body/List/index.tsx +7 -7
  27. package/src/app/[variants]/(main)/page/_layout/Body/index.tsx +15 -9
  28. package/src/app/[variants]/(main)/page/_layout/Body/useDropdownMenu.tsx +3 -3
  29. package/src/app/[variants]/(main)/page/_layout/DataSync.tsx +15 -0
  30. package/src/app/[variants]/(main)/page/_layout/Header/AddButton.tsx +2 -2
  31. package/src/app/[variants]/(main)/page/_layout/index.tsx +2 -0
  32. package/src/app/[variants]/(main)/page/index.tsx +3 -7
  33. package/src/components/Editor/AutoSaveHint.tsx +1 -1
  34. package/src/features/Conversation/Messages/User/Actions/index.tsx +5 -1
  35. package/src/features/EditorCanvas/AutoSaveHint.tsx +37 -0
  36. package/src/features/{PageEditor → EditorCanvas}/DiffAllToolbar.tsx +57 -16
  37. package/src/features/EditorCanvas/DocumentIdMode.tsx +111 -0
  38. package/src/features/EditorCanvas/EditorCanvas.tsx +148 -0
  39. package/src/features/EditorCanvas/EditorDataMode.tsx +64 -0
  40. package/src/features/EditorCanvas/ErrorBoundary.tsx +66 -0
  41. package/src/features/EditorCanvas/InlineToolbar.tsx +245 -0
  42. package/src/features/EditorCanvas/InternalEditor.tsx +134 -0
  43. package/src/features/{PageEditor/EditorCanvas → EditorCanvas}/actions.ts +10 -8
  44. package/src/features/EditorCanvas/index.ts +9 -0
  45. package/src/features/PageEditor/EditorCanvas/index.tsx +14 -111
  46. package/src/features/PageEditor/EditorCanvas/useAskCopilotItem.tsx +95 -0
  47. package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +1 -1
  48. package/src/features/PageEditor/Header/Breadcrumb.tsx +2 -2
  49. package/src/features/PageEditor/Header/index.tsx +15 -18
  50. package/src/features/PageEditor/Header/useMenu.tsx +12 -9
  51. package/src/features/PageEditor/PageEditor.tsx +45 -21
  52. package/src/features/PageEditor/PageEditorProvider.tsx +13 -1
  53. package/src/features/PageEditor/PageTitle/index.tsx +2 -2
  54. package/src/features/PageEditor/StoreUpdater.tsx +35 -308
  55. package/src/features/PageEditor/{Body/Title.tsx → TitleSection.tsx} +16 -16
  56. package/src/features/PageEditor/store/action.ts +96 -188
  57. package/src/features/PageEditor/store/initialState.ts +16 -21
  58. package/src/features/PageEditor/store/selectors.ts +3 -4
  59. package/src/features/PageExplorer/PageExplorerPlaceholder.tsx +22 -14
  60. package/src/features/PageExplorer/index.tsx +34 -67
  61. package/src/features/Portal/Artifacts/index.ts +0 -2
  62. package/src/features/Portal/Document/AutoSaveHint.tsx +7 -6
  63. package/src/features/Portal/Document/Body.tsx +1 -3
  64. package/src/features/Portal/Document/EditorCanvas.tsx +7 -50
  65. package/src/features/Portal/Document/Header.tsx +13 -10
  66. package/src/features/Portal/Document/TodoList.tsx +6 -4
  67. package/src/features/Portal/Document/Wrapper.tsx +3 -11
  68. package/src/features/Portal/Document/index.ts +0 -2
  69. package/src/features/Portal/FilePreview/index.ts +0 -2
  70. package/src/features/Portal/GroupThread/index.ts +0 -3
  71. package/src/features/Portal/MessageDetail/index.ts +0 -2
  72. package/src/features/Portal/Notebook/index.ts +0 -2
  73. package/src/features/Portal/Plugins/index.ts +0 -2
  74. package/src/features/Portal/Thread/index.ts +0 -3
  75. package/src/features/Portal/components/Header.tsx +18 -6
  76. package/src/features/Portal/router.tsx +34 -97
  77. package/src/features/Portal/type.ts +0 -2
  78. package/src/locales/default/plugin.ts +1 -0
  79. package/src/store/chat/slices/portal/action.test.ts +218 -15
  80. package/src/store/chat/slices/portal/action.ts +194 -41
  81. package/src/store/chat/slices/portal/initialState.ts +40 -1
  82. package/src/store/chat/slices/portal/selectors/thread.ts +44 -3
  83. package/src/store/chat/slices/portal/selectors.test.ts +119 -17
  84. package/src/store/chat/slices/portal/selectors.ts +117 -36
  85. package/src/store/document/index.ts +17 -5
  86. package/src/store/document/slices/document/action.ts +209 -0
  87. package/src/store/document/slices/document/index.ts +6 -0
  88. package/src/store/document/slices/editor/action.test.ts +340 -0
  89. package/src/store/document/slices/editor/action.ts +133 -149
  90. package/src/store/document/slices/editor/index.ts +9 -2
  91. package/src/store/document/slices/editor/initialState.ts +66 -29
  92. package/src/store/document/slices/editor/reducer.test.ts +217 -0
  93. package/src/store/document/slices/editor/reducer.ts +67 -0
  94. package/src/store/document/slices/editor/selectors.test.ts +395 -0
  95. package/src/store/document/slices/editor/selectors.ts +107 -5
  96. package/src/store/document/store.ts +12 -13
  97. package/src/store/file/slices/document/action.ts +19 -188
  98. package/src/store/file/slices/document/initialState.ts +0 -30
  99. package/src/store/file/slices/document/selectors.ts +25 -59
  100. package/src/store/notebook/index.ts +5 -4
  101. package/src/store/page/index.ts +2 -0
  102. package/src/store/page/initialState.ts +92 -0
  103. package/src/store/page/selectors.ts +5 -0
  104. package/src/store/page/slices/crud/action.ts +477 -0
  105. package/src/store/page/slices/crud/index.ts +2 -0
  106. package/src/store/page/slices/crud/initialState.ts +7 -0
  107. package/src/store/page/slices/internal/action.ts +32 -0
  108. package/src/store/page/slices/internal/index.ts +2 -0
  109. package/src/store/page/slices/internal/reducer.ts +105 -0
  110. package/src/store/page/slices/list/action.ts +206 -0
  111. package/src/store/page/slices/list/index.ts +3 -0
  112. package/src/store/page/slices/list/initialState.ts +29 -0
  113. package/src/store/page/slices/list/selectors.ts +90 -0
  114. package/src/store/page/slices/selection/action.ts +67 -0
  115. package/src/store/page/slices/selection/index.ts +2 -0
  116. package/src/store/page/slices/selection/initialState.ts +11 -0
  117. package/src/store/page/store.ts +29 -0
  118. package/src/store/tool/slices/lobehubSkillStore/selectors.ts +10 -20
  119. package/src/utils/identifier.ts +8 -2
  120. package/src/features/PageEditor/Body/index.tsx +0 -68
  121. package/src/features/PageEditor/EditorCanvas/InlineToolbar.tsx +0 -316
  122. package/src/features/PageEditor/Header/AutoSaveHint.tsx +0 -27
  123. package/src/features/Portal/Artifacts/useEnable.ts +0 -4
  124. package/src/features/Portal/Document/DocumentEditorProvider.tsx +0 -34
  125. package/src/features/Portal/Document/StoreUpdater.tsx +0 -80
  126. package/src/features/Portal/Document/Title.tsx +0 -54
  127. package/src/features/Portal/Document/store/action.ts +0 -114
  128. package/src/features/Portal/Document/store/index.ts +0 -21
  129. package/src/features/Portal/Document/store/initialState.ts +0 -24
  130. package/src/features/Portal/Document/useEnable.ts +0 -8
  131. package/src/features/Portal/FilePreview/useEnable.ts +0 -6
  132. package/src/features/Portal/GroupThread/hook.ts +0 -9
  133. package/src/features/Portal/MessageDetail/useEnable.ts +0 -4
  134. package/src/features/Portal/Notebook/useEnable.ts +0 -6
  135. package/src/features/Portal/Plugins/useEnable.ts +0 -6
  136. package/src/features/Portal/Thread/hook.ts +0 -8
  137. package/src/store/document/slices/notebook/action.ts +0 -119
  138. package/src/store/document/slices/notebook/index.ts +0 -3
  139. package/src/store/document/slices/notebook/initialState.ts +0 -12
  140. package/src/store/document/slices/notebook/selectors.ts +0 -26
@@ -0,0 +1,477 @@
1
+ import type { SWRResponse } from 'swr';
2
+ import { type StateCreator } from 'zustand/vanilla';
3
+
4
+ import { useClientDataSWRWithSync } from '@/libs/swr';
5
+ import { documentService } from '@/services/document';
6
+ import { DocumentSourceType, type LobeDocument } from '@/types/document';
7
+ import { standardizeIdentifier } from '@/utils/identifier';
8
+ import { setNamespace } from '@/utils/storeDebug';
9
+
10
+ import { type PageStore } from '../../store';
11
+
12
+ const n = setNamespace('page/crud');
13
+
14
+ const EDITOR_PAGE_FILE_TYPE = 'custom/document';
15
+
16
+ /**
17
+ * Page update parameters - flattened for easier use
18
+ */
19
+ export interface PageUpdateParams {
20
+ emoji?: string;
21
+ title?: string;
22
+ }
23
+
24
+ export interface CrudAction {
25
+ /**
26
+ * Create a new page with optimistic update (for page explorer)
27
+ */
28
+ createNewPage: (title: string) => Promise<string>;
29
+ /**
30
+ * Create a new optimistic page immediately in documents array
31
+ */
32
+ createOptimisticPage: (title?: string) => string;
33
+ /**
34
+ * Create a new page with markdown content (not optimistic, waits for server response)
35
+ */
36
+ createPage: (params: {
37
+ content?: string;
38
+ knowledgeBaseId?: string;
39
+ parentId?: string;
40
+ title: string;
41
+ }) => Promise<{ [key: string]: any; id: string }>;
42
+ /**
43
+ * Delete a page and update selection if needed
44
+ */
45
+ deletePage: (pageId: string) => Promise<void>;
46
+ /**
47
+ * Duplicate an existing page
48
+ */
49
+ duplicatePage: (pageId: string) => Promise<{ [key: string]: any; id: string }>;
50
+ /**
51
+ * Fetch full page detail by ID and update documents array
52
+ */
53
+ fetchPageDetail: (pageId: string) => Promise<void>;
54
+ navigateToPage: (pageId: string | null) => void;
55
+ /**
56
+ * Remove a page (deletes from documents table)
57
+ */
58
+ removePage: (pageId: string) => Promise<void>;
59
+ /**
60
+ * Remove a temp page from documents array
61
+ */
62
+ removeTempPage: (tempId: string) => void;
63
+ /**
64
+ * Rename a page
65
+ */
66
+ renamePage: (pageId: string, title: string, emoji?: string) => Promise<void>;
67
+ /**
68
+ * Replace a temp page with real page data
69
+ */
70
+ replaceTempPageWithReal: (tempId: string, realPage: LobeDocument) => void;
71
+ /**
72
+ * Update page directly (no optimistic update)
73
+ */
74
+ updatePage: (pageId: string, updates: Partial<LobeDocument>) => Promise<void>;
75
+ /**
76
+ * Optimistically update page in documents array and queue for DB sync
77
+ */
78
+ updatePageOptimistically: (pageId: string, updates: PageUpdateParams) => Promise<void>;
79
+ /**
80
+ * SWR hook to fetch page detail with caching and auto-sync to store
81
+ */
82
+ useFetchPageDetail: (pageId: string | undefined) => SWRResponse<LobeDocument | null>;
83
+ }
84
+
85
+ export const createCrudSlice: StateCreator<
86
+ PageStore,
87
+ [['zustand/devtools', never]],
88
+ [],
89
+ CrudAction
90
+ > = (set, get) => ({
91
+ createNewPage: async (title: string) => {
92
+ const { createOptimisticPage, createPage, replaceTempPageWithReal } = get();
93
+
94
+ // Create optimistic page immediately
95
+ const tempPageId = createOptimisticPage(title);
96
+ set({ isCreatingNew: true, selectedPageId: tempPageId }, false, n('createNewPage/start'));
97
+
98
+ try {
99
+ // Create real page
100
+ const newPage = await createPage({ content: '', title });
101
+
102
+ // Convert to LobeDocument
103
+ const realPage: LobeDocument = {
104
+ content: newPage.content || '',
105
+ createdAt: newPage.createdAt ? new Date(newPage.createdAt) : new Date(),
106
+ editorData:
107
+ typeof newPage.editorData === 'string'
108
+ ? JSON.parse(newPage.editorData)
109
+ : newPage.editorData || null,
110
+ fileType: 'custom/document',
111
+ filename: newPage.title || title,
112
+ id: newPage.id,
113
+ metadata: newPage.metadata || {},
114
+ source: 'document',
115
+ sourceType: DocumentSourceType.EDITOR,
116
+ title: newPage.title || title,
117
+ totalCharCount: newPage.content?.length || 0,
118
+ totalLineCount: 0,
119
+ updatedAt: newPage.updatedAt ? new Date(newPage.updatedAt) : new Date(),
120
+ };
121
+
122
+ // Replace optimistic with real
123
+ replaceTempPageWithReal(tempPageId, realPage);
124
+ set({ isCreatingNew: false, selectedPageId: newPage.id }, false, n('createNewPage/success'));
125
+
126
+ // Navigate to the new page
127
+ get().navigateToPage(newPage.id);
128
+
129
+ return newPage.id;
130
+ } catch (error) {
131
+ console.error('Failed to create page:', error);
132
+ get().removeTempPage(tempPageId);
133
+ set({ isCreatingNew: false, selectedPageId: null }, false, n('createNewPage/error'));
134
+ get().navigate?.('/page');
135
+
136
+ throw error;
137
+ }
138
+ },
139
+ createOptimisticPage: (title = 'Untitled') => {
140
+ // Generate temporary ID with prefix to identify optimistic pages
141
+ const tempId = `temp-page-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
142
+ const now = new Date();
143
+
144
+ const newPage: LobeDocument = {
145
+ content: null,
146
+ createdAt: now,
147
+ editorData: null,
148
+ fileType: EDITOR_PAGE_FILE_TYPE,
149
+ filename: title,
150
+ id: tempId,
151
+ metadata: {},
152
+ source: 'document',
153
+ sourceType: DocumentSourceType.EDITOR,
154
+ title: title,
155
+ totalCharCount: 0,
156
+ totalLineCount: 0,
157
+ updatedAt: now,
158
+ };
159
+
160
+ // Add to documents array via internal dispatch
161
+ get().internal_dispatchDocuments({ document: newPage, type: 'addDocument' });
162
+
163
+ return tempId;
164
+ },
165
+
166
+ createPage: async ({ title, content = '', knowledgeBaseId, parentId }) => {
167
+ const now = Date.now();
168
+
169
+ const newPage = await documentService.createDocument({
170
+ content,
171
+ editorData: '{}',
172
+ fileType: EDITOR_PAGE_FILE_TYPE,
173
+ knowledgeBaseId,
174
+ metadata: {
175
+ createdAt: now,
176
+ },
177
+ parentId,
178
+ title,
179
+ });
180
+
181
+ return newPage;
182
+ },
183
+
184
+ deletePage: async (pageId: string) => {
185
+ const { selectedPageId } = get();
186
+
187
+ if (selectedPageId === pageId) {
188
+ set({ isCreatingNew: false, selectedPageId: null }, false, n('deletePage'));
189
+ get().navigateToPage(null);
190
+ }
191
+ },
192
+
193
+ duplicatePage: async (pageId) => {
194
+ // Fetch the source page
195
+ const sourcePage = await documentService.getDocumentById(pageId);
196
+
197
+ if (!sourcePage) {
198
+ throw new Error(`Page with ID ${pageId} not found`);
199
+ }
200
+
201
+ // Create a new page with copied properties
202
+ const newPage = await documentService.createDocument({
203
+ content: sourcePage.content || '',
204
+ editorData: sourcePage.editorData
205
+ ? typeof sourcePage.editorData === 'string'
206
+ ? sourcePage.editorData
207
+ : JSON.stringify(sourcePage.editorData)
208
+ : '{}',
209
+ fileType: sourcePage.fileType,
210
+ metadata: {
211
+ ...sourcePage.metadata,
212
+ createdAt: Date.now(),
213
+ duplicatedFrom: pageId,
214
+ },
215
+ title: `${sourcePage.title} (Copy)`,
216
+ });
217
+
218
+ // Add the new page to documents array via internal dispatch
219
+ const editorPage: LobeDocument = {
220
+ content: newPage.content || null,
221
+ createdAt: newPage.createdAt ? new Date(newPage.createdAt) : new Date(),
222
+ editorData:
223
+ typeof newPage.editorData === 'string'
224
+ ? JSON.parse(newPage.editorData)
225
+ : newPage.editorData || null,
226
+ fileType: newPage.fileType,
227
+ filename: newPage.title || newPage.filename || '',
228
+ id: newPage.id,
229
+ metadata: newPage.metadata || {},
230
+ source: 'document',
231
+ sourceType: DocumentSourceType.EDITOR,
232
+ title: newPage.title || '',
233
+ totalCharCount: newPage.content?.length || 0,
234
+ totalLineCount: 0,
235
+ updatedAt: newPage.updatedAt ? new Date(newPage.updatedAt) : new Date(),
236
+ };
237
+
238
+ get().internal_dispatchDocuments({ document: editorPage, type: 'addDocument' });
239
+
240
+ return newPage;
241
+ },
242
+
243
+ fetchPageDetail: async (pageId) => {
244
+ try {
245
+ const document = await documentService.getDocumentById(pageId);
246
+
247
+ if (!document) {
248
+ console.warn(`[fetchPageDetail] Page not found: ${pageId}`);
249
+ return;
250
+ }
251
+
252
+ const fullPage: LobeDocument = {
253
+ content: document.content || null,
254
+ createdAt: document.createdAt ? new Date(document.createdAt) : new Date(),
255
+ editorData:
256
+ typeof document.editorData === 'string'
257
+ ? JSON.parse(document.editorData)
258
+ : document.editorData || null,
259
+ fileType: document.fileType,
260
+ filename: document.title || document.filename || 'Untitled',
261
+ id: document.id,
262
+ metadata: document.metadata || {},
263
+ source: 'document',
264
+ sourceType: DocumentSourceType.EDITOR,
265
+ title: document.title || '',
266
+ totalCharCount: document.content?.length || 0,
267
+ totalLineCount: 0,
268
+ updatedAt: document.updatedAt ? new Date(document.updatedAt) : new Date(),
269
+ };
270
+
271
+ // Update document via internal dispatch
272
+ const { documents } = get();
273
+ if (documents?.some((doc) => doc.id === pageId)) {
274
+ get().internal_dispatchDocuments({
275
+ document: fullPage,
276
+ id: pageId,
277
+ type: 'updateDocument',
278
+ });
279
+ } else {
280
+ get().internal_dispatchDocuments({ document: fullPage, type: 'addDocument' });
281
+ }
282
+ } catch (error) {
283
+ console.error('[fetchPageDetail] Failed to fetch page:', error);
284
+ }
285
+ },
286
+
287
+ navigateToPage: (pageId) => {
288
+ if (!pageId) {
289
+ get().navigate?.('/page');
290
+ } else {
291
+ get().navigate?.(`/page/${standardizeIdentifier(pageId)}`);
292
+ }
293
+ },
294
+
295
+ removePage: async (pageId) => {
296
+ const { documents, selectedPageId } = get();
297
+
298
+ // Store original documents for rollback
299
+ const originalDocuments = documents;
300
+
301
+ // Remove from documents array via internal dispatch (optimistic update)
302
+ get().internal_dispatchDocuments({ id: pageId, type: 'removeDocument' });
303
+
304
+ // Clear selected page ID if the deleted page is currently selected
305
+ if (selectedPageId === pageId) {
306
+ set({ selectedPageId: null }, false, n('removePage/clearSelection'));
307
+ get().navigateToPage(null);
308
+ }
309
+
310
+ try {
311
+ // Delete from documents table
312
+ await documentService.deleteDocument(pageId);
313
+ } catch (error) {
314
+ console.error('Failed to delete page:', error);
315
+ // Restore documents on error
316
+ if (originalDocuments) {
317
+ get().internal_dispatchDocuments({ documents: originalDocuments, type: 'setDocuments' });
318
+ }
319
+ if (selectedPageId === pageId) {
320
+ set({ selectedPageId: pageId }, false, n('removePage/restoreSelection'));
321
+ get().navigateToPage(pageId);
322
+ }
323
+ throw error;
324
+ }
325
+ },
326
+
327
+ removeTempPage: (tempId) => {
328
+ get().internal_dispatchDocuments({ id: tempId, type: 'removeDocument' });
329
+ },
330
+
331
+ renamePage: async (pageId: string, title: string, emoji?: string) => {
332
+ const { updatePageOptimistically } = get();
333
+
334
+ try {
335
+ await updatePageOptimistically(pageId, { emoji, title });
336
+ } catch (error) {
337
+ console.error('Failed to rename page:', error);
338
+ } finally {
339
+ set({ renamingPageId: null }, false, n('renamePage'));
340
+ }
341
+ },
342
+
343
+ replaceTempPageWithReal: (tempId, realPage) => {
344
+ get().internal_dispatchDocuments({
345
+ document: realPage,
346
+ oldId: tempId,
347
+ type: 'replaceDocument',
348
+ });
349
+ },
350
+
351
+ updatePage: async (id, updates) => {
352
+ await documentService.updateDocument({
353
+ content: updates.content ?? undefined,
354
+ editorData: updates.editorData
355
+ ? typeof updates.editorData === 'string'
356
+ ? updates.editorData
357
+ : JSON.stringify(updates.editorData)
358
+ : undefined,
359
+ id,
360
+ metadata: updates.metadata,
361
+ parentId: updates.parentId !== undefined ? updates.parentId : undefined,
362
+ title: updates.title,
363
+ });
364
+ await get().refreshDocuments();
365
+ },
366
+
367
+ updatePageOptimistically: async (pageId, updates) => {
368
+ const { documents } = get();
369
+
370
+ // Find the page in documents array
371
+ const existingPage = documents?.find((doc) => doc.id === pageId);
372
+
373
+ if (!existingPage) {
374
+ console.warn('[updatePageOptimistically] Page not found:', pageId);
375
+ return;
376
+ }
377
+
378
+ // Build updated metadata with emoji
379
+ const updatedMetadata = {
380
+ ...existingPage.metadata,
381
+ ...(updates.emoji !== undefined ? { emoji: updates.emoji } : {}),
382
+ };
383
+
384
+ // Clean up undefined values from metadata
385
+ const cleanedMetadata = Object.fromEntries(
386
+ Object.entries(updatedMetadata).filter(([, v]) => v !== undefined),
387
+ );
388
+
389
+ const updatedPage: LobeDocument = {
390
+ ...existingPage,
391
+ metadata: cleanedMetadata,
392
+ title: updates.title ?? existingPage.title,
393
+ updatedAt: new Date(),
394
+ };
395
+
396
+ // Update documents array via internal dispatch (optimistic)
397
+ get().internal_dispatchDocuments({ document: updatedPage, id: pageId, type: 'updateDocument' });
398
+
399
+ // Queue background sync to DB
400
+ try {
401
+ await documentService.updateDocument({
402
+ content: updatedPage.content || '',
403
+ editorData:
404
+ typeof updatedPage.editorData === 'string'
405
+ ? updatedPage.editorData
406
+ : JSON.stringify(updatedPage.editorData || {}),
407
+ id: pageId,
408
+ metadata: updatedPage.metadata || {},
409
+ parentId: updatedPage.parentId || undefined,
410
+ title: updatedPage.title || updatedPage.filename,
411
+ });
412
+
413
+ // After successful sync, refresh document list to get server state
414
+ await get().refreshDocuments();
415
+ } catch (error) {
416
+ console.error('[updatePageOptimistically] Failed to sync to DB:', error);
417
+ // On error, revert by restoring original page
418
+ get().internal_dispatchDocuments({
419
+ document: existingPage,
420
+ id: pageId,
421
+ type: 'updateDocument',
422
+ });
423
+ }
424
+ },
425
+
426
+ useFetchPageDetail: (pageId) => {
427
+ const swrKey = pageId ? ['pageDetail', pageId] : null;
428
+
429
+ return useClientDataSWRWithSync<LobeDocument | null>(
430
+ swrKey,
431
+ async () => {
432
+ if (!pageId) return null;
433
+
434
+ const document = await documentService.getDocumentById(pageId);
435
+ if (!document) {
436
+ console.warn(`[useFetchPageDetail] Page not found: ${pageId}`);
437
+ return null;
438
+ }
439
+
440
+ // Transform API response to LobeDocument format
441
+ const fullPage: LobeDocument = {
442
+ content: document.content || null,
443
+ createdAt: document.createdAt ? new Date(document.createdAt) : new Date(),
444
+ editorData:
445
+ typeof document.editorData === 'string'
446
+ ? JSON.parse(document.editorData)
447
+ : document.editorData || null,
448
+ fileType: document.fileType,
449
+ filename: document.title || document.filename || 'Untitled',
450
+ id: document.id,
451
+ metadata: document.metadata || {},
452
+ source: 'document',
453
+ sourceType: DocumentSourceType.EDITOR,
454
+ title: document.title || '',
455
+ totalCharCount: document.content?.length || 0,
456
+ totalLineCount: 0,
457
+ updatedAt: document.updatedAt ? new Date(document.updatedAt) : new Date(),
458
+ };
459
+
460
+ return fullPage;
461
+ },
462
+ {
463
+ focusThrottleInterval: 5000,
464
+ onData: (document) => {
465
+ if (!document || !pageId) return;
466
+
467
+ // Auto-sync to documents array via internal dispatch
468
+ const { documents } = get();
469
+ if (documents?.some((doc) => doc.id === pageId)) {
470
+ get().internal_dispatchDocuments({ document, id: pageId, type: 'updateDocument' });
471
+ }
472
+ },
473
+ revalidateOnFocus: true,
474
+ },
475
+ );
476
+ },
477
+ });
@@ -0,0 +1,2 @@
1
+ export { createCrudSlice, type CrudAction } from './action';
2
+ export { type CrudState,initialCrudState } from './initialState';
@@ -0,0 +1,7 @@
1
+ export interface CrudState {
2
+ isCreatingNew: boolean;
3
+ }
4
+
5
+ export const initialCrudState: CrudState = {
6
+ isCreatingNew: false,
7
+ };
@@ -0,0 +1,32 @@
1
+ import isEqual from 'fast-deep-equal';
2
+ import { type StateCreator } from 'zustand/vanilla';
3
+
4
+ import { setNamespace } from '@/utils/storeDebug';
5
+
6
+ import { type PageStore } from '../../store';
7
+ import { type DocumentsDispatch, documentsReducer } from './reducer';
8
+
9
+ const n = setNamespace('page/internal');
10
+
11
+ export interface InternalAction {
12
+ /**
13
+ * Dispatch action to update documents array
14
+ */
15
+ internal_dispatchDocuments: (payload: DocumentsDispatch, action?: string) => void;
16
+ }
17
+
18
+ export const createInternalSlice: StateCreator<
19
+ PageStore,
20
+ [['zustand/devtools', never]],
21
+ [],
22
+ InternalAction
23
+ > = (set, get) => ({
24
+ internal_dispatchDocuments: (payload, action) => {
25
+ const { documents } = get();
26
+ const nextDocuments = documentsReducer(documents, payload);
27
+
28
+ if (isEqual(documents, nextDocuments)) return;
29
+
30
+ set({ documents: nextDocuments }, false, action ?? n(`dispatchDocuments/${payload.type}`));
31
+ },
32
+ });
@@ -0,0 +1,2 @@
1
+ export * from './action';
2
+ export * from './reducer';
@@ -0,0 +1,105 @@
1
+ import { produce } from 'immer';
2
+
3
+ import { type LobeDocument } from '@/types/document';
4
+
5
+ // ============ Action Types ============
6
+
7
+ interface AddDocumentAction {
8
+ document: LobeDocument;
9
+ type: 'addDocument';
10
+ }
11
+
12
+ interface RemoveDocumentAction {
13
+ id: string;
14
+ type: 'removeDocument';
15
+ }
16
+
17
+ interface UpdateDocumentAction {
18
+ document: LobeDocument;
19
+ id: string;
20
+ type: 'updateDocument';
21
+ }
22
+
23
+ interface ReplaceDocumentAction {
24
+ document: LobeDocument;
25
+ oldId: string;
26
+ type: 'replaceDocument';
27
+ }
28
+
29
+ interface SetDocumentsAction {
30
+ documents: LobeDocument[];
31
+ type: 'setDocuments';
32
+ }
33
+
34
+ interface AppendDocumentsAction {
35
+ documents: LobeDocument[];
36
+ type: 'appendDocuments';
37
+ }
38
+
39
+ export type DocumentsDispatch =
40
+ | AddDocumentAction
41
+ | RemoveDocumentAction
42
+ | UpdateDocumentAction
43
+ | ReplaceDocumentAction
44
+ | SetDocumentsAction
45
+ | AppendDocumentsAction;
46
+
47
+ // ============ Reducer ============
48
+
49
+ export const documentsReducer = (
50
+ state: LobeDocument[] | undefined,
51
+ payload: DocumentsDispatch,
52
+ ): LobeDocument[] | undefined => {
53
+ switch (payload.type) {
54
+ case 'addDocument': {
55
+ return produce(state ?? [], (draft) => {
56
+ // Add to the beginning
57
+ draft.unshift(payload.document);
58
+ });
59
+ }
60
+
61
+ case 'removeDocument': {
62
+ if (!state) return state;
63
+ return produce(state, (draft) => {
64
+ const index = draft.findIndex((doc) => doc.id === payload.id);
65
+ if (index !== -1) {
66
+ draft.splice(index, 1);
67
+ }
68
+ });
69
+ }
70
+
71
+ case 'updateDocument': {
72
+ if (!state) return state;
73
+ return produce(state, (draft) => {
74
+ const index = draft.findIndex((doc) => doc.id === payload.id);
75
+ if (index !== -1) {
76
+ draft[index] = payload.document;
77
+ }
78
+ });
79
+ }
80
+
81
+ case 'replaceDocument': {
82
+ if (!state) return [payload.document];
83
+ return produce(state, (draft) => {
84
+ const index = draft.findIndex((doc) => doc.id === payload.oldId);
85
+ if (index !== -1) {
86
+ draft[index] = payload.document;
87
+ }
88
+ });
89
+ }
90
+
91
+ case 'setDocuments': {
92
+ return payload.documents;
93
+ }
94
+
95
+ case 'appendDocuments': {
96
+ return produce(state ?? [], (draft) => {
97
+ draft.push(...payload.documents);
98
+ });
99
+ }
100
+
101
+ default: {
102
+ return state;
103
+ }
104
+ }
105
+ };