@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.
Files changed (144) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -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/Copilot/index.tsx +16 -1
  46. package/src/features/PageEditor/EditorCanvas/index.tsx +14 -111
  47. package/src/features/PageEditor/EditorCanvas/useAskCopilotItem.tsx +95 -0
  48. package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +1 -1
  49. package/src/features/PageEditor/Header/Breadcrumb.tsx +2 -2
  50. package/src/features/PageEditor/Header/index.tsx +15 -18
  51. package/src/features/PageEditor/Header/useMenu.tsx +12 -9
  52. package/src/features/PageEditor/PageEditor.tsx +45 -21
  53. package/src/features/PageEditor/PageEditorProvider.tsx +13 -1
  54. package/src/features/PageEditor/PageTitle/index.tsx +2 -2
  55. package/src/features/PageEditor/StoreUpdater.tsx +35 -308
  56. package/src/features/PageEditor/{Body/Title.tsx → TitleSection.tsx} +16 -16
  57. package/src/features/PageEditor/store/action.ts +96 -188
  58. package/src/features/PageEditor/store/initialState.ts +16 -21
  59. package/src/features/PageEditor/store/selectors.ts +3 -4
  60. package/src/features/PageExplorer/PageExplorerPlaceholder.tsx +22 -14
  61. package/src/features/PageExplorer/index.tsx +34 -67
  62. package/src/features/Portal/Artifacts/index.ts +0 -2
  63. package/src/features/Portal/Document/AutoSaveHint.tsx +7 -6
  64. package/src/features/Portal/Document/Body.tsx +1 -3
  65. package/src/features/Portal/Document/EditorCanvas.tsx +7 -50
  66. package/src/features/Portal/Document/Header.tsx +13 -10
  67. package/src/features/Portal/Document/TodoList.tsx +6 -4
  68. package/src/features/Portal/Document/Wrapper.tsx +3 -11
  69. package/src/features/Portal/Document/index.ts +0 -2
  70. package/src/features/Portal/FilePreview/index.ts +0 -2
  71. package/src/features/Portal/GroupThread/index.ts +0 -3
  72. package/src/features/Portal/MessageDetail/index.ts +0 -2
  73. package/src/features/Portal/Notebook/index.ts +0 -2
  74. package/src/features/Portal/Plugins/index.ts +0 -2
  75. package/src/features/Portal/Thread/index.ts +0 -3
  76. package/src/features/Portal/components/Header.tsx +18 -6
  77. package/src/features/Portal/router.tsx +34 -97
  78. package/src/features/Portal/type.ts +0 -2
  79. package/src/features/RightPanel/index.tsx +11 -2
  80. package/src/locales/default/plugin.ts +1 -0
  81. package/src/store/chat/slices/portal/action.test.ts +218 -15
  82. package/src/store/chat/slices/portal/action.ts +194 -41
  83. package/src/store/chat/slices/portal/initialState.ts +40 -1
  84. package/src/store/chat/slices/portal/selectors/thread.ts +44 -3
  85. package/src/store/chat/slices/portal/selectors.test.ts +119 -17
  86. package/src/store/chat/slices/portal/selectors.ts +117 -36
  87. package/src/store/document/index.ts +17 -5
  88. package/src/store/document/slices/document/action.ts +209 -0
  89. package/src/store/document/slices/document/index.ts +6 -0
  90. package/src/store/document/slices/editor/action.test.ts +340 -0
  91. package/src/store/document/slices/editor/action.ts +133 -149
  92. package/src/store/document/slices/editor/index.ts +9 -2
  93. package/src/store/document/slices/editor/initialState.ts +66 -29
  94. package/src/store/document/slices/editor/reducer.test.ts +217 -0
  95. package/src/store/document/slices/editor/reducer.ts +67 -0
  96. package/src/store/document/slices/editor/selectors.test.ts +395 -0
  97. package/src/store/document/slices/editor/selectors.ts +107 -5
  98. package/src/store/document/store.ts +12 -13
  99. package/src/store/file/slices/document/action.ts +19 -188
  100. package/src/store/file/slices/document/initialState.ts +0 -30
  101. package/src/store/file/slices/document/selectors.ts +25 -59
  102. package/src/store/global/initialState.ts +2 -0
  103. package/src/store/global/selectors/systemStatus.ts +2 -0
  104. package/src/store/notebook/index.ts +5 -4
  105. package/src/store/page/index.ts +2 -0
  106. package/src/store/page/initialState.ts +92 -0
  107. package/src/store/page/selectors.ts +5 -0
  108. package/src/store/page/slices/crud/action.ts +477 -0
  109. package/src/store/page/slices/crud/index.ts +2 -0
  110. package/src/store/page/slices/crud/initialState.ts +7 -0
  111. package/src/store/page/slices/internal/action.ts +32 -0
  112. package/src/store/page/slices/internal/index.ts +2 -0
  113. package/src/store/page/slices/internal/reducer.ts +105 -0
  114. package/src/store/page/slices/list/action.ts +206 -0
  115. package/src/store/page/slices/list/index.ts +3 -0
  116. package/src/store/page/slices/list/initialState.ts +29 -0
  117. package/src/store/page/slices/list/selectors.ts +90 -0
  118. package/src/store/page/slices/selection/action.ts +67 -0
  119. package/src/store/page/slices/selection/index.ts +2 -0
  120. package/src/store/page/slices/selection/initialState.ts +11 -0
  121. package/src/store/page/store.ts +29 -0
  122. package/src/store/tool/slices/lobehubSkillStore/selectors.ts +10 -20
  123. package/src/utils/identifier.ts +8 -2
  124. package/src/features/PageEditor/Body/index.tsx +0 -68
  125. package/src/features/PageEditor/EditorCanvas/InlineToolbar.tsx +0 -316
  126. package/src/features/PageEditor/Header/AutoSaveHint.tsx +0 -27
  127. package/src/features/Portal/Artifacts/useEnable.ts +0 -4
  128. package/src/features/Portal/Document/DocumentEditorProvider.tsx +0 -34
  129. package/src/features/Portal/Document/StoreUpdater.tsx +0 -80
  130. package/src/features/Portal/Document/Title.tsx +0 -54
  131. package/src/features/Portal/Document/store/action.ts +0 -114
  132. package/src/features/Portal/Document/store/index.ts +0 -21
  133. package/src/features/Portal/Document/store/initialState.ts +0 -24
  134. package/src/features/Portal/Document/useEnable.ts +0 -8
  135. package/src/features/Portal/FilePreview/useEnable.ts +0 -6
  136. package/src/features/Portal/GroupThread/hook.ts +0 -9
  137. package/src/features/Portal/MessageDetail/useEnable.ts +0 -4
  138. package/src/features/Portal/Notebook/useEnable.ts +0 -6
  139. package/src/features/Portal/Plugins/useEnable.ts +0 -6
  140. package/src/features/Portal/Thread/hook.ts +0 -8
  141. package/src/store/document/slices/notebook/action.ts +0 -119
  142. package/src/store/document/slices/notebook/index.ts +0 -3
  143. package/src/store/document/slices/notebook/initialState.ts +0 -12
  144. package/src/store/document/slices/notebook/selectors.ts +0 -26
@@ -1,43 +1,54 @@
1
1
  'use client';
2
2
 
3
3
  import { BUILTIN_AGENT_SLUGS } from '@lobechat/builtin-agents';
4
+ import { EditorProvider } from '@lobehub/editor/react';
4
5
  import { Flexbox } from '@lobehub/ui';
5
6
  import { cssVar } from 'antd-style';
6
7
  import { type FC, memo, useEffect } from 'react';
7
8
 
8
9
  import Loading from '@/components/Loading/BrandTextLoading';
10
+ import DiffAllToolbar from '@/features/EditorCanvas/DiffAllToolbar';
9
11
  import WideScreenContainer from '@/features/WideScreenContainer';
10
- import { useRegisterFilesHotkeys, useSaveDocumentHotkey } from '@/hooks/useHotkeys';
12
+ import { useRegisterFilesHotkeys } from '@/hooks/useHotkeys';
11
13
  import { useAgentStore } from '@/store/agent';
12
14
  import { builtinAgentSelectors } from '@/store/agent/selectors';
13
- import { useFileStore } from '@/store/file';
15
+ import { useDocumentStore } from '@/store/document';
16
+ import { editorSelectors } from '@/store/document/slices/editor';
17
+ import { usePageStore } from '@/store/page';
14
18
 
15
- import Body from './Body';
16
19
  import Copilot from './Copilot';
17
- import DiffAllToolbar from './DiffAllToolbar';
20
+ import EditorCanvas from './EditorCanvas';
18
21
  import Header from './Header';
19
22
  import PageAgentProvider from './PageAgentProvider';
20
23
  import { PageEditorProvider } from './PageEditorProvider';
21
24
  import PageTitle from './PageTitle';
25
+ import TitleSection from './TitleSection';
22
26
  import { usePageEditorStore } from './store';
23
27
 
24
28
  interface PageEditorProps {
29
+ emoji?: string;
25
30
  knowledgeBaseId?: string;
26
31
  onBack?: () => void;
27
32
  onDelete?: () => void;
28
33
  onDocumentIdChange?: (newId: string) => void;
34
+ onEmojiChange?: (emoji: string | undefined) => void;
29
35
  onSave?: () => void;
36
+ onTitleChange?: (title: string) => void;
30
37
  pageId?: string;
38
+ title?: string;
31
39
  }
32
40
 
33
41
  const PageEditorCanvas = memo(() => {
34
42
  const editor = usePageEditorStore((s) => s.editor);
35
- const flushSave = usePageEditorStore((s) => s.flushSave);
36
- const isDirty = usePageEditorStore((s) => s.isDirty);
43
+ const documentId = usePageEditorStore((s) => s.documentId);
44
+
45
+ // Get isDirty from DocumentStore
46
+ const isDirty = useDocumentStore((s) =>
47
+ documentId ? editorSelectors.isDirty(documentId)(s) : false,
48
+ );
37
49
 
38
50
  // Register Files scope and save document hotkey
39
51
  useRegisterFilesHotkeys();
40
- useSaveDocumentHotkey(flushSave);
41
52
 
42
53
  // Warn user before leaving page with unsaved changes
43
54
  useEffect(() => {
@@ -75,10 +86,13 @@ const PageEditorCanvas = memo(() => {
75
86
  width={'100%'}
76
87
  >
77
88
  <WideScreenContainer onClick={() => editor?.focus()} wrapperStyle={{ cursor: 'text' }}>
78
- <Body />
89
+ <Flexbox flex={1} style={{ overflowY: 'auto', position: 'relative' }}>
90
+ <TitleSection />
91
+ <EditorCanvas />
92
+ </Flexbox>
79
93
  </WideScreenContainer>
80
94
  </Flexbox>
81
- <DiffAllToolbar />
95
+ {documentId && <DiffAllToolbar documentId={documentId} editor={editor!} />}
82
96
  </Flexbox>
83
97
  <Copilot />
84
98
  </Flexbox>
@@ -95,31 +109,41 @@ export const PageEditor: FC<PageEditorProps> = ({
95
109
  pageId,
96
110
  knowledgeBaseId,
97
111
  onDocumentIdChange,
112
+ onEmojiChange,
98
113
  onSave,
114
+ onTitleChange,
99
115
  onBack,
116
+ title,
117
+ emoji,
100
118
  }) => {
101
119
  const useInitBuiltinAgent = useAgentStore((s) => s.useInitBuiltinAgent);
102
120
  const pageAgentId = useAgentStore(builtinAgentSelectors.pageAgentId);
103
121
 
104
122
  useInitBuiltinAgent(BUILTIN_AGENT_SLUGS.pageAgent);
105
123
 
106
- const [deletePage] = useFileStore((s) => [s.deletePage]);
124
+ const deletePage = usePageStore((s) => s.deletePage);
107
125
 
108
126
  if (!pageAgentId) return <Loading debugId="PageEditor > PageAgent Init" />;
109
127
 
110
128
  return (
111
129
  <PageAgentProvider pageAgentId={pageAgentId}>
112
- <PageEditorProvider
113
- key={pageId}
114
- knowledgeBaseId={knowledgeBaseId}
115
- onBack={onBack}
116
- onDelete={() => deletePage(pageId || '')}
117
- onDocumentIdChange={onDocumentIdChange}
118
- onSave={onSave}
119
- pageId={pageId}
120
- >
121
- <PageEditorCanvas />
122
- </PageEditorProvider>
130
+ <EditorProvider>
131
+ <PageEditorProvider
132
+ emoji={emoji}
133
+ key={pageId}
134
+ knowledgeBaseId={knowledgeBaseId}
135
+ onBack={onBack}
136
+ onDelete={() => deletePage(pageId || '')}
137
+ onDocumentIdChange={onDocumentIdChange}
138
+ onEmojiChange={onEmojiChange}
139
+ onSave={onSave}
140
+ onTitleChange={onTitleChange}
141
+ pageId={pageId}
142
+ title={title}
143
+ >
144
+ <PageEditorCanvas />
145
+ </PageEditorProvider>
146
+ </EditorProvider>
123
147
  </PageAgentProvider>
124
148
  );
125
149
  };
@@ -19,10 +19,14 @@ export const PageEditorProvider = memo<PageEditorProviderProps>(
19
19
  pageId,
20
20
  knowledgeBaseId,
21
21
  onDocumentIdChange,
22
+ onEmojiChange,
22
23
  onSave,
24
+ onTitleChange,
23
25
  onDelete,
24
26
  onBack,
25
27
  parentId,
28
+ title,
29
+ emoji,
26
30
  }) => {
27
31
  const editor = useEditor();
28
32
 
@@ -30,25 +34,33 @@ export const PageEditorProvider = memo<PageEditorProviderProps>(
30
34
  <Provider
31
35
  createStore={() =>
32
36
  createStore({
37
+ documentId: pageId,
33
38
  editor,
39
+ emoji,
34
40
  knowledgeBaseId,
35
41
  onBack,
36
42
  onDelete,
37
43
  onDocumentIdChange,
44
+ onEmojiChange,
38
45
  onSave,
39
- pageId,
46
+ onTitleChange,
40
47
  parentId,
48
+ title,
41
49
  })
42
50
  }
43
51
  >
44
52
  <StoreUpdater
53
+ emoji={emoji}
45
54
  knowledgeBaseId={knowledgeBaseId}
46
55
  onBack={onBack}
47
56
  onDelete={onDelete}
48
57
  onDocumentIdChange={onDocumentIdChange}
58
+ onEmojiChange={onEmojiChange}
49
59
  onSave={onSave}
60
+ onTitleChange={onTitleChange}
50
61
  pageId={pageId}
51
62
  parentId={parentId}
63
+ title={title}
52
64
  />
53
65
  {children}
54
66
  </Provider>
@@ -6,8 +6,8 @@ import PageTitle from '@/components/PageTitle';
6
6
  import { selectors, usePageEditorStore } from '@/features/PageEditor/store';
7
7
 
8
8
  const Title = memo(() => {
9
- const pageTitle = usePageEditorStore(selectors.currentTitle);
10
- return <PageTitle title={pageTitle} />;
9
+ const pageTitle = usePageEditorStore(selectors.title);
10
+ return pageTitle && <PageTitle title={pageTitle} />;
11
11
  });
12
12
 
13
13
  export default Title;
@@ -1,319 +1,61 @@
1
1
  'use client';
2
2
 
3
- import { useEditorState } from '@lobehub/editor/react';
4
- import debug from 'debug';
5
- import React, { memo, useEffect, useRef } from 'react';
3
+ import { memo, useEffect } from 'react';
6
4
  import { createStoreUpdater } from 'zustand-utils';
7
5
 
8
- import { useFileStore } from '@/store/file';
9
- import { documentSelectors } from '@/store/file/slices/document/selectors';
10
6
  import { pageAgentRuntime } from '@/store/tool/slices/builtin/executors/lobe-page-agent';
11
7
 
12
8
  import { type PublicState, usePageEditorStore, useStoreApi } from './store';
13
9
 
14
- const log = debug('page:store-updater');
15
-
16
- export type StoreUpdaterProps = Partial<PublicState>;
17
-
18
- // State machine types
19
- type InitPhase =
20
- | 'idle' // Initial state
21
- | 'waiting-for-data' // Waiting for SWR to fetch/return cached data
22
- | 'initializing' // Setting metadata (currentDocId, title, emoji)
23
- | 'loading-content' // Loading content into editor
24
- | 'ready' // Initialization complete
25
- | 'error'; // Error occurred
26
-
27
- interface InitState {
28
- error?: Error;
29
- phase: InitPhase;
30
- targetPageId: string | undefined;
10
+ export interface StoreUpdaterProps extends Partial<PublicState> {
11
+ pageId?: string;
31
12
  }
32
13
 
14
+ /**
15
+ * StoreUpdater syncs PageEditorStore props and connects to page agent runtime.
16
+ *
17
+ * Note: Document content loading is handled by EditorCanvas via DocumentStore.
18
+ * Title/emoji are consumed from PageEditorStore (set via setCurrentTitle/setCurrentEmoji).
19
+ */
33
20
  const StoreUpdater = memo<StoreUpdaterProps>(
34
- ({ pageId, knowledgeBaseId, onDocumentIdChange, onSave, onDelete, onBack, parentId }) => {
21
+ ({
22
+ pageId,
23
+ knowledgeBaseId,
24
+ onDocumentIdChange,
25
+ onEmojiChange,
26
+ onSave,
27
+ onTitleChange,
28
+ onDelete,
29
+ onBack,
30
+ parentId,
31
+ title,
32
+ emoji,
33
+ }) => {
35
34
  const storeApi = useStoreApi();
36
35
  const useStoreUpdater = createStoreUpdater(storeApi);
37
36
 
38
37
  const editor = usePageEditorStore((s) => s.editor);
39
- const editorState = useEditorState(editor);
40
- const currentDocId = usePageEditorStore((s) => s.currentDocId);
41
-
42
- // Use SWR hook for document fetching with caching
43
- const { isLoading: isLoadingDetail, error: swrError } = useFileStore((s) =>
44
- s.useFetchDocumentDetail(pageId),
45
- );
46
- const currentPage = useFileStore(documentSelectors.getDocumentById(pageId));
47
-
48
- const [editorInit, setEditorInit] = React.useState(false);
49
- const [contentInit, setContentInit] = React.useState(false);
50
- const [phaseUpdateCounter, setPhaseUpdateCounter] = React.useState(0);
51
- const lastLoadedDocIdRef = useRef<string | undefined>(undefined);
52
- const initStateRef = useRef<InitState>({
53
- phase: 'idle',
54
- targetPageId: undefined,
55
- });
56
-
57
- // Helper to transition phase and trigger re-render
58
- const transitionPhase = React.useCallback((newPhase: InitPhase) => {
59
- log(`Transitioning phase: ${initStateRef.current.phase} -> ${newPhase}`);
60
- initStateRef.current.phase = newPhase;
61
- setPhaseUpdateCounter((n) => n + 1); // Trigger re-render
62
- }, []);
63
-
64
- // Update editorState in store
65
- useEffect(() => {
66
- storeApi.setState({ editorState });
67
- }, [editorState, storeApi]);
38
+ const initMeta = usePageEditorStore((s) => s.initMeta);
68
39
 
69
40
  // Update store with props
70
- useStoreUpdater('pageId', pageId);
41
+ useStoreUpdater('documentId', pageId);
71
42
  useStoreUpdater('knowledgeBaseId', knowledgeBaseId);
72
43
  useStoreUpdater('onDocumentIdChange', onDocumentIdChange);
44
+ useStoreUpdater('onEmojiChange', onEmojiChange);
73
45
  useStoreUpdater('onSave', onSave);
46
+ useStoreUpdater('onTitleChange', onTitleChange);
74
47
  useStoreUpdater('onDelete', onDelete);
75
48
  useStoreUpdater('onBack', onBack);
76
49
  useStoreUpdater('parentId', parentId);
77
50
 
78
- // State machine effect for deterministic initialization
51
+ // Initialize meta (title/emoji) with dirty tracking
79
52
  useEffect(() => {
80
- const state = initStateRef.current;
81
-
82
- // Phase handler functions
83
- const handleIdlePhase = () => {
84
- // Check if we can start initialization
85
- if (!pageId || !editor || !editorInit) {
86
- log('idle: Waiting for prerequisites', { editor: !!editor, editorInit, pageId });
87
- return;
88
- }
89
-
90
- // Transition to waiting-for-data
91
- log('idle -> waiting-for-data:', pageId);
92
-
93
- // Reset UI state
94
- setContentInit(false);
95
- storeApi.setState({
96
- currentTitle: '',
97
- isLoadingContent: true,
98
- wordCount: 0,
99
- });
100
-
101
- transitionPhase('waiting-for-data');
102
- };
103
-
104
- const handleWaitingForDataPhase = () => {
105
- // Check for errors
106
- if (swrError && !isLoadingDetail) {
107
- log('waiting-for-data: Error occurred', swrError);
108
- initStateRef.current.error = swrError as Error;
109
- storeApi.setState({ isLoadingContent: false });
110
- transitionPhase('error');
111
- return;
112
- }
113
-
114
- // Wait for SWR to finish loading
115
- if (isLoadingDetail) {
116
- log('waiting-for-data: Still loading...');
117
- return;
118
- }
119
-
120
- // Check if we have data
121
- if (!currentPage) {
122
- log('waiting-for-data: No data available yet');
123
- return;
124
- }
125
-
126
- // Transition to initializing
127
- log('waiting-for-data -> initializing');
128
- transitionPhase('initializing');
129
- };
130
-
131
- const handleInitializingPhase = () => {
132
- // Check if already initialized for this pageId
133
- if (lastLoadedDocIdRef.current === pageId && currentDocId === pageId) {
134
- log('initializing: Already initialized, moving to loading-content');
135
- transitionPhase('loading-content');
136
- return;
137
- }
138
-
139
- // Set metadata
140
- log('initializing: Setting metadata for pageId:', pageId, {
141
- hasEditorData: !!currentPage?.editorData,
142
- title: currentPage?.title,
143
- });
144
-
145
- lastLoadedDocIdRef.current = pageId;
146
- setContentInit(false);
147
-
148
- storeApi.setState({
149
- currentDocId: pageId,
150
- currentEmoji: currentPage?.metadata?.emoji,
151
- currentTitle: currentPage?.title || '',
152
- });
153
-
154
- // Transition to loading-content
155
- log('initializing -> loading-content');
156
- transitionPhase('loading-content');
157
- };
158
-
159
- const handleLoadingContentPhase = () => {
160
- // Prerequisites check
161
- if (!editor || !editorInit || contentInit) {
162
- log('loading-content: Waiting', { contentInit, editor: !!editor, editorInit });
163
- return;
164
- }
165
-
166
- // Safety check: Prevent loading stale content
167
- const currentState = storeApi.getState();
168
- if (currentState.currentDocId && currentState.currentDocId !== pageId) {
169
- log('loading-content: currentDocId mismatch, aborting', {
170
- currentDocId: currentState.currentDocId,
171
- pageId,
172
- });
173
- initStateRef.current.error = new Error('Document ID mismatch');
174
- storeApi.setState({ isLoadingContent: false });
175
- transitionPhase('error');
176
- return;
177
- }
178
-
179
- // Load content (defer to avoid flushSync warning)
180
- log('loading-content: Queueing content load');
181
-
182
- queueMicrotask(() => {
183
- try {
184
- // Re-check state in case pageId changed during microtask
185
- if (initStateRef.current.targetPageId !== pageId) {
186
- log('loading-content: PageId changed during queue, aborting');
187
- return;
188
- }
189
-
190
- log('Loading content for page:', pageId);
191
-
192
- // Helper to calculate word count
193
- const calculateWordCount = (text: string) =>
194
- text.trim().split(/\s+/).filter(Boolean).length;
195
-
196
- storeApi.setState({ lastUpdatedTime: null });
197
-
198
- // Check if editorData is valid and non-empty
199
- const hasValidEditorData =
200
- currentPage?.editorData &&
201
- typeof currentPage.editorData === 'object' &&
202
- Object.keys(currentPage.editorData).length > 0;
203
-
204
- // Load from editorData if available
205
- if (hasValidEditorData) {
206
- log('Loading from editorData');
207
- editor.setDocument('json', JSON.stringify(currentPage.editorData));
208
- const textContent = currentPage.content || '';
209
- storeApi.setState({ wordCount: calculateWordCount(textContent) });
210
- } else if (currentPage?.content && currentPage.content.trim()) {
211
- log('Loading from content - no valid editorData found');
212
- editor.setDocument('markdown', currentPage.content);
213
- storeApi.setState({ wordCount: calculateWordCount(currentPage.content) });
214
- } else if (currentPage?.pages) {
215
- // Fallback to pages content
216
- const pagesContent = currentPage.pages
217
- .map((page) => page.pageContent)
218
- .join('\n\n')
219
- .trim();
220
- if (pagesContent) {
221
- log('Loading from pages content');
222
- editor.setDocument('markdown', pagesContent);
223
- storeApi.setState({ wordCount: calculateWordCount(pagesContent) });
224
- } else {
225
- log('Clearing editor - empty pages');
226
- editor.setDocument('markdown', ' ');
227
- storeApi.setState({ wordCount: 0 });
228
- }
229
- } else {
230
- // Empty document or temp page - clear editor with minimal content
231
- log('Clearing editor - empty/new page');
232
- editor.setDocument('markdown', ' ');
233
- storeApi.setState({ wordCount: 0 });
234
- }
235
-
236
- setContentInit(true);
237
- storeApi.setState({ isLoadingContent: false });
238
-
239
- // Transition to ready
240
- log('loading-content -> ready');
241
- transitionPhase('ready');
242
- } catch (error) {
243
- log('Failed to load editor content:', error);
244
- storeApi.setState({ isLoadingContent: false });
245
- initStateRef.current.error = error as Error;
246
- transitionPhase('error');
247
- }
248
- });
249
- };
250
-
251
- const handleErrorPhase = () => {
252
- const error = initStateRef.current.error;
253
- log('error phase:', error?.message);
254
- // Error state is sticky until pageId changes
255
- };
256
-
257
- // Reset to idle if pageId changes
258
- if (pageId !== state.targetPageId) {
259
- log('PageId changed, resetting to idle', { from: state.targetPageId, to: pageId });
260
- initStateRef.current = { phase: 'idle', targetPageId: pageId };
261
- setContentInit(false);
262
- return;
263
- }
264
-
265
- // Early exit if already ready
266
- if (state.phase === 'ready') return;
267
-
268
- // Execute phase handler
269
- switch (state.phase) {
270
- case 'idle': {
271
- handleIdlePhase();
272
- break;
273
- }
274
- case 'waiting-for-data': {
275
- handleWaitingForDataPhase();
276
- break;
277
- }
278
- case 'initializing': {
279
- handleInitializingPhase();
280
- break;
281
- }
282
- case 'loading-content': {
283
- handleLoadingContentPhase();
284
- break;
285
- }
286
- case 'error': {
287
- handleErrorPhase();
288
- break;
289
- }
290
- }
291
- }, [
292
- contentInit,
293
- currentDocId,
294
- currentPage,
295
- editor,
296
- editorInit,
297
- isLoadingDetail,
298
- pageId,
299
- phaseUpdateCounter,
300
- storeApi,
301
- swrError,
302
- transitionPhase,
303
- ]);
304
-
305
- // Track editor initialization
306
- useEffect(() => {
307
- if (editor && !editorInit) {
308
- setEditorInit(true);
309
- }
310
- }, [editor, editorInit]);
53
+ initMeta(title, emoji);
54
+ }, [pageId, title, emoji]);
311
55
 
312
56
  // Connect editor to page agent runtime
313
57
  useEffect(() => {
314
58
  if (editor) {
315
- // for easier debug , mount editor instance to window
316
- window.__editor = editor;
317
59
  pageAgentRuntime.setEditor(editor);
318
60
  }
319
61
  return () => {
@@ -321,35 +63,20 @@ const StoreUpdater = memo<StoreUpdaterProps>(
321
63
  };
322
64
  }, [editor]);
323
65
 
324
- // Connect title handlers to page agent runtime
66
+ // Connect title handlers and document ID to page agent runtime
325
67
  useEffect(() => {
326
- const titleSetter = (title: string) => {
327
- storeApi.setState({ currentTitle: title });
328
- };
329
-
330
68
  const titleGetter = () => {
331
- return storeApi.getState().currentTitle;
69
+ return storeApi.getState().title || '';
332
70
  };
333
71
 
334
- pageAgentRuntime.setTitleHandlers(titleSetter, titleGetter);
72
+ pageAgentRuntime.setCurrentDocId(pageId);
73
+ pageAgentRuntime.setTitleHandlers(storeApi.getState().setTitle, titleGetter);
335
74
 
336
75
  return () => {
337
- pageAgentRuntime.setTitleHandlers(null, null);
338
- };
339
- }, [storeApi]);
340
-
341
- // Update current document ID in page agent runtime when page changes
342
- useEffect(() => {
343
- // Use currentDocId (which includes temp docs) or fallback to pageId
344
- const activeId = currentDocId || pageId;
345
- log('Updating currentDocId in page agent runtime:', activeId);
346
- pageAgentRuntime.setCurrentDocId(activeId);
347
-
348
- return () => {
349
- log('Clearing currentDocId on unmount');
350
76
  pageAgentRuntime.setCurrentDocId(undefined);
77
+ pageAgentRuntime.setTitleHandlers(null, null);
351
78
  };
352
- }, [currentDocId, pageId]);
79
+ }, [pageId, storeApi]);
353
80
 
354
81
  return null;
355
82
  },
@@ -11,16 +11,16 @@ import { useGlobalStore } from '@/store/global';
11
11
  import { globalGeneralSelectors } from '@/store/global/selectors';
12
12
  import { truncateByWeightedLength } from '@/utils/textLength';
13
13
 
14
- import { usePageEditorStore } from '../store';
14
+ import { usePageEditorStore } from './store';
15
15
 
16
- const Title = memo(() => {
16
+ const TitleSection = memo(() => {
17
17
  const { t } = useTranslation('file');
18
18
  const locale = useGlobalStore(globalGeneralSelectors.currentLanguage);
19
19
 
20
- const currentEmoji = usePageEditorStore((s) => s.currentEmoji);
21
- const currentTitle = usePageEditorStore((s) => s.currentTitle);
22
- const setCurrentEmoji = usePageEditorStore((s) => s.setCurrentEmoji);
23
- const setCurrentTitle = usePageEditorStore((s) => s.setCurrentTitle);
20
+ const emoji = usePageEditorStore((s) => s.emoji);
21
+ const title = usePageEditorStore((s) => s.title);
22
+ const setEmoji = usePageEditorStore((s) => s.setEmoji);
23
+ const setTitle = usePageEditorStore((s) => s.setTitle);
24
24
  const handleTitleSubmit = usePageEditorStore((s) => s.handleTitleSubmit);
25
25
 
26
26
  const [isHoveringTitle, setIsHoveringTitle] = useState(false);
@@ -41,16 +41,16 @@ const Title = memo(() => {
41
41
  }}
42
42
  >
43
43
  {/* Emoji picker above Choose Icon button */}
44
- {(currentEmoji || showEmojiPicker) && (
44
+ {(emoji || showEmojiPicker) && (
45
45
  <EmojiPicker
46
46
  allowDelete
47
47
  locale={locale}
48
- onChange={(emoji) => {
49
- setCurrentEmoji(emoji);
48
+ onChange={(e) => {
49
+ setEmoji(e);
50
50
  setShowEmojiPicker(false);
51
51
  }}
52
52
  onDelete={() => {
53
- setCurrentEmoji(undefined);
53
+ setEmoji(undefined);
54
54
  setShowEmojiPicker(false);
55
55
  }}
56
56
  onOpenChange={(open) => {
@@ -60,16 +60,16 @@ const Title = memo(() => {
60
60
  shape={'square'}
61
61
  size={72}
62
62
  title={t('pageEditor.chooseIcon')}
63
- value={currentEmoji}
63
+ value={emoji}
64
64
  />
65
65
  )}
66
66
 
67
67
  {/* Choose Icon button - only shown when no emoji */}
68
- {!currentEmoji && !showEmojiPicker && (
68
+ {!emoji && !showEmojiPicker && (
69
69
  <Button
70
70
  icon={<Icon icon={SmilePlus} />}
71
71
  onClick={() => {
72
- setCurrentEmoji('📄');
72
+ setEmoji('📄');
73
73
  setShowEmojiPicker(true);
74
74
  }}
75
75
  size="small"
@@ -89,7 +89,7 @@ const Title = memo(() => {
89
89
  autoSize={{ minRows: 1 }}
90
90
  onChange={(e) => {
91
91
  const truncated = truncateByWeightedLength(e.target.value, 100);
92
- setCurrentTitle(truncated);
92
+ setTitle(truncated);
93
93
  }}
94
94
  onKeyDown={(e) => {
95
95
  if (e.key === 'Enter') {
@@ -105,11 +105,11 @@ const Title = memo(() => {
105
105
  resize: 'none',
106
106
  width: '100%',
107
107
  }}
108
- value={currentTitle}
108
+ value={title}
109
109
  variant={'borderless'}
110
110
  />
111
111
  </Flexbox>
112
112
  );
113
113
  });
114
114
 
115
- export default Title;
115
+ export default TitleSection;