@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
@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import { PortalContent } from '@/features/Portal/router';
9
9
  import { useChatStore } from '@/store/chat';
10
+ import { portalThreadSelectors } from '@/store/chat/selectors';
10
11
 
11
12
  const styles = createStaticStyles(({ css, cssVar }) => ({
12
13
  container: css`
@@ -17,7 +18,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
17
18
  const Layout = () => {
18
19
  const [showMobilePortal, isPortalThread, togglePortal] = useChatStore((s) => [
19
20
  s.showPortal,
20
- !!s.portalThreadId,
21
+ portalThreadSelectors.showThread(s),
21
22
  s.togglePortal,
22
23
  ]);
23
24
  const { t } = useTranslation('portal');
@@ -13,8 +13,8 @@ import { DEFAULT_CHAT_GROUP_CHAT_CONFIG } from '@/const/settings';
13
13
  import { type GroupMemberConfig, chatGroupService } from '@/services/chatGroup';
14
14
  import { useAgentStore } from '@/store/agent';
15
15
  import { useAgentGroupStore } from '@/store/agentGroup';
16
- import { useFileStore } from '@/store/file';
17
16
  import { useHomeStore } from '@/store/home';
17
+ import { usePageStore } from '@/store/page';
18
18
 
19
19
  interface HostConfig {
20
20
  model?: string;
@@ -45,7 +45,7 @@ export const useCreateMenuItems = () => {
45
45
  s.switchToGroup,
46
46
  ]);
47
47
  const [createGroup, loadGroups] = useAgentGroupStore((s) => [s.createGroup, s.loadGroups]);
48
- const createNewPage = useFileStore((s) => s.createNewPage);
48
+ const createNewPage = usePageStore((s) => s.createNewPage);
49
49
 
50
50
  const [isCreatingGroup, setIsCreatingGroup] = useState(false);
51
51
  const [isCreatingSessionGroup, setIsCreatingSessionGroup] = useState(false);
@@ -11,4 +11,3 @@ const Title = memo(() => {
11
11
  Title.displayName = 'PageTitle';
12
12
 
13
13
  export default Title;
14
-
@@ -1 +1,43 @@
1
- export { default } from '../index';
1
+ 'use client';
2
+
3
+ import { useUnmount } from 'ahooks';
4
+ import { Suspense, memo } from 'react';
5
+ import { useParams } from 'react-router-dom';
6
+ import { createStoreUpdater } from 'zustand-utils';
7
+
8
+ import Loading from '@/components/Loading/BrandTextLoading';
9
+ import PageExplorer from '@/features/PageExplorer';
10
+ import { usePageStore } from '@/store/page';
11
+ import { getIdFromIdentifier } from '@/utils/identifier';
12
+
13
+ import PageTitle from '../PageTitle';
14
+
15
+ /**
16
+ * Pages route - dedicated page for managing documents/pages
17
+ * This is extracted from the /resource route to have its own dedicated space
18
+ */
19
+ const PagesPage = memo(() => {
20
+ const storeUpdater = createStoreUpdater(usePageStore);
21
+ const params = useParams<{ id: string }>();
22
+
23
+ const pageId = getIdFromIdentifier(params.id ?? '', 'docs');
24
+ storeUpdater('selectedPageId', pageId);
25
+
26
+ // Clear activeAgentId when unmounting (leaving chat page)
27
+ useUnmount(() => {
28
+ usePageStore.setState({ selectedPageId: undefined });
29
+ });
30
+
31
+ return (
32
+ <>
33
+ <PageTitle />
34
+ <Suspense fallback={<Loading debugId="PagesPage" />}>
35
+ <PageExplorer pageId={pageId} />
36
+ </Suspense>
37
+ </>
38
+ );
39
+ });
40
+
41
+ PagesPage.displayName = 'PagesPage';
42
+
43
+ export default PagesPage;
@@ -6,7 +6,7 @@ import { VList, type VListHandle } from 'virtua';
6
6
 
7
7
  import SkeletonList from '@/features/NavPanel/components/SkeletonList';
8
8
  import PageEmpty from '@/features/PageEmpty';
9
- import { documentSelectors, useFileStore } from '@/store/file';
9
+ import { pageSelectors, usePageStore } from '@/store/page';
10
10
  import { type LobeDocument } from '@/types/document';
11
11
 
12
12
  import Item from '../List/Item';
@@ -19,27 +19,27 @@ const Content = memo<ContentProps>(({ searchKeyword }) => {
19
19
  const virtuaRef = useRef<VListHandle>(null);
20
20
  const fetchedCountRef = useRef(-1);
21
21
 
22
- const [hasMore, isLoadingMore, loadMoreDocuments] = useFileStore((s) => [
23
- documentSelectors.hasMoreDocuments(s),
24
- documentSelectors.isLoadingMoreDocuments(s),
22
+ const [hasMore, isLoadingMore, loadMoreDocuments] = usePageStore((s) => [
23
+ pageSelectors.hasMoreDocuments(s),
24
+ pageSelectors.isLoadingMoreDocuments(s),
25
25
  s.loadMoreDocuments,
26
26
  ]);
27
27
 
28
- const allFilteredPages = useFileStore(documentSelectors.getFilteredPages);
28
+ const allFilteredDocuments = usePageStore(pageSelectors.getFilteredDocuments);
29
29
 
30
30
  // Filter by search keyword
31
- const displayPages = useMemo(() => {
32
- if (!searchKeyword.trim()) return allFilteredPages;
31
+ const displayDocuments = useMemo(() => {
32
+ if (!searchKeyword.trim()) return allFilteredDocuments;
33
33
 
34
34
  const keyword = searchKeyword.toLowerCase();
35
- return allFilteredPages.filter((page: LobeDocument) => {
36
- const content = page.content?.toLowerCase() || '';
37
- const title = page.title?.toLowerCase() || '';
35
+ return allFilteredDocuments.filter((doc: LobeDocument) => {
36
+ const content = doc.content?.toLowerCase() || '';
37
+ const title = doc.title?.toLowerCase() || '';
38
38
  return content.includes(keyword) || title.includes(keyword);
39
39
  });
40
- }, [allFilteredPages, searchKeyword]);
40
+ }, [allFilteredDocuments, searchKeyword]);
41
41
 
42
- const count = displayPages.length;
42
+ const count = displayDocuments.length;
43
43
  const isSearching = searchKeyword.trim().length > 0;
44
44
 
45
45
  // Handle scroll - use findItemIndex (official pattern)
@@ -74,9 +74,9 @@ const Content = memo<ContentProps>(({ searchKeyword }) => {
74
74
  ref={virtuaRef}
75
75
  style={{ height: '100%' }}
76
76
  >
77
- {displayPages.map((page) => (
78
- <Flexbox gap={1} key={page.id} padding={'4px 8px'}>
79
- <Item pageId={page.id} />
77
+ {displayDocuments.map((doc) => (
78
+ <Flexbox gap={1} key={doc.id} padding={'4px 8px'}>
79
+ <Item pageId={doc.id} />
80
80
  </Flexbox>
81
81
  ))}
82
82
  {showLoading && (
@@ -4,9 +4,9 @@ import { useTranslation } from 'react-i18next';
4
4
 
5
5
  import EmojiPicker from '@/components/EmojiPicker';
6
6
  import { useIsDark } from '@/hooks/useIsDark';
7
- import { useFileStore } from '@/store/file';
8
7
  import { useGlobalStore } from '@/store/global';
9
8
  import { globalGeneralSelectors } from '@/store/global/selectors';
9
+ import { usePageStore } from '@/store/page';
10
10
 
11
11
  interface EditingProps {
12
12
  currentEmoji?: string;
@@ -20,7 +20,7 @@ const Editing = memo<EditingProps>(({ documentId, title, currentEmoji, toggleEdi
20
20
  const isDarkMode = useIsDark();
21
21
  const { t } = useTranslation('file');
22
22
 
23
- const editing = useFileStore((s) => s.renamingPageId === documentId);
23
+ const editing = usePageStore((s) => s.renamingPageId === documentId);
24
24
 
25
25
  const [newTitle, setNewTitle] = useState(title);
26
26
  const [newEmoji, setNewEmoji] = useState(currentEmoji);
@@ -35,7 +35,7 @@ const Editing = memo<EditingProps>(({ documentId, title, currentEmoji, toggleEdi
35
35
  if (newTitle && title !== newTitle) updates.title = newTitle;
36
36
  if (newEmoji !== undefined && currentEmoji !== newEmoji) updates.emoji = newEmoji;
37
37
 
38
- await useFileStore.getState().renamePage(documentId, updates.title || title, updates.emoji);
38
+ await usePageStore.getState().renamePage(documentId, updates.title || title, updates.emoji);
39
39
  } catch (error) {
40
40
  console.error('Failed to update page:', error);
41
41
  }
@@ -4,7 +4,7 @@ import { memo, useCallback, useMemo } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
6
6
  import NavItem from '@/features/NavPanel/components/NavItem';
7
- import { documentSelectors, useFileStore } from '@/store/file';
7
+ import { pageSelectors, usePageStore } from '@/store/page';
8
8
 
9
9
  import Actions from './Actions';
10
10
  import Editing from './Editing';
@@ -17,18 +17,13 @@ interface DocumentItemProps {
17
17
 
18
18
  const PageListItem = memo<DocumentItemProps>(({ pageId, className }) => {
19
19
  const { t } = useTranslation('file');
20
- const [editing, selectedPageId, document] = useFileStore(
21
- useCallback(
22
- (s) => {
23
- const doc = documentSelectors.getDocumentById(pageId)(s);
24
- return [s.renamingPageId === pageId, s.selectedPageId, doc] as const;
25
- },
26
- [pageId],
27
- ),
28
- );
20
+ const [editing, selectedPageId, document] = usePageStore((s) => {
21
+ const doc = pageSelectors.getDocumentById(pageId)(s);
22
+ return [s.renamingPageId === pageId, s.selectedPageId, doc] as const;
23
+ });
29
24
 
30
- const selectPage = useFileStore((s) => s.selectPage);
31
- const setRenamingPageId = useFileStore((s) => s.setRenamingPageId);
25
+ const selectPage = usePageStore((s) => s.selectPage);
26
+ const setRenamingPageId = usePageStore((s) => s.setRenamingPageId);
32
27
 
33
28
  const active = selectedPageId === pageId;
34
29
  const title = document?.title || t('pageList.untitled');
@@ -4,7 +4,7 @@ import { Copy, CopyPlus, Pencil, Trash2 } from 'lucide-react';
4
4
  import { useCallback } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
- import { useFileStore } from '@/store/file';
7
+ import { usePageStore } from '@/store/page';
8
8
 
9
9
  interface ActionProps {
10
10
  documentContent?: string;
@@ -19,8 +19,8 @@ export const useDropdownMenu = ({
19
19
  }: ActionProps): (() => MenuProps['items']) => {
20
20
  const { t } = useTranslation(['common', 'file']);
21
21
  const { message, modal } = App.useApp();
22
- const removeDocument = useFileStore((s) => s.removeDocument);
23
- const duplicateDocument = useFileStore((s) => s.duplicateDocument);
22
+ const removePage = usePageStore((s) => s.removePage);
23
+ const duplicatePage = usePageStore((s) => s.duplicatePage);
24
24
 
25
25
  const handleDelete = () => {
26
26
  modal.confirm({
@@ -30,7 +30,7 @@ export const useDropdownMenu = ({
30
30
  okText: t('delete'),
31
31
  onOk: async () => {
32
32
  try {
33
- await removeDocument(pageId);
33
+ await removePage(pageId);
34
34
  message.success(t('pageEditor.deleteSuccess', { ns: 'file' }));
35
35
  } catch (error) {
36
36
  console.error('Failed to delete page:', error);
@@ -53,7 +53,7 @@ export const useDropdownMenu = ({
53
53
 
54
54
  const handleDuplicate = async () => {
55
55
  try {
56
- await duplicateDocument(pageId);
56
+ await duplicatePage(pageId);
57
57
  } catch (error) {
58
58
  console.error('Failed to duplicate page:', error);
59
59
  }
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import NavItem from '@/features/NavPanel/components/NavItem';
8
8
  import SkeletonList from '@/features/NavPanel/components/SkeletonList';
9
- import { documentSelectors, useFileStore } from '@/store/file';
9
+ import { pageSelectors, usePageStore } from '@/store/page';
10
10
 
11
11
  import Item from './Item';
12
12
 
@@ -16,17 +16,17 @@ import Item from './Item';
16
16
  const PageList = () => {
17
17
  const { t } = useTranslation(['file', 'common']);
18
18
 
19
- const [filteredPages, hasMore, isLoadingMore, openAllPagesDrawer] = useFileStore((s) => [
20
- documentSelectors.getFilteredPagesLimited(s),
21
- documentSelectors.hasMoreFilteredPages(s),
22
- documentSelectors.isLoadingMoreDocuments(s),
19
+ const [filteredDocuments, hasMore, isLoadingMore, openAllPagesDrawer] = usePageStore((s) => [
20
+ pageSelectors.getFilteredDocumentsLimited(s),
21
+ pageSelectors.hasMoreFilteredDocuments(s),
22
+ pageSelectors.isLoadingMoreDocuments(s),
23
23
  s.openAllPagesDrawer,
24
24
  ]);
25
25
 
26
26
  return (
27
27
  <Flexbox gap={1}>
28
- {filteredPages.map((page) => (
29
- <Item key={page.id} pageId={page.id} />
28
+ {filteredDocuments.map((doc) => (
29
+ <Item key={doc.id} pageId={doc.id} />
30
30
  ))}
31
31
  {isLoadingMore && <SkeletonList rows={3} />}
32
32
  {hasMore && !isLoadingMore && (
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import SkeletonList from '@/features/NavPanel/components/SkeletonList';
8
8
  import PageEmpty from '@/features/PageEmpty';
9
- import { documentSelectors, useFileStore } from '@/store/file';
9
+ import { pageSelectors, usePageStore } from '@/store/page';
10
10
 
11
11
  import Actions from './Actions';
12
12
  import AllPagesDrawer from './AllPagesDrawer';
@@ -22,12 +22,18 @@ export enum GroupKey {
22
22
  */
23
23
  const Body = memo(() => {
24
24
  const { t } = useTranslation('file');
25
- const isDocumentListLoading = useFileStore((s) => s.isDocumentListLoading);
26
- const filteredPagesCount = useFileStore(documentSelectors.filteredPagesCount);
27
- const filteredPages = useFileStore(documentSelectors.getFilteredPagesLimited);
28
- const searchKeywords = useFileStore((s) => s.searchKeywords);
25
+
26
+ // Initialize documents list via SWR
27
+ const useFetchDocuments = usePageStore((s) => s.useFetchDocuments);
28
+ useFetchDocuments();
29
+
30
+ const isLoading = usePageStore(pageSelectors.isDocumentsLoading);
31
+
32
+ const filteredDocumentsCount = usePageStore(pageSelectors.filteredDocumentsCount);
33
+ const filteredDocuments = usePageStore(pageSelectors.getFilteredDocumentsLimited);
34
+ const searchKeywords = usePageStore((s) => s.searchKeywords);
29
35
  const dropdownMenu = useDropdownMenu();
30
- const [allPagesDrawerOpen, closeAllPagesDrawer] = useFileStore((s) => [
36
+ const [allPagesDrawerOpen, closeAllPagesDrawer] = usePageStore((s) => [
31
37
  s.allPagesDrawerOpen,
32
38
  s.closeAllPagesDrawer,
33
39
  ]);
@@ -53,16 +59,16 @@ const Body = memo(() => {
53
59
  title={
54
60
  <Text ellipsis fontSize={12} type={'secondary'} weight={500}>
55
61
  {t('pageList.title')}
56
- {filteredPagesCount > 0 && ` ${filteredPagesCount}`}
62
+ {filteredDocumentsCount > 0 && ` ${filteredDocumentsCount}`}
57
63
  </Text>
58
64
  }
59
65
  >
60
66
  <Suspense fallback={<SkeletonList />}>
61
- {isDocumentListLoading ? (
67
+ {isLoading ? (
62
68
  <SkeletonList />
63
69
  ) : (
64
70
  <Flexbox gap={1} paddingBlock={1}>
65
- {filteredPages.length === 0 ? (
71
+ {filteredDocuments.length === 0 ? (
66
72
  <PageEmpty search={Boolean(searchKeywords.trim())} />
67
73
  ) : (
68
74
  <List />
@@ -6,14 +6,14 @@ import { Hash, LucideCheck } from 'lucide-react';
6
6
  import { useMemo } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
 
9
- import { useFileStore } from '@/store/file';
10
9
  import { useGlobalStore } from '@/store/global';
11
10
  import { systemStatusSelectors } from '@/store/global/selectors';
11
+ import { usePageStore } from '@/store/page';
12
12
 
13
13
  export const useDropdownMenu = (): MenuProps['items'] => {
14
14
  const { t } = useTranslation();
15
- const showOnlyPagesNotInLibrary = useFileStore((s) => s.showOnlyPagesNotInLibrary);
16
- const setShowOnlyPagesNotInLibrary = useFileStore((s) => s.setShowOnlyPagesNotInLibrary);
15
+ const showOnlyPagesNotInLibrary = usePageStore((s) => s.showOnlyPagesNotInLibrary);
16
+ const setShowOnlyPagesNotInLibrary = usePageStore((s) => s.setShowOnlyPagesNotInLibrary);
17
17
 
18
18
  const [pagePageSize, updateSystemStatus] = useGlobalStore((s) => [
19
19
  systemStatusSelectors.pagePageSize(s),
@@ -0,0 +1,15 @@
1
+ import { useNavigate } from 'react-router-dom';
2
+ import { createStoreUpdater } from 'zustand-utils';
3
+
4
+ import { usePageStore } from '@/store/page';
5
+
6
+ const DataSync = () => {
7
+ const usePageStoreUpdater = createStoreUpdater(usePageStore);
8
+
9
+ const navigate = useNavigate();
10
+ usePageStoreUpdater('navigate', navigate);
11
+
12
+ return null;
13
+ };
14
+
15
+ export default DataSync;
@@ -5,12 +5,12 @@ import { SquarePenIcon } from 'lucide-react';
5
5
  import { memo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
- import { useFileStore } from '@/store/file';
8
+ import { usePageStore } from '@/store/page';
9
9
 
10
10
  const AddButton = memo(() => {
11
11
  const { t } = useTranslation('file');
12
12
 
13
- const createNewPage = useFileStore((s) => s.createNewPage);
13
+ const createNewPage = usePageStore((s) => s.createNewPage);
14
14
 
15
15
  const handleNewDocument = () => {
16
16
  const untitledTitle = t('pageList.untitled');
@@ -4,6 +4,7 @@ import { Flexbox } from '@lobehub/ui';
4
4
  import { type FC } from 'react';
5
5
  import { Outlet } from 'react-router-dom';
6
6
 
7
+ import DataSync from './DataSync';
7
8
  import Sidebar from './Sidebar';
8
9
  import { styles } from './style';
9
10
 
@@ -14,6 +15,7 @@ const DesktopPagesLayout: FC = () => {
14
15
  <Flexbox className={styles.mainContainer} flex={1} height={'100%'}>
15
16
  <Outlet />
16
17
  </Flexbox>
18
+ <DataSync />
17
19
  </>
18
20
  );
19
21
  };
@@ -1,26 +1,22 @@
1
1
  'use client';
2
2
 
3
3
  import { Suspense, memo } from 'react';
4
- import { useParams } from 'react-router-dom';
5
4
 
6
5
  import Loading from '@/components/Loading/BrandTextLoading';
7
- import PageExplorer from '@/features/PageExplorer';
8
- import { standardizeIdentifier } from '@/utils/identifier';
6
+ import PageExplorerPlaceholder from '@/features/PageExplorer/PageExplorerPlaceholder';
9
7
 
10
- import PageTitle from './features/PageTitle';
8
+ import PageTitle from './PageTitle';
11
9
 
12
10
  /**
13
11
  * Pages route - dedicated page for managing documents/pages
14
12
  * This is extracted from the /resource route to have its own dedicated space
15
13
  */
16
14
  const PagesPage = memo(() => {
17
- const { id } = useParams<{ id: string }>();
18
-
19
15
  return (
20
16
  <>
21
17
  <PageTitle />
22
18
  <Suspense fallback={<Loading debugId="PagesPage" />}>
23
- <PageExplorer pageId={standardizeIdentifier(id ?? '', 'docs')} />
19
+ <PageExplorerPlaceholder />
24
20
  </Suspense>
25
21
  </>
26
22
  );
@@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next';
10
10
  dayjs.extend(relativeTime);
11
11
 
12
12
  interface AutoSaveHintProps {
13
- lastUpdatedTime?: Date | null;
13
+ lastUpdatedTime?: string | Date | null;
14
14
  saveStatus: 'idle' | 'saving' | 'saved';
15
15
  style?: CSSProperties;
16
16
  }
@@ -76,7 +76,11 @@ export const UserActionsBar = memo<UserActionsProps>(({ actionsConfig, id, data
76
76
  // Use external config if provided, otherwise use defaults
77
77
  // Append extra actions from factories
78
78
  const barItems = useMemo(() => {
79
- const base = actionsConfig?.bar ?? [defaultActions.regenerate, defaultActions.edit];
79
+ const base = actionsConfig?.bar ?? [
80
+ defaultActions.regenerate,
81
+ defaultActions.edit,
82
+ defaultActions.copy,
83
+ ];
80
84
  return [...base, ...extraBarItems];
81
85
  }, [actionsConfig?.bar, defaultActions.regenerate, defaultActions.edit, extraBarItems]);
82
86
 
@@ -0,0 +1,37 @@
1
+ 'use client';
2
+
3
+ import { type CSSProperties, memo } from 'react';
4
+
5
+ import AutoSaveHintBase from '@/components/Editor/AutoSaveHint';
6
+ import { useDocumentStore } from '@/store/document';
7
+ import { editorSelectors } from '@/store/document/slices/editor';
8
+
9
+ export interface AutoSaveHintProps {
10
+ /**
11
+ * Document ID to get save status from DocumentStore
12
+ */
13
+ documentId: string;
14
+ /**
15
+ * Custom styles
16
+ */
17
+ style?: CSSProperties;
18
+ }
19
+
20
+ /**
21
+ * AutoSave hint component that reads from DocumentStore
22
+ * Use this component externally to display save status for a document
23
+ */
24
+ const AutoSaveHint = memo<AutoSaveHintProps>(({ documentId, style }) => {
25
+ const saveStatus = useDocumentStore((s) => editorSelectors.saveStatus(documentId)(s));
26
+ const lastUpdatedTime = useDocumentStore(
27
+ (s) => editorSelectors.lastUpdatedTime(documentId)(s) ?? null,
28
+ );
29
+
30
+ return (
31
+ <AutoSaveHintBase lastUpdatedTime={lastUpdatedTime} saveStatus={saveStatus} style={style} />
32
+ );
33
+ });
34
+
35
+ AutoSaveHint.displayName = 'AutoSaveHint';
36
+
37
+ export default AutoSaveHint;
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { DiffAction, LITEXML_DIFFNODE_ALL_COMMAND } from '@lobehub/editor';
3
+ import { DiffAction, IEditor, LITEXML_DIFFNODE_ALL_COMMAND } from '@lobehub/editor';
4
4
  import { Block, Icon } from '@lobehub/ui';
5
5
  import { Button, Space } from 'antd';
6
6
  import { createStaticStyles, cssVar, cx } from 'antd-style';
@@ -9,8 +9,7 @@ import { memo, useEffect, useState } from 'react';
9
9
  import { useTranslation } from 'react-i18next';
10
10
 
11
11
  import { useIsDark } from '@/hooks/useIsDark';
12
-
13
- import { usePageEditorStore } from './store';
12
+ import { useDocumentStore } from '@/store/document';
14
13
 
15
14
  const styles = createStaticStyles(({ css }) => ({
16
15
  container: css`
@@ -36,18 +35,36 @@ const styles = createStaticStyles(({ css }) => ({
36
35
  `,
37
36
  }));
38
37
 
39
- const DiffAllToolbar = memo(() => {
40
- const { t } = useTranslation('editor');
41
- const isDarkMode = useIsDark();
42
- const [editor, performSave] = usePageEditorStore((s) => [s.editor, s.performSave]);
38
+ const useIsEditorInit = (editor: IEditor) => {
39
+ const [isEditInit, setEditInit] = useState<boolean>(!!editor.getLexicalEditor());
40
+
41
+ useEffect(() => {
42
+ if (!editor) return;
43
+
44
+ const onInit = () => {
45
+ console.log('init: id', editor.getLexicalEditor()?._key);
46
+ setEditInit(true);
47
+ };
48
+ editor.on('initialized', onInit);
49
+ return () => {
50
+ editor.off('initialized', onInit);
51
+ };
52
+ }, [editor]);
53
+
54
+ return isEditInit;
55
+ };
56
+
57
+ const useEditorHasPendingDiffs = (editor: IEditor) => {
43
58
  const [hasPendingDiffs, setHasPendingDiffs] = useState(false);
59
+ const isEditInit = useIsEditorInit(editor);
44
60
 
45
61
  // Listen to editor state changes to detect diff nodes
46
62
  useEffect(() => {
47
63
  if (!editor) return;
48
64
 
49
65
  const lexicalEditor = editor.getLexicalEditor();
50
- if (!lexicalEditor) return;
66
+
67
+ if (!lexicalEditor || !isEditInit) return;
51
68
 
52
69
  const checkForDiffNodes = () => {
53
70
  const editorState = lexicalEditor.getEditorState();
@@ -67,15 +84,39 @@ const DiffAllToolbar = memo(() => {
67
84
  // Check initially
68
85
  checkForDiffNodes();
69
86
 
70
- // Register update listener
71
87
  const unregister = lexicalEditor.registerUpdateListener(() => {
72
88
  checkForDiffNodes();
73
89
  });
90
+ // Register update listener
91
+ return () => {
92
+ unregister();
93
+ };
94
+ }, [editor, isEditInit]);
74
95
 
75
- return unregister;
76
- }, [editor]);
96
+ return hasPendingDiffs;
97
+ };
98
+
99
+ interface DiffAllToolbarProps {
100
+ documentId: string;
101
+ editor: IEditor;
102
+ }
103
+ const DiffAllToolbar = memo<DiffAllToolbarProps>(({ documentId }) => {
104
+ const { t } = useTranslation('editor');
105
+ const isDarkMode = useIsDark();
106
+ const [editor, performSave, markDirty] = useDocumentStore((s) => [
107
+ s.editor!,
108
+ s.performSave,
109
+ s.markDirty,
110
+ ]);
111
+
112
+ const hasPendingDiffs = useEditorHasPendingDiffs(editor);
113
+
114
+ if (!hasPendingDiffs) return null;
77
115
 
78
- if (!editor || !hasPendingDiffs) return null;
116
+ const handleSave = async () => {
117
+ markDirty(documentId);
118
+ await performSave();
119
+ };
79
120
 
80
121
  return (
81
122
  <div className={styles.container}>
@@ -90,10 +131,10 @@ const DiffAllToolbar = memo(() => {
90
131
  <Space>
91
132
  <Button
92
133
  onClick={async () => {
93
- editor.dispatchCommand(LITEXML_DIFFNODE_ALL_COMMAND, {
134
+ editor?.dispatchCommand(LITEXML_DIFFNODE_ALL_COMMAND, {
94
135
  action: DiffAction.Reject,
95
136
  });
96
- await performSave({ force: true });
137
+ await handleSave();
97
138
  }}
98
139
  size={'small'}
99
140
  type="text"
@@ -104,10 +145,10 @@ const DiffAllToolbar = memo(() => {
104
145
  <Button
105
146
  color={'default'}
106
147
  onClick={async () => {
107
- editor.dispatchCommand(LITEXML_DIFFNODE_ALL_COMMAND, {
148
+ editor?.dispatchCommand(LITEXML_DIFFNODE_ALL_COMMAND, {
108
149
  action: DiffAction.Accept,
109
150
  });
110
- await performSave({ force: true });
151
+ await handleSave();
111
152
  }}
112
153
  size={'small'}
113
154
  variant="filled"