@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
@@ -4,7 +4,7 @@ import { BUILTIN_AGENT_SLUGS } from '@lobechat/builtin-agents';
4
4
  import { ActionIcon, Flexbox } from '@lobehub/ui';
5
5
  import { Modal } from 'antd';
6
6
  import { cssVar, useTheme } from 'antd-style';
7
- import { ArrowLeftIcon, BotMessageSquareIcon, DownloadIcon, InfoIcon } from 'lucide-react';
7
+ import { ArrowLeftIcon, DownloadIcon, InfoIcon } from 'lucide-react';
8
8
  import { memo, useState } from 'react';
9
9
  import { useTranslation } from 'react-i18next';
10
10
 
@@ -13,7 +13,6 @@ import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/featur
13
13
  import Loading from '@/components/Loading/BrandTextLoading';
14
14
  import NavHeader from '@/features/NavHeader';
15
15
  import PageAgentProvider from '@/features/PageEditor/PageAgentProvider';
16
- import ToggleRightPanelButton from '@/features/RightPanel/ToggleRightPanelButton';
17
16
  import { useAgentStore } from '@/store/agent';
18
17
  import { builtinAgentSelectors } from '@/store/agent/selectors';
19
18
  import { fileManagerSelectors, useFileStore } from '@/store/file';
@@ -56,7 +55,7 @@ const FileEditorCanvas = memo<FileEditorProps>(({ onBack }) => {
56
55
  }
57
56
  right={
58
57
  <Flexbox gap={8} horizontal>
59
- <ToggleRightPanelButton icon={BotMessageSquareIcon} showActive={true} size={20} />
58
+ {/* <ToggleRightPanelButton icon={BotMessageSquareIcon} showActive={true} size={20} /> */}
60
59
  {fileDetail?.url && (
61
60
  <ActionIcon
62
61
  icon={DownloadIcon}
@@ -29,18 +29,18 @@ const Header = memo(() => {
29
29
  ]);
30
30
  const toggleCommandMenu = useGlobalStore((s) => s.toggleCommandMenu);
31
31
 
32
- // Disable batch actions dropdown when no items selected and not in any library
33
- const isBatchActionsDisabled = selectFileIds.length === 0 && !libraryId;
34
-
35
- // If no libraryId, show just the category name
36
- const leftContent =
37
- !libraryId && category && category !== FilesTabs.All ? (
38
- <Flexbox style={{ marginLeft: 8 }}>{t(`tab.${category as FilesTabs}` as any)}</Flexbox>
39
- ) : (
40
- <Flexbox style={{ marginLeft: 8 }}>
41
- <Breadcrumb category={category} knowledgeBaseId={libraryId} />
42
- </Flexbox>
43
- );
32
+ // If no libraryId, show category name or "Resource" for All
33
+ const leftContent = !libraryId ? (
34
+ <Flexbox style={{ marginLeft: 8 }}>
35
+ {category === FilesTabs.All
36
+ ? t('resource', { defaultValue: 'Resource' })
37
+ : t(`tab.${category as FilesTabs}` as any)}
38
+ </Flexbox>
39
+ ) : (
40
+ <Flexbox style={{ marginLeft: 8 }}>
41
+ <Breadcrumb category={category} knowledgeBaseId={libraryId} />
42
+ </Flexbox>
43
+ );
44
44
 
45
45
  return (
46
46
  <NavHeader
@@ -49,11 +49,7 @@ const Header = memo(() => {
49
49
  <>
50
50
  <ActionIcon icon={SearchIcon} onClick={() => toggleCommandMenu(true)} />
51
51
  <SortDropdown />
52
- <BatchActionsDropdown
53
- disabled={isBatchActionsDisabled}
54
- onActionClick={onActionClick}
55
- selectCount={selectFileIds.length}
56
- />
52
+ <BatchActionsDropdown onActionClick={onActionClick} selectCount={selectFileIds.length} />
57
53
  <ViewSwitcher />
58
54
  <Flexbox style={{ marginLeft: 8 }}>
59
55
  <AddButton />
@@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next';
15
15
  import { shallow } from 'zustand/shallow';
16
16
 
17
17
  import RepoIcon from '@/components/LibIcon';
18
- import { clearTreeFolderCache } from '@/features/ResourceManager/components/Tree';
18
+ import { clearTreeFolderCache } from '@/features/ResourceManager/components/LibraryHierarchy';
19
19
  import { PAGE_FILE_TYPE } from '@/features/ResourceManager/constants';
20
20
  import { documentService } from '@/services/document';
21
21
  import { useFileStore } from '@/store/file';
@@ -0,0 +1,130 @@
1
+ import { memo, useEffect, useRef, useState } from 'react';
2
+
3
+ interface TruncatedFileNameProps {
4
+ className?: string;
5
+ name: string;
6
+ }
7
+
8
+ /**
9
+ * Truncates file name from the center, preserving the extension at the end
10
+ * Similar to macOS Finder behavior
11
+ */
12
+ const TruncatedFileName = memo<TruncatedFileNameProps>(({ name, className }) => {
13
+ const containerRef = useRef<HTMLSpanElement>(null);
14
+ const [displayName, setDisplayName] = useState(name);
15
+
16
+ useEffect(() => {
17
+ const container = containerRef.current;
18
+ if (!container) return;
19
+
20
+ const updateTruncation = () => {
21
+ const containerWidth = container.offsetWidth;
22
+ if (containerWidth === 0) return;
23
+
24
+ // Create a temporary span to measure text width
25
+ const measureSpan = document.createElement('span');
26
+ measureSpan.style.visibility = 'hidden';
27
+ measureSpan.style.position = 'absolute';
28
+ measureSpan.style.whiteSpace = 'nowrap';
29
+ measureSpan.style.font = window.getComputedStyle(container).font;
30
+ document.body.append(measureSpan);
31
+
32
+ // Measure full name
33
+ measureSpan.textContent = name;
34
+ const fullWidth = measureSpan.offsetWidth;
35
+
36
+ // If it fits, show the full name
37
+ if (fullWidth <= containerWidth) {
38
+ setDisplayName(name);
39
+ measureSpan.remove();
40
+ return;
41
+ }
42
+
43
+ // Split filename and extension
44
+ const lastDotIndex = name.lastIndexOf('.');
45
+ let baseName = name;
46
+ let extension = '';
47
+
48
+ // Only treat as extension if dot is not at the start and there's content after it
49
+ if (lastDotIndex > 0 && lastDotIndex < name.length - 1) {
50
+ baseName = name.slice(0, lastDotIndex);
51
+ extension = name.slice(lastDotIndex); // includes the dot
52
+ }
53
+
54
+ // Measure ellipsis width
55
+ measureSpan.textContent = '...';
56
+ const ellipsisWidth = measureSpan.offsetWidth;
57
+
58
+ // Measure extension width
59
+ measureSpan.textContent = extension;
60
+ const extensionWidth = measureSpan.offsetWidth;
61
+
62
+ // Calculate available width for base name
63
+ const availableWidth = containerWidth - ellipsisWidth - extensionWidth;
64
+
65
+ if (availableWidth <= 0) {
66
+ // Not enough space, just show ellipsis + extension
67
+ setDisplayName(`...${extension}`);
68
+ measureSpan.remove();
69
+ return;
70
+ }
71
+
72
+ // Binary search to find the optimal split point
73
+ let left = 0;
74
+ let right = baseName.length;
75
+ let bestFit = '';
76
+
77
+ while (left <= right) {
78
+ const mid = Math.floor((left + right) / 2);
79
+ const startChars = Math.ceil(mid / 2);
80
+ const endChars = Math.floor(mid / 2);
81
+
82
+ const truncated =
83
+ baseName.slice(0, startChars) + (mid > 0 ? baseName.slice(-endChars) : '');
84
+
85
+ measureSpan.textContent = truncated;
86
+ const truncatedWidth = measureSpan.offsetWidth;
87
+
88
+ if (truncatedWidth <= availableWidth) {
89
+ bestFit = truncated;
90
+ left = mid + 1;
91
+ } else {
92
+ right = mid - 1;
93
+ }
94
+ }
95
+
96
+ measureSpan.remove();
97
+
98
+ // Construct final truncated name
99
+ if (bestFit.length === 0) {
100
+ setDisplayName(`...${extension}`);
101
+ } else {
102
+ const startChars = Math.ceil(bestFit.length / 2);
103
+ const endChars = Math.floor(bestFit.length / 2);
104
+ setDisplayName(
105
+ `${baseName.slice(0, startChars)}...${baseName.slice(-endChars)}${extension}`,
106
+ );
107
+ }
108
+ };
109
+
110
+ updateTruncation();
111
+
112
+ // Use ResizeObserver to handle container size changes
113
+ const resizeObserver = new ResizeObserver(updateTruncation);
114
+ resizeObserver.observe(container);
115
+
116
+ return () => {
117
+ resizeObserver.disconnect();
118
+ };
119
+ }, [name]);
120
+
121
+ return (
122
+ <span className={className} ref={containerRef} title={name}>
123
+ {displayName}
124
+ </span>
125
+ );
126
+ });
127
+
128
+ TruncatedFileName.displayName = 'TruncatedFileName';
129
+
130
+ export default TruncatedFileName;
@@ -17,7 +17,7 @@ import {
17
17
  } from '@/app/[variants]/(main)/resource/features/DndContextWrapper';
18
18
  import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
19
19
  import FileIcon from '@/components/FileIcon';
20
- import { clearTreeFolderCache } from '@/features/ResourceManager/components/Tree';
20
+ import { clearTreeFolderCache } from '@/features/ResourceManager/components/LibraryHierarchy';
21
21
  import { PAGE_FILE_TYPE } from '@/features/ResourceManager/constants';
22
22
  import { fileManagerSelectors, useFileStore } from '@/store/file';
23
23
  import { type FileListItem as FileListItemType } from '@/types/files';
@@ -27,6 +27,7 @@ import { isChunkingUnsupported } from '@/utils/isChunkingUnsupported';
27
27
  import DropdownMenu from '../../ItemDropdown/DropdownMenu';
28
28
  import { useFileItemDropdown } from '../../ItemDropdown/useFileItemDropdown';
29
29
  import ChunksBadge from './ChunkTag';
30
+ import TruncatedFileName from './TruncatedFileName';
30
31
 
31
32
  // Initialize dayjs plugin once at module level
32
33
  dayjs.extend(relativeTime);
@@ -40,6 +41,7 @@ const styles = createStaticStyles(({ css }) => {
40
41
  cursor: pointer;
41
42
  min-width: 800px;
42
43
 
44
+ /* Hover effect for individual rows */
43
45
  &:hover {
44
46
  background: ${cssVar.colorFillTertiary};
45
47
  }
@@ -59,6 +61,25 @@ const styles = createStaticStyles(({ css }) => {
59
61
  opacity: 0.5;
60
62
  `,
61
63
 
64
+ evenRow: css`
65
+ background: ${cssVar.colorFillQuaternary};
66
+
67
+ /* Hover effect overrides zebra striping on the hovered row only */
68
+ &:hover {
69
+ background: ${cssVar.colorFillTertiary};
70
+ }
71
+
72
+ /* Hide zebra striping when any row is hovered */
73
+ .any-row-hovered & {
74
+ background: transparent;
75
+ }
76
+
77
+ /* But keep hover effect on the actual hovered row */
78
+ .any-row-hovered &:hover {
79
+ background: ${cssVar.colorFillTertiary};
80
+ }
81
+ `,
82
+
62
83
  hover: css`
63
84
  opacity: 0;
64
85
 
@@ -80,7 +101,6 @@ const styles = createStaticStyles(({ css }) => {
80
101
  margin-inline-start: 12px;
81
102
 
82
103
  color: ${cssVar.colorText};
83
- text-overflow: ellipsis;
84
104
  white-space: nowrap;
85
105
  `,
86
106
  nameContainer: css`
@@ -105,6 +125,8 @@ interface FileListItemProps extends FileListItemType {
105
125
  size: number;
106
126
  };
107
127
  index: number;
128
+ isAnyRowHovered: boolean;
129
+ onHoverChange: (isHovered: boolean) => void;
108
130
  onSelectedChange: (id: string, selected: boolean, shiftKey: boolean, index: number) => void;
109
131
  pendingRenameItemId?: string | null;
110
132
  selected?: boolean;
@@ -133,6 +155,7 @@ const FileListItem = memo<FileListItemProps>(
133
155
  sourceType,
134
156
  slug,
135
157
  pendingRenameItemId,
158
+ onHoverChange,
136
159
  }) => {
137
160
  const { t } = useTranslation(['components', 'file']);
138
161
  const { message } = App.useApp();
@@ -376,12 +399,14 @@ const FileListItem = memo<FileListItemProps>(
376
399
  className={cx(
377
400
  styles.container,
378
401
  'file-list-item-group',
402
+ index % 2 === 0 && styles.evenRow,
379
403
  selected && styles.selected,
380
404
  isDragging && styles.dragging,
381
405
  isOver && styles.dragOver,
382
406
  )}
383
407
  data-drop-target-id={id}
384
408
  data-is-folder={String(isFolder)}
409
+ data-row-index={index}
385
410
  draggable={!!resourceManagerState.libraryId}
386
411
  height={48}
387
412
  horizontal
@@ -390,6 +415,8 @@ const FileListItem = memo<FileListItemProps>(
390
415
  onDragOver={handleDragOver}
391
416
  onDragStart={handleDragStart}
392
417
  onDrop={handleDrop}
418
+ onMouseEnter={() => onHoverChange(true)}
419
+ onMouseLeave={() => onHoverChange(false)}
393
420
  paddingInline={8}
394
421
  style={{
395
422
  borderBlockEnd: `1px solid ${cssVar.colorBorderSecondary}`,
@@ -469,7 +496,10 @@ const FileListItem = memo<FileListItemProps>(
469
496
  value={renamingValue}
470
497
  />
471
498
  ) : (
472
- <span className={styles.name}>{name || t('file:pageList.untitled')}</span>
499
+ <TruncatedFileName
500
+ className={styles.name}
501
+ name={name || t('file:pageList.untitled')}
502
+ />
473
503
  )}
474
504
  </Flexbox>
475
505
  <Flexbox
@@ -482,6 +512,7 @@ const FileListItem = memo<FileListItemProps>(
482
512
  onPointerDown={(e) => e.stopPropagation()}
483
513
  >
484
514
  {!isFolder &&
515
+ !isPage &&
485
516
  (fileStoreState.isCreatingFileParseTask ||
486
517
  isNull(chunkingStatus) ||
487
518
  !chunkingStatus ? (
@@ -562,7 +593,8 @@ const FileListItem = memo<FileListItemProps>(
562
593
  prevProps.url === nextProps.url &&
563
594
  prevProps.columnWidths.name === nextProps.columnWidths.name &&
564
595
  prevProps.columnWidths.date === nextProps.columnWidths.date &&
565
- prevProps.columnWidths.size === nextProps.columnWidths.size
596
+ prevProps.columnWidths.size === nextProps.columnWidths.size &&
597
+ prevProps.isAnyRowHovered === nextProps.isAnyRowHovered
566
598
  );
567
599
  },
568
600
  );
@@ -29,6 +29,7 @@ const ListViewSkeleton = ({
29
29
  key={index}
30
30
  paddingInline={8}
31
31
  style={{
32
+ background: index % 2 === 0 ? cssVar.colorFillQuaternary : 'transparent',
32
33
  borderBlockEnd: `1px solid ${cssVar.colorBorderSecondary}`,
33
34
  opacity: getOpacity(index),
34
35
  }}
@@ -39,21 +40,21 @@ const ListViewSkeleton = ({
39
40
  <Flexbox
40
41
  align={'center'}
41
42
  horizontal
42
- paddingInline={8}
43
43
  style={{
44
44
  flexShrink: 0,
45
45
  maxWidth: columnWidths.name,
46
46
  minWidth: columnWidths.name,
47
+ paddingInline: 8,
47
48
  width: columnWidths.name,
48
49
  }}
49
50
  >
50
51
  <Skeleton.Avatar active shape={'square'} size={24} style={{ marginInline: 8 }} />
51
52
  <Skeleton.Button active style={{ height: 16, width: '60%' }} />
52
53
  </Flexbox>
53
- <Flexbox paddingInline={24} style={{ flexShrink: 0 }} width={columnWidths.date}>
54
+ <Flexbox style={{ flexShrink: 0, paddingInline: '0 24px' }} width={columnWidths.date}>
54
55
  <Skeleton.Button active style={{ height: 16, width: '80%' }} />
55
56
  </Flexbox>
56
- <Flexbox paddingInline={24} style={{ flexShrink: 0 }} width={columnWidths.size}>
57
+ <Flexbox style={{ flexShrink: 0, paddingInline: '0 24px' }} width={columnWidths.size}>
57
58
  <Skeleton.Button active style={{ height: 16, width: '60%' }} />
58
59
  </Flexbox>
59
60
  </Flexbox>
@@ -15,6 +15,7 @@ import {
15
15
  } from '@/app/[variants]/(main)/resource/features/store';
16
16
  import { sortFileList } from '@/app/[variants]/(main)/resource/features/store/selectors';
17
17
  import { useFileStore } from '@/store/file';
18
+ import { useFetchResources } from '@/store/file/slices/resource/hooks';
18
19
  import { useGlobalStore } from '@/store/global';
19
20
  import { INITIAL_STATUS } from '@/store/global/initialState';
20
21
  import { type AsyncTaskStatus } from '@/types/asyncTask';
@@ -53,8 +54,11 @@ const styles = createStaticStyles(({ css }) => ({
53
54
  `,
54
55
  }));
55
56
 
56
- const ListView = memo(() => {
57
+ const ListView = memo(function ListView() {
57
58
  const [
59
+ libraryId,
60
+ category,
61
+ searchQuery,
58
62
  selectFileIds,
59
63
  setSelectedFileIds,
60
64
  pendingRenameItemId,
@@ -62,7 +66,11 @@ const ListView = memo(() => {
62
66
  loadMoreKnowledgeItems,
63
67
  sorter,
64
68
  sortType,
69
+ storeIsTransitioning,
65
70
  ] = useResourceManagerStore((s) => [
71
+ s.libraryId,
72
+ s.category,
73
+ s.searchQuery,
66
74
  s.selectedFileIds,
67
75
  s.setSelectedFileIds,
68
76
  s.pendingRenameItemId,
@@ -70,6 +78,7 @@ const ListView = memo(() => {
70
78
  s.loadMoreKnowledgeItems,
71
79
  s.sorter,
72
80
  s.sortType,
81
+ s.isTransitioning,
73
82
  ]);
74
83
 
75
84
  // Access column widths from Global store
@@ -83,6 +92,7 @@ const ListView = memo(() => {
83
92
  const [isLoadingMore, setIsLoadingMore] = useState(false);
84
93
  const isDragActive = useDragActive();
85
94
  const [isDropZoneActive, setIsDropZoneActive] = useState(false);
95
+ const [isAnyRowHovered, setIsAnyRowHovered] = useState(false);
86
96
  const scrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
87
97
  const autoScrollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
88
98
  const containerRef = useRef<HTMLDivElement>(null);
@@ -94,6 +104,33 @@ const ListView = memo(() => {
94
104
  // Get current folder ID - either from breadcrumb or null for root
95
105
  const currentFolderId = folderBreadcrumb?.at(-1)?.id || null;
96
106
 
107
+ const queryParams = useMemo(
108
+ () => ({
109
+ category: libraryId ? undefined : category,
110
+ libraryId,
111
+ parentId: currentFolderSlug || null,
112
+ q: searchQuery ?? undefined,
113
+ showFilesInKnowledgeBase: false,
114
+ sortType,
115
+ sorter,
116
+ }),
117
+ [category, currentFolderSlug, libraryId, searchQuery, sorter, sortType],
118
+ );
119
+
120
+ const { isLoading, isValidating } = useFetchResources(queryParams);
121
+ const { queryParams: currentQueryParams } = useFileStore();
122
+
123
+ const isNavigating = useMemo(() => {
124
+ if (!currentQueryParams || !queryParams) return false;
125
+
126
+ return (
127
+ currentQueryParams.libraryId !== queryParams.libraryId ||
128
+ currentQueryParams.parentId !== queryParams.parentId ||
129
+ currentQueryParams.category !== queryParams.category ||
130
+ currentQueryParams.q !== queryParams.q
131
+ );
132
+ }, [currentQueryParams, queryParams]);
133
+
97
134
  const resourceList = useFileStore((s) => s.resourceList);
98
135
 
99
136
  // Map ResourceItem[] to FileListItem[] for compatibility
@@ -112,6 +149,17 @@ const ListView = memo(() => {
112
149
  // Sort data using current sort settings
113
150
  const data = sortFileList(rawData, sorter, sortType) || [];
114
151
 
152
+ const dataLength = data.length;
153
+ const effectiveIsLoading = isLoading ?? false;
154
+ const effectiveIsNavigating = isNavigating ?? false;
155
+ const effectiveIsTransitioning = storeIsTransitioning ?? false;
156
+ const effectiveIsValidating = isValidating ?? false;
157
+
158
+ const showSkeleton =
159
+ (effectiveIsLoading && dataLength === 0) ||
160
+ (effectiveIsNavigating && effectiveIsValidating) ||
161
+ effectiveIsTransitioning;
162
+
115
163
  const dataRef = useRef<FileListItemType[]>(data);
116
164
 
117
165
  useEffect(() => {
@@ -286,6 +334,8 @@ const ListView = memo(() => {
286
334
  return <ListViewSkeleton columnWidths={columnWidths} />;
287
335
  }, [isLoadingMore, fileListHasMore, columnWidths]);
288
336
 
337
+ if (showSkeleton) return <ListViewSkeleton columnWidths={columnWidths} />;
338
+
289
339
  return (
290
340
  <Flexbox height={'100%'}>
291
341
  <div className={styles.scrollContainer}>
@@ -360,7 +410,11 @@ const ListView = memo(() => {
360
410
  </Flexbox>
361
411
  </Flexbox>
362
412
  <div
363
- className={cx(styles.dropZone, isDropZoneActive && styles.dropZoneActive)}
413
+ className={cx(
414
+ styles.dropZone,
415
+ isDropZoneActive && styles.dropZoneActive,
416
+ isAnyRowHovered && 'any-row-hovered',
417
+ )}
364
418
  data-drop-target-id={currentFolderId || undefined}
365
419
  data-is-folder="true"
366
420
  onDragLeave={handleDropZoneDragLeave}
@@ -385,7 +439,9 @@ const ListView = memo(() => {
385
439
  <FileListItem
386
440
  columnWidths={columnWidths}
387
441
  index={index}
442
+ isAnyRowHovered={isAnyRowHovered}
388
443
  key={item.id}
444
+ onHoverChange={setIsAnyRowHovered}
389
445
  onSelectedChange={handleSelectionChange}
390
446
  pendingRenameItemId={pendingRenameItemId}
391
447
  selected={selectFileIds.includes(item.id)}
@@ -9,24 +9,31 @@ import { useTranslation } from 'react-i18next';
9
9
  import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
10
10
  import { sortFileList } from '@/app/[variants]/(main)/resource/features/store/selectors';
11
11
  import { useFileStore } from '@/store/file';
12
+ import { useFetchResources } from '@/store/file/slices/resource/hooks';
12
13
  import { type FileListItem } from '@/types/files';
13
14
 
14
15
  import { useMasonryColumnCount } from '../useMasonryColumnCount';
15
16
  import MasonryItemWrapper from './MasonryFileItem/MasonryItemWrapper';
17
+ import MasonryViewSkeleton from './Skeleton';
16
18
 
17
- const MasonryView = memo(() => {
19
+ const MasonryView = memo(function MasonryView() {
18
20
  // Access all state from Resource Manager store
19
21
  const [
20
22
  libraryId,
23
+ category,
24
+ searchQuery,
21
25
  selectedFileIds,
22
26
  setSelectedFileIds,
23
27
  loadMoreKnowledgeItems,
24
28
  fileListHasMore,
25
- isMasonryReady,
29
+ storeIsMasonryReady,
26
30
  sorter,
27
31
  sortType,
32
+ storeIsTransitioning,
28
33
  ] = useResourceManagerStore((s) => [
29
34
  s.libraryId,
35
+ s.category,
36
+ s.searchQuery,
30
37
  s.selectedFileIds,
31
38
  s.setSelectedFileIds,
32
39
  s.loadMoreKnowledgeItems,
@@ -34,6 +41,7 @@ const MasonryView = memo(() => {
34
41
  s.isMasonryReady,
35
42
  s.sorter,
36
43
  s.sortType,
44
+ s.isTransitioning,
37
45
  ]);
38
46
 
39
47
  const { t } = useTranslation('file');
@@ -43,6 +51,33 @@ const MasonryView = memo(() => {
43
51
  // NEW: Read from resource store instead of fetching independently
44
52
  const resourceList = useFileStore((s) => s.resourceList);
45
53
 
54
+ const queryParams = useMemo(
55
+ () => ({
56
+ category: libraryId ? undefined : category,
57
+ libraryId,
58
+ parentId: null,
59
+ q: searchQuery ?? undefined,
60
+ showFilesInKnowledgeBase: false,
61
+ sortType,
62
+ sorter,
63
+ }),
64
+ [category, libraryId, searchQuery, sorter, sortType],
65
+ );
66
+
67
+ const { isLoading, isValidating } = useFetchResources(queryParams);
68
+ const { queryParams: currentQueryParams } = useFileStore();
69
+
70
+ const isNavigating = useMemo(() => {
71
+ if (!currentQueryParams || !queryParams) return false;
72
+
73
+ return (
74
+ currentQueryParams.libraryId !== queryParams.libraryId ||
75
+ currentQueryParams.parentId !== queryParams.parentId ||
76
+ currentQueryParams.category !== queryParams.category ||
77
+ currentQueryParams.q !== queryParams.q
78
+ );
79
+ }, [currentQueryParams, queryParams]);
80
+
46
81
  // Map ResourceItem[] to FileListItem[] for compatibility
47
82
  const rawData = resourceList?.map(
48
83
  (item): FileListItem => ({
@@ -69,7 +104,20 @@ const MasonryView = memo(() => {
69
104
  );
70
105
 
71
106
  // Sort data using current sort settings
72
- const data = sortFileList(rawData, sorter, sortType);
107
+ const data = sortFileList(rawData, sorter, sortType) || [];
108
+
109
+ const dataLength = data.length;
110
+ const effectiveIsLoading = isLoading ?? false;
111
+ const effectiveIsNavigating = isNavigating ?? false;
112
+ const effectiveIsValidating = isValidating ?? false;
113
+ const effectiveIsTransitioning = storeIsTransitioning ?? false;
114
+ const effectiveIsMasonryReady = storeIsMasonryReady;
115
+
116
+ const showSkeleton =
117
+ (effectiveIsLoading && dataLength === 0) ||
118
+ (effectiveIsNavigating && effectiveIsValidating) ||
119
+ effectiveIsTransitioning ||
120
+ !effectiveIsMasonryReady;
73
121
 
74
122
  const masonryContext = useMemo(
75
123
  () => ({
@@ -108,13 +156,15 @@ const MasonryView = memo(() => {
108
156
  [handleLoadMore],
109
157
  );
110
158
 
111
- return (
159
+ return showSkeleton ? (
160
+ <MasonryViewSkeleton columnCount={columnCount} />
161
+ ) : (
112
162
  <div
113
163
  onScroll={handleScroll}
114
164
  style={{
115
165
  flex: 1,
116
166
  height: '100%',
117
- opacity: isMasonryReady ? 1 : 0,
167
+ opacity: effectiveIsMasonryReady ? 1 : 0,
118
168
  overflowY: 'auto',
119
169
  transition: 'opacity 0.2s ease-in-out',
120
170
  }}
@@ -124,7 +174,7 @@ const MasonryView = memo(() => {
124
174
  ItemContent={MasonryItemWrapper}
125
175
  columnCount={columnCount}
126
176
  context={masonryContext}
127
- data={data || []}
177
+ data={data}
128
178
  style={{
129
179
  gap: '16px',
130
180
  overflow: 'hidden',
@@ -147,4 +197,6 @@ const MasonryView = memo(() => {
147
197
  );
148
198
  });
149
199
 
200
+ MasonryView.displayName = 'MasonryView';
201
+
150
202
  export default MasonryView;
@@ -5,7 +5,7 @@ import { memo, useCallback, useEffect, useState } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import FolderTree, { type FolderTreeItem } from '@/features/ResourceManager/components/FolderTree';
8
- import { clearTreeFolderCache } from '@/features/ResourceManager/components/Tree';
8
+ import { clearTreeFolderCache } from '@/features/ResourceManager/components/LibraryHierarchy';
9
9
  import { fileService } from '@/services/file';
10
10
  import { useFileStore } from '@/store/file';
11
11
 
@@ -28,10 +28,7 @@ const MoveToFolderModal = memo<MoveToFolderModalProps>(
28
28
  const [loadedFolders, setLoadedFolders] = useState<Set<string>>(new Set());
29
29
  const [isCreatingFolder, setIsCreatingFolder] = useState(false);
30
30
 
31
- const [moveResource, createFolder] = useFileStore((s) => [
32
- s.moveResource,
33
- s.createFolder,
34
- ]);
31
+ const [moveResource, createFolder] = useFileStore((s) => [s.moveResource, s.createFolder]);
35
32
 
36
33
  // Sort items: folders only
37
34
  const sortItems = useCallback((items: FolderTreeItem[]): FolderTreeItem[] => {