@lobehub/chat 1.142.2 → 1.142.3
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 +26 -0
- package/changelog/v1.json +9 -0
- package/locales/ar/chat.json +4 -4
- package/locales/ar/file.json +1 -0
- package/locales/ar/models.json +1 -1
- package/locales/bg-BG/chat.json +4 -4
- package/locales/bg-BG/file.json +1 -0
- package/locales/bg-BG/models.json +1 -1
- package/locales/de-DE/chat.json +4 -4
- package/locales/de-DE/file.json +1 -0
- package/locales/de-DE/models.json +1 -1
- package/locales/en-US/chat.json +4 -4
- package/locales/en-US/file.json +1 -0
- package/locales/en-US/models.json +1 -1
- package/locales/es-ES/chat.json +4 -4
- package/locales/es-ES/file.json +1 -0
- package/locales/es-ES/models.json +1 -1
- package/locales/fa-IR/chat.json +4 -4
- package/locales/fa-IR/file.json +1 -0
- package/locales/fa-IR/models.json +1 -1
- package/locales/fr-FR/chat.json +4 -4
- package/locales/fr-FR/file.json +1 -0
- package/locales/fr-FR/models.json +1 -1
- package/locales/it-IT/chat.json +4 -4
- package/locales/it-IT/file.json +1 -0
- package/locales/ja-JP/chat.json +4 -4
- package/locales/ja-JP/file.json +1 -0
- package/locales/ja-JP/models.json +1 -1
- package/locales/ko-KR/chat.json +4 -4
- package/locales/ko-KR/file.json +1 -0
- package/locales/ko-KR/models.json +1 -1
- package/locales/nl-NL/chat.json +4 -4
- package/locales/nl-NL/file.json +1 -0
- package/locales/nl-NL/models.json +1 -1
- package/locales/pl-PL/chat.json +4 -4
- package/locales/pl-PL/file.json +1 -0
- package/locales/pl-PL/models.json +1 -1
- package/locales/pt-BR/chat.json +4 -4
- package/locales/pt-BR/file.json +1 -0
- package/locales/ru-RU/chat.json +4 -4
- package/locales/ru-RU/file.json +1 -0
- package/locales/ru-RU/models.json +1 -1
- package/locales/tr-TR/chat.json +4 -4
- package/locales/tr-TR/file.json +1 -0
- package/locales/tr-TR/models.json +1 -1
- package/locales/vi-VN/chat.json +4 -4
- package/locales/vi-VN/file.json +1 -0
- package/locales/vi-VN/models.json +1 -1
- package/locales/zh-CN/chat.json +4 -4
- package/locales/zh-CN/file.json +1 -0
- package/locales/zh-TW/chat.json +4 -4
- package/locales/zh-TW/file.json +1 -0
- package/locales/zh-TW/models.json +1 -1
- package/package.json +3 -2
- package/packages/const/src/file.ts +2 -0
- package/packages/obervability-otel/package.json +5 -5
- package/src/app/[variants]/(main)/settings/provider/features/ModelList/CreateNewModelModal/index.tsx +7 -0
- package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelConfigModal/index.tsx +7 -0
- package/src/features/FileManager/FileList/FileListItem/index.tsx +3 -2
- package/src/features/FileManager/FileList/MasonryFileItem/MasonryItemWrapper.tsx +1 -1
- package/src/features/FileManager/FileList/MasonryFileItem/index.tsx +2 -4
- package/src/features/FileManager/FileList/MasonrySkeleton.tsx +11 -5
- package/src/features/FileManager/FileList/ToolBar/MultiSelectActions.tsx +1 -1
- package/src/features/FileManager/FileList/index.tsx +51 -9
- package/src/locales/default/chat.ts +4 -4
- package/src/locales/default/file.ts +1 -0
- package/src/store/file/slices/fileManager/action.test.ts +136 -1
- package/src/store/file/slices/fileManager/action.ts +30 -8
- package/src/utils/unzipFile.test.ts +128 -0
- package/src/utils/unzipFile.ts +122 -0
- package/vitest.config.mts +1 -0
package/locales/vi-VN/chat.json
CHANGED
|
@@ -163,9 +163,9 @@
|
|
|
163
163
|
"addMember": "Thêm thành viên",
|
|
164
164
|
"allMembers": "Tất cả thành viên",
|
|
165
165
|
"createGroup": "Tạo nhóm Agent",
|
|
166
|
-
"noAvailableAgents": "Không có
|
|
167
|
-
"noSelectedAgents": "Chưa chọn
|
|
168
|
-
"searchAgents": "Tìm
|
|
166
|
+
"noAvailableAgents": "Không có Agent nào để mời",
|
|
167
|
+
"noSelectedAgents": "Chưa chọn Agent nào",
|
|
168
|
+
"searchAgents": "Tìm kiếm Agent...",
|
|
169
169
|
"setInitialMembers": "Chọn thành viên nhóm"
|
|
170
170
|
},
|
|
171
171
|
"members": "Thành viên",
|
|
@@ -229,7 +229,7 @@
|
|
|
229
229
|
"jumpToMessage": "Chuyển đến tin nhắn thứ {{index}}",
|
|
230
230
|
"nextMessage": "Tin nhắn tiếp theo",
|
|
231
231
|
"previousMessage": "Tin nhắn trước",
|
|
232
|
-
"senderAssistant": "
|
|
232
|
+
"senderAssistant": "Agent",
|
|
233
233
|
"senderUser": "Bạn"
|
|
234
234
|
},
|
|
235
235
|
"newAgent": "Tạo trợ lý mới",
|
package/locales/vi-VN/file.json
CHANGED
package/locales/zh-CN/chat.json
CHANGED
|
@@ -163,9 +163,9 @@
|
|
|
163
163
|
"addMember": "添加成员",
|
|
164
164
|
"allMembers": "全体成员",
|
|
165
165
|
"createGroup": "创建 Agent 团队",
|
|
166
|
-
"noAvailableAgents": "
|
|
167
|
-
"noSelectedAgents": "
|
|
168
|
-
"searchAgents": "
|
|
166
|
+
"noAvailableAgents": "没有可邀请的 Agent",
|
|
167
|
+
"noSelectedAgents": "还未选择 Agent",
|
|
168
|
+
"searchAgents": "搜索 Agent...",
|
|
169
169
|
"setInitialMembers": "选择团队成员"
|
|
170
170
|
},
|
|
171
171
|
"members": "Members",
|
|
@@ -229,7 +229,7 @@
|
|
|
229
229
|
"jumpToMessage": "跳转至第 {{index}} 条消息",
|
|
230
230
|
"nextMessage": "下一条消息",
|
|
231
231
|
"previousMessage": "上一条消息",
|
|
232
|
-
"senderAssistant": "
|
|
232
|
+
"senderAssistant": "Agent",
|
|
233
233
|
"senderUser": "你"
|
|
234
234
|
},
|
|
235
235
|
"newAgent": "新建助手",
|
package/locales/zh-CN/file.json
CHANGED
package/locales/zh-TW/chat.json
CHANGED
|
@@ -163,9 +163,9 @@
|
|
|
163
163
|
"addMember": "添加成員",
|
|
164
164
|
"allMembers": "所有成員",
|
|
165
165
|
"createGroup": "建立 Agent 團隊",
|
|
166
|
-
"noAvailableAgents": "
|
|
167
|
-
"noSelectedAgents": "
|
|
168
|
-
"searchAgents": "
|
|
166
|
+
"noAvailableAgents": "沒有可邀請的 Agent",
|
|
167
|
+
"noSelectedAgents": "尚未選擇 Agent",
|
|
168
|
+
"searchAgents": "搜尋 Agent...",
|
|
169
169
|
"setInitialMembers": "選擇團隊成員"
|
|
170
170
|
},
|
|
171
171
|
"members": "成員",
|
|
@@ -229,7 +229,7 @@
|
|
|
229
229
|
"jumpToMessage": "跳轉至第 {{index}} 條訊息",
|
|
230
230
|
"nextMessage": "下一條訊息",
|
|
231
231
|
"previousMessage": "上一條訊息",
|
|
232
|
-
"senderAssistant": "
|
|
232
|
+
"senderAssistant": "Agent",
|
|
233
233
|
"senderUser": "您"
|
|
234
234
|
},
|
|
235
235
|
"newAgent": "新建助手",
|
package/locales/zh-TW/file.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "1.142.
|
|
3
|
+
"version": "1.142.3",
|
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot 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",
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
"@neondatabase/serverless": "^1.0.2",
|
|
177
177
|
"@next/third-parties": "^15.5.4",
|
|
178
178
|
"@opentelemetry/exporter-jaeger": "^2.1.0",
|
|
179
|
-
"@opentelemetry/winston-transport": "^0.
|
|
179
|
+
"@opentelemetry/winston-transport": "^0.18.0",
|
|
180
180
|
"@react-pdf/renderer": "^4.3.0",
|
|
181
181
|
"@react-spring/web": "^9.7.5",
|
|
182
182
|
"@saintno/comfyui-sdk": "^0.2.48",
|
|
@@ -208,6 +208,7 @@
|
|
|
208
208
|
"drizzle-zod": "^0.5.1",
|
|
209
209
|
"epub2": "^3.0.2",
|
|
210
210
|
"fast-deep-equal": "^3.1.3",
|
|
211
|
+
"fflate": "^0.8.2",
|
|
211
212
|
"file-type": "^21.0.0",
|
|
212
213
|
"framer-motion": "^12.23.24",
|
|
213
214
|
"gpt-tokenizer": "^3.2.0",
|
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@opentelemetry/api": "^1.9.0",
|
|
10
|
-
"@opentelemetry/auto-instrumentations-node": "^0.
|
|
10
|
+
"@opentelemetry/auto-instrumentations-node": "^0.66.0",
|
|
11
11
|
"@opentelemetry/exporter-metrics-otlp-http": "^0.206.0",
|
|
12
|
-
"@opentelemetry/exporter-trace-otlp-http": "^0.
|
|
12
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
|
|
13
13
|
"@opentelemetry/instrumentation": "^0.206.0",
|
|
14
|
-
"@opentelemetry/instrumentation-http": "^0.
|
|
15
|
-
"@opentelemetry/instrumentation-pg": "^0.
|
|
14
|
+
"@opentelemetry/instrumentation-http": "^0.207.0",
|
|
15
|
+
"@opentelemetry/instrumentation-pg": "^0.60.0",
|
|
16
16
|
"@opentelemetry/resources": "^2.0.1",
|
|
17
17
|
"@opentelemetry/sdk-metrics": "^2.0.1",
|
|
18
|
-
"@opentelemetry/sdk-node": "^0.
|
|
18
|
+
"@opentelemetry/sdk-node": "^0.207.0",
|
|
19
19
|
"@opentelemetry/sdk-trace-node": "^2.0.1",
|
|
20
20
|
"@opentelemetry/semantic-conventions": "^1.36.0",
|
|
21
21
|
"@vercel/otel": "^1.13.0",
|
package/src/app/[variants]/(main)/settings/provider/features/ModelList/CreateNewModelModal/index.tsx
CHANGED
|
@@ -64,6 +64,13 @@ const ModelConfigModal = memo<ModelConfigModalProps>(({ open, setOpen }) => {
|
|
|
64
64
|
maskClosable
|
|
65
65
|
onCancel={closeModal}
|
|
66
66
|
open={open}
|
|
67
|
+
styles={{
|
|
68
|
+
content: {
|
|
69
|
+
display: 'flex',
|
|
70
|
+
flexDirection: 'column',
|
|
71
|
+
maxHeight: 'calc(100vh - 150px)',
|
|
72
|
+
},
|
|
73
|
+
}}
|
|
67
74
|
title={t('providerModels.createNew.title')}
|
|
68
75
|
zIndex={1251} // Select is 1150
|
|
69
76
|
>
|
package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelConfigModal/index.tsx
CHANGED
|
@@ -59,6 +59,13 @@ const ModelConfigModal = memo<ModelConfigModalProps>(({ id, open, setOpen }) =>
|
|
|
59
59
|
maskClosable
|
|
60
60
|
onCancel={closeModal}
|
|
61
61
|
open={open}
|
|
62
|
+
styles={{
|
|
63
|
+
content: {
|
|
64
|
+
display: 'flex',
|
|
65
|
+
flexDirection: 'column',
|
|
66
|
+
maxHeight: 'calc(100vh - 150px)',
|
|
67
|
+
},
|
|
68
|
+
}}
|
|
62
69
|
title={t('llm.customModelCards.modelConfig.modalTitle', { ns: 'setting' })}
|
|
63
70
|
zIndex={1251} // Select is 1150
|
|
64
71
|
>
|
|
@@ -79,7 +79,7 @@ const useStyles = createStyles(({ css, token, cx, isDarkMode }) => {
|
|
|
79
79
|
interface FileRenderItemProps extends FileListItem {
|
|
80
80
|
index: number;
|
|
81
81
|
knowledgeBaseId?: string;
|
|
82
|
-
onSelectedChange: (id: string, selected: boolean) => void;
|
|
82
|
+
onSelectedChange: (id: string, selected: boolean, shiftKey: boolean, index: number) => void;
|
|
83
83
|
selected?: boolean;
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -100,6 +100,7 @@ const FileRenderItem = memo<FileRenderItemProps>(
|
|
|
100
100
|
chunkingStatus,
|
|
101
101
|
onSelectedChange,
|
|
102
102
|
knowledgeBaseId,
|
|
103
|
+
index,
|
|
103
104
|
}) => {
|
|
104
105
|
const { t } = useTranslation('components');
|
|
105
106
|
const { styles, cx } = useStyles();
|
|
@@ -140,7 +141,7 @@ const FileRenderItem = memo<FileRenderItemProps>(
|
|
|
140
141
|
onClick={(e) => {
|
|
141
142
|
e.stopPropagation();
|
|
142
143
|
|
|
143
|
-
onSelectedChange(id, !selected);
|
|
144
|
+
onSelectedChange(id, !selected, e.shiftKey, index);
|
|
144
145
|
}}
|
|
145
146
|
style={{ paddingInline: 4 }}
|
|
146
147
|
>
|
|
@@ -21,7 +21,7 @@ const MasonryItemWrapper = memo<MasonryItemWrapperProps>(({ data: item, context
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
return (
|
|
24
|
-
<div style={{ padding: '8px' }}>
|
|
24
|
+
<div style={{ padding: '8px 4px' }}>
|
|
25
25
|
<MasonryFileItem
|
|
26
26
|
knowledgeBaseId={context.knowledgeBaseId}
|
|
27
27
|
onSelectedChange={(id, checked) => {
|
|
@@ -111,8 +111,6 @@ const useStyles = createStyles(({ css, token }) => ({
|
|
|
111
111
|
inset-block-end: 8px;
|
|
112
112
|
inset-inline-end: 8px;
|
|
113
113
|
|
|
114
|
-
padding-block: 4px;
|
|
115
|
-
padding-inline: 8px;
|
|
116
114
|
border-radius: ${token.borderRadius}px;
|
|
117
115
|
|
|
118
116
|
opacity: 0;
|
|
@@ -320,8 +318,8 @@ const MasonryFileItem = memo<MasonryFileItemProps>(
|
|
|
320
318
|
});
|
|
321
319
|
},
|
|
322
320
|
{
|
|
323
|
-
rootMargin: '
|
|
324
|
-
threshold: 0.
|
|
321
|
+
rootMargin: '200px', // Increased margin to load content earlier
|
|
322
|
+
threshold: 0.01, // Lower threshold for earlier triggering
|
|
325
323
|
},
|
|
326
324
|
);
|
|
327
325
|
|
|
@@ -26,6 +26,9 @@ const MasonrySkeleton = memo<MasonrySkeletonProps>(({ columnCount }) => {
|
|
|
26
26
|
// Generate varying heights for more natural masonry look
|
|
27
27
|
const heights = [180, 220, 200, 190, 240, 210, 200, 230, 180, 220, 210, 190];
|
|
28
28
|
|
|
29
|
+
// Calculate number of items based on viewport and column count
|
|
30
|
+
const itemCount = Math.min(columnCount * 3, 12);
|
|
31
|
+
|
|
29
32
|
return (
|
|
30
33
|
<div
|
|
31
34
|
className={styles.grid}
|
|
@@ -33,18 +36,21 @@ const MasonrySkeleton = memo<MasonrySkeletonProps>(({ columnCount }) => {
|
|
|
33
36
|
gridTemplateColumns: `repeat(${columnCount}, 1fr)`,
|
|
34
37
|
}}
|
|
35
38
|
>
|
|
36
|
-
{Array.from({ length:
|
|
39
|
+
{Array.from({ length: itemCount }).map((_, index) => (
|
|
37
40
|
<div className={styles.card} key={index}>
|
|
38
41
|
<Skeleton
|
|
39
42
|
active
|
|
43
|
+
avatar={false}
|
|
40
44
|
paragraph={{
|
|
41
|
-
rows:
|
|
42
|
-
width: ['100%', '
|
|
45
|
+
rows: 4,
|
|
46
|
+
width: ['100%', '90%', '70%', '50%'],
|
|
43
47
|
}}
|
|
44
48
|
style={{
|
|
45
|
-
height: heights[index],
|
|
49
|
+
height: heights[index % heights.length],
|
|
50
|
+
}}
|
|
51
|
+
title={{
|
|
52
|
+
width: '100%',
|
|
46
53
|
}}
|
|
47
|
-
title={false}
|
|
48
54
|
/>
|
|
49
55
|
</div>
|
|
50
56
|
))}
|
|
@@ -51,10 +51,15 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category }) => {
|
|
|
51
51
|
|
|
52
52
|
const [selectFileIds, setSelectedFileIds] = useState<string[]>([]);
|
|
53
53
|
const [viewConfig, setViewConfig] = useState({ showFilesInKnowledgeBase: false });
|
|
54
|
+
const [lastSelectedIndex, setLastSelectedIndex] = useState<number | null>(null);
|
|
55
|
+
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
54
56
|
|
|
55
57
|
const viewMode = useGlobalStore((s) => s.status.fileManagerViewMode || 'list') as ViewMode;
|
|
56
58
|
const updateSystemStatus = useGlobalStore((s) => s.updateSystemStatus);
|
|
57
|
-
const setViewMode = (mode: ViewMode) =>
|
|
59
|
+
const setViewMode = (mode: ViewMode) => {
|
|
60
|
+
setIsTransitioning(true);
|
|
61
|
+
updateSystemStatus({ fileManagerViewMode: mode });
|
|
62
|
+
};
|
|
58
63
|
|
|
59
64
|
const [columnCount, setColumnCount] = useState(4);
|
|
60
65
|
|
|
@@ -105,6 +110,19 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category }) => {
|
|
|
105
110
|
...viewConfig,
|
|
106
111
|
});
|
|
107
112
|
|
|
113
|
+
// Handle view transition with a brief delay to show skeleton
|
|
114
|
+
React.useEffect(() => {
|
|
115
|
+
if (isTransitioning && data) {
|
|
116
|
+
// Use requestAnimationFrame to ensure smooth transition
|
|
117
|
+
requestAnimationFrame(() => {
|
|
118
|
+
const timer = setTimeout(() => {
|
|
119
|
+
setIsTransitioning(false);
|
|
120
|
+
}, 100);
|
|
121
|
+
return () => clearTimeout(timer);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}, [isTransitioning, viewMode, data]);
|
|
125
|
+
|
|
108
126
|
useCheckTaskStatus(data);
|
|
109
127
|
|
|
110
128
|
// Clean up selected files that no longer exist in the data
|
|
@@ -118,6 +136,13 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category }) => {
|
|
|
118
136
|
}
|
|
119
137
|
}, [data]);
|
|
120
138
|
|
|
139
|
+
// Reset lastSelectedIndex when selection is cleared
|
|
140
|
+
React.useEffect(() => {
|
|
141
|
+
if (selectFileIds.length === 0) {
|
|
142
|
+
setLastSelectedIndex(null);
|
|
143
|
+
}
|
|
144
|
+
}, [selectFileIds.length]);
|
|
145
|
+
|
|
121
146
|
// Memoize context object to avoid recreating on every render
|
|
122
147
|
const masonryContext = useMemo(
|
|
123
148
|
() => ({
|
|
@@ -161,7 +186,7 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category }) => {
|
|
|
161
186
|
</Flexbox>
|
|
162
187
|
)}
|
|
163
188
|
</Flexbox>
|
|
164
|
-
{isLoading ? (
|
|
189
|
+
{isLoading || isTransitioning ? (
|
|
165
190
|
viewMode === 'masonry' ? (
|
|
166
191
|
<MasonrySkeleton columnCount={columnCount} />
|
|
167
192
|
) : (
|
|
@@ -184,13 +209,30 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category }) => {
|
|
|
184
209
|
index={index}
|
|
185
210
|
key={item.id}
|
|
186
211
|
knowledgeBaseId={knowledgeBaseId}
|
|
187
|
-
onSelectedChange={(id, checked) => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
212
|
+
onSelectedChange={(id, checked, shiftKey, clickedIndex) => {
|
|
213
|
+
if (shiftKey && lastSelectedIndex !== null && selectFileIds.length > 0 && data) {
|
|
214
|
+
// Range selection with shift key
|
|
215
|
+
const start = Math.min(lastSelectedIndex, clickedIndex);
|
|
216
|
+
const end = Math.max(lastSelectedIndex, clickedIndex);
|
|
217
|
+
const rangeIds = data.slice(start, end + 1).map((item) => item.id);
|
|
218
|
+
|
|
219
|
+
setSelectedFileIds((prev) => {
|
|
220
|
+
// Create a Set for efficient lookup
|
|
221
|
+
const prevSet = new Set(prev);
|
|
222
|
+
// Add all items in range
|
|
223
|
+
rangeIds.forEach((rangeId) => prevSet.add(rangeId));
|
|
224
|
+
return Array.from(prevSet);
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
// Normal selection
|
|
228
|
+
setSelectedFileIds((prev) => {
|
|
229
|
+
if (checked) {
|
|
230
|
+
return [...prev, id];
|
|
231
|
+
}
|
|
232
|
+
return prev.filter((item) => item !== id);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
setLastSelectedIndex(clickedIndex);
|
|
194
236
|
}}
|
|
195
237
|
selected={selectFileIds.includes(item.id)}
|
|
196
238
|
{...item}
|
|
@@ -175,9 +175,9 @@ export default {
|
|
|
175
175
|
addMember: '添加成员',
|
|
176
176
|
allMembers: '全体成员',
|
|
177
177
|
createGroup: '创建 Agent 团队',
|
|
178
|
-
noAvailableAgents: '
|
|
179
|
-
noSelectedAgents: '
|
|
180
|
-
searchAgents: '
|
|
178
|
+
noAvailableAgents: '没有可邀请的 Agent',
|
|
179
|
+
noSelectedAgents: '还未选择 Agent',
|
|
180
|
+
searchAgents: '搜索 Agent...',
|
|
181
181
|
setInitialMembers: '选择团队成员',
|
|
182
182
|
},
|
|
183
183
|
|
|
@@ -247,7 +247,7 @@ export default {
|
|
|
247
247
|
jumpToMessage: '跳转至第 {{index}} 条消息',
|
|
248
248
|
nextMessage: '下一条消息',
|
|
249
249
|
previousMessage: '上一条消息',
|
|
250
|
-
senderAssistant: '
|
|
250
|
+
senderAssistant: 'Agent',
|
|
251
251
|
senderUser: '你',
|
|
252
252
|
},
|
|
253
253
|
|
|
@@ -2,17 +2,50 @@ import { act, renderHook, waitFor } from '@testing-library/react';
|
|
|
2
2
|
import { mutate } from 'swr';
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { message } from '@/components/AntdStaticMethods';
|
|
6
|
+
import { FILE_UPLOAD_BLACKLIST, MAX_UPLOAD_FILE_COUNT } from '@/const/file';
|
|
6
7
|
import { lambdaClient } from '@/libs/trpc/client';
|
|
7
8
|
import { fileService } from '@/services/file';
|
|
8
9
|
import { ragService } from '@/services/rag';
|
|
9
10
|
import { FileListItem } from '@/types/files';
|
|
10
11
|
import { UploadFileItem } from '@/types/files/upload';
|
|
12
|
+
import { unzipFile } from '@/utils/unzipFile';
|
|
11
13
|
|
|
12
14
|
import { useFileStore as useStore } from '../../store';
|
|
13
15
|
|
|
14
16
|
vi.mock('zustand/traditional');
|
|
15
17
|
|
|
18
|
+
// Mock i18next translation function
|
|
19
|
+
vi.mock('i18next', () => ({
|
|
20
|
+
t: (key: string, options?: any) => {
|
|
21
|
+
// Return a mock translation string that includes the options for verification
|
|
22
|
+
if (key === 'uploadDock.fileQueueInfo' && options?.count !== undefined) {
|
|
23
|
+
return `Uploading ${options.count} files, ${options.remaining} queued`;
|
|
24
|
+
}
|
|
25
|
+
return key;
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// Mock message
|
|
30
|
+
vi.mock('@/components/AntdStaticMethods', () => ({
|
|
31
|
+
message: {
|
|
32
|
+
info: vi.fn(),
|
|
33
|
+
warning: vi.fn(),
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
// Mock unzipFile
|
|
38
|
+
vi.mock('@/utils/unzipFile', () => ({
|
|
39
|
+
unzipFile: vi.fn(),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
// Mock p-map to run sequentially for easier testing
|
|
43
|
+
vi.mock('p-map', () => ({
|
|
44
|
+
default: vi.fn(async (items, mapper) => {
|
|
45
|
+
return Promise.all(items.map(mapper));
|
|
46
|
+
}),
|
|
47
|
+
}));
|
|
48
|
+
|
|
16
49
|
// Mock SWR
|
|
17
50
|
vi.mock('swr', async () => {
|
|
18
51
|
const actual = await vi.importActual('swr');
|
|
@@ -398,6 +431,108 @@ describe('FileManagerActions', () => {
|
|
|
398
431
|
// Should not auto-parse when upload returns undefined
|
|
399
432
|
expect(parseSpy).not.toHaveBeenCalled();
|
|
400
433
|
});
|
|
434
|
+
|
|
435
|
+
it('should enforce file count limit and queue excess files', async () => {
|
|
436
|
+
const { result } = renderHook(() => useStore());
|
|
437
|
+
|
|
438
|
+
// Create more files than the limit
|
|
439
|
+
const totalFiles = MAX_UPLOAD_FILE_COUNT + 5;
|
|
440
|
+
const files = Array.from(
|
|
441
|
+
{ length: totalFiles },
|
|
442
|
+
(_, i) => new File(['content'], `file-${i}.txt`, { type: 'text/plain' }),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
vi.spyOn(result.current, 'uploadWithProgress').mockResolvedValue({
|
|
446
|
+
id: 'file-1',
|
|
447
|
+
url: 'http://example.com/file-1',
|
|
448
|
+
});
|
|
449
|
+
vi.spyOn(result.current, 'refreshFileList').mockResolvedValue();
|
|
450
|
+
vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue();
|
|
451
|
+
const dispatchSpy = vi.spyOn(result.current, 'dispatchDockFileList');
|
|
452
|
+
|
|
453
|
+
await act(async () => {
|
|
454
|
+
await result.current.pushDockFileList(files);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Should add all files to dock (not just first MAX_UPLOAD_FILE_COUNT)
|
|
458
|
+
expect(dispatchSpy).toHaveBeenCalledWith({
|
|
459
|
+
atStart: true,
|
|
460
|
+
files: expect.arrayContaining([
|
|
461
|
+
expect.objectContaining({ file: expect.any(File), status: 'pending' }),
|
|
462
|
+
]),
|
|
463
|
+
type: 'addFiles',
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// Verify all files were dispatched
|
|
467
|
+
const dispatchCall = dispatchSpy.mock.calls.find((call) => call[0].type === 'addFiles');
|
|
468
|
+
expect(dispatchCall?.[0]).toHaveProperty('files');
|
|
469
|
+
if (dispatchCall && 'files' in dispatchCall[0]) {
|
|
470
|
+
expect(dispatchCall[0].files).toHaveLength(totalFiles);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should extract ZIP files and upload contents', async () => {
|
|
475
|
+
const { result } = renderHook(() => useStore());
|
|
476
|
+
|
|
477
|
+
const zipFile = new File(['zip content'], 'archive.zip', { type: 'application/zip' });
|
|
478
|
+
const extractedFiles = [
|
|
479
|
+
new File(['file1'], 'file1.txt', { type: 'text/plain' }),
|
|
480
|
+
new File(['file2'], 'file2.txt', { type: 'text/plain' }),
|
|
481
|
+
];
|
|
482
|
+
|
|
483
|
+
vi.mocked(unzipFile).mockResolvedValue(extractedFiles);
|
|
484
|
+
vi.spyOn(result.current, 'uploadWithProgress').mockResolvedValue({
|
|
485
|
+
id: 'file-1',
|
|
486
|
+
url: 'http://example.com/file-1',
|
|
487
|
+
});
|
|
488
|
+
vi.spyOn(result.current, 'refreshFileList').mockResolvedValue();
|
|
489
|
+
vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue();
|
|
490
|
+
const dispatchSpy = vi.spyOn(result.current, 'dispatchDockFileList');
|
|
491
|
+
|
|
492
|
+
await act(async () => {
|
|
493
|
+
await result.current.pushDockFileList([zipFile]);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Should extract ZIP file
|
|
497
|
+
expect(unzipFile).toHaveBeenCalledWith(zipFile);
|
|
498
|
+
|
|
499
|
+
// Should upload extracted files
|
|
500
|
+
expect(dispatchSpy).toHaveBeenCalledWith({
|
|
501
|
+
atStart: true,
|
|
502
|
+
files: extractedFiles.map((file) => ({ file, id: file.name, status: 'pending' })),
|
|
503
|
+
type: 'addFiles',
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('should handle ZIP extraction errors gracefully', async () => {
|
|
508
|
+
const { result } = renderHook(() => useStore());
|
|
509
|
+
|
|
510
|
+
const zipFile = new File(['zip content'], 'archive.zip', { type: 'application/zip' });
|
|
511
|
+
|
|
512
|
+
vi.mocked(unzipFile).mockRejectedValue(new Error('Extraction failed'));
|
|
513
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
514
|
+
vi.spyOn(result.current, 'uploadWithProgress').mockResolvedValue({
|
|
515
|
+
id: 'file-1',
|
|
516
|
+
url: 'http://example.com/file-1',
|
|
517
|
+
});
|
|
518
|
+
vi.spyOn(result.current, 'refreshFileList').mockResolvedValue();
|
|
519
|
+
vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue();
|
|
520
|
+
const dispatchSpy = vi.spyOn(result.current, 'dispatchDockFileList');
|
|
521
|
+
|
|
522
|
+
await act(async () => {
|
|
523
|
+
await result.current.pushDockFileList([zipFile]);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Should log error
|
|
527
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
528
|
+
|
|
529
|
+
// Should fallback to uploading the ZIP file itself
|
|
530
|
+
expect(dispatchSpy).toHaveBeenCalledWith({
|
|
531
|
+
atStart: true,
|
|
532
|
+
files: [{ file: zipFile, id: zipFile.name, status: 'pending' }],
|
|
533
|
+
type: 'addFiles',
|
|
534
|
+
});
|
|
535
|
+
});
|
|
401
536
|
});
|
|
402
537
|
|
|
403
538
|
describe('reEmbeddingChunks', () => {
|