@lobehub/lobehub 2.0.0-next.210 → 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 +50 -0
- package/changelog/v1.json +14 -0
- package/package.json +1 -1
- package/src/app/(backend)/f/[id]/route.ts +2 -2
- package/src/app/(backend)/market/social/[[...segments]]/route.ts +3 -3
- package/src/app/[variants]/(main)/community/(detail)/assistant/features/Header.tsx +47 -34
- package/src/app/[variants]/(main)/resource/library/_layout/Header/LibraryHead.tsx +28 -18
- package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +68 -3
- package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/MasonryItemWrapper.tsx +0 -2
- package/src/features/ResourceManager/components/Explorer/MasonryView/index.tsx +114 -86
- package/src/features/ResourceManager/components/Explorer/ToolBar/BatchActionsDropdown.tsx +72 -32
- package/src/features/ResourceManager/components/Explorer/index.tsx +1 -14
- package/src/libs/better-auth/define-config.ts +1 -4
- package/src/libs/redis/upstash.ts +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
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
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
## [Version 2.0.0-next.211](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.210...v2.0.0-next.211)
|
|
31
|
+
|
|
32
|
+
<sup>Released on **2026-01-05**</sup>
|
|
33
|
+
|
|
34
|
+
#### 🐛 Bug Fixes
|
|
35
|
+
|
|
36
|
+
- **misc**: Add lost like button in discover detail page.
|
|
37
|
+
|
|
38
|
+
<br/>
|
|
39
|
+
|
|
40
|
+
<details>
|
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
42
|
+
|
|
43
|
+
#### What's fixed
|
|
44
|
+
|
|
45
|
+
- **misc**: Add lost like button in discover detail page, closes [#11182](https://github.com/lobehub/lobe-chat/issues/11182) ([41215d4](https://github.com/lobehub/lobe-chat/commit/41215d4))
|
|
46
|
+
|
|
47
|
+
</details>
|
|
48
|
+
|
|
49
|
+
<div align="right">
|
|
50
|
+
|
|
51
|
+
[](#readme-top)
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
5
55
|
## [Version 2.0.0-next.210](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.209...v2.0.0-next.210)
|
|
6
56
|
|
|
7
57
|
<sup>Released on **2026-01-04**</sup>
|
package/changelog/v1.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"children": {},
|
|
4
|
+
"date": "2026-01-05",
|
|
5
|
+
"version": "2.0.0-next.212"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"children": {
|
|
9
|
+
"fixes": [
|
|
10
|
+
"Add lost like button in discover detail page."
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"date": "2026-01-05",
|
|
14
|
+
"version": "2.0.0-next.211"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"children": {},
|
|
4
18
|
"date": "2026-01-04",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
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
|
-
|
|
47
|
-
const cached = (
|
|
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);
|
|
@@ -158,7 +158,7 @@ export const GET = async (req: NextRequest, context: RouteContext) => {
|
|
|
158
158
|
// Follow queries
|
|
159
159
|
case 'follow-status': {
|
|
160
160
|
const targetUserId = Number(segments[1]);
|
|
161
|
-
if (!accessToken) {
|
|
161
|
+
if (!accessToken && !trustedClientToken) {
|
|
162
162
|
return NextResponse.json({ isFollowing: false, isMutual: false });
|
|
163
163
|
}
|
|
164
164
|
const result = await market.follows.checkFollowStatus(targetUserId);
|
|
@@ -193,7 +193,7 @@ export const GET = async (req: NextRequest, context: RouteContext) => {
|
|
|
193
193
|
case 'favorite-status': {
|
|
194
194
|
const targetType = segments[1] as 'agent' | 'plugin';
|
|
195
195
|
const targetIdOrIdentifier = segments[2];
|
|
196
|
-
if (!accessToken) {
|
|
196
|
+
if (!accessToken && !trustedClientToken) {
|
|
197
197
|
return NextResponse.json({ isFavorited: false });
|
|
198
198
|
}
|
|
199
199
|
// SDK accepts both number (targetId) and string (identifier)
|
|
@@ -236,7 +236,7 @@ export const GET = async (req: NextRequest, context: RouteContext) => {
|
|
|
236
236
|
case 'like-status': {
|
|
237
237
|
const targetType = segments[1] as 'agent' | 'plugin';
|
|
238
238
|
const targetIdOrIdentifier = segments[2];
|
|
239
|
-
if (!accessToken) {
|
|
239
|
+
if (!accessToken && !trustedClientToken) {
|
|
240
240
|
return NextResponse.json({ isLiked: false });
|
|
241
241
|
}
|
|
242
242
|
const isNumeric = /^\d+$/.test(targetIdOrIdentifier);
|
|
@@ -13,7 +13,14 @@ import {
|
|
|
13
13
|
} from '@lobehub/ui';
|
|
14
14
|
import { App } from 'antd';
|
|
15
15
|
import { createStaticStyles, cssVar, useResponsive } from 'antd-style';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
BookTextIcon,
|
|
18
|
+
BookmarkCheckIcon,
|
|
19
|
+
BookmarkIcon,
|
|
20
|
+
CoinsIcon,
|
|
21
|
+
DotIcon,
|
|
22
|
+
HeartIcon,
|
|
23
|
+
} from 'lucide-react';
|
|
17
24
|
import qs from 'query-string';
|
|
18
25
|
import { memo, useState } from 'react';
|
|
19
26
|
import { useTranslation } from 'react-i18next';
|
|
@@ -54,8 +61,7 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
|
|
|
54
61
|
const { mobile = isMobile } = useResponsive();
|
|
55
62
|
const { isAuthenticated, signIn, session } = useMarketAuth();
|
|
56
63
|
const [favoriteLoading, setFavoriteLoading] = useState(false);
|
|
57
|
-
|
|
58
|
-
// const [likeLoading, setLikeLoading] = useState(false);
|
|
64
|
+
const [likeLoading, setLikeLoading] = useState(false);
|
|
59
65
|
|
|
60
66
|
// Set access token for social service
|
|
61
67
|
if (session?.accessToken) {
|
|
@@ -71,13 +77,13 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
|
|
|
71
77
|
|
|
72
78
|
const isFavorited = favoriteStatus?.isFavorited ?? false;
|
|
73
79
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
// Fetch like status
|
|
81
|
+
const { data: likeStatus, mutate: mutateLike } = useSWR(
|
|
82
|
+
identifier && isAuthenticated ? ['like-status', 'agent', identifier] : null,
|
|
83
|
+
() => socialService.checkLikeStatus('agent', identifier!),
|
|
84
|
+
{ revalidateOnFocus: false },
|
|
85
|
+
);
|
|
86
|
+
const isLiked = likeStatus?.isLiked ?? false;
|
|
81
87
|
|
|
82
88
|
const handleFavoriteClick = async () => {
|
|
83
89
|
if (!isAuthenticated) {
|
|
@@ -105,30 +111,29 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
|
|
|
105
111
|
}
|
|
106
112
|
};
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// };
|
|
114
|
+
const handleLikeClick = async () => {
|
|
115
|
+
if (!isAuthenticated) {
|
|
116
|
+
await signIn();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!identifier) return;
|
|
120
|
+
setLikeLoading(true);
|
|
121
|
+
try {
|
|
122
|
+
if (isLiked) {
|
|
123
|
+
await socialService.unlike('agent', identifier);
|
|
124
|
+
message.success(t('assistant.unlikeSuccess'));
|
|
125
|
+
} else {
|
|
126
|
+
await socialService.like('agent', identifier);
|
|
127
|
+
message.success(t('assistant.likeSuccess'));
|
|
128
|
+
}
|
|
129
|
+
await mutateLike();
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('Like action failed:', error);
|
|
132
|
+
message.error(t('assistant.likeFailed'));
|
|
133
|
+
} finally {
|
|
134
|
+
setLikeLoading(false);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
132
137
|
|
|
133
138
|
const categories = useCategory();
|
|
134
139
|
const cate = categories.find((c) => c.key === category);
|
|
@@ -186,6 +191,14 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
|
|
|
186
191
|
{title}
|
|
187
192
|
</Text>
|
|
188
193
|
</Flexbox>
|
|
194
|
+
<Tooltip title={isLiked ? t('assistant.unlike') : t('assistant.like')}>
|
|
195
|
+
<ActionIcon
|
|
196
|
+
icon={HeartIcon}
|
|
197
|
+
loading={likeLoading}
|
|
198
|
+
onClick={handleLikeClick}
|
|
199
|
+
style={isLiked ? { color: '#ff4d4f' } : undefined}
|
|
200
|
+
/>
|
|
201
|
+
</Tooltip>
|
|
189
202
|
<Tooltip title={isFavorited ? t('assistant.unfavorite') : t('assistant.favorite')}>
|
|
190
203
|
<ActionIcon
|
|
191
204
|
icon={isFavorited ? BookmarkCheckIcon : BookmarkIcon}
|
|
@@ -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,
|
|
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={
|
|
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 {
|
|
10
|
-
import {
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
49
|
+
const { currentFolderSlug } = useFolderPath();
|
|
32
50
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
if (!hasMore || isLoadingMore) return;
|
|
60
|
+
// Sort data using current sort settings
|
|
61
|
+
const data = sortFileList(rawData, sorter, sortType);
|
|
46
62
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
setIsLoadingMore(true);
|
|
77
|
+
try {
|
|
78
|
+
await loadMoreKnowledgeItems();
|
|
79
|
+
} finally {
|
|
80
|
+
setIsLoadingMore(false);
|
|
81
|
+
}
|
|
82
|
+
}, [fileListHasMore, loadMoreKnowledgeItems, isLoadingMore]);
|
|
62
83
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
125
|
+
color: cssVar.colorTextDescription,
|
|
126
|
+
fontSize: 14,
|
|
127
|
+
marginBlockStart: 16,
|
|
128
|
+
minHeight: 40,
|
|
91
129
|
}}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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) =>
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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(
|
|
28
|
+
this.client = new Redis({
|
|
29
|
+
...clientOptions,
|
|
30
|
+
automaticDeserialization: false,
|
|
31
|
+
} as RedisConfigNodejs);
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
/**
|