@lobehub/lobehub 2.0.0-next.249 → 2.0.0-next.250

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 (87) hide show
  1. package/.github/workflows/release.yml +4 -0
  2. package/CHANGELOG.md +25 -0
  3. package/changelog/v1.json +5 -0
  4. package/locales/en-US/file.json +2 -0
  5. package/locales/zh-CN/discover.json +3 -0
  6. package/locales/zh-CN/file.json +2 -0
  7. package/package.json +3 -3
  8. package/packages/types/package.json +2 -2
  9. package/packages/types/src/discover/mcp.ts +3 -1
  10. package/src/app/[variants]/(main)/community/(list)/(home)/index.tsx +2 -0
  11. package/src/app/[variants]/(main)/community/(list)/features/SortButton/index.tsx +4 -0
  12. package/src/app/[variants]/(main)/community/(list)/mcp/features/Category/index.tsx +7 -3
  13. package/src/app/[variants]/(main)/community/(list)/mcp/index.tsx +2 -2
  14. package/src/app/[variants]/(main)/home/_layout/Body/Project/List/Editing.tsx +1 -1
  15. package/src/app/[variants]/(main)/home/_layout/Body/Project/List/Item.tsx +1 -1
  16. package/src/app/[variants]/(main)/home/_layout/Body/Project/List/index.tsx +1 -1
  17. package/src/app/[variants]/(main)/home/_layout/Body/Project/List/useDropdownMenu.tsx +1 -1
  18. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/{List/Item → Item}/Editing.tsx +1 -1
  19. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/{List/Item → Item}/index.tsx +1 -1
  20. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/{List/Item → Item}/useDropdownMenu.tsx +1 -1
  21. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/index.tsx +16 -6
  22. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/{KnowledgeBase.tsx → index.tsx} +2 -3
  23. package/src/app/[variants]/(main)/resource/(home)/_layout/Sidebar.tsx +2 -2
  24. package/src/app/[variants]/(main)/resource/(home)/index.tsx +23 -10
  25. package/src/app/[variants]/(main)/resource/features/DndContextWrapper.tsx +12 -37
  26. package/src/app/[variants]/(main)/resource/features/hooks/useKnowledgeItem.ts +1 -1
  27. package/src/app/[variants]/(main)/resource/features/store/action.ts +9 -39
  28. package/src/app/[variants]/(main)/resource/library/_layout/Header/LibraryHead.tsx +1 -1
  29. package/src/app/[variants]/(main)/resource/library/index.tsx +13 -6
  30. package/src/features/LibraryModal/AddFilesToKnowledgeBase/SelectForm.tsx +1 -1
  31. package/src/features/LibraryModal/CreateNew/CreateForm.tsx +1 -1
  32. package/src/features/PageEditor/Header/Breadcrumb.tsx +1 -1
  33. package/src/features/PageEditor/store/action.ts +5 -2
  34. package/src/features/PageExplorer/PageExplorerPlaceholder.tsx +5 -7
  35. package/src/features/ResourceManager/components/Explorer/Header/Breadcrumb.tsx +1 -1
  36. package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +57 -26
  37. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +35 -6
  38. package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +20 -14
  39. package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +41 -31
  40. package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/index.tsx +1 -1
  41. package/src/features/ResourceManager/components/Explorer/MasonryView/Skeleton.tsx +6 -2
  42. package/src/features/ResourceManager/components/Explorer/MasonryView/index.tsx +29 -18
  43. package/src/features/ResourceManager/components/Explorer/MoveToFolderModal.tsx +7 -34
  44. package/src/features/ResourceManager/components/Explorer/ToolBar/BatchActionsDropdown.tsx +1 -1
  45. package/src/features/ResourceManager/components/Explorer/index.tsx +58 -18
  46. package/src/features/ResourceManager/components/Explorer/useCheckTaskStatus.ts +6 -4
  47. package/src/features/ResourceManager/components/Header/AddButton.tsx +58 -35
  48. package/src/features/ResourceManager/components/Header/hooks/useNotionImport.ts +5 -5
  49. package/src/features/ResourceManager/components/Tree/TreeSkeleton.tsx +19 -9
  50. package/src/features/ResourceManager/components/Tree/index.tsx +110 -5
  51. package/src/features/ResourceManager/components/UploadDock/index.tsx +2 -1
  52. package/src/features/ResourceManager/constants.ts +3 -0
  53. package/src/hooks/useMCPCategory.tsx +7 -0
  54. package/src/locales/default/discover.ts +3 -0
  55. package/src/locales/default/file.ts +2 -0
  56. package/src/services/file/index.ts +34 -1
  57. package/src/services/resource/index.ts +249 -0
  58. package/src/store/discover/slices/mcp/action.ts +1 -1
  59. package/src/store/file/slices/chat/action.ts +2 -1
  60. package/src/store/file/slices/document/action.ts +10 -7
  61. package/src/store/file/slices/fileManager/action.ts +14 -4
  62. package/src/store/file/slices/fileManager/initialState.ts +2 -0
  63. package/src/store/file/slices/resource/action.ts +432 -0
  64. package/src/store/file/slices/resource/hooks.ts +82 -0
  65. package/src/store/file/slices/resource/initialState.ts +67 -0
  66. package/src/store/file/slices/resource/syncEngine.ts +326 -0
  67. package/src/store/file/store.ts +6 -1
  68. package/src/store/{knowledgeBase → library}/initialState.ts +2 -2
  69. package/src/store/{knowledgeBase → library}/slices/content/action.test.ts +37 -51
  70. package/src/store/{knowledgeBase → library}/slices/content/action.ts +8 -4
  71. package/src/store/{knowledgeBase → library}/slices/crud/action.ts +1 -1
  72. package/src/store/{knowledgeBase → library}/slices/crud/selectors.ts +1 -1
  73. package/src/store/{knowledgeBase → library}/slices/ragEval/actions/dataset.ts +1 -1
  74. package/src/store/{knowledgeBase → library}/slices/ragEval/actions/evaluation.ts +1 -1
  75. package/src/store/{knowledgeBase → library}/slices/ragEval/actions/index.ts +1 -1
  76. package/src/types/resource.ts +133 -0
  77. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/List/index.tsx +0 -25
  78. /package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/{List/Item → Item}/Actions.tsx +0 -0
  79. /package/src/store/{knowledgeBase → library}/index.ts +0 -0
  80. /package/src/store/{knowledgeBase → library}/selectors.ts +0 -0
  81. /package/src/store/{knowledgeBase → library}/slices/content/index.ts +0 -0
  82. /package/src/store/{knowledgeBase → library}/slices/crud/action.test.ts +0 -0
  83. /package/src/store/{knowledgeBase → library}/slices/crud/index.ts +0 -0
  84. /package/src/store/{knowledgeBase → library}/slices/crud/initialState.ts +0 -0
  85. /package/src/store/{knowledgeBase → library}/slices/ragEval/index.ts +0 -0
  86. /package/src/store/{knowledgeBase → library}/slices/ragEval/initialState.ts +0 -0
  87. /package/src/store/{knowledgeBase → library}/store.ts +0 -0
@@ -34,8 +34,11 @@ const MasonryViewSkeleton = memo<MasonrySkeletonProps>(({ columnCount }) => {
34
34
  // Generate varying heights for more natural masonry look
35
35
  const heights = [180, 220, 200, 190, 240, 210, 200, 230, 180, 220, 210, 190];
36
36
 
37
- // Calculate number of items based on viewport and column count
38
- const itemCount = Math.min(columnCount * 3, 12);
37
+ // Calculate number of items based on viewport and column count, minimum 6 items
38
+ const itemCount = Math.max(Math.min(columnCount * 3, 12), 6);
39
+
40
+ // Calculate opacity gradient from 100% to 20%
41
+ const getOpacity = (index: number) => 1 - (index / (itemCount - 1)) * 0.8;
39
42
 
40
43
  return (
41
44
  <div
@@ -50,6 +53,7 @@ const MasonryViewSkeleton = memo<MasonrySkeletonProps>(({ columnCount }) => {
50
53
  key={index}
51
54
  style={{
52
55
  height: heights[index % heights.length],
56
+ opacity: getOpacity(index),
53
57
  }}
54
58
  />
55
59
  ))}
@@ -6,12 +6,10 @@ import { cssVar } from 'antd-style';
6
6
  import { type UIEvent, memo, useCallback, useMemo, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
 
9
- import { useFolderPath } from '@/app/[variants]/(main)/resource/features/hooks/useFolderPath';
10
- import {
11
- useResourceManagerFetchKnowledgeItems,
12
- useResourceManagerStore,
13
- } from '@/app/[variants]/(main)/resource/features/store';
9
+ import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
14
10
  import { sortFileList } from '@/app/[variants]/(main)/resource/features/store/selectors';
11
+ import { useFileStore } from '@/store/file';
12
+ import { type FileListItem } from '@/types/files';
15
13
 
16
14
  import { useMasonryColumnCount } from '../useMasonryColumnCount';
17
15
  import MasonryItemWrapper from './MasonryFileItem/MasonryItemWrapper';
@@ -20,8 +18,6 @@ const MasonryView = memo(() => {
20
18
  // Access all state from Resource Manager store
21
19
  const [
22
20
  libraryId,
23
- category,
24
- searchQuery,
25
21
  selectedFileIds,
26
22
  setSelectedFileIds,
27
23
  loadMoreKnowledgeItems,
@@ -31,8 +27,6 @@ const MasonryView = memo(() => {
31
27
  sortType,
32
28
  ] = useResourceManagerStore((s) => [
33
29
  s.libraryId,
34
- s.category,
35
- s.searchQuery,
36
30
  s.selectedFileIds,
37
31
  s.setSelectedFileIds,
38
32
  s.loadMoreKnowledgeItems,
@@ -46,16 +40,33 @@ const MasonryView = memo(() => {
46
40
  const columnCount = useMasonryColumnCount();
47
41
  const [isLoadingMore, setIsLoadingMore] = useState(false);
48
42
 
49
- const { currentFolderSlug } = useFolderPath();
43
+ // NEW: Read from resource store instead of fetching independently
44
+ const resourceList = useFileStore((s) => s.resourceList);
50
45
 
51
- // Fetch data with SWR
52
- const { data: rawData } = useResourceManagerFetchKnowledgeItems({
53
- category,
54
- knowledgeBaseId: libraryId,
55
- parentId: currentFolderSlug || null,
56
- q: searchQuery ?? undefined,
57
- showFilesInKnowledgeBase: false,
58
- });
46
+ // Map ResourceItem[] to FileListItem[] for compatibility
47
+ const rawData = resourceList?.map(
48
+ (item): FileListItem => ({
49
+ chunkCount: item.chunkCount ?? null,
50
+ chunkingError: item.chunkingError ?? null,
51
+ chunkingStatus: (item.chunkingStatus as any) ?? null,
52
+ content: item.content,
53
+ createdAt: item.createdAt,
54
+ editorData: item.editorData,
55
+ embeddingError: item.embeddingError ?? null,
56
+ embeddingStatus: (item.embeddingStatus as any) ?? null,
57
+ fileType: item.fileType,
58
+ finishEmbedding: item.finishEmbedding ?? false,
59
+ id: item.id,
60
+ metadata: item.metadata,
61
+ name: item.name,
62
+ parentId: item.parentId,
63
+ size: item.size,
64
+ slug: item.slug,
65
+ sourceType: item.sourceType,
66
+ updatedAt: item.updatedAt,
67
+ url: item.url ?? '',
68
+ }),
69
+ );
59
70
 
60
71
  // Sort data using current sort settings
61
72
  const data = sortFileList(rawData, sorter, sortType);
@@ -11,14 +11,13 @@ import { useFileStore } from '@/store/file';
11
11
 
12
12
  interface MoveToFolderModalProps {
13
13
  fileId: string;
14
- fileType?: string;
15
14
  knowledgeBaseId?: string;
16
15
  onClose: () => void;
17
16
  open: boolean;
18
17
  }
19
18
 
20
19
  const MoveToFolderModal = memo<MoveToFolderModalProps>(
21
- ({ open, onClose, fileId, fileType, knowledgeBaseId }) => {
20
+ ({ open, onClose, fileId, knowledgeBaseId }) => {
22
21
  const { t } = useTranslation('components');
23
22
  const { message } = App.useApp();
24
23
 
@@ -29,11 +28,9 @@ const MoveToFolderModal = memo<MoveToFolderModalProps>(
29
28
  const [loadedFolders, setLoadedFolders] = useState<Set<string>>(new Set());
30
29
  const [isCreatingFolder, setIsCreatingFolder] = useState(false);
31
30
 
32
- const [moveFileToFolder, refreshFileList, createFolder, updateDocument] = useFileStore((s) => [
33
- s.moveFileToFolder,
34
- s.refreshFileList,
31
+ const [moveResource, createFolder] = useFileStore((s) => [
32
+ s.moveResource,
35
33
  s.createFolder,
36
- s.updateDocument,
37
34
  ]);
38
35
 
39
36
  // Sort items: folders only
@@ -226,20 +223,8 @@ const MoveToFolderModal = memo<MoveToFolderModalProps>(
226
223
 
227
224
  const handleMove = async () => {
228
225
  try {
229
- // Detect if item is a document/folder (vs regular file)
230
- const isDocument =
231
- fileType === 'custom/document' || fileType === 'custom/folder';
232
-
233
- if (isDocument) {
234
- // Use updateDocument for Pages and folders
235
- await updateDocument(fileId, { parentId: selectedFolderId });
236
- } else {
237
- // Use moveFileToFolder for regular files
238
- await moveFileToFolder(fileId, selectedFolderId);
239
- }
240
-
241
- // Refresh file list to invalidate SWR cache for both Explorer and Tree
242
- await refreshFileList();
226
+ // Use optimistic moveResource for instant UI update
227
+ await moveResource(fileId, selectedFolderId);
243
228
 
244
229
  // Clear and reload all expanded folders in Tree's module-level cache
245
230
  if (knowledgeBaseId) {
@@ -256,20 +241,8 @@ const MoveToFolderModal = memo<MoveToFolderModalProps>(
256
241
 
257
242
  const handleMoveToRoot = async () => {
258
243
  try {
259
- // Detect if item is a document/folder (vs regular file)
260
- const isDocument =
261
- fileType === 'custom/document' || fileType === 'custom/folder';
262
-
263
- if (isDocument) {
264
- // Use updateDocument for Pages and folders
265
- await updateDocument(fileId, { parentId: null });
266
- } else {
267
- // Use moveFileToFolder for regular files
268
- await moveFileToFolder(fileId, null);
269
- }
270
-
271
- // Refresh file list to invalidate SWR cache for both Explorer and Tree
272
- await refreshFileList();
244
+ // Use optimistic moveResource for instant UI update
245
+ await moveResource(fileId, null);
273
246
 
274
247
  // Clear and reload all expanded folders in Tree's module-level cache
275
248
  if (knowledgeBaseId) {
@@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next';
12
12
 
13
13
  import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
14
14
  import RepoIcon from '@/components/LibIcon';
15
- import { useKnowledgeBaseStore } from '@/store/knowledgeBase';
15
+ import { useKnowledgeBaseStore } from '@/store/library';
16
16
 
17
17
  import ActionIconWithChevron from './ActionIconWithChevron';
18
18
 
@@ -1,15 +1,13 @@
1
1
  'use client';
2
2
 
3
3
  import { Flexbox } from '@lobehub/ui';
4
- import { memo, useEffect } from 'react';
4
+ import { memo, useEffect, useMemo } from 'react';
5
5
 
6
6
  import { useFolderPath } from '@/app/[variants]/(main)/resource/features/hooks/useFolderPath';
7
7
  import { useResourceManagerUrlSync } from '@/app/[variants]/(main)/resource/features/hooks/useResourceManagerUrlSync';
8
- import {
9
- useResourceManagerFetchKnowledgeItems,
10
- useResourceManagerStore,
11
- } from '@/app/[variants]/(main)/resource/features/store';
8
+ import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
12
9
  import { sortFileList } from '@/app/[variants]/(main)/resource/features/store/selectors';
10
+ import { useFetchResources, useResourceStore } from '@/store/file/slices/resource/hooks';
13
11
 
14
12
  import EmptyPlaceholder from './EmptyPlaceholder';
15
13
  import Header from './Header';
@@ -59,14 +57,53 @@ const ResourceExplorer = memo(() => {
59
57
  // Get folder path for empty state check
60
58
  const { currentFolderSlug } = useFolderPath();
61
59
 
62
- // Fetch data with SWR - uses built-in cache for instant category switching
63
- const { data: rawData, isLoading } = useResourceManagerFetchKnowledgeItems({
64
- category,
65
- knowledgeBaseId: libraryId,
66
- parentId: currentFolderSlug || null,
67
- q: searchQuery ?? undefined,
68
- showFilesInKnowledgeBase: false,
69
- });
60
+ // Build query params for SWR
61
+ const queryParams = useMemo(
62
+ () => ({
63
+ // Only use category filter when NOT in a specific library
64
+ // When viewing a library, show all items regardless of category
65
+ category: libraryId ? undefined : category,
66
+ libraryId,
67
+ parentId: currentFolderSlug || null,
68
+ q: searchQuery ?? undefined,
69
+ showFilesInKnowledgeBase: false,
70
+ sortType,
71
+ sorter,
72
+ }),
73
+ [category, libraryId, currentFolderSlug, searchQuery, sortType, sorter],
74
+ );
75
+
76
+ // Use SWR for data fetching with automatic caching and revalidation
77
+ const { isLoading, isValidating } = useFetchResources(queryParams);
78
+
79
+ // Get resource data from store (updated by SWR hook)
80
+ const { resourceList, queryParams: currentQueryParams } = useResourceStore();
81
+
82
+ // Check if we're navigating to a different view (different query params)
83
+ const isNavigating = useMemo(() => {
84
+ if (!currentQueryParams || !queryParams) return false;
85
+
86
+ return (
87
+ currentQueryParams.libraryId !== queryParams.libraryId ||
88
+ currentQueryParams.parentId !== queryParams.parentId ||
89
+ currentQueryParams.category !== queryParams.category ||
90
+ currentQueryParams.q !== queryParams.q
91
+ );
92
+ }, [currentQueryParams, queryParams]);
93
+
94
+ // Map ResourceItem[] to FileListItem[] for compatibility
95
+ // TODO: Eventually update all consumers to use ResourceItem directly
96
+ const rawData = resourceList?.map((item) => ({
97
+ ...item,
98
+ // Ensure all FileListItem fields are present with proper types
99
+ chunkCount: item.chunkCount ?? null,
100
+ chunkingError: item.chunkingError ?? null,
101
+ chunkingStatus: (item.chunkingStatus ?? null) as any,
102
+ embeddingError: item.embeddingError ?? null,
103
+ embeddingStatus: (item.embeddingStatus ?? null) as any,
104
+ finishEmbedding: item.finishEmbedding ?? false,
105
+ url: item.url ?? '',
106
+ }));
70
107
 
71
108
  // Sort data using current sort settings
72
109
  const data = sortFileList(rawData, sorter, sortType);
@@ -83,17 +120,20 @@ const ResourceExplorer = memo(() => {
83
120
  }, [category, libraryId, searchQuery, setSelectedFileIds]);
84
121
 
85
122
  // Computed values
86
- const showEmptyStatus = !isLoading && data?.length === 0 && !currentFolderSlug;
87
-
88
123
  const columnCount = useMasonryColumnCount();
89
124
 
90
- // Only show skeleton on INITIAL load or view transitions, not during revalidation
91
- // This allows cached data to show instantly while revalidating in background
125
+ // Show skeleton when:
126
+ // 1. Initial load with no data (isLoading && no data)
127
+ // 2. Navigating to different folder/category (isNavigating && isValidating)
128
+ // 3. View mode transitions
92
129
  const showSkeleton =
93
- (isLoading && !data) || // Only show skeleton if truly loading with no cached data
130
+ (isLoading && (!data || data.length >= 5)) ||
131
+ (isNavigating && isValidating) ||
94
132
  (viewMode === 'list' && isTransitioning) ||
95
133
  (viewMode === 'masonry' && (isTransitioning || !isMasonryReady));
96
134
 
135
+ const showEmptyStatus = !isLoading && !isValidating && data?.length === 0 && !currentFolderSlug;
136
+
97
137
  return (
98
138
  <Flexbox height={'100%'}>
99
139
  <Header />
@@ -1,11 +1,10 @@
1
1
  import { useEffect } from 'react';
2
2
 
3
- import { useFileStore } from '@/store/file';
4
3
  import { AsyncTaskStatus } from '@/types/asyncTask';
4
+ import { revalidateResources } from '@/store/file/slices/resource/hooks';
5
5
  import { type FileListItem } from '@/types/files';
6
6
 
7
7
  export const useCheckTaskStatus = (data: FileListItem[] | undefined) => {
8
- const [refreshFileList] = useFileStore((s) => [s.refreshFileList]);
9
8
  const hasProcessingChunkTask = data?.some(
10
9
  (item) => item.chunkingStatus === AsyncTaskStatus.Processing,
11
10
  );
@@ -15,11 +14,14 @@ export const useCheckTaskStatus = (data: FileListItem[] | undefined) => {
15
14
 
16
15
  const isProcessing = hasProcessingChunkTask || hasProcessingEmbeddingTask;
17
16
 
18
- // every 3s to check if the chunking status is changed
17
+ // Poll every 5s to check if chunking/embedding status has changed
19
18
  useEffect(() => {
20
19
  if (!isProcessing) return;
21
20
 
22
- const interval = setInterval(refreshFileList, 5000);
21
+ const interval = setInterval(() => {
22
+ // Re-fetch with the same query params used for initial load
23
+ revalidateResources();
24
+ }, 5000);
23
25
  return () => {
24
26
  clearInterval(interval);
25
27
  };
@@ -9,11 +9,11 @@ import { useCallback, useMemo } from 'react';
9
9
  import { useTranslation } from 'react-i18next';
10
10
 
11
11
  import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
12
+ import { message } from '@/components/AntdStaticMethods';
12
13
  import DragUpload from '@/components/DragUpload';
13
14
  import GuideModal from '@/components/GuideModal';
14
15
  import GuideVideo from '@/components/GuideVideo';
15
16
  import { useFileStore } from '@/store/file';
16
- import { DocumentSourceType } from '@/types/document';
17
17
 
18
18
  import useNotionImport from './hooks/useNotionImport';
19
19
  import useUploadFolder from './hooks/useUploadFolder';
@@ -22,9 +22,12 @@ const AddButton = () => {
22
22
  const { t } = useTranslation('file');
23
23
  const pushDockFileList = useFileStore((s) => s.pushDockFileList);
24
24
  const uploadFolderWithStructure = useFileStore((s) => s.uploadFolderWithStructure);
25
- const createFolder = useFileStore((s) => s.createFolder);
25
+ const createResource = useFileStore((s) => s.createResource);
26
+ const createResourceAndSync = useFileStore((s) => s.createResourceAndSync);
27
+
28
+ // TODO: Migrate Notion import to use createResource
29
+ // Keep old functions temporarily for components not yet migrated
26
30
  const createDocument = useFileStore((s) => s.createDocument);
27
- const refreshFileList = useFileStore((s) => s.refreshFileList);
28
31
 
29
32
  const [libraryId, currentFolderId, setCurrentViewItemId, setMode, setPendingRenameItemId] =
30
33
  useResourceManagerStore((s) => [
@@ -36,47 +39,64 @@ const AddButton = () => {
36
39
  ]);
37
40
 
38
41
  const handleOpenPageEditor = useCallback(async () => {
39
- // Create a new page directly and switch to page view
42
+ // Create a new page with optimistic update - instant UI feedback
40
43
  const untitledTitle = t('pageList.untitled');
41
- const newPage = await createDocument({
44
+ const tempId = await createResource({
42
45
  content: '',
46
+ fileType: 'custom/document',
43
47
  knowledgeBaseId: libraryId,
44
48
  parentId: currentFolderId ?? undefined,
49
+ sourceType: 'document',
45
50
  title: untitledTitle,
46
51
  });
47
52
 
48
- // Add to local document map for immediate availability
49
- const newDocumentMap = new Map(useFileStore.getState().localDocumentMap);
50
- newDocumentMap.set(newPage.id, {
51
- content: newPage.content || '',
52
- createdAt: newPage.createdAt ? new Date(newPage.createdAt) : new Date(),
53
- editorData:
54
- typeof newPage.editorData === 'string'
55
- ? JSON.parse(newPage.editorData)
56
- : newPage.editorData || null,
57
- fileType: 'custom/document',
58
- filename: newPage.title || untitledTitle,
59
- id: newPage.id,
60
- metadata: newPage.metadata || {},
61
- source: 'document',
62
- sourceType: DocumentSourceType.EDITOR,
63
- title: newPage.title || untitledTitle,
64
- totalCharCount: newPage.content?.length || 0,
65
- totalLineCount: 0,
66
- updatedAt: newPage.updatedAt ? new Date(newPage.updatedAt) : new Date(),
67
- });
68
- useFileStore.setState({ localDocumentMap: newDocumentMap });
69
-
70
- // Switch to page view mode
71
- setCurrentViewItemId(newPage.id);
53
+ // Switch to page view mode immediately (temp ID works)
54
+ setCurrentViewItemId(tempId);
72
55
  setMode('page');
73
- }, [createDocument, currentFolderId, libraryId, setCurrentViewItemId, setMode, t]);
56
+ }, [createResource, currentFolderId, libraryId, setCurrentViewItemId, setMode, t]);
74
57
 
75
58
  const handleCreateFolder = useCallback(async () => {
76
- const folderId = await createFolder('Untitled', currentFolderId ?? undefined, libraryId);
77
- // Trigger auto-rename
78
- setPendingRenameItemId(folderId);
79
- }, [createFolder, currentFolderId, libraryId, setPendingRenameItemId]);
59
+ // Create folder and wait for sync to complete before triggering rename
60
+ try {
61
+ // Get current resource list to check for duplicate folder names
62
+ const resourceList = useFileStore.getState().resourceList || [];
63
+
64
+ // Filter for folders at the same level
65
+ const foldersAtSameLevel = resourceList.filter(
66
+ (item) =>
67
+ item.fileType === 'custom/folder' &&
68
+ (item.parentId ?? null) === (currentFolderId ?? null),
69
+ );
70
+
71
+ // Generate unique folder name
72
+ const baseName = 'Untitled';
73
+ const existingNames = new Set(foldersAtSameLevel.map((folder) => folder.name));
74
+
75
+ let uniqueName = baseName;
76
+ let counter = 1;
77
+
78
+ while (existingNames.has(uniqueName)) {
79
+ uniqueName = `${baseName} ${counter}`;
80
+ counter++;
81
+ }
82
+
83
+ // Wait for sync to complete to get the real ID
84
+ const realId = await createResourceAndSync({
85
+ content: '',
86
+ fileType: 'custom/folder',
87
+ knowledgeBaseId: libraryId,
88
+ parentId: currentFolderId ?? undefined,
89
+ sourceType: 'document',
90
+ title: uniqueName,
91
+ });
92
+
93
+ // Trigger auto-rename with the real ID (after sync completes)
94
+ setPendingRenameItemId(realId);
95
+ } catch (error) {
96
+ message.error(t('header.actions.createFolderError'));
97
+ console.error('Failed to create folder:', error);
98
+ }
99
+ }, [createResourceAndSync, currentFolderId, libraryId, setPendingRenameItemId, t]);
80
100
 
81
101
  const {
82
102
  handleCloseNotionGuide,
@@ -89,7 +109,10 @@ const AddButton = () => {
89
109
  createDocument,
90
110
  currentFolderId,
91
111
  libraryId,
92
- refreshFileList,
112
+ refetchResources: async () => {
113
+ const { revalidateResources } = await import('@/store/file/slices/resource/hooks');
114
+ await revalidateResources();
115
+ },
93
116
  t,
94
117
  });
95
118
 
@@ -11,7 +11,7 @@ interface UseNotionImportOptions {
11
11
  createDocument: DocumentAction['createDocument'];
12
12
  currentFolderId?: string | null;
13
13
  libraryId?: string | null;
14
- refreshFileList: () => Promise<void>;
14
+ refetchResources: () => Promise<void>;
15
15
  t: TFunction<'file'>;
16
16
  }
17
17
 
@@ -19,7 +19,7 @@ const useNotionImport = ({
19
19
  createDocument,
20
20
  currentFolderId,
21
21
  libraryId,
22
- refreshFileList,
22
+ refetchResources,
23
23
  t,
24
24
  }: UseNotionImportOptions) => {
25
25
  const notionInputRef = useRef<HTMLInputElement>(null);
@@ -171,8 +171,8 @@ const useNotionImport = ({
171
171
  );
172
172
  }
173
173
 
174
- // Refresh file list to show imported documents
175
- await refreshFileList();
174
+ // Refetch resources to show imported documents
175
+ await refetchResources();
176
176
  } catch (error) {
177
177
  console.error('Failed to import Notion export:', error);
178
178
  const { message } = await import('antd');
@@ -182,7 +182,7 @@ const useNotionImport = ({
182
182
  // Reset input to allow re-uploading
183
183
  event.target.value = '';
184
184
  },
185
- [createDocument, currentFolderId, libraryId, refreshFileList, t],
185
+ [createDocument, currentFolderId, libraryId, refetchResources, t],
186
186
  );
187
187
 
188
188
  return {
@@ -17,9 +17,13 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
17
17
  `,
18
18
  }));
19
19
 
20
- const TreeSkeletonItem = memo(() => {
20
+ interface TreeSkeletonItemProps {
21
+ opacity?: number;
22
+ }
23
+
24
+ const TreeSkeletonItem = memo<TreeSkeletonItemProps>(({ opacity = 1 }) => {
21
25
  return (
22
- <Flexbox className={styles.container} horizontal>
26
+ <Flexbox className={styles.container} horizontal style={{ opacity }}>
23
27
  <Skeleton.Button
24
28
  active
25
29
  size={'small'}
@@ -43,13 +47,19 @@ const TreeSkeletonItem = memo(() => {
43
47
 
44
48
  TreeSkeletonItem.displayName = 'TreeSkeletonItem';
45
49
 
46
- const TreeSkeleton = memo(() => (
47
- <Flexbox gap={2}>
48
- {Array.from({ length: 8 }).map((_, i) => (
49
- <TreeSkeletonItem key={i} />
50
- ))}
51
- </Flexbox>
52
- ));
50
+ const TreeSkeleton = memo(() => {
51
+ const count = 6;
52
+ // Calculate opacity gradient from 100% to 20%
53
+ const getOpacity = (index: number) => 1 - (index / (count - 1)) * 0.8;
54
+
55
+ return (
56
+ <Flexbox gap={2}>
57
+ {Array.from({ length: count }).map((_, i) => (
58
+ <TreeSkeletonItem key={i} opacity={getOpacity(i)} />
59
+ ))}
60
+ </Flexbox>
61
+ );
62
+ });
53
63
 
54
64
  TreeSkeleton.displayName = 'TreeSkeleton';
55
65