@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
@@ -20,6 +20,7 @@ import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/featur
20
20
  import FileIcon from '@/components/FileIcon';
21
21
  import { fileService } from '@/services/file';
22
22
  import { useFileStore } from '@/store/file';
23
+ import type { ResourceItem } from '@/types/resource';
23
24
 
24
25
  import { useFileItemDropdown } from '../Explorer/ItemDropdown/useFileItemDropdown';
25
26
  import TreeSkeleton from './TreeSkeleton';
@@ -35,6 +36,17 @@ const treeState = new Map<
35
36
  }
36
37
  >();
37
38
 
39
+ const TREE_REFRESH_EVENT = 'resource-tree-refresh';
40
+
41
+ const emitTreeRefresh = (knowledgeBaseId: string) => {
42
+ if (typeof window === 'undefined') return;
43
+ window.dispatchEvent(
44
+ new CustomEvent(TREE_REFRESH_EVENT, {
45
+ detail: { knowledgeBaseId },
46
+ }),
47
+ );
48
+ };
49
+
38
50
  const getTreeState = (knowledgeBaseId: string) => {
39
51
  if (!treeState.has(knowledgeBaseId)) {
40
52
  treeState.set(knowledgeBaseId, {
@@ -56,6 +68,47 @@ export const clearTreeFolderCache = async (knowledgeBaseId: string) => {
56
68
  const state = treeState.get(knowledgeBaseId);
57
69
  if (!state) return;
58
70
 
71
+ const { resourceList } = useFileStore.getState();
72
+
73
+ const resolveParentId = (key: string | null | undefined) => {
74
+ if (!key) return null;
75
+ // Prefer id match
76
+ const byId = resourceList.find(
77
+ (item) => item.knowledgeBaseId === knowledgeBaseId && item.id === key,
78
+ );
79
+ if (byId) return byId.id;
80
+ // Fallback to slug match
81
+ const bySlug = resourceList.find(
82
+ (item) => item.knowledgeBaseId === knowledgeBaseId && item.slug === key,
83
+ );
84
+ return bySlug?.id ?? key;
85
+ };
86
+
87
+ const buildChildrenFromStore = (parentKey: string | null) => {
88
+ const parentId = resolveParentId(parentKey);
89
+ return resourceList
90
+ .filter(
91
+ (item) =>
92
+ item.knowledgeBaseId === knowledgeBaseId &&
93
+ (item.parentId ?? null) === (parentId ?? null),
94
+ )
95
+ .map<ResourceItem>((item) => item)
96
+ .map((item) => ({
97
+ fileType: item.fileType,
98
+ id: item.id,
99
+ isFolder: item.fileType === 'custom/folder',
100
+ name: item.name,
101
+ slug: item.slug,
102
+ sourceType: item.sourceType,
103
+ url: item.url || '',
104
+ }))
105
+ .sort((a, b) => {
106
+ if (a.isFolder && !b.isFolder) return -1;
107
+ if (!a.isFolder && b.isFolder) return 1;
108
+ return a.name.localeCompare(b.name);
109
+ });
110
+ };
111
+
59
112
  // Get list of all currently expanded folders before clearing
60
113
  const expandedFoldersList = Array.from(state.expandedFolders);
61
114
 
@@ -65,9 +118,16 @@ export const clearTreeFolderCache = async (knowledgeBaseId: string) => {
65
118
 
66
119
  // Reload each expanded folder
67
120
  for (const folderKey of expandedFoldersList) {
121
+ // Prefer local store (explorer data) to avoid stale remote state
122
+ const localChildren = buildChildrenFromStore(folderKey);
123
+ if (localChildren.length > 0) {
124
+ state.folderChildrenCache.set(folderKey, localChildren);
125
+ state.loadedFolders.add(folderKey);
126
+ continue;
127
+ }
128
+
129
+ // Fallback to remote fetch if store has no data (e.g., initial load)
68
130
  try {
69
- // The API expects document ID, but folderKey could be slug or ID
70
- // We'll use it as is and let the API handle it
71
131
  const response = await fileService.getKnowledgeItems({
72
132
  knowledgeBaseId,
73
133
  parentId: folderKey,
@@ -92,7 +152,6 @@ export const clearTreeFolderCache = async (knowledgeBaseId: string) => {
92
152
  return a.name.localeCompare(b.name);
93
153
  });
94
154
 
95
- // Update cache using the same key that was used before
96
155
  state.folderChildrenCache.set(folderKey, sortedChildren);
97
156
  state.loadedFolders.add(folderKey);
98
157
  }
@@ -100,6 +159,33 @@ export const clearTreeFolderCache = async (knowledgeBaseId: string) => {
100
159
  console.error(`Failed to reload folder ${folderKey}:`, error);
101
160
  }
102
161
  }
162
+
163
+ // Revalidate SWR caches for root and expanded folders to keep list and tree in sync
164
+ try {
165
+ const { mutate } = await import('swr');
166
+ const revalidateFolder = (parentId: string | null) =>
167
+ mutate(
168
+ [
169
+ 'useFetchKnowledgeItems',
170
+ {
171
+ knowledgeBaseId,
172
+ parentId,
173
+ showFilesInKnowledgeBase: false,
174
+ },
175
+ ],
176
+ undefined,
177
+ { revalidate: true },
178
+ );
179
+
180
+ await Promise.all([
181
+ revalidateFolder(null),
182
+ ...expandedFoldersList.map((folderKey) => revalidateFolder(folderKey)),
183
+ ]);
184
+ } catch (error) {
185
+ console.error('Failed to revalidate tree SWR cache:', error);
186
+ }
187
+
188
+ emitTreeRefresh(knowledgeBaseId);
103
189
  };
104
190
 
105
191
  const styles = createStaticStyles(({ css, cssVar }) => ({
@@ -201,13 +287,16 @@ const FileTreeRow = memo<{
201
287
 
202
288
  try {
203
289
  await renameFolder(item.id, renamingValue.trim());
290
+ if (libraryId) {
291
+ await clearTreeFolderCache(libraryId);
292
+ }
204
293
  message.success('Renamed successfully');
205
294
  setIsRenaming(false);
206
295
  } catch (error) {
207
296
  console.error('Rename error:', error);
208
297
  message.error('Rename failed');
209
298
  }
210
- }, [item.id, item.name, renamingValue, renameFolder, message]);
299
+ }, [item.id, item.name, libraryId, renamingValue, renameFolder, message]);
211
300
 
212
301
  const handleRenameCancel = useCallback(() => {
213
302
  setIsRenaming(false);
@@ -218,7 +307,7 @@ const FileTreeRow = memo<{
218
307
  fileType: item.fileType,
219
308
  filename: item.name,
220
309
  id: item.id,
221
- knowledgeBaseId: libraryId,
310
+ libraryId,
222
311
  onRenameStart: item.isFolder ? handleRenameStart : undefined,
223
312
  sourceType: item.sourceType,
224
313
  url: item.url,
@@ -659,6 +748,22 @@ const FileTree = memo(() => {
659
748
  parentFolderKeyRef.current = null;
660
749
  }, [libraryId]);
661
750
 
751
+ // Listen for external tree refresh events (triggered when cache is cleared)
752
+ React.useEffect(() => {
753
+ if (typeof window === 'undefined') return;
754
+
755
+ const handleTreeRefresh = (event: Event) => {
756
+ const detail = (event as CustomEvent<{ knowledgeBaseId?: string }>).detail;
757
+ if (detail?.knowledgeBaseId && libraryId && detail.knowledgeBaseId !== libraryId) return;
758
+ forceUpdate();
759
+ };
760
+
761
+ window.addEventListener(TREE_REFRESH_EVENT, handleTreeRefresh);
762
+ return () => {
763
+ window.removeEventListener(TREE_REFRESH_EVENT, handleTreeRefresh);
764
+ };
765
+ }, [libraryId, forceUpdate]);
766
+
662
767
  // Auto-expand folders when navigating to a folder in Explorer
663
768
  React.useEffect(() => {
664
769
  if (!folderBreadcrumb || folderBreadcrumb.length === 0) return;
@@ -50,10 +50,11 @@ const styles = createStaticStyles(({ css }) => {
50
50
 
51
51
  const UploadDock = memo(() => {
52
52
  const { t } = useTranslation('file');
53
- const [expand, setExpand] = useState(true);
54
53
  const [show, setShow] = useState(true);
55
54
 
56
55
  const dispatchDockFileList = useFileStore((s) => s.dispatchDockFileList);
56
+ const expand = useFileStore((s) => s.uploadDockExpanded);
57
+ const setExpand = useFileStore((s) => s.setUploadDockExpanded);
57
58
  const totalUploadingProgress = useFileStore(fileManagerSelectors.overviewUploadingProgress);
58
59
  const fileList = useFileStore(fileManagerSelectors.dockFileList, isEqual);
59
60
  const overviewUploadingStatus = useFileStore(
@@ -0,0 +1,3 @@
1
+ const PAGE_FILE_TYPE = 'custom/document';
2
+
3
+ export { PAGE_FILE_TYPE };
@@ -2,6 +2,7 @@ import {
2
2
  BriefcaseIcon,
3
3
  CheckSquareIcon,
4
4
  CloudIcon,
5
+ CompassIcon,
5
6
  CodeIcon,
6
7
  CoffeeIcon,
7
8
  DollarSignIcon,
@@ -25,6 +26,12 @@ export const useCategory = () => {
25
26
  const { t } = useTranslation('discover');
26
27
  return useMemo(
27
28
  () => [
29
+ {
30
+ icon: CompassIcon,
31
+ key: McpCategory.Discover,
32
+ label: t('mcp.categories.discover.name'),
33
+ title: t('mcp.categories.discover.description'),
34
+ },
28
35
  {
29
36
  icon: LayoutPanelTopIcon,
30
37
  key: McpCategory.All,
@@ -165,6 +165,8 @@ export default {
165
165
  'mcp.categories.business.name': 'Business Services',
166
166
  'mcp.categories.developer.description': 'Developer-related Tools and Services',
167
167
  'mcp.categories.developer.name': 'Developer Tools',
168
+ 'mcp.categories.discover.description': 'Recommended and trending MCP servers',
169
+ 'mcp.categories.discover.name': 'Discover',
168
170
  'mcp.categories.gaming-entertainment.description': 'Games, Entertainment, and Leisure Activities',
169
171
  'mcp.categories.gaming-entertainment.name': 'Gaming & Entertainment',
170
172
  'mcp.categories.health-wellness.description': 'Health, Fitness, and Wellness',
@@ -380,6 +382,7 @@ export default {
380
382
  'mcp.sorts.isValidated': 'Validated Skills',
381
383
  'mcp.sorts.promptsCount': 'Number of Prompts',
382
384
  'mcp.sorts.ratingCount': 'Number of Ratings',
385
+ 'mcp.sorts.recommended': 'Recommended',
383
386
  'mcp.sorts.resourcesCount': 'Number of Resources',
384
387
  'mcp.sorts.toolsCount': 'Number of Tools',
385
388
  'mcp.sorts.updatedAt': 'Recently Updated',
@@ -18,6 +18,8 @@ export default {
18
18
  'empty': 'No files or folders have been uploaded yet.',
19
19
  'header.actions.builtInBlockList.filtered': '{{ignored}} files filtered (out of {{total}} total)',
20
20
  'header.actions.connect': 'Connect...',
21
+ 'header.actions.createFolderError': 'Failed to create folder',
22
+ 'header.actions.creatingFolder': 'Creating folder...',
21
23
  'header.actions.gitignore.apply': 'Apply Rules',
22
24
  'header.actions.gitignore.cancel': 'Ignore Rules',
23
25
  'header.actions.gitignore.content':
@@ -2,6 +2,7 @@ import { lambdaClient } from '@/libs/trpc/client';
2
2
  import {
3
3
  type CheckFileHashResult,
4
4
  type FileItem,
5
+ type FileListItem,
5
6
  type QueryFileListParams,
6
7
  type QueryFileListSchemaType,
7
8
  type UploadFileParams,
@@ -58,8 +59,40 @@ export class FileService {
58
59
  };
59
60
 
60
61
  // V2.0 Migrate from getFileItem to getKnowledgeItem
62
+ // This method handles both files (file_ prefix) and documents (docs_ prefix)
61
63
  getKnowledgeItem = async (id: string) => {
62
- return lambdaClient.file.getFileItemById.query({ id });
64
+ // Detect type based on ID prefix
65
+ if (id.startsWith('docs_')) {
66
+ // Document (including folders) - use document endpoint
67
+ const doc = await lambdaClient.document.getDocumentById.query({ id });
68
+ if (!doc) return null;
69
+
70
+ // Convert document to FileListItem format
71
+ return {
72
+ chunkCount: null,
73
+ chunkingError: null,
74
+ chunkingStatus: null,
75
+ content: doc.content,
76
+ createdAt: doc.createdAt ? new Date(doc.createdAt) : new Date(),
77
+ editorData: doc.editorData,
78
+ embeddingError: null,
79
+ embeddingStatus: null,
80
+ fileType: doc.fileType || 'custom/document',
81
+ finishEmbedding: false,
82
+ id: doc.id,
83
+ metadata: doc.metadata,
84
+ name: doc.title || doc.filename || 'Untitled',
85
+ parentId: doc.parentId,
86
+ size: doc.totalCharCount || 0,
87
+ slug: doc.slug,
88
+ sourceType: 'document',
89
+ updatedAt: doc.updatedAt ? new Date(doc.updatedAt) : new Date(),
90
+ url: doc.source || '',
91
+ } as FileListItem;
92
+ } else {
93
+ // File - use dedicated file endpoint
94
+ return lambdaClient.file.getFileItemById.query({ id });
95
+ }
63
96
  };
64
97
 
65
98
  getFolderBreadcrumb = async (slug: string) => {
@@ -0,0 +1,249 @@
1
+ import type { FileListItem } from '@/types/files';
2
+ import type {
3
+ CreateResourceParams,
4
+ ResourceItem,
5
+ ResourceQueryParams,
6
+ UpdateResourceParams,
7
+ } from '@/types/resource';
8
+
9
+ import { type CreateDocumentParams, documentService } from '../document';
10
+ import { fileService } from '../file';
11
+
12
+ /**
13
+ * Map FileListItem to ResourceItem
14
+ */
15
+ const mapToResourceItem = (item: FileListItem): ResourceItem => {
16
+ return {
17
+ chunkCount: item.chunkCount,
18
+ chunkTaskId: item.chunkingStatus ? 'placeholder' : null,
19
+ chunkingError: item.chunkingError,
20
+ chunkingStatus: item.chunkingStatus,
21
+ // Document-specific fields
22
+ content: item.content,
23
+
24
+ createdAt: item.createdAt,
25
+
26
+ editorData: item.editorData,
27
+
28
+ embeddingError: item.embeddingError,
29
+
30
+ embeddingStatus: item.embeddingStatus,
31
+
32
+ embeddingTaskId: item.embeddingStatus ? 'placeholder' : null,
33
+
34
+ fileType: item.fileType,
35
+
36
+ finishEmbedding: item.finishEmbedding,
37
+
38
+ id: item.id,
39
+
40
+ // Metadata
41
+ metadata: item.metadata || undefined,
42
+
43
+ name: item.name,
44
+
45
+ parentId: item.parentId,
46
+
47
+ size: item.size,
48
+
49
+ slug: item.slug,
50
+
51
+ sourceType: item.sourceType as 'file' | 'document',
52
+
53
+ updatedAt: item.updatedAt,
54
+
55
+ // File-specific fields
56
+ url: item.url,
57
+ };
58
+ };
59
+
60
+ /**
61
+ * ResourceService - Unified service for both files and documents
62
+ * Provides a thin wrapper over FileService and DocumentService
63
+ * Used by ResourceManager for optimistic updates
64
+ */
65
+ export class ResourceService {
66
+ /**
67
+ * Query resources (unified files + documents)
68
+ * Uses KnowledgeRepo UNION ALL query
69
+ */
70
+ async queryResources(params: ResourceQueryParams): Promise<{
71
+ hasMore: boolean;
72
+ items: ResourceItem[];
73
+ total?: number;
74
+ }> {
75
+ // Map frontend parameter names to backend parameter names
76
+ const backendParams = {
77
+ ...params,
78
+ knowledgeBaseId: params.libraryId, // Map libraryId to knowledgeBaseId
79
+ libraryId: undefined, // Remove the frontend-specific parameter
80
+ };
81
+
82
+ const response = await fileService.getKnowledgeItems(backendParams);
83
+
84
+ return {
85
+ hasMore: response.hasMore,
86
+ items: response.items.map(mapToResourceItem),
87
+ total: 'total' in response ? (response.total as number) : undefined,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Get a single resource by ID
93
+ */
94
+ async getResource(id: string): Promise<ResourceItem | undefined> {
95
+ const item = await fileService.getKnowledgeItem(id);
96
+ return item ? mapToResourceItem(item) : undefined;
97
+ }
98
+
99
+ /**
100
+ * Create a new resource (file or document)
101
+ */
102
+ async createResource(params: CreateResourceParams): Promise<ResourceItem> {
103
+ if (params.sourceType === 'file') {
104
+ // Create file
105
+ const result = await fileService.createFile(
106
+ {
107
+ fileType: params.fileType,
108
+ name: params.name,
109
+ parentId: params.parentId,
110
+ size: params.size,
111
+ url: params.url,
112
+ },
113
+ params.knowledgeBaseId,
114
+ );
115
+
116
+ // Fetch the created file to get full details
117
+ const created = await fileService.getKnowledgeItem(result.id);
118
+ if (!created) throw new Error('Failed to fetch created file');
119
+
120
+ return mapToResourceItem(created);
121
+ } else {
122
+ // Create document
123
+ const documentParams: CreateDocumentParams = {
124
+ content: params.content || '',
125
+ editorData: JSON.stringify(params.editorData || {}),
126
+ fileType: params.fileType,
127
+ knowledgeBaseId: params.knowledgeBaseId,
128
+ metadata: params.metadata,
129
+ parentId: params.parentId,
130
+ slug: params.slug,
131
+ title: params.title,
132
+ };
133
+
134
+ const created = await documentService.createDocument(documentParams);
135
+
136
+ // Map to ResourceItem
137
+ return {
138
+ content: created.content,
139
+ createdAt: created.createdAt ? new Date(created.createdAt) : new Date(),
140
+ editorData:
141
+ typeof created.editorData === 'string'
142
+ ? JSON.parse(created.editorData)
143
+ : created.editorData,
144
+ fileType: created.fileType || 'custom/document',
145
+ id: created.id,
146
+ metadata: created.metadata || undefined,
147
+ name: created.title || 'Untitled',
148
+ parentId: created.parentId,
149
+ size: created.totalCharCount || 0,
150
+ slug: created.slug || undefined,
151
+ sourceType: 'document',
152
+ title: created.title || undefined,
153
+ updatedAt: created.updatedAt ? new Date(created.updatedAt) : new Date(),
154
+ url: created.source || '',
155
+ };
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Update a resource
161
+ */
162
+ async updateResource(id: string, updates: UpdateResourceParams): Promise<ResourceItem> {
163
+ // Check if this is a file or document by fetching it first
164
+ const existing = await this.getResource(id);
165
+ if (!existing) throw new Error('Resource not found');
166
+
167
+ if (existing.sourceType === 'file') {
168
+ // Update file (currently only supports parentId)
169
+ if (updates.parentId !== undefined) {
170
+ await fileService.updateFile(id, { parentId: updates.parentId });
171
+ }
172
+
173
+ // Fetch updated file
174
+ const updated = await fileService.getKnowledgeItem(id);
175
+ if (!updated) throw new Error('Failed to fetch updated file');
176
+
177
+ return mapToResourceItem(updated);
178
+ } else {
179
+ // Update document
180
+ await documentService.updateDocument({
181
+ content: updates.content,
182
+ editorData: updates.editorData ? JSON.stringify(updates.editorData) : undefined,
183
+ id,
184
+ metadata: updates.metadata,
185
+ // Keep null as null (for moving to root), don't convert to undefined
186
+ parentId: updates.parentId !== undefined ? updates.parentId : undefined,
187
+ title: updates.title || updates.name,
188
+ });
189
+
190
+ // Fetch updated document
191
+ const updated = await fileService.getKnowledgeItem(id);
192
+ if (!updated) throw new Error('Failed to fetch updated document');
193
+
194
+ return mapToResourceItem(updated);
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Delete a resource
200
+ */
201
+ async deleteResource(id: string): Promise<void> {
202
+ // Check if this is a file or document
203
+ const existing = await this.getResource(id);
204
+ if (!existing) return; // Already deleted
205
+
206
+ if (existing.sourceType === 'file') {
207
+ await fileService.removeFile(id);
208
+ } else {
209
+ await documentService.deleteDocument(id);
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Batch delete resources
215
+ */
216
+ async deleteResources(ids: string[]): Promise<void> {
217
+ // Separate files and documents
218
+ const fileIds: string[] = [];
219
+ const documentIds: string[] = [];
220
+
221
+ await Promise.all(
222
+ ids.map(async (id) => {
223
+ const item = await this.getResource(id);
224
+ if (item) {
225
+ if (item.sourceType === 'file') {
226
+ fileIds.push(id);
227
+ } else {
228
+ documentIds.push(id);
229
+ }
230
+ }
231
+ }),
232
+ );
233
+
234
+ // Batch delete
235
+ await Promise.all([
236
+ fileIds.length > 0 ? fileService.removeFiles(fileIds) : Promise.resolve(),
237
+ documentIds.length > 0 ? documentService.deleteDocuments(documentIds) : Promise.resolve(),
238
+ ]);
239
+ }
240
+
241
+ /**
242
+ * Move a resource to a different parent folder
243
+ */
244
+ async moveResource(id: string, parentId: string | null): Promise<ResourceItem> {
245
+ return this.updateResource(id, { parentId });
246
+ }
247
+ }
248
+
249
+ export const resourceService = new ResourceService();
@@ -36,7 +36,7 @@ export const createMCPSlice: StateCreator<
36
36
  );
37
37
  },
38
38
 
39
- useFetchMcpList: (params: any) => {
39
+ useFetchMcpList: (params) => {
40
40
  const locale = globalHelpers.getCurrentLanguage();
41
41
  return useClientDataSWR(
42
42
  ['mcp-list', locale, ...Object.values(params)].filter(Boolean).join('-'),
@@ -88,7 +88,8 @@ export const createFileSlice: StateCreator<
88
88
  let fileItem: FileListItem | undefined = undefined;
89
89
 
90
90
  try {
91
- fileItem = await fileService.getKnowledgeItem(id);
91
+ const result = await fileService.getKnowledgeItem(id);
92
+ fileItem = result ?? undefined;
92
93
  } catch (e) {
93
94
  console.error('getFileItem Error:', e);
94
95
  continue;
@@ -194,9 +194,9 @@ export const createDocumentSlice: StateCreator<
194
194
  title: name,
195
195
  });
196
196
 
197
- // Refresh file list to show the new folder
198
- // Note: refreshFileList now keeps cache to avoid skeleton flash
199
- await get().refreshFileList();
197
+ // Refetch resource list to show the new folder
198
+ const { revalidateResources } = await import('../resource/hooks');
199
+ await revalidateResources();
200
200
 
201
201
  return folder.id;
202
202
  },
@@ -641,7 +641,10 @@ export const createDocumentSlice: StateCreator<
641
641
  parentId: updates.parentId !== undefined ? updates.parentId : undefined,
642
642
  title: updates.title,
643
643
  });
644
- await get().refreshFileList();
644
+
645
+ // Refetch resource list to show updated document
646
+ const { revalidateResources } = await import('../resource/hooks');
647
+ await revalidateResources();
645
648
  },
646
649
 
647
650
  updateDocumentOptimistically: async (documentId, updates) => {
@@ -697,9 +700,9 @@ export const createDocumentSlice: StateCreator<
697
700
  title: updatedPage.title || updatedPage.filename,
698
701
  });
699
702
 
700
- // After successful sync, refresh file list to get server state
701
- // This will eventually sync back to the map via syncDocumentMapWithServer
702
- await get().refreshFileList();
703
+ // After successful sync, refetch resources to get server state
704
+ const { revalidateResources } = await import('../resource/hooks');
705
+ await revalidateResources();
703
706
  } catch (error) {
704
707
  console.error('[updateDocumentOptimistically] Failed to sync to DB:', error);
705
708
  // On error, revert the optimistic update
@@ -45,6 +45,10 @@ export interface FileManageAction {
45
45
 
46
46
  reEmbeddingChunks: (id: string) => Promise<void>;
47
47
  reParseFile: (id: string) => Promise<void>;
48
+ /**
49
+ * @deprecated Use fetchResources(queryParams) from resource slice instead
50
+ * This method is kept for backward compatibility with non-ResourceManager code
51
+ */
48
52
  refreshFileList: () => Promise<void>;
49
53
  removeAllFiles: () => Promise<void>;
50
54
  removeFileItem: (id: string) => Promise<void>;
@@ -53,6 +57,7 @@ export interface FileManageAction {
53
57
 
54
58
  setCurrentFolderId: (folderId: string | null | undefined) => void;
55
59
  setPendingRenameItemId: (id: string | null) => void;
60
+ setUploadDockExpanded: (expanded: boolean) => void;
56
61
 
57
62
  toggleEmbeddingIds: (ids: string[], loading?: boolean) => void;
58
63
  toggleParsingIds: (ids: string[], loading?: boolean) => void;
@@ -252,7 +257,7 @@ export const createFileManageSlice: StateCreator<
252
257
  { concurrency: MAX_UPLOAD_FILE_COUNT },
253
258
  );
254
259
 
255
- // Refresh the file list once after all uploads are complete
260
+ // Refresh file list to show newly uploaded files
256
261
  await get().refreshFileList();
257
262
 
258
263
  // 5. auto-embed files that support chunking
@@ -349,6 +354,10 @@ export const createFileManageSlice: StateCreator<
349
354
  set({ pendingRenameItemId: id }, false, 'setPendingRenameItemId');
350
355
  },
351
356
 
357
+ setUploadDockExpanded: (expanded) => {
358
+ set({ uploadDockExpanded: expanded }, false, 'setUploadDockExpanded');
359
+ },
360
+
352
361
  toggleEmbeddingIds: (ids, loading) => {
353
362
  set((state) => {
354
363
  const nextValue = new Set(state.creatingEmbeddingTaskIds);
@@ -534,9 +543,10 @@ export const createFileManageSlice: StateCreator<
534
543
  }),
535
544
 
536
545
  useFetchKnowledgeItem: (id) =>
537
- useClientDataSWR<FileListItem | undefined>(!id ? null : ['useFetchKnowledgeItem', id], () =>
538
- serverFileService.getKnowledgeItem(id!),
539
- ),
546
+ useClientDataSWR<FileListItem | undefined>(!id ? null : ['useFetchKnowledgeItem', id], async () => {
547
+ const response = await serverFileService.getKnowledgeItem(id!);
548
+ return response ?? undefined;
549
+ }),
540
550
 
541
551
  useFetchKnowledgeItems: (params) =>
542
552
  useClientDataSWR<FileListItem[]>([FETCH_ALL_KNOWLEDGE_KEY, params], async () => {
@@ -12,6 +12,7 @@ export interface FileManagerState {
12
12
  fileListOffset: number;
13
13
  pendingRenameItemId: string | null;
14
14
  queryListParams?: QueryFileListParams;
15
+ uploadDockExpanded: boolean;
15
16
  }
16
17
 
17
18
  export const initialFileManagerState: FileManagerState = {
@@ -23,4 +24,5 @@ export const initialFileManagerState: FileManagerState = {
23
24
  fileListHasMore: false,
24
25
  fileListOffset: 0,
25
26
  pendingRenameItemId: null,
27
+ uploadDockExpanded: true,
26
28
  };