@lobehub/lobehub 2.0.0-next.211 → 2.0.0-next.212

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/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.212](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.211...v2.0.0-next.212)
6
+
7
+ <sup>Released on **2026-01-05**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **redis**: Disable automatic deserialization in upstash provider.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Code refactoring
19
+
20
+ - **redis**: Disable automatic deserialization in upstash provider, closes [#11210](https://github.com/lobehub/lobe-chat/issues/11210) ([eb5c76c](https://github.com/lobehub/lobe-chat/commit/eb5c76c))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.211](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.210...v2.0.0-next.211)
6
31
 
7
32
  <sup>Released on **2026-01-05**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,9 @@
1
1
  [
2
+ {
3
+ "children": {},
4
+ "date": "2026-01-05",
5
+ "version": "2.0.0-next.212"
6
+ },
2
7
  {
3
8
  "children": {
4
9
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.211",
3
+ "version": "2.0.0-next.212",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -43,8 +43,8 @@ export const GET = async (_req: Request, segmentData: { params: Params }) => {
43
43
 
44
44
  const cacheKey = buildCacheKey(id);
45
45
  if (redisClient) {
46
- // Upstash Redis auto-deserializes JSON, so cached is already an object
47
- const cached = (await redisClient.get(cacheKey)) as CachedFileData | null;
46
+ const cachedStr = await redisClient.get(cacheKey);
47
+ const cached = cachedStr ? (JSON.parse(cachedStr) as CachedFileData) : null;
48
48
  if (cached?.redirectUrl) {
49
49
  log('Cache hit for file: %s', id);
50
50
  return Response.redirect(cached.redirectUrl, 302);
@@ -1,10 +1,9 @@
1
1
  'use client';
2
2
 
3
- import { useDroppable } from '@dnd-kit/core';
4
3
  import { Center, type DropdownItem, DropdownMenu, Flexbox, Skeleton, Text } from '@lobehub/ui';
5
4
  import { createStaticStyles, cx } from 'antd-style';
6
5
  import { ChevronsUpDown } from 'lucide-react';
7
- import { memo, useCallback, useMemo } from 'react';
6
+ import { type DragEvent, memo, useCallback, useMemo, useState } from 'react';
8
7
  import { useNavigate } from 'react-router-dom';
9
8
 
10
9
  import { useDragActive } from '@/app/[variants]/(main)/resource/features/DndContextWrapper';
@@ -48,24 +47,11 @@ const Head = memo<{ id: string }>(({ id }) => {
48
47
  const name = useKnowledgeBaseStore(knowledgeBaseSelectors.getKnowledgeBaseNameById(id));
49
48
  const setMode = useResourceManagerStore((s) => s.setMode);
50
49
  const isDragActive = useDragActive();
50
+ const [isDropZoneActive, setIsDropZoneActive] = useState(false);
51
51
 
52
52
  const useFetchKnowledgeBaseList = useKnowledgeBaseStore((s) => s.useFetchKnowledgeBaseList);
53
53
  const { data: libraries } = useFetchKnowledgeBaseList();
54
54
 
55
- // Special droppable ID for root folder - matches the pattern expected by DndContextWrapper
56
- const ROOT_DROP_ID = `__root__:${id}`;
57
-
58
- const { setNodeRef, isOver } = useDroppable({
59
- data: {
60
- fileType: 'custom/folder',
61
- isFolder: true,
62
- name: 'Root',
63
- targetId: null,
64
- },
65
- disabled: !isDragActive,
66
- id: ROOT_DROP_ID,
67
- });
68
-
69
55
  const handleClick = useCallback(() => {
70
56
  navigate(`/resource/library/${id}`);
71
57
  setMode('explorer');
@@ -79,6 +65,25 @@ const Head = memo<{ id: string }>(({ id }) => {
79
65
  [navigate, setMode],
80
66
  );
81
67
 
68
+ // Native HTML5 drag-and-drop handlers for root directory drop
69
+ const handleDragOver = useCallback(
70
+ (e: DragEvent<HTMLDivElement>) => {
71
+ if (!isDragActive) return;
72
+ e.preventDefault();
73
+ e.stopPropagation();
74
+ setIsDropZoneActive(true);
75
+ },
76
+ [isDragActive],
77
+ );
78
+
79
+ const handleDragLeave = useCallback(() => {
80
+ setIsDropZoneActive(false);
81
+ }, []);
82
+
83
+ const handleDrop = useCallback(() => {
84
+ setIsDropZoneActive(false);
85
+ }, []);
86
+
82
87
  const menuItems = useMemo<DropdownItem[]>(() => {
83
88
  if (!libraries) return [];
84
89
 
@@ -98,12 +103,17 @@ const Head = memo<{ id: string }>(({ id }) => {
98
103
  return (
99
104
  <Flexbox
100
105
  align={'center'}
101
- className={cx(styles.clickableHeader, isOver && styles.dropZoneActive)}
106
+ className={cx(styles.clickableHeader, isDropZoneActive && styles.dropZoneActive)}
107
+ data-drop-target-id="root"
108
+ data-is-folder="true"
109
+ data-root-drop="true"
102
110
  gap={8}
103
111
  horizontal
112
+ onDragLeave={handleDragLeave}
113
+ onDragOver={handleDragOver}
114
+ onDrop={handleDrop}
104
115
  paddingBlock={6}
105
116
  paddingInline={'12px 14px'}
106
- ref={setNodeRef}
107
117
  >
108
118
  <Center style={{ minWidth: 24 }} width={24}>
109
119
  <RepoIcon />
@@ -78,6 +78,9 @@ const ListView = memo(() => {
78
78
  const [lastSelectedIndex, setLastSelectedIndex] = useState<number | null>(null);
79
79
  const isDragActive = useDragActive();
80
80
  const [isDropZoneActive, setIsDropZoneActive] = useState(false);
81
+ const scrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
82
+ const autoScrollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
83
+ const containerRef = useRef<HTMLDivElement>(null);
81
84
 
82
85
  const { currentFolderSlug } = useFolderPath();
83
86
  const { data: folderBreadcrumb } = useResourceManagerFetchFolderBreadcrumb(currentFolderSlug);
@@ -174,6 +177,18 @@ const ListView = memo(() => {
174
177
  }
175
178
  }, [fileListHasMore, loadMoreKnowledgeItems, isLoadingMore]);
176
179
 
180
+ // Clear auto-scroll timers
181
+ const clearScrollTimers = useCallback(() => {
182
+ if (scrollTimerRef.current) {
183
+ clearTimeout(scrollTimerRef.current);
184
+ scrollTimerRef.current = null;
185
+ }
186
+ if (autoScrollIntervalRef.current) {
187
+ clearInterval(autoScrollIntervalRef.current);
188
+ autoScrollIntervalRef.current = null;
189
+ }
190
+ }, []);
191
+
177
192
  // Drop zone handlers for dragging to blank space
178
193
  const handleDropZoneDragOver = useCallback(
179
194
  (e: DragEvent) => {
@@ -187,11 +202,57 @@ const ListView = memo(() => {
187
202
 
188
203
  const handleDropZoneDragLeave = useCallback(() => {
189
204
  setIsDropZoneActive(false);
190
- }, []);
205
+ clearScrollTimers();
206
+ }, [clearScrollTimers]);
191
207
 
192
208
  const handleDropZoneDrop = useCallback(() => {
193
209
  setIsDropZoneActive(false);
194
- }, []);
210
+ clearScrollTimers();
211
+ }, [clearScrollTimers]);
212
+
213
+ // Handle auto-scroll during drag
214
+ const handleDragMove = useCallback(
215
+ (e: DragEvent<HTMLDivElement>) => {
216
+ if (!isDragActive || !containerRef.current) return;
217
+
218
+ const container = containerRef.current;
219
+ const rect = container.getBoundingClientRect();
220
+ const mouseY = e.clientY;
221
+ const bottomThreshold = 200; // pixels from bottom edge
222
+ const distanceFromBottom = rect.bottom - mouseY;
223
+
224
+ // Check if mouse is near the bottom edge
225
+ if (distanceFromBottom > 0 && distanceFromBottom <= bottomThreshold) {
226
+ // If not already started, start the 2-second timer
227
+ if (!scrollTimerRef.current && !autoScrollIntervalRef.current) {
228
+ scrollTimerRef.current = setTimeout(() => {
229
+ // After 2 seconds, start auto-scrolling
230
+ autoScrollIntervalRef.current = setInterval(() => {
231
+ virtuosoRef.current?.scrollBy({ top: 50 });
232
+ }, 100); // Scroll every 100ms for smooth scrolling
233
+ scrollTimerRef.current = null;
234
+ }, 2000);
235
+ }
236
+ } else {
237
+ // Mouse moved away from bottom edge, clear timers
238
+ clearScrollTimers();
239
+ }
240
+ },
241
+ [isDragActive, clearScrollTimers],
242
+ );
243
+
244
+ // Clean up timers when drag ends or component unmounts
245
+ useEffect(() => {
246
+ if (!isDragActive) {
247
+ clearScrollTimers();
248
+ }
249
+ }, [isDragActive, clearScrollTimers]);
250
+
251
+ useEffect(() => {
252
+ return () => {
253
+ clearScrollTimers();
254
+ };
255
+ }, [clearScrollTimers]);
195
256
 
196
257
  return (
197
258
  <Flexbox height={'100%'}>
@@ -227,8 +288,12 @@ const ListView = memo(() => {
227
288
  data-drop-target-id={currentFolderId || undefined}
228
289
  data-is-folder="true"
229
290
  onDragLeave={handleDropZoneDragLeave}
230
- onDragOver={handleDropZoneDragOver}
291
+ onDragOver={(e) => {
292
+ handleDropZoneDragOver(e);
293
+ handleDragMove(e);
294
+ }}
231
295
  onDrop={handleDropZoneDrop}
296
+ ref={containerRef}
232
297
  style={{ flex: 1, overflow: 'hidden', position: 'relative' }}
233
298
  >
234
299
  <Virtuoso
@@ -7,7 +7,6 @@ import MasonryFileItem from '.';
7
7
  interface MasonryItemWrapperProps {
8
8
  context: {
9
9
  knowledgeBaseId?: string;
10
- openFile?: (id: string) => void;
11
10
  selectFileIds: string[];
12
11
  setSelectedFileIds: (ids: string[]) => void;
13
12
  };
@@ -25,7 +24,6 @@ const MasonryItemWrapper = memo<MasonryItemWrapperProps>(({ data: item, context
25
24
  <div style={{ padding: '8px 4px' }}>
26
25
  <MasonryFileItem
27
26
  knowledgeBaseId={context.knowledgeBaseId}
28
- onOpen={context.openFile}
29
27
  onSelectedChange={(id, checked) => {
30
28
  if (checked) {
31
29
  context.setSelectedFileIds([...context.selectFileIds, id]);
@@ -6,106 +6,134 @@ import { cssVar } from 'antd-style';
6
6
  import { type UIEvent, memo, useCallback, useMemo, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
 
9
- import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
10
- import { type FileListItem } from '@/types/files';
9
+ import { useFolderPath } from '@/app/[variants]/(main)/resource/features/hooks/useFolderPath';
10
+ import {
11
+ useResourceManagerFetchKnowledgeItems,
12
+ useResourceManagerStore,
13
+ } from '@/app/[variants]/(main)/resource/features/store';
14
+ import { sortFileList } from '@/app/[variants]/(main)/resource/features/store/selectors';
11
15
 
12
16
  import { useMasonryColumnCount } from '../useMasonryColumnCount';
13
17
  import MasonryItemWrapper from './MasonryFileItem/MasonryItemWrapper';
14
18
 
15
- interface MasonryViewProps {
16
- data: FileListItem[] | undefined;
17
- hasMore: boolean;
18
- isMasonryReady: boolean;
19
- loadMore: () => Promise<void>;
20
- onOpenFile?: (id: string) => void;
21
- selectFileIds: string[];
22
- setSelectedFileIds: (ids: string[]) => void;
23
- }
19
+ const MasonryView = memo(() => {
20
+ // Access all state from Resource Manager store
21
+ const [
22
+ libraryId,
23
+ category,
24
+ searchQuery,
25
+ selectedFileIds,
26
+ setSelectedFileIds,
27
+ loadMoreKnowledgeItems,
28
+ fileListHasMore,
29
+ isMasonryReady,
30
+ sorter,
31
+ sortType,
32
+ ] = useResourceManagerStore((s) => [
33
+ s.libraryId,
34
+ s.category,
35
+ s.searchQuery,
36
+ s.selectedFileIds,
37
+ s.setSelectedFileIds,
38
+ s.loadMoreKnowledgeItems,
39
+ s.fileListHasMore,
40
+ s.isMasonryReady,
41
+ s.sorter,
42
+ s.sortType,
43
+ ]);
24
44
 
25
- const MasonryView = memo<MasonryViewProps>(
26
- ({ data, hasMore, isMasonryReady, loadMore, onOpenFile, selectFileIds, setSelectedFileIds }) => {
27
- const { t } = useTranslation('file');
28
- const columnCount = useMasonryColumnCount();
29
- const [isLoadingMore, setIsLoadingMore] = useState(false);
45
+ const { t } = useTranslation('file');
46
+ const columnCount = useMasonryColumnCount();
47
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
30
48
 
31
- const libraryId = useResourceManagerStore((s) => s.libraryId);
49
+ const { currentFolderSlug } = useFolderPath();
32
50
 
33
- const masonryContext = useMemo(
34
- () => ({
35
- knowledgeBaseId: libraryId,
36
- openFile: onOpenFile,
37
- selectFileIds,
38
- setSelectedFileIds,
39
- }),
40
- [onOpenFile, libraryId, selectFileIds, setSelectedFileIds],
41
- );
51
+ // Fetch data with SWR
52
+ const { data: rawData } = useResourceManagerFetchKnowledgeItems({
53
+ category,
54
+ knowledgeBaseId: libraryId,
55
+ parentId: currentFolderSlug || null,
56
+ q: searchQuery ?? undefined,
57
+ showFilesInKnowledgeBase: false,
58
+ });
42
59
 
43
- // Handle automatic load more when scrolling to bottom
44
- const handleLoadMore = useCallback(async () => {
45
- if (!hasMore || isLoadingMore) return;
60
+ // Sort data using current sort settings
61
+ const data = sortFileList(rawData, sorter, sortType);
46
62
 
47
- setIsLoadingMore(true);
48
- try {
49
- await loadMore();
50
- } finally {
51
- setIsLoadingMore(false);
52
- }
53
- }, [hasMore, loadMore, isLoadingMore]);
63
+ const masonryContext = useMemo(
64
+ () => ({
65
+ knowledgeBaseId: libraryId,
66
+ selectFileIds: selectedFileIds,
67
+ setSelectedFileIds,
68
+ }),
69
+ [libraryId, selectedFileIds, setSelectedFileIds],
70
+ );
71
+
72
+ // Handle automatic load more when scrolling to bottom
73
+ const handleLoadMore = useCallback(async () => {
74
+ if (!fileListHasMore || isLoadingMore) return;
54
75
 
55
- // Handle scroll event to detect when near bottom
56
- const handleScroll = useCallback(
57
- (e: UIEvent<HTMLDivElement>) => {
58
- const target = e.currentTarget;
59
- const scrollTop = target.scrollTop;
60
- const scrollHeight = target.scrollHeight;
61
- const clientHeight = target.clientHeight;
76
+ setIsLoadingMore(true);
77
+ try {
78
+ await loadMoreKnowledgeItems();
79
+ } finally {
80
+ setIsLoadingMore(false);
81
+ }
82
+ }, [fileListHasMore, loadMoreKnowledgeItems, isLoadingMore]);
62
83
 
63
- // Trigger load when within 300px of bottom
64
- if (scrollHeight - scrollTop - clientHeight < 300) {
65
- handleLoadMore();
66
- }
67
- },
68
- [handleLoadMore],
69
- );
84
+ // Handle scroll event to detect when near bottom
85
+ const handleScroll = useCallback(
86
+ (e: UIEvent<HTMLDivElement>) => {
87
+ const target = e.currentTarget;
88
+ const scrollTop = target.scrollTop;
89
+ const scrollHeight = target.scrollHeight;
90
+ const clientHeight = target.clientHeight;
91
+
92
+ // Trigger load when within 300px of bottom
93
+ if (scrollHeight - scrollTop - clientHeight < 300) {
94
+ handleLoadMore();
95
+ }
96
+ },
97
+ [handleLoadMore],
98
+ );
70
99
 
71
- return (
72
- <div
73
- onScroll={handleScroll}
74
- style={{
75
- flex: 1,
76
- height: '100%',
77
- opacity: isMasonryReady ? 1 : 0,
78
- overflowY: 'auto',
79
- transition: 'opacity 0.2s ease-in-out',
80
- }}
81
- >
82
- <div style={{ paddingBlockEnd: 24, paddingBlockStart: 12, paddingInline: 24 }}>
83
- <VirtuosoMasonry
84
- ItemContent={MasonryItemWrapper}
85
- columnCount={columnCount}
86
- context={masonryContext}
87
- data={data || []}
100
+ return (
101
+ <div
102
+ onScroll={handleScroll}
103
+ style={{
104
+ flex: 1,
105
+ height: '100%',
106
+ opacity: isMasonryReady ? 1 : 0,
107
+ overflowY: 'auto',
108
+ transition: 'opacity 0.2s ease-in-out',
109
+ }}
110
+ >
111
+ <div style={{ paddingBlockEnd: 24, paddingBlockStart: 12, paddingInline: 24 }}>
112
+ <VirtuosoMasonry
113
+ ItemContent={MasonryItemWrapper}
114
+ columnCount={columnCount}
115
+ context={masonryContext}
116
+ data={data || []}
117
+ style={{
118
+ gap: '16px',
119
+ overflow: 'hidden',
120
+ }}
121
+ />
122
+ {isLoadingMore && (
123
+ <Center
88
124
  style={{
89
- gap: '16px',
90
- overflow: 'hidden',
125
+ color: cssVar.colorTextDescription,
126
+ fontSize: 14,
127
+ marginBlockStart: 16,
128
+ minHeight: 40,
91
129
  }}
92
- />
93
- {isLoadingMore && (
94
- <Center
95
- style={{
96
- color: cssVar.colorTextDescription,
97
- fontSize: 14,
98
- marginBlockStart: 16,
99
- minHeight: 40,
100
- }}
101
- >
102
- {t('loading', { defaultValue: 'Loading...' })}
103
- </Center>
104
- )}
105
- </div>
130
+ >
131
+ {t('loading', { defaultValue: 'Loading...' })}
132
+ </Center>
133
+ )}
106
134
  </div>
107
- );
108
- },
109
- );
135
+ </div>
136
+ );
137
+ });
110
138
 
111
139
  export default MasonryView;
@@ -11,6 +11,8 @@ import { memo, useMemo } from 'react';
11
11
  import { useTranslation } from 'react-i18next';
12
12
 
13
13
  import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
14
+ import RepoIcon from '@/components/LibIcon';
15
+ import { useKnowledgeBaseStore } from '@/store/knowledgeBase';
14
16
 
15
17
  import ActionIconWithChevron from './ActionIconWithChevron';
16
18
 
@@ -30,10 +32,18 @@ interface BatchActionsDropdownProps {
30
32
 
31
33
  const BatchActionsDropdown = memo<BatchActionsDropdownProps>(
32
34
  ({ selectCount, onActionClick, disabled }) => {
33
- const { t } = useTranslation(['components', 'common', 'file']);
35
+ const { t } = useTranslation(['components', 'common', 'file', 'knowledgeBase']);
34
36
  const { modal, message } = App.useApp();
35
37
 
36
- const libraryId = useResourceManagerStore((s) => s.libraryId);
38
+ const [libraryId, selectedFileIds] = useResourceManagerStore((s) => [
39
+ s.libraryId,
40
+ s.selectedFileIds,
41
+ ]);
42
+ const [useFetchKnowledgeBaseList, addFilesToKnowledgeBase] = useKnowledgeBaseStore((s) => [
43
+ s.useFetchKnowledgeBaseList,
44
+ s.addFilesToKnowledgeBase,
45
+ ]);
46
+ const { data: knowledgeBases } = useFetchKnowledgeBaseList();
37
47
 
38
48
  const menuItems = useMemo<DropdownItem[]>(() => {
39
49
  const items: DropdownItem[] = [];
@@ -60,44 +70,64 @@ const BatchActionsDropdown = memo<BatchActionsDropdownProps>(
60
70
  return items;
61
71
  }
62
72
 
73
+ // Filter out current knowledge base and create submenu items
74
+ const availableKnowledgeBases = (knowledgeBases || []).filter((kb) => kb.id !== libraryId);
75
+
76
+ const addToKnowledgeBaseSubmenu: DropdownItem[] = availableKnowledgeBases.map((kb) => ({
77
+ icon: <RepoIcon />,
78
+ key: `add-to-kb-${kb.id}`,
79
+ label: <span style={{ marginLeft: 8 }}>{kb.name}</span>,
80
+ onClick: async () => {
81
+ try {
82
+ await addFilesToKnowledgeBase(kb.id, selectedFileIds);
83
+ message.success(
84
+ t('addToKnowledgeBase.addSuccess', {
85
+ count: selectCount,
86
+ ns: 'knowledgeBase',
87
+ }),
88
+ );
89
+ } catch (e) {
90
+ console.error(e);
91
+ message.error(t('addToKnowledgeBase.error', { ns: 'knowledgeBase' }));
92
+ }
93
+ },
94
+ }));
95
+
63
96
  if (libraryId) {
64
- items.push(
65
- {
66
- icon: <Icon icon={BookMinusIcon} />,
67
- key: 'removeFromKnowledgeBase',
68
- label: t('FileManager.actions.removeFromKnowledgeBase'),
69
- onClick: () => {
70
- modal.confirm({
71
- okButtonProps: {
72
- danger: true,
73
- },
74
- onOk: async () => {
75
- await onActionClick('removeFromKnowledgeBase');
76
- message.success(t('FileManager.actions.removeFromKnowledgeBaseSuccess'));
77
- },
78
- title: t('FileManager.actions.confirmRemoveFromKnowledgeBase', {
79
- count: selectCount,
80
- }),
81
- });
82
- },
97
+ items.push({
98
+ icon: <Icon icon={BookMinusIcon} />,
99
+ key: 'removeFromKnowledgeBase',
100
+ label: t('FileManager.actions.removeFromKnowledgeBase'),
101
+ onClick: () => {
102
+ modal.confirm({
103
+ okButtonProps: {
104
+ danger: true,
105
+ },
106
+ onOk: async () => {
107
+ await onActionClick('removeFromKnowledgeBase');
108
+ message.success(t('FileManager.actions.removeFromKnowledgeBaseSuccess'));
109
+ },
110
+ title: t('FileManager.actions.confirmRemoveFromKnowledgeBase', {
111
+ count: selectCount,
112
+ }),
113
+ });
83
114
  },
84
- {
115
+ });
116
+
117
+ if (availableKnowledgeBases.length > 0) {
118
+ items.push({
119
+ children: addToKnowledgeBaseSubmenu as any,
85
120
  icon: <Icon icon={BookPlusIcon} />,
86
121
  key: 'addToOtherKnowledgeBase',
87
122
  label: t('FileManager.actions.addToOtherKnowledgeBase'),
88
- onClick: () => {
89
- onActionClick('addToOtherKnowledgeBase');
90
- },
91
- },
92
- );
93
- } else {
123
+ });
124
+ }
125
+ } else if (availableKnowledgeBases.length > 0) {
94
126
  items.push({
127
+ children: addToKnowledgeBaseSubmenu as any,
95
128
  icon: <Icon icon={BookPlusIcon} />,
96
129
  key: 'addToKnowledgeBase',
97
130
  label: t('FileManager.actions.addToKnowledgeBase'),
98
- onClick: () => {
99
- onActionClick('addToKnowledgeBase');
100
- },
101
131
  });
102
132
  }
103
133
 
@@ -134,7 +164,17 @@ const BatchActionsDropdown = memo<BatchActionsDropdownProps>(
134
164
  );
135
165
 
136
166
  return items;
137
- }, [libraryId, selectCount, onActionClick, t, modal, message]);
167
+ }, [
168
+ libraryId,
169
+ selectCount,
170
+ selectedFileIds,
171
+ onActionClick,
172
+ addFilesToKnowledgeBase,
173
+ t,
174
+ modal,
175
+ message,
176
+ knowledgeBases,
177
+ ]);
138
178
 
139
179
  return (
140
180
  <DropdownMenu items={menuItems} placement="bottomLeft" triggerProps={{ disabled }}>
@@ -41,10 +41,7 @@ const ResourceExplorer = memo(() => {
41
41
  isTransitioning,
42
42
  isMasonryReady,
43
43
  searchQuery,
44
- selectedFileIds,
45
44
  setSelectedFileIds,
46
- loadMoreKnowledgeItems,
47
- fileListHasMore,
48
45
  sorter,
49
46
  sortType,
50
47
  ] = useResourceManagerStore((s) => [
@@ -54,10 +51,7 @@ const ResourceExplorer = memo(() => {
54
51
  s.isTransitioning,
55
52
  s.isMasonryReady,
56
53
  s.searchQuery,
57
- s.selectedFileIds,
58
54
  s.setSelectedFileIds,
59
- s.loadMoreKnowledgeItems,
60
- s.fileListHasMore,
61
55
  s.sorter,
62
56
  s.sortType,
63
57
  ]);
@@ -114,14 +108,7 @@ const ResourceExplorer = memo(() => {
114
108
  ) : viewMode === 'list' ? (
115
109
  <ListView />
116
110
  ) : (
117
- <MasonryView
118
- data={data}
119
- hasMore={fileListHasMore}
120
- isMasonryReady={isMasonryReady}
121
- loadMore={loadMoreKnowledgeItems}
122
- selectFileIds={selectedFileIds}
123
- setSelectedFileIds={setSelectedFileIds}
124
- />
111
+ <MasonryView />
125
112
  )}
126
113
  </Flexbox>
127
114
  );
@@ -64,10 +64,7 @@ const enabledSSOProviders = parseSSOProviders(authEnv.AUTH_SSO_PROVIDERS);
64
64
  const { socialProviders, genericOAuthProviders } = initBetterAuthSSOProviders();
65
65
 
66
66
  async function customEmailValidator(email: string): Promise<boolean> {
67
- if (ENABLE_BUSINESS_FEATURES && !(await businessEmailValidator(email))) {
68
- return false;
69
- }
70
- return validateEmail(email);
67
+ return ENABLE_BUSINESS_FEATURES ? businessEmailValidator(email) : validateEmail(email);
71
68
  }
72
69
 
73
70
  interface CustomBetterAuthOptions {
@@ -25,7 +25,10 @@ export class UpstashRedisProvider implements BaseRedisProvider {
25
25
  constructor(options: UpstashConfig | RedisConfigNodejs) {
26
26
  const { prefix, ...clientOptions } = options as UpstashConfig & RedisConfigNodejs;
27
27
  this.prefix = prefix ? `${prefix}:` : '';
28
- this.client = new Redis(clientOptions as RedisConfigNodejs);
28
+ this.client = new Redis({
29
+ ...clientOptions,
30
+ automaticDeserialization: false,
31
+ } as RedisConfigNodejs);
29
32
  }
30
33
 
31
34
  /**