@lobehub/lobehub 2.0.0-next.267 → 2.0.0-next.268
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/.cursor/rules/microcopy-cn.mdc +75 -63
- package/.cursor/rules/microcopy-en.mdc +4 -8
- package/CHANGELOG.md +25 -0
- package/README.md +8 -8
- package/README.zh-CN.md +8 -8
- package/apps/desktop/src/main/locales/default/common.ts +2 -2
- package/changelog/v1.json +5 -0
- package/docs/development/database-schema.dbml +4 -0
- package/e2e/CLAUDE.md +9 -8
- package/e2e/cucumber.config.js +1 -0
- package/e2e/src/features/page/README.md +118 -0
- package/e2e/src/features/page/crud.feature +62 -0
- package/e2e/src/features/page/editor-content.feature +93 -0
- package/e2e/src/features/page/editor-meta.feature +60 -0
- package/e2e/src/steps/agent/conversation.steps.ts +4 -4
- package/e2e/src/steps/home/sidebarAgent.steps.ts +91 -94
- package/e2e/src/steps/home/sidebarGroup.steps.ts +4 -4
- package/e2e/src/steps/hooks.ts +2 -0
- package/e2e/src/steps/page/editor-content.steps.ts +344 -0
- package/e2e/src/steps/page/editor-meta.steps.ts +410 -0
- package/e2e/src/steps/page/page-crud.steps.ts +363 -0
- package/e2e/src/support/world.ts +12 -0
- package/locales/ar/file.json +2 -0
- package/locales/bg-BG/file.json +2 -0
- package/locales/de-DE/file.json +2 -0
- package/locales/en-US/auth.json +1 -1
- package/locales/en-US/file.json +2 -0
- package/locales/en-US/metadata.json +2 -2
- package/locales/es-ES/file.json +2 -0
- package/locales/fa-IR/file.json +2 -0
- package/locales/fr-FR/file.json +2 -0
- package/locales/it-IT/file.json +2 -0
- package/locales/ja-JP/file.json +2 -0
- package/locales/ko-KR/file.json +2 -0
- package/locales/nl-NL/file.json +2 -0
- package/locales/pl-PL/file.json +2 -0
- package/locales/pt-BR/file.json +2 -0
- package/locales/ru-RU/file.json +2 -0
- package/locales/tr-TR/file.json +2 -0
- package/locales/vi-VN/file.json +2 -0
- package/locales/zh-CN/file.json +2 -0
- package/locales/zh-TW/file.json +2 -0
- package/package.json +1 -1
- package/packages/builtin-agents/src/agents/agent-builder/index.ts +1 -1
- package/packages/builtin-agents/src/agents/group-agent-builder/index.ts +1 -1
- package/packages/builtin-agents/src/agents/page-agent/index.ts +1 -1
- package/packages/const/src/settings/group.ts +0 -10
- package/packages/database/migrations/0068_update_group_data.sql +4 -0
- package/packages/database/migrations/meta/0068_snapshot.json +9588 -0
- package/packages/database/migrations/meta/_journal.json +7 -0
- package/packages/database/src/models/__tests__/chatGroup.test.ts +5 -7
- package/packages/database/src/models/__tests__/knowledgeBase.test.ts +185 -0
- package/packages/database/src/models/knowledgeBase.ts +67 -3
- package/packages/database/src/repositories/agentGroup/index.test.ts +23 -29
- package/packages/database/src/repositories/agentGroup/index.ts +4 -9
- package/packages/database/src/repositories/knowledge/index.ts +3 -3
- package/packages/database/src/schemas/chatGroup.ts +4 -3
- package/packages/database/src/types/chatGroup.ts +0 -7
- package/packages/types/src/agentGroup/index.ts +30 -9
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/ModalProvider.tsx +9 -32
- package/src/app/[variants]/(main)/home/_layout/hooks/useCreateMenuItems.tsx +3 -37
- package/src/app/[variants]/(main)/home/_layout/hooks/useSessionGroupMenuItems.tsx +7 -53
- package/src/app/[variants]/(main)/home/features/RecentPage/List.tsx +2 -1
- package/src/app/[variants]/(main)/resource/features/DndContextWrapper.tsx +1 -1
- package/src/app/[variants]/(main)/resource/library/_layout/Sidebar.tsx +2 -2
- package/src/app/[variants]/(main)/resource/library/features/LibraryMenu.tsx +2 -2
- package/src/app/[variants]/(mobile)/chat/settings/features/SettingButton.tsx +2 -12
- package/src/components/ChatGroupWizard/ChatGroupWizard.tsx +5 -27
- package/src/components/DragUpload/index.tsx +24 -27
- package/src/components/MemberSelectionModal/MemberSelectionModal.tsx +2 -11
- package/src/features/CommandMenu/useCommandMenu.ts +4 -14
- package/src/features/ResourceManager/components/Editor/index.tsx +2 -3
- package/src/features/ResourceManager/components/Explorer/Header/index.tsx +13 -17
- package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +1 -1
- package/src/features/ResourceManager/components/Explorer/ListView/ListItem/TruncatedFileName.tsx +130 -0
- package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +36 -4
- package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +4 -3
- package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +58 -2
- package/src/features/ResourceManager/components/Explorer/MasonryView/index.tsx +58 -6
- package/src/features/ResourceManager/components/Explorer/MoveToFolderModal.tsx +2 -5
- package/src/features/ResourceManager/components/Explorer/ToolBar/BatchActionsDropdown.tsx +9 -5
- package/src/features/ResourceManager/components/Explorer/index.tsx +11 -56
- package/src/features/ResourceManager/components/Header/AddButton.tsx +5 -6
- package/src/features/ResourceManager/components/LibraryHierarchy/HierarchyNode.tsx +382 -0
- package/src/features/ResourceManager/components/LibraryHierarchy/index.tsx +396 -0
- package/src/features/ResourceManager/components/LibraryHierarchy/styles.ts +19 -0
- package/src/features/ResourceManager/components/LibraryHierarchy/treeState.ts +178 -0
- package/src/features/ResourceManager/components/LibraryHierarchy/types.ts +10 -0
- package/src/features/ResourceManager/index.tsx +3 -0
- package/src/layout/GlobalProvider/GroupWizardProvider.tsx +6 -29
- package/src/locales/default/auth.ts +1 -1
- package/src/locales/default/file.ts +2 -0
- package/src/locales/default/metadata.ts +2 -2
- package/src/server/modules/AgentRuntime/AgentRuntimeCoordinator.ts +30 -30
- package/src/server/modules/AgentRuntime/AgentStateManager.ts +23 -23
- package/src/server/modules/AgentRuntime/InMemoryAgentStateManager.ts +16 -16
- package/src/server/modules/AgentRuntime/InMemoryStreamEventManager.ts +13 -13
- package/src/server/modules/AgentRuntime/RuntimeExecutors.ts +2 -2
- package/src/server/modules/AgentRuntime/StreamEventManager.ts +18 -18
- package/src/server/modules/AgentRuntime/types.ts +21 -21
- package/src/server/routers/lambda/__tests__/agentGroup.test.ts +8 -8
- package/src/server/routers/lambda/agentGroup.ts +10 -12
- package/src/server/services/document/index.ts +1 -0
- package/src/store/agentGroup/slices/curd.test.ts +4 -4
- package/src/store/file/slices/fileManager/action.ts +12 -4
- package/src/store/home/slices/homeInput/action.ts +0 -3
- package/src/store/session/slices/session/action.ts +5 -9
- package/src/app/[variants]/(mobile)/chat/settings/features/AgentTeamSettings/index.tsx +0 -95
- package/src/features/GroupChatSettings/AgentCard.tsx +0 -154
- package/src/features/GroupChatSettings/AgentTeamChatSettings.tsx +0 -179
- package/src/features/GroupChatSettings/AgentTeamMembersSettings.tsx +0 -244
- package/src/features/GroupChatSettings/AgentTeamMetaSettings.tsx +0 -94
- package/src/features/GroupChatSettings/AgentTeamSettings.tsx +0 -54
- package/src/features/GroupChatSettings/GroupCategory/index.tsx +0 -30
- package/src/features/GroupChatSettings/GroupCategory/useGroupCategory.tsx +0 -42
- package/src/features/GroupChatSettings/GroupChatSettingsProvider.tsx +0 -19
- package/src/features/GroupChatSettings/HostMemberCard.tsx +0 -113
- package/src/features/GroupChatSettings/StoreUpdater.tsx +0 -34
- package/src/features/GroupChatSettings/hooks/useGroupChatSettings.ts +0 -25
- package/src/features/GroupChatSettings/index.ts +0 -16
- package/src/features/GroupChatSettings/store/action.ts +0 -105
- package/src/features/GroupChatSettings/store/index.ts +0 -18
- package/src/features/GroupChatSettings/store/initialState.ts +0 -23
- package/src/features/GroupChatSettings/store/selectors.ts +0 -13
- package/src/features/ResourceManager/components/Tree/index.tsx +0 -883
- /package/src/features/ResourceManager/components/{Tree → LibraryHierarchy}/TreeSkeleton.tsx +0 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Flexbox } from '@lobehub/ui';
|
|
4
|
+
import { memo, useCallback, useEffect, useMemo, useReducer, useRef } from 'react';
|
|
5
|
+
import { VList } from 'virtua';
|
|
6
|
+
|
|
7
|
+
import { useFolderPath } from '@/app/[variants]/(main)/resource/features/hooks/useFolderPath';
|
|
8
|
+
import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
|
|
9
|
+
import { fileService } from '@/services/file';
|
|
10
|
+
import { useFileStore } from '@/store/file';
|
|
11
|
+
import type { ResourceQueryParams } from '@/types/resource';
|
|
12
|
+
|
|
13
|
+
import { HierarchyNode } from './HierarchyNode';
|
|
14
|
+
import TreeSkeleton from './TreeSkeleton';
|
|
15
|
+
import {
|
|
16
|
+
TREE_REFRESH_EVENT,
|
|
17
|
+
getTreeState,
|
|
18
|
+
resourceItemToTreeItem,
|
|
19
|
+
sortTreeItems,
|
|
20
|
+
} from './treeState';
|
|
21
|
+
import type { TreeItem } from './types';
|
|
22
|
+
|
|
23
|
+
// Export for external use
|
|
24
|
+
export { clearTreeFolderCache } from './treeState';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* As a sidebar along with the Explorer
|
|
28
|
+
*/
|
|
29
|
+
const LibraryHierarchy = memo(() => {
|
|
30
|
+
const { currentFolderSlug } = useFolderPath();
|
|
31
|
+
|
|
32
|
+
const [useFetchKnowledgeItems, useFetchFolderBreadcrumb, useFetchKnowledgeItem] = useFileStore(
|
|
33
|
+
(s) => [s.useFetchKnowledgeItems, s.useFetchFolderBreadcrumb, s.useFetchKnowledgeItem],
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const [resourceList, resourceQueryParams] = useFileStore((s) => [s.resourceList, s.queryParams]);
|
|
37
|
+
|
|
38
|
+
const [libraryId, currentViewItemId] = useResourceManagerStore((s) => [
|
|
39
|
+
s.libraryId,
|
|
40
|
+
s.currentViewItemId,
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
// Force re-render when tree state changes
|
|
44
|
+
const [updateKey, forceUpdate] = useReducer((x) => x + 1, 0);
|
|
45
|
+
|
|
46
|
+
// Get the persisted state for this knowledge base
|
|
47
|
+
const state = useMemo(() => getTreeState(libraryId || ''), [libraryId]);
|
|
48
|
+
const { expandedFolders, folderChildrenCache, loadingFolders } = state;
|
|
49
|
+
|
|
50
|
+
// Fetch breadcrumb for current folder
|
|
51
|
+
const { data: folderBreadcrumb } = useFetchFolderBreadcrumb(currentFolderSlug);
|
|
52
|
+
|
|
53
|
+
// Fetch current file when viewing a file
|
|
54
|
+
const { data: currentFile } = useFetchKnowledgeItem(currentViewItemId);
|
|
55
|
+
|
|
56
|
+
// Track parent folder key for file selection - stored in a ref to avoid hook order issues
|
|
57
|
+
const parentFolderKeyRef = useRef<string | null>(null);
|
|
58
|
+
|
|
59
|
+
// Fetch root level data using SWR
|
|
60
|
+
const { data: rootData, isLoading } = useFetchKnowledgeItems({
|
|
61
|
+
knowledgeBaseId: libraryId,
|
|
62
|
+
parentId: null,
|
|
63
|
+
showFilesInKnowledgeBase: false,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const isExplorerCacheActiveForTree = useMemo(() => {
|
|
67
|
+
if (!libraryId) return false;
|
|
68
|
+
if (!resourceQueryParams) return false;
|
|
69
|
+
|
|
70
|
+
// We intentionally ignore search per requirement: tree always shows full hierarchy
|
|
71
|
+
if (resourceQueryParams.q) return false;
|
|
72
|
+
|
|
73
|
+
return resourceQueryParams.libraryId === libraryId;
|
|
74
|
+
}, [libraryId, resourceQueryParams]);
|
|
75
|
+
|
|
76
|
+
const explorerParentKey = useMemo(() => {
|
|
77
|
+
if (!isExplorerCacheActiveForTree) return null;
|
|
78
|
+
return (resourceQueryParams as ResourceQueryParams).parentId ?? null;
|
|
79
|
+
}, [isExplorerCacheActiveForTree, resourceQueryParams]);
|
|
80
|
+
|
|
81
|
+
const explorerChildren = useMemo(() => {
|
|
82
|
+
if (!isExplorerCacheActiveForTree) return [];
|
|
83
|
+
return sortTreeItems(resourceList.map(resourceItemToTreeItem));
|
|
84
|
+
}, [isExplorerCacheActiveForTree, resourceList]);
|
|
85
|
+
|
|
86
|
+
const isSameTreeItems = useCallback((a: TreeItem[] | undefined, b: TreeItem[]) => {
|
|
87
|
+
if (!a) return false;
|
|
88
|
+
if (a.length !== b.length) return false;
|
|
89
|
+
// Compare minimal stable identity for change detection
|
|
90
|
+
let i = 0;
|
|
91
|
+
for (const item of a) {
|
|
92
|
+
if (item.id !== b[i]?.id) return false;
|
|
93
|
+
i += 1;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
// Convert root data to tree items
|
|
99
|
+
const items: TreeItem[] = useMemo(() => {
|
|
100
|
+
// If Explorer has loaded root for this library, use its cache to ensure identical state
|
|
101
|
+
if (isExplorerCacheActiveForTree && explorerParentKey === null) return explorerChildren;
|
|
102
|
+
if (!rootData) return [];
|
|
103
|
+
|
|
104
|
+
const mappedItems: TreeItem[] = rootData.map((item) => ({
|
|
105
|
+
fileType: item.fileType,
|
|
106
|
+
id: item.id,
|
|
107
|
+
isFolder: item.fileType === 'custom/folder',
|
|
108
|
+
name: item.name,
|
|
109
|
+
slug: item.slug,
|
|
110
|
+
sourceType: item.sourceType,
|
|
111
|
+
url: item.url,
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
return sortTreeItems(mappedItems);
|
|
115
|
+
}, [explorerChildren, explorerParentKey, rootData, updateKey]);
|
|
116
|
+
|
|
117
|
+
// Hydrate tree cache for the folder Explorer has loaded (non-root only).
|
|
118
|
+
// This ensures the tree and explorer render identical children for that folder.
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!isExplorerCacheActiveForTree) return;
|
|
121
|
+
if (!explorerParentKey) return; // root handled via `items` memo above
|
|
122
|
+
|
|
123
|
+
const existing = state.folderChildrenCache.get(explorerParentKey);
|
|
124
|
+
if (isSameTreeItems(existing, explorerChildren)) return;
|
|
125
|
+
|
|
126
|
+
state.folderChildrenCache.set(explorerParentKey, explorerChildren);
|
|
127
|
+
state.loadedFolders.add(explorerParentKey);
|
|
128
|
+
forceUpdate();
|
|
129
|
+
// NOTE: folderChildrenCache / loadedFolders are mutated in-place
|
|
130
|
+
}, [
|
|
131
|
+
explorerChildren,
|
|
132
|
+
explorerParentKey,
|
|
133
|
+
isExplorerCacheActiveForTree,
|
|
134
|
+
isSameTreeItems,
|
|
135
|
+
state,
|
|
136
|
+
forceUpdate,
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
const visibleNodes = useMemo(() => {
|
|
140
|
+
interface VisibleNode {
|
|
141
|
+
item: TreeItem;
|
|
142
|
+
key: string;
|
|
143
|
+
level: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const result: VisibleNode[] = [];
|
|
147
|
+
|
|
148
|
+
const walk = (nodes: TreeItem[], level: number) => {
|
|
149
|
+
for (const node of nodes) {
|
|
150
|
+
const key = node.slug || node.id;
|
|
151
|
+
|
|
152
|
+
result.push({ item: node, key, level });
|
|
153
|
+
|
|
154
|
+
if (!node.isFolder) continue;
|
|
155
|
+
if (!expandedFolders.has(key)) continue;
|
|
156
|
+
|
|
157
|
+
const children = folderChildrenCache.get(key);
|
|
158
|
+
if (!children || children.length === 0) continue;
|
|
159
|
+
|
|
160
|
+
walk(children, level + 1);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
walk(items, 0);
|
|
165
|
+
|
|
166
|
+
return result;
|
|
167
|
+
// NOTE: expandedFolders / folderChildrenCache are mutated in-place, so rely on updateKey for recompute
|
|
168
|
+
}, [items, expandedFolders, folderChildrenCache, updateKey]);
|
|
169
|
+
|
|
170
|
+
const handleLoadFolder = useCallback(
|
|
171
|
+
async (folderId: string) => {
|
|
172
|
+
// Set loading state
|
|
173
|
+
state.loadingFolders.add(folderId);
|
|
174
|
+
forceUpdate();
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
// Prefer Explorer's cache when it matches this folder (keeps tree + explorer identical)
|
|
178
|
+
if (isExplorerCacheActiveForTree && explorerParentKey === folderId) {
|
|
179
|
+
state.folderChildrenCache.set(folderId, explorerChildren);
|
|
180
|
+
state.loadedFolders.add(folderId);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Use SWR mutate to trigger a fetch that will be cached and shared with FileExplorer
|
|
185
|
+
const { mutate: swrMutate } = await import('swr');
|
|
186
|
+
const response = await swrMutate(
|
|
187
|
+
[
|
|
188
|
+
'useFetchKnowledgeItems',
|
|
189
|
+
{
|
|
190
|
+
knowledgeBaseId: libraryId,
|
|
191
|
+
parentId: folderId,
|
|
192
|
+
showFilesInKnowledgeBase: false,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
() =>
|
|
196
|
+
fileService.getKnowledgeItems({
|
|
197
|
+
knowledgeBaseId: libraryId,
|
|
198
|
+
parentId: folderId,
|
|
199
|
+
showFilesInKnowledgeBase: false,
|
|
200
|
+
}),
|
|
201
|
+
{
|
|
202
|
+
revalidate: false, // Don't revalidate immediately after mutation
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
if (!response || !response.items) {
|
|
207
|
+
console.error('Failed to load folder contents: no data returned');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const childItems: TreeItem[] = response.items.map((item) => ({
|
|
212
|
+
fileType: item.fileType,
|
|
213
|
+
id: item.id,
|
|
214
|
+
isFolder: item.fileType === 'custom/folder',
|
|
215
|
+
name: item.name,
|
|
216
|
+
slug: item.slug,
|
|
217
|
+
sourceType: item.sourceType,
|
|
218
|
+
url: item.url,
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
// Sort children: folders first, then files
|
|
222
|
+
const sortedChildren = sortTreeItems(childItems);
|
|
223
|
+
|
|
224
|
+
// Store children in cache
|
|
225
|
+
state.folderChildrenCache.set(folderId, sortedChildren);
|
|
226
|
+
state.loadedFolders.add(folderId);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error('Failed to load folder contents:', error);
|
|
229
|
+
} finally {
|
|
230
|
+
// Clear loading state
|
|
231
|
+
state.loadingFolders.delete(folderId);
|
|
232
|
+
// Trigger re-render
|
|
233
|
+
forceUpdate();
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
[
|
|
237
|
+
explorerChildren,
|
|
238
|
+
explorerParentKey,
|
|
239
|
+
forceUpdate,
|
|
240
|
+
isExplorerCacheActiveForTree,
|
|
241
|
+
libraryId,
|
|
242
|
+
state,
|
|
243
|
+
],
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const handleToggleFolder = useCallback(
|
|
247
|
+
(folderId: string) => {
|
|
248
|
+
if (state.expandedFolders.has(folderId)) {
|
|
249
|
+
state.expandedFolders.delete(folderId);
|
|
250
|
+
} else {
|
|
251
|
+
state.expandedFolders.add(folderId);
|
|
252
|
+
}
|
|
253
|
+
// Trigger re-render
|
|
254
|
+
forceUpdate();
|
|
255
|
+
},
|
|
256
|
+
[state, forceUpdate],
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Reset parent folder key when switching libraries
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
parentFolderKeyRef.current = null;
|
|
262
|
+
}, [libraryId]);
|
|
263
|
+
|
|
264
|
+
// Listen for external tree refresh events (triggered when cache is cleared)
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
if (typeof window === 'undefined') return;
|
|
267
|
+
|
|
268
|
+
const handleTreeRefresh = (event: Event) => {
|
|
269
|
+
const detail = (event as CustomEvent<{ knowledgeBaseId?: string }>).detail;
|
|
270
|
+
if (detail?.knowledgeBaseId && libraryId && detail.knowledgeBaseId !== libraryId) return;
|
|
271
|
+
forceUpdate();
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
window.addEventListener(TREE_REFRESH_EVENT, handleTreeRefresh);
|
|
275
|
+
return () => {
|
|
276
|
+
window.removeEventListener(TREE_REFRESH_EVENT, handleTreeRefresh);
|
|
277
|
+
};
|
|
278
|
+
}, [libraryId, forceUpdate]);
|
|
279
|
+
|
|
280
|
+
// Auto-expand folders when navigating to a folder in Explorer
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
if (!folderBreadcrumb || folderBreadcrumb.length === 0) return;
|
|
283
|
+
|
|
284
|
+
let hasChanges = false;
|
|
285
|
+
|
|
286
|
+
// Expand all folders in the breadcrumb path
|
|
287
|
+
for (const crumb of folderBreadcrumb) {
|
|
288
|
+
const key = crumb.slug || crumb.id;
|
|
289
|
+
if (!state.expandedFolders.has(key)) {
|
|
290
|
+
state.expandedFolders.add(key);
|
|
291
|
+
hasChanges = true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Load folder contents if not already loaded
|
|
295
|
+
if (!state.loadedFolders.has(key) && !state.loadingFolders.has(key)) {
|
|
296
|
+
handleLoadFolder(key);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (hasChanges) {
|
|
301
|
+
forceUpdate();
|
|
302
|
+
}
|
|
303
|
+
}, [folderBreadcrumb, state, forceUpdate, handleLoadFolder]);
|
|
304
|
+
|
|
305
|
+
// Auto-expand parent folder when viewing a file
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
if (!currentFile || !currentViewItemId) {
|
|
308
|
+
parentFolderKeyRef.current = null;
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// If the file has a parent folder, expand the path to it
|
|
313
|
+
if (currentFile.parentId) {
|
|
314
|
+
// Fetch the parent folder's breadcrumb to get the full path
|
|
315
|
+
const fetchParentPath = async () => {
|
|
316
|
+
try {
|
|
317
|
+
const parentBreadcrumb = await fileService.getFolderBreadcrumb(currentFile.parentId!);
|
|
318
|
+
|
|
319
|
+
if (!parentBreadcrumb || parentBreadcrumb.length === 0) return;
|
|
320
|
+
|
|
321
|
+
let hasChanges = false;
|
|
322
|
+
|
|
323
|
+
// The last item in breadcrumb is the immediate parent folder
|
|
324
|
+
const parentFolder = parentBreadcrumb.at(-1)!;
|
|
325
|
+
const parentKey = parentFolder.slug || parentFolder.id;
|
|
326
|
+
parentFolderKeyRef.current = parentKey;
|
|
327
|
+
|
|
328
|
+
// Expand all folders in the parent's breadcrumb path
|
|
329
|
+
for (const crumb of parentBreadcrumb) {
|
|
330
|
+
const key = crumb.slug || crumb.id;
|
|
331
|
+
if (!state.expandedFolders.has(key)) {
|
|
332
|
+
state.expandedFolders.add(key);
|
|
333
|
+
hasChanges = true;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Load folder contents if not already loaded
|
|
337
|
+
if (!state.loadedFolders.has(key) && !state.loadingFolders.has(key)) {
|
|
338
|
+
handleLoadFolder(key);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (hasChanges) {
|
|
343
|
+
forceUpdate();
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error('Failed to fetch parent folder breadcrumb:', error);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
fetchParentPath();
|
|
351
|
+
} else {
|
|
352
|
+
parentFolderKeyRef.current = null;
|
|
353
|
+
}
|
|
354
|
+
}, [currentFile, currentViewItemId, state, forceUpdate, handleLoadFolder]);
|
|
355
|
+
|
|
356
|
+
if (isLoading) {
|
|
357
|
+
return <TreeSkeleton />;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Determine which item should be highlighted
|
|
361
|
+
// If viewing a file, highlight its parent folder
|
|
362
|
+
// Otherwise, highlight the current folder
|
|
363
|
+
const selectedKey =
|
|
364
|
+
currentViewItemId && parentFolderKeyRef.current
|
|
365
|
+
? parentFolderKeyRef.current
|
|
366
|
+
: currentFolderSlug;
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<Flexbox paddingInline={4} style={{ height: '100%' }}>
|
|
370
|
+
<VList
|
|
371
|
+
bufferSize={typeof window !== 'undefined' ? window.innerHeight : 0}
|
|
372
|
+
style={{ height: '100%' }}
|
|
373
|
+
>
|
|
374
|
+
{visibleNodes.map(({ item, key, level }) => (
|
|
375
|
+
<div key={key} style={{ paddingBottom: 2 }}>
|
|
376
|
+
<HierarchyNode
|
|
377
|
+
expandedFolders={expandedFolders}
|
|
378
|
+
folderChildrenCache={folderChildrenCache}
|
|
379
|
+
item={item}
|
|
380
|
+
level={level}
|
|
381
|
+
loadingFolders={loadingFolders}
|
|
382
|
+
onLoadFolder={handleLoadFolder}
|
|
383
|
+
onToggleFolder={handleToggleFolder}
|
|
384
|
+
selectedKey={selectedKey}
|
|
385
|
+
updateKey={updateKey}
|
|
386
|
+
/>
|
|
387
|
+
</div>
|
|
388
|
+
))}
|
|
389
|
+
</VList>
|
|
390
|
+
</Flexbox>
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
LibraryHierarchy.displayName = 'FileTree';
|
|
395
|
+
|
|
396
|
+
export default LibraryHierarchy;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createStaticStyles } from 'antd-style';
|
|
2
|
+
|
|
3
|
+
export const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
4
|
+
dragging: css`
|
|
5
|
+
will-change: transform;
|
|
6
|
+
opacity: 0.5;
|
|
7
|
+
`,
|
|
8
|
+
fileItemDragOver: css`
|
|
9
|
+
color: ${cssVar.colorBgElevated} !important;
|
|
10
|
+
background-color: ${cssVar.colorText} !important;
|
|
11
|
+
|
|
12
|
+
* {
|
|
13
|
+
color: ${cssVar.colorBgElevated} !important;
|
|
14
|
+
}
|
|
15
|
+
`,
|
|
16
|
+
treeItem: css`
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
`,
|
|
19
|
+
}));
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { fileService } from '@/services/file';
|
|
2
|
+
import { useFileStore } from '@/store/file';
|
|
3
|
+
import type { ResourceItem } from '@/types/resource';
|
|
4
|
+
|
|
5
|
+
import type { TreeItem } from './types';
|
|
6
|
+
|
|
7
|
+
export const sortTreeItems = <T extends TreeItem>(items: T[]): T[] => {
|
|
8
|
+
return [...items].sort((a, b) => {
|
|
9
|
+
// Folders first
|
|
10
|
+
if (a.isFolder && !b.isFolder) return -1;
|
|
11
|
+
if (!a.isFolder && b.isFolder) return 1;
|
|
12
|
+
// Then alphabetically by name
|
|
13
|
+
return a.name.localeCompare(b.name);
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const resourceItemToTreeItem = (item: ResourceItem): TreeItem => {
|
|
18
|
+
return {
|
|
19
|
+
fileType: item.fileType,
|
|
20
|
+
id: item.id,
|
|
21
|
+
isFolder: item.fileType === 'custom/folder',
|
|
22
|
+
name: item.name,
|
|
23
|
+
slug: item.slug,
|
|
24
|
+
sourceType: item.sourceType,
|
|
25
|
+
url: item.url || '',
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Module-level state to persist expansion across re-renders
|
|
30
|
+
const treeState = new Map<
|
|
31
|
+
string,
|
|
32
|
+
{
|
|
33
|
+
expandedFolders: Set<string>;
|
|
34
|
+
folderChildrenCache: Map<string, TreeItem[]>;
|
|
35
|
+
loadedFolders: Set<string>;
|
|
36
|
+
loadingFolders: Set<string>;
|
|
37
|
+
}
|
|
38
|
+
>();
|
|
39
|
+
|
|
40
|
+
export const TREE_REFRESH_EVENT = 'resource-tree-refresh';
|
|
41
|
+
|
|
42
|
+
export const emitTreeRefresh = (knowledgeBaseId: string) => {
|
|
43
|
+
if (typeof window === 'undefined') return;
|
|
44
|
+
window.dispatchEvent(
|
|
45
|
+
new CustomEvent(TREE_REFRESH_EVENT, {
|
|
46
|
+
detail: { knowledgeBaseId },
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const getTreeState = (knowledgeBaseId: string) => {
|
|
52
|
+
if (!treeState.has(knowledgeBaseId)) {
|
|
53
|
+
treeState.set(knowledgeBaseId, {
|
|
54
|
+
expandedFolders: new Set(),
|
|
55
|
+
folderChildrenCache: new Map(),
|
|
56
|
+
loadedFolders: new Set(),
|
|
57
|
+
loadingFolders: new Set(),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return treeState.get(knowledgeBaseId)!;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Clear and reload all expanded folders
|
|
65
|
+
* This should be called along with file store's refreshFileList()
|
|
66
|
+
* Simpler approach: reload all expanded folders to avoid ID vs slug issues
|
|
67
|
+
*/
|
|
68
|
+
export const clearTreeFolderCache = async (knowledgeBaseId: string) => {
|
|
69
|
+
const state = treeState.get(knowledgeBaseId);
|
|
70
|
+
if (!state) return;
|
|
71
|
+
|
|
72
|
+
const { resourceList } = useFileStore.getState();
|
|
73
|
+
|
|
74
|
+
const resolveParentId = (key: string | null | undefined) => {
|
|
75
|
+
if (!key) return null;
|
|
76
|
+
// Prefer id match
|
|
77
|
+
const byId = resourceList.find(
|
|
78
|
+
(item) => item.knowledgeBaseId === knowledgeBaseId && item.id === key,
|
|
79
|
+
);
|
|
80
|
+
if (byId) return byId.id;
|
|
81
|
+
// Fallback to slug match
|
|
82
|
+
const bySlug = resourceList.find(
|
|
83
|
+
(item) => item.knowledgeBaseId === knowledgeBaseId && item.slug === key,
|
|
84
|
+
);
|
|
85
|
+
return bySlug?.id ?? key;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const buildChildrenFromStore = (parentKey: string | null) => {
|
|
89
|
+
const parentId = resolveParentId(parentKey);
|
|
90
|
+
const items = resourceList
|
|
91
|
+
.filter(
|
|
92
|
+
(item) =>
|
|
93
|
+
item.knowledgeBaseId === knowledgeBaseId &&
|
|
94
|
+
(item.parentId ?? null) === (parentId ?? null),
|
|
95
|
+
)
|
|
96
|
+
.map(resourceItemToTreeItem);
|
|
97
|
+
|
|
98
|
+
return sortTreeItems(items);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Get list of all currently expanded folders before clearing
|
|
102
|
+
const expandedFoldersList = Array.from(state.expandedFolders);
|
|
103
|
+
|
|
104
|
+
// Clear all caches
|
|
105
|
+
state.folderChildrenCache.clear();
|
|
106
|
+
state.loadedFolders.clear();
|
|
107
|
+
|
|
108
|
+
// Reload each expanded folder
|
|
109
|
+
for (const folderKey of expandedFoldersList) {
|
|
110
|
+
// Prefer local store (explorer data) to avoid stale remote state
|
|
111
|
+
const localChildren = buildChildrenFromStore(folderKey);
|
|
112
|
+
if (localChildren.length > 0) {
|
|
113
|
+
state.folderChildrenCache.set(folderKey, localChildren);
|
|
114
|
+
state.loadedFolders.add(folderKey);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Fallback to remote fetch if store has no data (e.g., initial load)
|
|
119
|
+
try {
|
|
120
|
+
const response = await fileService.getKnowledgeItems({
|
|
121
|
+
knowledgeBaseId,
|
|
122
|
+
parentId: folderKey,
|
|
123
|
+
showFilesInKnowledgeBase: false,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (response?.items) {
|
|
127
|
+
const childItems = response.items.map((item) => ({
|
|
128
|
+
fileType: item.fileType,
|
|
129
|
+
id: item.id,
|
|
130
|
+
isFolder: item.fileType === 'custom/folder',
|
|
131
|
+
name: item.name,
|
|
132
|
+
slug: item.slug,
|
|
133
|
+
sourceType: item.sourceType,
|
|
134
|
+
url: item.url,
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
// Sort children: folders first, then files
|
|
138
|
+
const sortedChildren = childItems.sort((a, b) => {
|
|
139
|
+
if (a.isFolder && !b.isFolder) return -1;
|
|
140
|
+
if (!a.isFolder && b.isFolder) return 1;
|
|
141
|
+
return a.name.localeCompare(b.name);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
state.folderChildrenCache.set(folderKey, sortedChildren);
|
|
145
|
+
state.loadedFolders.add(folderKey);
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error(`Failed to reload folder ${folderKey}:`, error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Revalidate SWR caches for root and expanded folders to keep list and tree in sync
|
|
153
|
+
try {
|
|
154
|
+
const { mutate } = await import('swr');
|
|
155
|
+
const revalidateFolder = (parentId: string | null) =>
|
|
156
|
+
mutate(
|
|
157
|
+
[
|
|
158
|
+
'useFetchKnowledgeItems',
|
|
159
|
+
{
|
|
160
|
+
knowledgeBaseId,
|
|
161
|
+
parentId,
|
|
162
|
+
showFilesInKnowledgeBase: false,
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
undefined,
|
|
166
|
+
{ revalidate: true },
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
await Promise.all([
|
|
170
|
+
revalidateFolder(null),
|
|
171
|
+
...expandedFoldersList.map((folderKey) => revalidateFolder(folderKey)),
|
|
172
|
+
]);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error('Failed to revalidate tree SWR cache:', error);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
emitTreeRefresh(knowledgeBaseId);
|
|
178
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { BRANDING_NAME } from '@lobechat/business-const';
|
|
3
4
|
import { Flexbox } from '@lobehub/ui';
|
|
4
5
|
import { createStyles, cssVar } from 'antd-style';
|
|
5
6
|
import dynamic from 'next/dynamic';
|
|
@@ -91,6 +92,8 @@ const ResourceManager = memo(() => {
|
|
|
91
92
|
prev.delete('file');
|
|
92
93
|
return prev;
|
|
93
94
|
});
|
|
95
|
+
// Reset document title to default
|
|
96
|
+
document.title = BRANDING_NAME;
|
|
94
97
|
};
|
|
95
98
|
|
|
96
99
|
return (
|
|
@@ -9,17 +9,8 @@ const ChatGroupWizard = lazy(() =>
|
|
|
9
9
|
|
|
10
10
|
interface GroupWizardCallbacks {
|
|
11
11
|
onCancel?: () => void;
|
|
12
|
-
onCreateCustom?: (
|
|
13
|
-
|
|
14
|
-
hostConfig?: { model?: string; provider?: string },
|
|
15
|
-
enableSupervisor?: boolean,
|
|
16
|
-
) => Promise<void>;
|
|
17
|
-
onCreateFromTemplate?: (
|
|
18
|
-
templateId: string,
|
|
19
|
-
hostConfig?: { model?: string; provider?: string },
|
|
20
|
-
enableSupervisor?: boolean,
|
|
21
|
-
selectedMemberTitles?: string[],
|
|
22
|
-
) => Promise<void>;
|
|
12
|
+
onCreateCustom?: (selectedAgents: string[]) => Promise<void>;
|
|
13
|
+
onCreateFromTemplate?: (templateId: string, selectedMemberTitles?: string[]) => Promise<void>;
|
|
23
14
|
}
|
|
24
15
|
|
|
25
16
|
interface GroupWizardContextValue {
|
|
@@ -56,32 +47,18 @@ const GroupWizardProviderInner = memo<GroupWizardProviderProps>(({ children }) =
|
|
|
56
47
|
setIsLoading(false);
|
|
57
48
|
};
|
|
58
49
|
|
|
59
|
-
const handleCreateCustom = async (
|
|
60
|
-
selectedAgents: string[],
|
|
61
|
-
hostConfig?: { model?: string; provider?: string },
|
|
62
|
-
enableSupervisor?: boolean,
|
|
63
|
-
) => {
|
|
50
|
+
const handleCreateCustom = async (selectedAgents: string[]) => {
|
|
64
51
|
if (callbacks.onCreateCustom) {
|
|
65
|
-
await callbacks.onCreateCustom(selectedAgents
|
|
52
|
+
await callbacks.onCreateCustom(selectedAgents);
|
|
66
53
|
closeGroupWizard();
|
|
67
54
|
}
|
|
68
55
|
};
|
|
69
56
|
|
|
70
|
-
const handleCreateFromTemplate = async (
|
|
71
|
-
templateId: string,
|
|
72
|
-
hostConfig?: { model?: string; provider?: string },
|
|
73
|
-
enableSupervisor?: boolean,
|
|
74
|
-
selectedMemberTitles?: string[],
|
|
75
|
-
) => {
|
|
57
|
+
const handleCreateFromTemplate = async (templateId: string, selectedMemberTitles?: string[]) => {
|
|
76
58
|
if (callbacks.onCreateFromTemplate) {
|
|
77
59
|
setIsLoading(true);
|
|
78
60
|
try {
|
|
79
|
-
await callbacks.onCreateFromTemplate(
|
|
80
|
-
templateId,
|
|
81
|
-
hostConfig,
|
|
82
|
-
enableSupervisor,
|
|
83
|
-
selectedMemberTitles,
|
|
84
|
-
);
|
|
61
|
+
await callbacks.onCreateFromTemplate(templateId, selectedMemberTitles);
|
|
85
62
|
closeGroupWizard();
|
|
86
63
|
} finally {
|
|
87
64
|
setIsLoading(false);
|
|
@@ -194,7 +194,7 @@ export default {
|
|
|
194
194
|
'profile.usernameRule': 'Username can only contain letters, numbers, or underscores',
|
|
195
195
|
'profile.usernameUpdateFailed': 'Failed to update username, please try again later',
|
|
196
196
|
'signin.subtitle': 'Sign up or log in to your {{appName}} account',
|
|
197
|
-
'signin.title': '
|
|
197
|
+
'signin.title': 'Where Agents Collaborate',
|
|
198
198
|
'signout': 'Log Out',
|
|
199
199
|
'signup': 'Sign Up',
|
|
200
200
|
'stats.aiheatmaps': 'Activity Index',
|
|
@@ -20,6 +20,7 @@ export default {
|
|
|
20
20
|
'header.actions.connect': 'Connect...',
|
|
21
21
|
'header.actions.createFolderError': 'Failed to create folder',
|
|
22
22
|
'header.actions.creatingFolder': 'Creating folder...',
|
|
23
|
+
'header.actions.deleteLibrary': 'Delete Library',
|
|
23
24
|
'header.actions.gitignore.apply': 'Apply Rules',
|
|
24
25
|
'header.actions.gitignore.cancel': 'Ignore Rules',
|
|
25
26
|
'header.actions.gitignore.content':
|
|
@@ -118,6 +119,7 @@ export default {
|
|
|
118
119
|
'preview.downloadFile': 'Download File',
|
|
119
120
|
'preview.unsupportedFileAndContact':
|
|
120
121
|
'This file format is not currently supported for online preview. If you have a request for previewing, feel free to <1>contact us</1>.',
|
|
122
|
+
'resource': 'Resource',
|
|
121
123
|
'searchFilePlaceholder': 'Search Files',
|
|
122
124
|
'searchPagePlaceholder': 'Search Pages',
|
|
123
125
|
'tab.all': 'All',
|