@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
@@ -3,18 +3,15 @@ import debug from 'debug';
3
3
  import { debounce } from 'es-toolkit/compat';
4
4
  import { type StateCreator } from 'zustand';
5
5
 
6
- import { documentService } from '@/services/document';
6
+ import { useDocumentStore } from '@/store/document';
7
7
  import { useFileStore } from '@/store/file';
8
- import { pageAgentRuntime } from '@/store/tool/slices/builtin/executors/lobe-page-agent';
9
- import { DocumentSourceType, type LobeDocument } from '@/types/document';
10
8
 
11
9
  import { type State, initialState } from './initialState';
12
10
 
13
11
  const log = debug('page:editor');
14
12
 
15
13
  export interface Action {
16
- flushSave: () => void;
17
- handleContentChange: () => void;
14
+ flushMetaSave: () => void;
18
15
  handleCopyLink: (t: (key: string) => string, message: any) => void;
19
16
  handleDelete: (
20
17
  t: (key: string) => string,
@@ -23,66 +20,48 @@ export interface Action {
23
20
  onDeleteCallback?: () => void,
24
21
  ) => Promise<void>;
25
22
  handleTitleSubmit: () => Promise<void>;
26
- onEditorInit: () => void;
27
- performSave: (options?: { force?: boolean }) => Promise<void>;
28
- setCurrentEmoji: (emoji: string | undefined) => void;
29
- setCurrentTitle: (title: string) => void;
23
+ initMeta: (title?: string, emoji?: string) => void;
24
+ performMetaSave: () => Promise<void>;
25
+ setEmoji: (emoji: string | undefined) => void;
26
+ setTitle: (title: string) => void;
27
+ triggerDebouncedMetaSave: () => void;
30
28
  }
31
29
 
32
30
  export type Store = State & Action;
33
31
 
34
- // Create debounced save function outside of store for reuse
35
- const createDebouncedSave = (get: () => Store) =>
36
- debounce(
37
- async () => {
38
- try {
39
- await get().performSave();
40
- } catch (error) {
41
- log('Failed to auto-save:', error);
42
- }
43
- },
44
- EDITOR_DEBOUNCE_TIME,
45
- { leading: false, maxWait: EDITOR_MAX_WAIT, trailing: true },
46
- );
47
-
48
32
  export const store: (initState?: Partial<State>) => StateCreator<Store> =
49
33
  (initState) => (set, get) => {
50
- const debouncedSave = createDebouncedSave(get);
34
+ // Debounced save function for meta (title/emoji)
35
+ let debouncedMetaSave: ReturnType<typeof debounce> | null = null;
36
+
37
+ const getOrCreateDebouncedMetaSave = () => {
38
+ if (!debouncedMetaSave) {
39
+ debouncedMetaSave = debounce(
40
+ async () => {
41
+ try {
42
+ await get().performMetaSave();
43
+ } catch (error) {
44
+ console.error('[PageEditor] Failed to auto-save meta:', error);
45
+ }
46
+ },
47
+ EDITOR_DEBOUNCE_TIME,
48
+ { leading: false, maxWait: EDITOR_MAX_WAIT, trailing: true },
49
+ );
50
+ }
51
+ return debouncedMetaSave;
52
+ };
51
53
 
52
54
  return {
53
55
  ...initialState,
54
56
  ...initState,
55
57
 
56
- flushSave: () => {
57
- debouncedSave.flush();
58
- },
59
-
60
- handleContentChange: () => {
61
- const { editor, lastSavedContent } = get();
62
- if (!editor) return;
63
-
64
- try {
65
- const textContent = (editor.getDocument('text') as unknown as string) || '';
66
- const markdownContent = (editor.getDocument('markdown') as unknown as string) || '';
67
- const wordCount = textContent.trim().split(/\s+/).filter(Boolean).length;
68
-
69
- // Check if content actually changed
70
- const contentChanged = markdownContent !== lastSavedContent;
71
-
72
- set({ isDirty: contentChanged, wordCount });
73
-
74
- // Only trigger auto-save if content actually changed
75
- if (contentChanged) {
76
- debouncedSave();
77
- }
78
- } catch (error) {
79
- log('Failed to update content:', error);
80
- }
58
+ flushMetaSave: () => {
59
+ debouncedMetaSave?.flush();
81
60
  },
82
61
 
83
62
  handleCopyLink: (t, message) => {
84
- const { currentDocId } = get();
85
- if (currentDocId) {
63
+ const { documentId } = get();
64
+ if (documentId) {
86
65
  const url = `${window.location.origin}${window.location.pathname}`;
87
66
  navigator.clipboard.writeText(url);
88
67
  message.success(t('pageEditor.linkCopied'));
@@ -90,8 +69,8 @@ export const store: (initState?: Partial<State>) => StateCreator<Store> =
90
69
  },
91
70
 
92
71
  handleDelete: async (t, message, modal, onDeleteCallback) => {
93
- const { currentDocId } = get();
94
- if (!currentDocId) return;
72
+ const { documentId } = get();
73
+ if (!documentId) return;
95
74
 
96
75
  return new Promise((resolve, reject) => {
97
76
  modal.confirm({
@@ -102,7 +81,7 @@ export const store: (initState?: Partial<State>) => StateCreator<Store> =
102
81
  onOk: async () => {
103
82
  try {
104
83
  const { removeDocument } = useFileStore.getState();
105
- await removeDocument(currentDocId);
84
+ await removeDocument(documentId);
106
85
  message.success(t('pageEditor.deleteSuccess'));
107
86
  onDeleteCallback?.();
108
87
  resolve();
@@ -118,163 +97,92 @@ export const store: (initState?: Partial<State>) => StateCreator<Store> =
118
97
  },
119
98
 
120
99
  handleTitleSubmit: async () => {
121
- const { performSave, editor } = get();
122
- await performSave();
100
+ const { editor, flushMetaSave } = get();
101
+
102
+ // Flush pending save and focus editor
103
+ flushMetaSave();
123
104
  editor?.focus();
124
105
  },
125
106
 
126
- onEditorInit: () => {
127
- // Called when editor is initialized
128
- const { editor, setCurrentTitle, currentTitle } = get();
129
-
130
- if (editor) {
131
- // Connect the editor instance to the page agent runtime
132
- pageAgentRuntime.setEditor(editor);
133
-
134
- // Set up title handlers for the runtime
135
- pageAgentRuntime.setTitleHandlers(
136
- (title: string) => setCurrentTitle(title),
137
- () => currentTitle,
138
- );
139
-
140
- log('Connected editor to page agent runtime');
141
- }
107
+ initMeta: (title, emoji) => {
108
+ set({
109
+ emoji,
110
+ isMetaDirty: false,
111
+ lastSavedEmoji: emoji,
112
+ lastSavedTitle: title,
113
+ metaSaveStatus: 'idle',
114
+ title,
115
+ });
142
116
  },
143
117
 
144
- performSave: async (options) => {
118
+ performMetaSave: async () => {
145
119
  const {
146
- editor,
147
- currentDocId,
148
- currentTitle,
149
- currentEmoji,
150
- knowledgeBaseId,
151
- parentId,
152
- onDocumentIdChange,
153
- onSave,
154
- isDirty,
120
+ documentId,
121
+ title,
122
+ emoji,
123
+ lastSavedTitle,
124
+ lastSavedEmoji,
125
+ isMetaDirty,
126
+ onTitleChange,
127
+ onEmojiChange,
155
128
  } = get();
156
129
 
157
- if (!editor) return;
130
+ if (!documentId || !isMetaDirty) return;
158
131
 
159
- // Skip save if no changes (unless force is true)
160
- if (
161
- !options?.force &&
162
- !isDirty &&
163
- currentDocId &&
164
- !currentDocId.startsWith('temp-document-')
165
- ) {
166
- return;
167
- }
168
-
169
- set({ saveStatus: 'saving' });
132
+ set({ metaSaveStatus: 'saving' });
170
133
 
171
134
  try {
172
- const editorElement = editor.getRootElement();
173
- const hadFocus = editorElement?.contains(document.activeElement) ?? false;
174
-
175
- const currentEditorData = editor.getDocument('json');
176
- const currentContent = (editor.getDocument('markdown') as unknown as string) || '';
177
-
178
- // Don't save if there's no content AND no title/emoji changes
179
- if (!currentContent?.trim() && !currentTitle?.trim() && !currentEmoji) {
180
- set({ saveStatus: 'idle' });
181
- return;
182
- }
183
-
184
- const { updateDocumentOptimistically, replaceTempDocumentWithReal } =
185
- useFileStore.getState();
186
-
187
- if (currentDocId && !currentDocId.startsWith('temp-document-')) {
188
- await updateDocumentOptimistically(currentDocId, {
189
- content: currentContent,
190
- editorData: structuredClone(currentEditorData),
191
- metadata: currentEmoji ? { emoji: currentEmoji } : { emoji: undefined },
192
- title: currentTitle,
193
- updatedAt: new Date(),
194
- });
195
- } else {
196
- const now = Date.now();
197
- const timestamp = new Date(now).toLocaleString('en-US', {
198
- day: '2-digit',
199
- hour: '2-digit',
200
- minute: '2-digit',
201
- month: 'short',
202
- year: 'numeric',
203
- });
204
- const finalTitle = currentTitle || `Page - ${timestamp}`;
205
-
206
- const newPage = await documentService.createDocument({
207
- content: currentContent,
208
- editorData: JSON.stringify(currentEditorData),
209
- fileType: 'custom/document',
210
- knowledgeBaseId,
211
- metadata: {
212
- createdAt: now,
213
- ...(currentEmoji ? { emoji: currentEmoji } : {}),
214
- },
215
- parentId,
216
- title: finalTitle,
217
- });
218
-
219
- const realPage: LobeDocument = {
220
- content: currentContent,
221
- createdAt: new Date(now),
222
- editorData: structuredClone(currentEditorData) || null,
223
- fileType: 'custom/document' as const,
224
- filename: finalTitle,
225
- id: newPage.id,
226
- metadata: {
227
- createdAt: now,
228
- ...(currentEmoji ? { emoji: currentEmoji } : {}),
229
- },
230
- source: 'document',
231
- sourceType: DocumentSourceType.EDITOR,
232
- title: finalTitle,
233
- totalCharCount: currentContent.length,
234
- totalLineCount: 0,
235
- updatedAt: new Date(now),
236
- };
237
-
238
- if (currentDocId?.startsWith('temp-document-')) {
239
- replaceTempDocumentWithReal(currentDocId, realPage);
240
- }
241
-
242
- set({ currentDocId: newPage.id });
243
- onDocumentIdChange?.(newPage.id);
135
+ // Trigger save via DocumentStore with metadata
136
+ await useDocumentStore.getState().performSave(documentId, {
137
+ emoji,
138
+ title,
139
+ });
244
140
 
245
- // Refetch resource list to show newly created page
246
- const { revalidateResources } = await import('@/store/file/slices/resource/hooks');
247
- await revalidateResources();
141
+ // Notify parent after successful save
142
+ if (title !== lastSavedTitle) {
143
+ onTitleChange?.(title || '');
248
144
  }
249
-
250
- if (hadFocus) {
251
- setTimeout(() => {
252
- editor?.focus();
253
- }, 0);
145
+ if (emoji !== lastSavedEmoji) {
146
+ onEmojiChange?.(emoji);
254
147
  }
255
148
 
256
- // Mark as clean and update save status
257
149
  set({
258
- isDirty: false,
259
- lastSavedContent: currentContent,
260
- lastUpdatedTime: new Date(),
261
- saveStatus: 'saved',
150
+ isMetaDirty: false,
151
+ lastSavedEmoji: emoji,
152
+ lastSavedTitle: title,
153
+ metaSaveStatus: 'saved',
262
154
  });
263
- onSave?.();
264
155
  } catch (error) {
265
- log('Failed to save:', error);
266
- set({ saveStatus: 'idle' });
156
+ console.error('[PageEditor] Failed to save meta:', error);
157
+ set({ metaSaveStatus: 'idle' });
158
+ }
159
+ },
160
+
161
+ setEmoji: (emoji: string | undefined) => {
162
+ const { lastSavedEmoji, triggerDebouncedMetaSave } = get();
163
+
164
+ const isDirty = emoji !== lastSavedEmoji;
165
+ set({ emoji, isMetaDirty: isDirty });
166
+
167
+ if (isDirty) {
168
+ triggerDebouncedMetaSave();
267
169
  }
268
170
  },
269
171
 
270
- setCurrentEmoji: (emoji: string | undefined) => {
271
- set({ currentEmoji: emoji, isDirty: true });
272
- debouncedSave();
172
+ setTitle: (title: string) => {
173
+ const { lastSavedTitle, triggerDebouncedMetaSave } = get();
174
+
175
+ const isDirty = title !== lastSavedTitle;
176
+ set({ isMetaDirty: isDirty, title });
177
+
178
+ if (isDirty) {
179
+ triggerDebouncedMetaSave();
180
+ }
273
181
  },
274
182
 
275
- setCurrentTitle: (title: string) => {
276
- set({ currentTitle: title, isDirty: true });
277
- debouncedSave();
183
+ triggerDebouncedMetaSave: () => {
184
+ const save = getOrCreateDebouncedMetaSave();
185
+ save();
278
186
  },
279
187
  };
280
188
  };
@@ -1,40 +1,35 @@
1
1
  import { type IEditor } from '@lobehub/editor';
2
- import { type EditorState } from '@lobehub/editor/react';
2
+
3
+ export type MetaSaveStatus = 'idle' | 'saving' | 'saved';
3
4
 
4
5
  export interface PublicState {
5
6
  autoSave?: boolean;
7
+ emoji?: string;
6
8
  knowledgeBaseId?: string;
7
9
  onBack?: () => void;
8
10
  onDelete?: () => void;
9
11
  onDocumentIdChange?: (newId: string) => void;
12
+ onEmojiChange?: (emoji: string | undefined) => void;
10
13
  onSave?: () => void;
11
- pageId?: string;
14
+ onTitleChange?: (title: string) => void;
12
15
  parentId?: string;
16
+ title?: string;
13
17
  }
14
18
 
15
19
  export interface State extends PublicState {
16
- currentDocId: string | undefined;
17
- currentEmoji: string | undefined;
18
- currentTitle: string;
20
+ documentId: string | undefined;
19
21
  editor?: IEditor;
20
- editorState?: EditorState;
21
- isDirty: boolean; // Track if there are unsaved changes
22
- isLoadingContent: boolean; // Track if content is being loaded
23
- lastSavedContent: string; // Last saved content hash for comparison
24
- lastUpdatedTime: Date | null;
25
- saveStatus: 'idle' | 'saving' | 'saved';
26
- wordCount: number;
22
+ isMetaDirty?: boolean;
23
+ lastSavedEmoji?: string;
24
+ lastSavedTitle?: string;
25
+ metaSaveStatus?: MetaSaveStatus;
27
26
  }
28
27
 
29
28
  export const initialState: State = {
30
29
  autoSave: true,
31
- currentDocId: undefined,
32
- currentEmoji: undefined,
33
- currentTitle: '',
34
- isDirty: false,
35
- isLoadingContent: false,
36
- lastSavedContent: '',
37
- lastUpdatedTime: null,
38
- saveStatus: 'idle',
39
- wordCount: 0,
30
+ documentId: undefined,
31
+ emoji: undefined,
32
+ isMetaDirty: false,
33
+ metaSaveStatus: 'idle',
34
+ title: undefined,
40
35
  };
@@ -1,9 +1,8 @@
1
1
  import { type Store } from './action';
2
2
 
3
3
  export const selectors = {
4
- currentDocId: (s: Store) => s.currentDocId,
5
- currentTitle: (s: Store) => s.currentTitle,
4
+ documentId: (s: Store) => s.documentId,
6
5
  editor: (s: Store) => s.editor,
7
- saveStatus: (s: Store) => s.saveStatus,
8
- wordCount: (s: Store) => s.wordCount,
6
+ emoji: (s: Store) => s.emoji,
7
+ title: (s: Store) => s.title,
9
8
  };
@@ -12,6 +12,7 @@ import GuideVideo from '@/components/GuideVideo';
12
12
  import NavHeader from '@/features/NavHeader';
13
13
  import useNotionImport from '@/features/ResourceManager/components/Header/hooks/useNotionImport';
14
14
  import { useFileStore } from '@/store/file';
15
+ import { usePageStore } from '@/store/page';
15
16
  import { DocumentSourceType } from '@/types/document';
16
17
  import { standardizeIdentifier } from '@/utils/identifier';
17
18
 
@@ -76,22 +77,25 @@ const PageExplorerPlaceholder = memo<PageExplorerPlaceholderProps>(
76
77
  ({ hasPages = false, knowledgeBaseId }) => {
77
78
  const { t } = useTranslation(['file', 'common']);
78
79
  const [isUploading, setIsUploading] = useState(false);
80
+
81
+ // Page-specific operations from pageStore
79
82
  const [
80
83
  createNewPage,
81
- createDocument,
82
- createOptimisticDocument,
83
- replaceTempDocumentWithReal,
84
+ createOptimisticPage,
85
+ replaceTempPageWithReal,
84
86
  setSelectedPageId,
85
87
  fetchDocuments,
86
- ] = useFileStore((s) => [
88
+ ] = usePageStore((s) => [
87
89
  s.createNewPage,
88
- s.createDocument,
89
- s.createOptimisticDocument,
90
- s.replaceTempDocumentWithReal,
90
+ s.createOptimisticPage,
91
+ s.replaceTempPageWithReal,
91
92
  s.setSelectedPageId,
92
93
  s.fetchDocuments,
93
94
  ]);
94
95
 
96
+ // File operations from FileStore (for uploads and notion import)
97
+ const [createDocument] = useFileStore((s) => [s.createDocument]);
98
+
95
99
  const notionImport = useNotionImport({
96
100
  createDocument,
97
101
  currentFolderId: null,
@@ -99,7 +103,7 @@ const PageExplorerPlaceholder = memo<PageExplorerPlaceholderProps>(
99
103
  refetchResources: async () => {
100
104
  const { revalidateResources } = await import('@/store/file/slices/resource/hooks');
101
105
  await revalidateResources();
102
- await fetchDocuments({ pageOnly: true });
106
+ await fetchDocuments();
103
107
  },
104
108
  t,
105
109
  });
@@ -109,6 +113,10 @@ const PageExplorerPlaceholder = memo<PageExplorerPlaceholderProps>(
109
113
  event: React.ChangeEvent<HTMLInputElement>,
110
114
  ) => {
111
115
  await notionImport.handleNotionImport(event);
116
+ // Fetch documents to update the UI immediately
117
+ // The hook calls refreshFileList which invalidates SWR cache,
118
+ // but we need to explicitly fetch to update the zustand store
119
+ await fetchDocuments();
112
120
  };
113
121
 
114
122
  const handleCreateDocument = async (content: string, title: string) => {
@@ -119,7 +127,7 @@ const PageExplorerPlaceholder = memo<PageExplorerPlaceholderProps>(
119
127
  }
120
128
 
121
129
  // For markdown uploads with content, use optimistic pattern similar to createNewPage
122
- const tempPageId = createOptimisticDocument(title);
130
+ const tempPageId = createOptimisticPage(title);
123
131
  // Set selected page to temp ID immediately (with URL update disabled for temp IDs)
124
132
  setSelectedPageId(tempPageId, false);
125
133
 
@@ -151,13 +159,13 @@ const PageExplorerPlaceholder = memo<PageExplorerPlaceholderProps>(
151
159
  };
152
160
 
153
161
  // Replace optimistic with real
154
- replaceTempDocumentWithReal(tempPageId, realPage);
162
+ replaceTempPageWithReal(tempPageId, realPage);
155
163
  // Update selected page ID and URL to the real page
156
164
  setSelectedPageId(newDoc.id);
157
165
  } catch (error) {
158
166
  console.error('Failed to create page:', error);
159
167
  // Remove temp document on error
160
- useFileStore.getState().removeTempDocument(tempPageId);
168
+ usePageStore.getState().removeTempPage(tempPageId);
161
169
  setSelectedPageId(null);
162
170
  throw error;
163
171
  }
@@ -179,7 +187,7 @@ const PageExplorerPlaceholder = memo<PageExplorerPlaceholderProps>(
179
187
  const fileName = file.name.replace(/\.(pdf|docx)$/i, '');
180
188
 
181
189
  // Create optimistic document but don't select it yet
182
- const tempPageId = createOptimisticDocument(fileName);
190
+ const tempPageId = createOptimisticPage(fileName);
183
191
 
184
192
  try {
185
193
  // Upload file to server
@@ -219,7 +227,7 @@ const PageExplorerPlaceholder = memo<PageExplorerPlaceholderProps>(
219
227
  };
220
228
 
221
229
  // Replace optimistic with real document in the store
222
- replaceTempDocumentWithReal(tempPageId, realPage);
230
+ replaceTempPageWithReal(tempPageId, realPage);
223
231
 
224
232
  // Update selected page ID in store (with full ID including prefix)
225
233
  setSelectedPageId(parsedDocument.id, false);
@@ -231,7 +239,7 @@ const PageExplorerPlaceholder = memo<PageExplorerPlaceholderProps>(
231
239
  } catch (error) {
232
240
  console.error('Failed to upload and parse file:', error);
233
241
  // Remove temp document on error
234
- useFileStore.getState().removeTempDocument(tempPageId);
242
+ usePageStore.getState().removeTempPage(tempPageId);
235
243
  throw error;
236
244
  }
237
245
  }
@@ -1,15 +1,12 @@
1
1
  'use client';
2
2
 
3
- import { memo, useEffect, useRef } from 'react';
3
+ import { memo, useCallback } from 'react';
4
4
 
5
5
  import { PageEditor } from '@/features/PageEditor';
6
- import { useFileStore } from '@/store/file';
7
-
8
- import PageExplorerPlaceholder from './PageExplorerPlaceholder';
6
+ import { pageSelectors, usePageStore } from '@/store/page';
9
7
 
10
8
  interface PageExplorerProps {
11
- // Current opened page id
12
- pageId?: string;
9
+ pageId: string;
13
10
  }
14
11
 
15
12
  /**
@@ -18,67 +15,37 @@ interface PageExplorerProps {
18
15
  * Work together with a sidebar src/app/[variants]/(main)/page/_layout/Body/index.tsx
19
16
  */
20
17
  const PageExplorer = memo<PageExplorerProps>(({ pageId }) => {
21
- const [
22
- selectedPageId,
23
- setSelectedPageId,
24
- pages,
25
- fetchDocuments,
26
- fetchDocumentDetail,
27
- isDocumentListLoading,
28
- ] = useFileStore((s) => [
29
- s.selectedPageId,
30
- s.setSelectedPageId,
31
- s.getOptimisticDocuments(), // Call inside selector to subscribe to changes
32
- s.fetchDocuments,
33
- s.fetchDocumentDetail,
34
- s.isDocumentListLoading,
35
- ]);
36
-
37
- // Track previous pageId to detect actual URL changes (start undefined to run on first load)
38
- const prevPageIdRef = useRef<string | undefined>(undefined);
39
-
40
- // Fetch documents on mount
41
- useEffect(() => {
42
- fetchDocuments({ pageOnly: true });
43
- }, [fetchDocuments]);
44
-
45
- // Check if pageId is valid (not undefined or "docs_undefined")
46
- const isValidPageId = pageId && !pageId.includes('undefined');
47
-
48
- // When pageId prop changes (from URL navigation), update selected page and fetch details
49
- // Use ref to only sync when pageId actually changes, avoiding conflicts with sidebar selection
50
- useEffect(() => {
51
- if (isValidPageId && pageId !== prevPageIdRef.current) {
52
- prevPageIdRef.current = pageId;
53
- setSelectedPageId(pageId, false);
54
- // Fetch the document detail to ensure it's loaded in the local map
55
- fetchDocumentDetail(pageId);
56
- } else if (!isValidPageId && prevPageIdRef.current !== undefined) {
57
- // When navigating to /page without a doc id, clear the selection
58
- prevPageIdRef.current = undefined;
59
- setSelectedPageId(null, false);
60
- }
61
- }, [pageId, isValidPageId, setSelectedPageId, fetchDocumentDetail]);
62
-
63
- // Prioritize selectedPageId from store for immediate updates when clicking from sidebar
64
- // Only show placeholder when both selectedPageId is null AND pageId is invalid
65
- const currentPageId = selectedPageId || (isValidPageId ? pageId : undefined);
66
-
67
- // Check if the current page exists in the pages list
68
- const currentPageExists = currentPageId && pages.some((page) => page.id === currentPageId);
69
-
70
- // When we have a pageId from URL but document list is not yet loaded (empty list or still loading),
71
- // proceed to show the editor instead of placeholder. The editor handles its own loading state.
72
- // This prevents the placeholder flash on page refresh.
73
- const isWaitingForDocuments = pages.length === 0 || isDocumentListLoading;
74
- const shouldShowEditor =
75
- currentPageId && (currentPageExists || (isValidPageId && isWaitingForDocuments));
76
-
77
- if (!shouldShowEditor) {
78
- return <PageExplorerPlaceholder hasPages={pages?.length > 0} />;
79
- }
80
-
81
- return <PageEditor pageId={currentPageId} />;
18
+ const updatePageOptimistically = usePageStore((s) => s.updatePageOptimistically);
19
+
20
+ // Get document title and emoji from PageStore
21
+ const document = usePageStore(pageSelectors.getDocumentById(pageId));
22
+ const title = document?.title;
23
+ const emoji = document?.metadata?.emoji as string | undefined;
24
+
25
+ // Optimistic update handlers for title and emoji
26
+ const handleTitleChange = useCallback(
27
+ (newTitle: string) => {
28
+ updatePageOptimistically(pageId, { title: newTitle });
29
+ },
30
+ [pageId, updatePageOptimistically],
31
+ );
32
+
33
+ const handleEmojiChange = useCallback(
34
+ (newEmoji: string | undefined) => {
35
+ updatePageOptimistically(pageId, { emoji: newEmoji });
36
+ },
37
+ [pageId, updatePageOptimistically],
38
+ );
39
+
40
+ return (
41
+ <PageEditor
42
+ emoji={emoji}
43
+ onEmojiChange={handleEmojiChange}
44
+ onTitleChange={handleTitleChange}
45
+ pageId={pageId}
46
+ title={title}
47
+ />
48
+ );
82
49
  });
83
50
 
84
51
  export default PageExplorer;
@@ -1,10 +1,8 @@
1
1
  import { type PortalImpl } from '../type';
2
2
  import Body from './Body';
3
3
  import Title from './Title';
4
- import { useEnable } from './useEnable';
5
4
 
6
5
  export const Artifacts: PortalImpl = {
7
6
  Body,
8
7
  Title,
9
- useEnable,
10
8
  };