@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.
- package/.github/workflows/release.yml +4 -0
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +5 -0
- package/locales/en-US/file.json +2 -0
- package/locales/zh-CN/discover.json +3 -0
- package/locales/zh-CN/file.json +2 -0
- package/package.json +3 -3
- package/packages/types/package.json +2 -2
- package/packages/types/src/discover/mcp.ts +3 -1
- package/src/app/[variants]/(main)/community/(list)/(home)/index.tsx +2 -0
- package/src/app/[variants]/(main)/community/(list)/features/SortButton/index.tsx +4 -0
- package/src/app/[variants]/(main)/community/(list)/mcp/features/Category/index.tsx +7 -3
- package/src/app/[variants]/(main)/community/(list)/mcp/index.tsx +2 -2
- package/src/app/[variants]/(main)/home/_layout/Body/Project/List/Editing.tsx +1 -1
- package/src/app/[variants]/(main)/home/_layout/Body/Project/List/Item.tsx +1 -1
- package/src/app/[variants]/(main)/home/_layout/Body/Project/List/index.tsx +1 -1
- package/src/app/[variants]/(main)/home/_layout/Body/Project/List/useDropdownMenu.tsx +1 -1
- package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/{List/Item → Item}/Editing.tsx +1 -1
- package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/{List/Item → Item}/index.tsx +1 -1
- package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/{List/Item → Item}/useDropdownMenu.tsx +1 -1
- package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/index.tsx +16 -6
- package/src/app/[variants]/(main)/resource/(home)/_layout/Body/{KnowledgeBase.tsx → index.tsx} +2 -3
- package/src/app/[variants]/(main)/resource/(home)/_layout/Sidebar.tsx +2 -2
- package/src/app/[variants]/(main)/resource/(home)/index.tsx +23 -10
- package/src/app/[variants]/(main)/resource/features/DndContextWrapper.tsx +12 -37
- package/src/app/[variants]/(main)/resource/features/hooks/useKnowledgeItem.ts +1 -1
- package/src/app/[variants]/(main)/resource/features/store/action.ts +9 -39
- package/src/app/[variants]/(main)/resource/library/_layout/Header/LibraryHead.tsx +1 -1
- package/src/app/[variants]/(main)/resource/library/index.tsx +13 -6
- package/src/features/LibraryModal/AddFilesToKnowledgeBase/SelectForm.tsx +1 -1
- package/src/features/LibraryModal/CreateNew/CreateForm.tsx +1 -1
- package/src/features/PageEditor/Header/Breadcrumb.tsx +1 -1
- package/src/features/PageEditor/store/action.ts +5 -2
- package/src/features/PageExplorer/PageExplorerPlaceholder.tsx +5 -7
- package/src/features/ResourceManager/components/Explorer/Header/Breadcrumb.tsx +1 -1
- package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +57 -26
- package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +35 -6
- package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +20 -14
- package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +41 -31
- package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/index.tsx +1 -1
- package/src/features/ResourceManager/components/Explorer/MasonryView/Skeleton.tsx +6 -2
- package/src/features/ResourceManager/components/Explorer/MasonryView/index.tsx +29 -18
- package/src/features/ResourceManager/components/Explorer/MoveToFolderModal.tsx +7 -34
- package/src/features/ResourceManager/components/Explorer/ToolBar/BatchActionsDropdown.tsx +1 -1
- package/src/features/ResourceManager/components/Explorer/index.tsx +58 -18
- package/src/features/ResourceManager/components/Explorer/useCheckTaskStatus.ts +6 -4
- package/src/features/ResourceManager/components/Header/AddButton.tsx +58 -35
- package/src/features/ResourceManager/components/Header/hooks/useNotionImport.ts +5 -5
- package/src/features/ResourceManager/components/Tree/TreeSkeleton.tsx +19 -9
- package/src/features/ResourceManager/components/Tree/index.tsx +110 -5
- package/src/features/ResourceManager/components/UploadDock/index.tsx +2 -1
- package/src/features/ResourceManager/constants.ts +3 -0
- package/src/hooks/useMCPCategory.tsx +7 -0
- package/src/locales/default/discover.ts +3 -0
- package/src/locales/default/file.ts +2 -0
- package/src/services/file/index.ts +34 -1
- package/src/services/resource/index.ts +249 -0
- package/src/store/discover/slices/mcp/action.ts +1 -1
- package/src/store/file/slices/chat/action.ts +2 -1
- package/src/store/file/slices/document/action.ts +10 -7
- package/src/store/file/slices/fileManager/action.ts +14 -4
- package/src/store/file/slices/fileManager/initialState.ts +2 -0
- package/src/store/file/slices/resource/action.ts +432 -0
- package/src/store/file/slices/resource/hooks.ts +82 -0
- package/src/store/file/slices/resource/initialState.ts +67 -0
- package/src/store/file/slices/resource/syncEngine.ts +326 -0
- package/src/store/file/store.ts +6 -1
- package/src/store/{knowledgeBase → library}/initialState.ts +2 -2
- package/src/store/{knowledgeBase → library}/slices/content/action.test.ts +37 -51
- package/src/store/{knowledgeBase → library}/slices/content/action.ts +8 -4
- package/src/store/{knowledgeBase → library}/slices/crud/action.ts +1 -1
- package/src/store/{knowledgeBase → library}/slices/crud/selectors.ts +1 -1
- package/src/store/{knowledgeBase → library}/slices/ragEval/actions/dataset.ts +1 -1
- package/src/store/{knowledgeBase → library}/slices/ragEval/actions/evaluation.ts +1 -1
- package/src/store/{knowledgeBase → library}/slices/ragEval/actions/index.ts +1 -1
- package/src/types/resource.ts +133 -0
- package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/List/index.tsx +0 -25
- /package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/{List/Item → Item}/Actions.tsx +0 -0
- /package/src/store/{knowledgeBase → library}/index.ts +0 -0
- /package/src/store/{knowledgeBase → library}/selectors.ts +0 -0
- /package/src/store/{knowledgeBase → library}/slices/content/index.ts +0 -0
- /package/src/store/{knowledgeBase → library}/slices/crud/action.test.ts +0 -0
- /package/src/store/{knowledgeBase → library}/slices/crud/index.ts +0 -0
- /package/src/store/{knowledgeBase → library}/slices/crud/initialState.ts +0 -0
- /package/src/store/{knowledgeBase → library}/slices/ragEval/index.ts +0 -0
- /package/src/store/{knowledgeBase → library}/slices/ragEval/initialState.ts +0 -0
- /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
|
-
|
|
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(
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
198
|
-
|
|
199
|
-
await
|
|
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
|
-
|
|
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,
|
|
701
|
-
|
|
702
|
-
await
|
|
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
|
|
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
|
};
|