@lobehub/lobehub 2.0.0-next.255 → 2.0.0-next.257
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.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/locales/en-US/plugin.json +1 -0
- package/locales/zh-CN/plugin.json +1 -0
- package/package.json +1 -1
- package/packages/builtin-tool-notebook/src/client/Placeholder/CreateDocument.tsx +6 -6
- package/packages/builtin-tool-notebook/src/client/Render/CreateDocument/DocumentCard.tsx +23 -10
- package/packages/builtin-tool-notebook/src/client/Streaming/CreateDocument/index.tsx +1 -1
- package/packages/database/src/models/__tests__/agent.test.ts +91 -4
- package/packages/database/src/models/agent.ts +15 -7
- package/packages/editor-runtime/src/EditorRuntime.ts +1 -1
- package/packages/editor-runtime/src/__tests__/EditorRuntime.real.test.ts +65 -4
- package/packages/editor-runtime/src/__tests__/__snapshots__/EditorRuntime.real.test.ts.snap +108 -17
- package/packages/editor-runtime/src/__tests__/fixtures/remove-then-add.json +636 -0
- package/packages/editor-runtime/src/__tests__/fixtures/remove.json +1 -0
- package/packages/types/src/agent/agentConfig.ts +8 -8
- package/src/app/[variants]/(main)/chat/features/Portal/_layout/Mobile.tsx +2 -1
- package/src/app/[variants]/(main)/group/features/Portal/_layout/Mobile.tsx +2 -1
- package/src/app/[variants]/(main)/home/_layout/hooks/useCreateMenuItems.tsx +2 -2
- package/src/app/[variants]/(main)/page/{features/PageTitle → PageTitle}/index.tsx +0 -1
- package/src/app/[variants]/(main)/page/[id]/index.tsx +43 -1
- package/src/app/[variants]/(main)/page/_layout/Body/AllPagesDrawer/Content.tsx +15 -15
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/Editing.tsx +3 -3
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/index.tsx +7 -12
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/useDropdownMenu.tsx +5 -5
- package/src/app/[variants]/(main)/page/_layout/Body/List/index.tsx +7 -7
- package/src/app/[variants]/(main)/page/_layout/Body/index.tsx +15 -9
- package/src/app/[variants]/(main)/page/_layout/Body/useDropdownMenu.tsx +3 -3
- package/src/app/[variants]/(main)/page/_layout/DataSync.tsx +15 -0
- package/src/app/[variants]/(main)/page/_layout/Header/AddButton.tsx +2 -2
- package/src/app/[variants]/(main)/page/_layout/index.tsx +2 -0
- package/src/app/[variants]/(main)/page/index.tsx +3 -7
- package/src/components/Editor/AutoSaveHint.tsx +1 -1
- package/src/features/Conversation/Messages/User/Actions/index.tsx +5 -1
- package/src/features/EditorCanvas/AutoSaveHint.tsx +37 -0
- package/src/features/{PageEditor → EditorCanvas}/DiffAllToolbar.tsx +57 -16
- package/src/features/EditorCanvas/DocumentIdMode.tsx +111 -0
- package/src/features/EditorCanvas/EditorCanvas.tsx +148 -0
- package/src/features/EditorCanvas/EditorDataMode.tsx +64 -0
- package/src/features/EditorCanvas/ErrorBoundary.tsx +66 -0
- package/src/features/EditorCanvas/InlineToolbar.tsx +245 -0
- package/src/features/EditorCanvas/InternalEditor.tsx +134 -0
- package/src/features/{PageEditor/EditorCanvas → EditorCanvas}/actions.ts +10 -8
- package/src/features/EditorCanvas/index.ts +9 -0
- package/src/features/PageEditor/Copilot/index.tsx +16 -1
- package/src/features/PageEditor/EditorCanvas/index.tsx +14 -111
- package/src/features/PageEditor/EditorCanvas/useAskCopilotItem.tsx +95 -0
- package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +1 -1
- package/src/features/PageEditor/Header/Breadcrumb.tsx +2 -2
- package/src/features/PageEditor/Header/index.tsx +15 -18
- package/src/features/PageEditor/Header/useMenu.tsx +12 -9
- package/src/features/PageEditor/PageEditor.tsx +45 -21
- package/src/features/PageEditor/PageEditorProvider.tsx +13 -1
- package/src/features/PageEditor/PageTitle/index.tsx +2 -2
- package/src/features/PageEditor/StoreUpdater.tsx +35 -308
- package/src/features/PageEditor/{Body/Title.tsx → TitleSection.tsx} +16 -16
- package/src/features/PageEditor/store/action.ts +96 -188
- package/src/features/PageEditor/store/initialState.ts +16 -21
- package/src/features/PageEditor/store/selectors.ts +3 -4
- package/src/features/PageExplorer/PageExplorerPlaceholder.tsx +22 -14
- package/src/features/PageExplorer/index.tsx +34 -67
- package/src/features/Portal/Artifacts/index.ts +0 -2
- package/src/features/Portal/Document/AutoSaveHint.tsx +7 -6
- package/src/features/Portal/Document/Body.tsx +1 -3
- package/src/features/Portal/Document/EditorCanvas.tsx +7 -50
- package/src/features/Portal/Document/Header.tsx +13 -10
- package/src/features/Portal/Document/TodoList.tsx +6 -4
- package/src/features/Portal/Document/Wrapper.tsx +3 -11
- package/src/features/Portal/Document/index.ts +0 -2
- package/src/features/Portal/FilePreview/index.ts +0 -2
- package/src/features/Portal/GroupThread/index.ts +0 -3
- package/src/features/Portal/MessageDetail/index.ts +0 -2
- package/src/features/Portal/Notebook/index.ts +0 -2
- package/src/features/Portal/Plugins/index.ts +0 -2
- package/src/features/Portal/Thread/index.ts +0 -3
- package/src/features/Portal/components/Header.tsx +18 -6
- package/src/features/Portal/router.tsx +34 -97
- package/src/features/Portal/type.ts +0 -2
- package/src/features/RightPanel/index.tsx +11 -2
- package/src/locales/default/plugin.ts +1 -0
- package/src/store/chat/slices/portal/action.test.ts +218 -15
- package/src/store/chat/slices/portal/action.ts +194 -41
- package/src/store/chat/slices/portal/initialState.ts +40 -1
- package/src/store/chat/slices/portal/selectors/thread.ts +44 -3
- package/src/store/chat/slices/portal/selectors.test.ts +119 -17
- package/src/store/chat/slices/portal/selectors.ts +117 -36
- package/src/store/document/index.ts +17 -5
- package/src/store/document/slices/document/action.ts +209 -0
- package/src/store/document/slices/document/index.ts +6 -0
- package/src/store/document/slices/editor/action.test.ts +340 -0
- package/src/store/document/slices/editor/action.ts +133 -149
- package/src/store/document/slices/editor/index.ts +9 -2
- package/src/store/document/slices/editor/initialState.ts +66 -29
- package/src/store/document/slices/editor/reducer.test.ts +217 -0
- package/src/store/document/slices/editor/reducer.ts +67 -0
- package/src/store/document/slices/editor/selectors.test.ts +395 -0
- package/src/store/document/slices/editor/selectors.ts +107 -5
- package/src/store/document/store.ts +12 -13
- package/src/store/file/slices/document/action.ts +19 -188
- package/src/store/file/slices/document/initialState.ts +0 -30
- package/src/store/file/slices/document/selectors.ts +25 -59
- package/src/store/global/initialState.ts +2 -0
- package/src/store/global/selectors/systemStatus.ts +2 -0
- package/src/store/notebook/index.ts +5 -4
- package/src/store/page/index.ts +2 -0
- package/src/store/page/initialState.ts +92 -0
- package/src/store/page/selectors.ts +5 -0
- package/src/store/page/slices/crud/action.ts +477 -0
- package/src/store/page/slices/crud/index.ts +2 -0
- package/src/store/page/slices/crud/initialState.ts +7 -0
- package/src/store/page/slices/internal/action.ts +32 -0
- package/src/store/page/slices/internal/index.ts +2 -0
- package/src/store/page/slices/internal/reducer.ts +105 -0
- package/src/store/page/slices/list/action.ts +206 -0
- package/src/store/page/slices/list/index.ts +3 -0
- package/src/store/page/slices/list/initialState.ts +29 -0
- package/src/store/page/slices/list/selectors.ts +90 -0
- package/src/store/page/slices/selection/action.ts +67 -0
- package/src/store/page/slices/selection/index.ts +2 -0
- package/src/store/page/slices/selection/initialState.ts +11 -0
- package/src/store/page/store.ts +29 -0
- package/src/store/tool/slices/lobehubSkillStore/selectors.ts +10 -20
- package/src/utils/identifier.ts +8 -2
- package/src/features/PageEditor/Body/index.tsx +0 -68
- package/src/features/PageEditor/EditorCanvas/InlineToolbar.tsx +0 -316
- package/src/features/PageEditor/Header/AutoSaveHint.tsx +0 -27
- package/src/features/Portal/Artifacts/useEnable.ts +0 -4
- package/src/features/Portal/Document/DocumentEditorProvider.tsx +0 -34
- package/src/features/Portal/Document/StoreUpdater.tsx +0 -80
- package/src/features/Portal/Document/Title.tsx +0 -54
- package/src/features/Portal/Document/store/action.ts +0 -114
- package/src/features/Portal/Document/store/index.ts +0 -21
- package/src/features/Portal/Document/store/initialState.ts +0 -24
- package/src/features/Portal/Document/useEnable.ts +0 -8
- package/src/features/Portal/FilePreview/useEnable.ts +0 -6
- package/src/features/Portal/GroupThread/hook.ts +0 -9
- package/src/features/Portal/MessageDetail/useEnable.ts +0 -4
- package/src/features/Portal/Notebook/useEnable.ts +0 -6
- package/src/features/Portal/Plugins/useEnable.ts +0 -6
- package/src/features/Portal/Thread/hook.ts +0 -8
- package/src/store/document/slices/notebook/action.ts +0 -119
- package/src/store/document/slices/notebook/index.ts +0 -3
- package/src/store/document/slices/notebook/initialState.ts +0 -12
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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 {
|
|
85
|
-
if (
|
|
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 {
|
|
94
|
-
if (!
|
|
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(
|
|
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 {
|
|
122
|
-
|
|
100
|
+
const { editor, flushMetaSave } = get();
|
|
101
|
+
|
|
102
|
+
// Flush pending save and focus editor
|
|
103
|
+
flushMetaSave();
|
|
123
104
|
editor?.focus();
|
|
124
105
|
},
|
|
125
106
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
118
|
+
performMetaSave: async () => {
|
|
145
119
|
const {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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 (!
|
|
130
|
+
if (!documentId || !isMetaDirty) return;
|
|
158
131
|
|
|
159
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
141
|
+
// Notify parent after successful save
|
|
142
|
+
if (title !== lastSavedTitle) {
|
|
143
|
+
onTitleChange?.(title || '');
|
|
248
144
|
}
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
150
|
+
isMetaDirty: false,
|
|
151
|
+
lastSavedEmoji: emoji,
|
|
152
|
+
lastSavedTitle: title,
|
|
153
|
+
metaSaveStatus: 'saved',
|
|
262
154
|
});
|
|
263
|
-
onSave?.();
|
|
264
155
|
} catch (error) {
|
|
265
|
-
|
|
266
|
-
set({
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
+
onTitleChange?: (title: string) => void;
|
|
12
15
|
parentId?: string;
|
|
16
|
+
title?: string;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export interface State extends PublicState {
|
|
16
|
-
|
|
17
|
-
currentEmoji: string | undefined;
|
|
18
|
-
currentTitle: string;
|
|
20
|
+
documentId: string | undefined;
|
|
19
21
|
editor?: IEditor;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
5
|
-
currentTitle: (s: Store) => s.currentTitle,
|
|
4
|
+
documentId: (s: Store) => s.documentId,
|
|
6
5
|
editor: (s: Store) => s.editor,
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
replaceTempDocumentWithReal,
|
|
84
|
+
createOptimisticPage,
|
|
85
|
+
replaceTempPageWithReal,
|
|
84
86
|
setSelectedPageId,
|
|
85
87
|
fetchDocuments,
|
|
86
|
-
] =
|
|
88
|
+
] = usePageStore((s) => [
|
|
87
89
|
s.createNewPage,
|
|
88
|
-
s.
|
|
89
|
-
s.
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
3
|
+
import { memo, useCallback } from 'react';
|
|
4
4
|
|
|
5
5
|
import { PageEditor } from '@/features/PageEditor';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
import PageExplorerPlaceholder from './PageExplorerPlaceholder';
|
|
6
|
+
import { pageSelectors, usePageStore } from '@/store/page';
|
|
9
7
|
|
|
10
8
|
interface PageExplorerProps {
|
|
11
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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;
|