@lobehub/lobehub 2.0.0-next.266 → 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.
Files changed (136) hide show
  1. package/.cursor/rules/microcopy-cn.mdc +75 -63
  2. package/.cursor/rules/microcopy-en.mdc +4 -8
  3. package/CHANGELOG.md +50 -0
  4. package/README.md +8 -8
  5. package/README.zh-CN.md +8 -8
  6. package/apps/desktop/src/main/locales/default/common.ts +2 -2
  7. package/changelog/v1.json +10 -0
  8. package/docs/development/database-schema.dbml +4 -0
  9. package/e2e/CLAUDE.md +43 -81
  10. package/e2e/cucumber.config.js +1 -0
  11. package/e2e/docs/local-setup.md +67 -219
  12. package/e2e/scripts/setup.ts +529 -0
  13. package/e2e/src/features/home/sidebarAgent.feature +62 -0
  14. package/e2e/src/features/home/sidebarGroup.feature +62 -0
  15. package/e2e/src/features/page/README.md +118 -0
  16. package/e2e/src/features/page/crud.feature +62 -0
  17. package/e2e/src/features/page/editor-content.feature +93 -0
  18. package/e2e/src/features/page/editor-meta.feature +60 -0
  19. package/e2e/src/steps/agent/conversation.steps.ts +4 -4
  20. package/e2e/src/steps/home/sidebarAgent.steps.ts +370 -0
  21. package/e2e/src/steps/home/sidebarGroup.steps.ts +168 -0
  22. package/e2e/src/steps/hooks.ts +4 -0
  23. package/e2e/src/steps/page/editor-content.steps.ts +344 -0
  24. package/e2e/src/steps/page/editor-meta.steps.ts +410 -0
  25. package/e2e/src/steps/page/page-crud.steps.ts +363 -0
  26. package/e2e/src/support/world.ts +12 -0
  27. package/locales/ar/file.json +2 -0
  28. package/locales/bg-BG/file.json +2 -0
  29. package/locales/de-DE/file.json +2 -0
  30. package/locales/en-US/auth.json +1 -1
  31. package/locales/en-US/file.json +2 -0
  32. package/locales/en-US/metadata.json +2 -2
  33. package/locales/es-ES/file.json +2 -0
  34. package/locales/fa-IR/file.json +2 -0
  35. package/locales/fr-FR/file.json +2 -0
  36. package/locales/it-IT/file.json +2 -0
  37. package/locales/ja-JP/file.json +2 -0
  38. package/locales/ko-KR/file.json +2 -0
  39. package/locales/nl-NL/file.json +2 -0
  40. package/locales/pl-PL/file.json +2 -0
  41. package/locales/pt-BR/file.json +2 -0
  42. package/locales/ru-RU/file.json +2 -0
  43. package/locales/tr-TR/file.json +2 -0
  44. package/locales/vi-VN/file.json +2 -0
  45. package/locales/zh-CN/file.json +2 -0
  46. package/locales/zh-TW/file.json +2 -0
  47. package/package.json +3 -3
  48. package/packages/builtin-agents/src/agents/agent-builder/index.ts +1 -1
  49. package/packages/builtin-agents/src/agents/group-agent-builder/index.ts +1 -1
  50. package/packages/builtin-agents/src/agents/page-agent/index.ts +1 -1
  51. package/packages/const/src/settings/group.ts +0 -10
  52. package/packages/database/migrations/0068_update_group_data.sql +4 -0
  53. package/packages/database/migrations/meta/0068_snapshot.json +9588 -0
  54. package/packages/database/migrations/meta/_journal.json +7 -0
  55. package/packages/database/src/models/__tests__/chatGroup.test.ts +5 -7
  56. package/packages/database/src/models/__tests__/knowledgeBase.test.ts +185 -0
  57. package/packages/database/src/models/knowledgeBase.ts +67 -3
  58. package/packages/database/src/repositories/agentGroup/index.test.ts +23 -29
  59. package/packages/database/src/repositories/agentGroup/index.ts +4 -9
  60. package/packages/database/src/repositories/knowledge/index.ts +3 -3
  61. package/packages/database/src/schemas/chatGroup.ts +4 -3
  62. package/packages/database/src/types/chatGroup.ts +0 -7
  63. package/packages/types/src/agentGroup/index.ts +30 -9
  64. package/packages/utils/src/multimodalContent.test.ts +302 -0
  65. package/packages/utils/src/server/__tests__/sse.test.ts +353 -0
  66. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/Editing.tsx +4 -11
  67. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx +3 -3
  68. package/src/app/[variants]/(main)/home/_layout/Body/Agent/ModalProvider.tsx +9 -32
  69. package/src/app/[variants]/(main)/home/_layout/hooks/useCreateMenuItems.tsx +3 -37
  70. package/src/app/[variants]/(main)/home/_layout/hooks/useSessionGroupMenuItems.tsx +7 -53
  71. package/src/app/[variants]/(main)/home/features/RecentPage/List.tsx +2 -1
  72. package/src/app/[variants]/(main)/resource/features/DndContextWrapper.tsx +1 -1
  73. package/src/app/[variants]/(main)/resource/library/_layout/Sidebar.tsx +2 -2
  74. package/src/app/[variants]/(main)/resource/library/features/LibraryMenu.tsx +2 -2
  75. package/src/app/[variants]/(mobile)/chat/settings/features/SettingButton.tsx +2 -12
  76. package/src/components/ChatGroupWizard/ChatGroupWizard.tsx +5 -27
  77. package/src/components/DragUpload/index.tsx +24 -27
  78. package/src/components/MemberSelectionModal/MemberSelectionModal.tsx +2 -11
  79. package/src/features/ChatInput/ActionBar/Params/Controls.tsx +42 -7
  80. package/src/features/CommandMenu/useCommandMenu.ts +4 -14
  81. package/src/features/ResourceManager/components/Editor/index.tsx +2 -3
  82. package/src/features/ResourceManager/components/Explorer/Header/index.tsx +13 -17
  83. package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +1 -1
  84. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/TruncatedFileName.tsx +130 -0
  85. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +36 -4
  86. package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +4 -3
  87. package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +58 -2
  88. package/src/features/ResourceManager/components/Explorer/MasonryView/index.tsx +58 -6
  89. package/src/features/ResourceManager/components/Explorer/MoveToFolderModal.tsx +2 -5
  90. package/src/features/ResourceManager/components/Explorer/ToolBar/BatchActionsDropdown.tsx +9 -5
  91. package/src/features/ResourceManager/components/Explorer/index.tsx +11 -56
  92. package/src/features/ResourceManager/components/Header/AddButton.tsx +5 -6
  93. package/src/features/ResourceManager/components/LibraryHierarchy/HierarchyNode.tsx +382 -0
  94. package/src/features/ResourceManager/components/LibraryHierarchy/index.tsx +396 -0
  95. package/src/features/ResourceManager/components/LibraryHierarchy/styles.ts +19 -0
  96. package/src/features/ResourceManager/components/LibraryHierarchy/treeState.ts +178 -0
  97. package/src/features/ResourceManager/components/LibraryHierarchy/types.ts +10 -0
  98. package/src/features/ResourceManager/index.tsx +3 -0
  99. package/src/layout/GlobalProvider/GroupWizardProvider.tsx +6 -29
  100. package/src/locales/default/auth.ts +1 -1
  101. package/src/locales/default/file.ts +2 -0
  102. package/src/locales/default/metadata.ts +2 -2
  103. package/src/server/modules/AgentRuntime/AgentRuntimeCoordinator.ts +30 -30
  104. package/src/server/modules/AgentRuntime/AgentStateManager.ts +23 -23
  105. package/src/server/modules/AgentRuntime/InMemoryAgentStateManager.ts +16 -16
  106. package/src/server/modules/AgentRuntime/InMemoryStreamEventManager.ts +13 -13
  107. package/src/server/modules/AgentRuntime/RuntimeExecutors.ts +2 -2
  108. package/src/server/modules/AgentRuntime/StreamEventManager.ts +18 -18
  109. package/src/server/modules/AgentRuntime/types.ts +21 -21
  110. package/src/server/routers/lambda/__tests__/agentGroup.test.ts +8 -8
  111. package/src/server/routers/lambda/agentGroup.ts +10 -12
  112. package/src/server/services/document/index.ts +1 -0
  113. package/src/store/agentGroup/slices/curd.test.ts +4 -4
  114. package/src/store/file/slices/fileManager/action.ts +12 -4
  115. package/src/store/home/slices/homeInput/action.ts +0 -3
  116. package/src/store/home/slices/sidebarUI/action.ts +9 -0
  117. package/src/store/session/slices/session/action.ts +5 -9
  118. package/src/app/[variants]/(mobile)/chat/settings/features/AgentTeamSettings/index.tsx +0 -95
  119. package/src/features/GroupChatSettings/AgentCard.tsx +0 -154
  120. package/src/features/GroupChatSettings/AgentTeamChatSettings.tsx +0 -179
  121. package/src/features/GroupChatSettings/AgentTeamMembersSettings.tsx +0 -244
  122. package/src/features/GroupChatSettings/AgentTeamMetaSettings.tsx +0 -94
  123. package/src/features/GroupChatSettings/AgentTeamSettings.tsx +0 -54
  124. package/src/features/GroupChatSettings/GroupCategory/index.tsx +0 -30
  125. package/src/features/GroupChatSettings/GroupCategory/useGroupCategory.tsx +0 -42
  126. package/src/features/GroupChatSettings/GroupChatSettingsProvider.tsx +0 -19
  127. package/src/features/GroupChatSettings/HostMemberCard.tsx +0 -113
  128. package/src/features/GroupChatSettings/StoreUpdater.tsx +0 -34
  129. package/src/features/GroupChatSettings/hooks/useGroupChatSettings.ts +0 -25
  130. package/src/features/GroupChatSettings/index.ts +0 -16
  131. package/src/features/GroupChatSettings/store/action.ts +0 -105
  132. package/src/features/GroupChatSettings/store/index.ts +0 -18
  133. package/src/features/GroupChatSettings/store/initialState.ts +0 -23
  134. package/src/features/GroupChatSettings/store/selectors.ts +0 -13
  135. package/src/features/ResourceManager/components/Tree/index.tsx +0 -883
  136. /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
+ };
@@ -0,0 +1,10 @@
1
+ export interface TreeItem {
2
+ children?: TreeItem[];
3
+ fileType: string;
4
+ id: string;
5
+ isFolder: boolean;
6
+ name: string;
7
+ slug?: string | null;
8
+ sourceType?: string;
9
+ url: string;
10
+ }
@@ -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
- selectedAgents: string[],
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, hostConfig, enableSupervisor);
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': 'For Agents collaboration',
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',