@lobehub/chat 1.141.2 → 1.141.4
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/.github/workflows/sync.yml +2 -2
- package/CHANGELOG.md +50 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/changelog/v1.json +18 -0
- package/locales/ar/error.json +12 -0
- package/locales/ar/modelProvider.json +52 -0
- package/locales/ar/models.json +33 -0
- package/locales/ar/providers.json +3 -3
- package/locales/bg-BG/error.json +12 -0
- package/locales/bg-BG/modelProvider.json +52 -0
- package/locales/bg-BG/models.json +33 -0
- package/locales/bg-BG/providers.json +3 -3
- package/locales/de-DE/error.json +12 -0
- package/locales/de-DE/modelProvider.json +52 -0
- package/locales/de-DE/models.json +33 -0
- package/locales/de-DE/providers.json +3 -3
- package/locales/en-US/error.json +12 -0
- package/locales/en-US/modelProvider.json +14 -14
- package/locales/en-US/models.json +33 -0
- package/locales/en-US/providers.json +3 -3
- package/locales/es-ES/error.json +12 -0
- package/locales/es-ES/modelProvider.json +52 -0
- package/locales/es-ES/models.json +33 -0
- package/locales/es-ES/providers.json +3 -3
- package/locales/fa-IR/error.json +12 -0
- package/locales/fa-IR/modelProvider.json +52 -0
- package/locales/fa-IR/models.json +33 -0
- package/locales/fa-IR/providers.json +3 -3
- package/locales/fr-FR/error.json +12 -0
- package/locales/fr-FR/modelProvider.json +52 -0
- package/locales/fr-FR/models.json +33 -0
- package/locales/fr-FR/providers.json +3 -3
- package/locales/it-IT/error.json +12 -0
- package/locales/it-IT/modelProvider.json +52 -0
- package/locales/it-IT/models.json +33 -0
- package/locales/it-IT/providers.json +3 -3
- package/locales/ja-JP/error.json +12 -0
- package/locales/ja-JP/modelProvider.json +52 -0
- package/locales/ja-JP/models.json +33 -0
- package/locales/ja-JP/providers.json +3 -3
- package/locales/ko-KR/error.json +12 -0
- package/locales/ko-KR/modelProvider.json +52 -0
- package/locales/ko-KR/models.json +33 -0
- package/locales/ko-KR/providers.json +3 -3
- package/locales/nl-NL/error.json +12 -0
- package/locales/nl-NL/modelProvider.json +52 -0
- package/locales/nl-NL/models.json +33 -0
- package/locales/nl-NL/providers.json +3 -3
- package/locales/pl-PL/error.json +12 -0
- package/locales/pl-PL/modelProvider.json +52 -0
- package/locales/pl-PL/models.json +33 -0
- package/locales/pl-PL/providers.json +3 -3
- package/locales/pt-BR/error.json +12 -0
- package/locales/pt-BR/modelProvider.json +52 -0
- package/locales/pt-BR/models.json +33 -0
- package/locales/pt-BR/providers.json +3 -3
- package/locales/ru-RU/error.json +12 -0
- package/locales/ru-RU/modelProvider.json +52 -0
- package/locales/ru-RU/models.json +33 -0
- package/locales/ru-RU/providers.json +3 -0
- package/locales/tr-TR/error.json +12 -0
- package/locales/tr-TR/modelProvider.json +52 -0
- package/locales/tr-TR/models.json +33 -0
- package/locales/tr-TR/providers.json +3 -3
- package/locales/vi-VN/error.json +12 -0
- package/locales/vi-VN/modelProvider.json +52 -0
- package/locales/vi-VN/models.json +33 -0
- package/locales/vi-VN/providers.json +3 -3
- package/locales/zh-CN/chat.json +1 -1
- package/locales/zh-CN/components.json +4 -4
- package/locales/zh-CN/error.json +12 -0
- package/locales/zh-CN/modelProvider.json +14 -14
- package/locales/zh-CN/models.json +8 -27
- package/locales/zh-TW/error.json +12 -0
- package/locales/zh-TW/modelProvider.json +52 -0
- package/locales/zh-TW/models.json +33 -0
- package/locales/zh-TW/providers.json +3 -3
- package/package.json +1 -1
- package/packages/agent-runtime/src/core/runtime.ts +23 -12
- package/packages/agent-runtime/src/types/instruction.ts +4 -0
- package/packages/const/src/currency.ts +2 -2
- package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.test.ts +205 -12
- package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.ts +47 -11
- package/src/app/__tests__/desktop.routes.test.ts +18 -0
- package/src/features/FileManager/FileList/MasonryFileItem/index.tsx +56 -25
- package/src/features/FileManager/FileList/index.tsx +12 -3
- package/src/store/file/slices/fileManager/action.test.ts +88 -0
- package/src/store/file/slices/fileManager/action.ts +21 -9
|
@@ -5,7 +5,7 @@ import { VirtuosoMasonry } from '@virtuoso.dev/masonry';
|
|
|
5
5
|
import { createStyles } from 'antd-style';
|
|
6
6
|
import { useQueryState } from 'nuqs';
|
|
7
7
|
import { rgba } from 'polished';
|
|
8
|
-
import React, { memo, useState } from 'react';
|
|
8
|
+
import React, { memo, useMemo, useState } from 'react';
|
|
9
9
|
import { useTranslation } from 'react-i18next';
|
|
10
10
|
import { Center, Flexbox } from 'react-layout-kit';
|
|
11
11
|
import { Virtuoso } from 'react-virtuoso';
|
|
@@ -118,6 +118,16 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category }) => {
|
|
|
118
118
|
}
|
|
119
119
|
}, [data]);
|
|
120
120
|
|
|
121
|
+
// Memoize context object to avoid recreating on every render
|
|
122
|
+
const masonryContext = useMemo(
|
|
123
|
+
() => ({
|
|
124
|
+
knowledgeBaseId,
|
|
125
|
+
selectFileIds,
|
|
126
|
+
setSelectedFileIds,
|
|
127
|
+
}),
|
|
128
|
+
[knowledgeBaseId, selectFileIds],
|
|
129
|
+
);
|
|
130
|
+
|
|
121
131
|
return !isLoading && data?.length === 0 ? (
|
|
122
132
|
<EmptyStatus knowledgeBaseId={knowledgeBaseId} showKnowledgeBase={!knowledgeBaseId} />
|
|
123
133
|
) : (
|
|
@@ -195,9 +205,8 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category }) => {
|
|
|
195
205
|
<VirtuosoMasonry
|
|
196
206
|
ItemContent={MasonryItemWrapper}
|
|
197
207
|
columnCount={columnCount}
|
|
198
|
-
context={
|
|
208
|
+
context={masonryContext}
|
|
199
209
|
data={data || []}
|
|
200
|
-
key={`masonry-${query || 'all'}-${data?.length || 0}`}
|
|
201
210
|
style={{
|
|
202
211
|
gap: '16px',
|
|
203
212
|
}}
|
|
@@ -234,6 +234,7 @@ describe('FileManagerActions', () => {
|
|
|
234
234
|
.mockResolvedValue({ id: 'file-1', url: 'http://example.com/file-1' });
|
|
235
235
|
const refreshSpy = vi.spyOn(result.current, 'refreshFileList').mockResolvedValue();
|
|
236
236
|
const dispatchSpy = vi.spyOn(result.current, 'dispatchDockFileList');
|
|
237
|
+
const parseSpy = vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue();
|
|
237
238
|
|
|
238
239
|
await act(async () => {
|
|
239
240
|
await result.current.pushDockFileList([validFile, blacklistedFile]);
|
|
@@ -252,6 +253,8 @@ describe('FileManagerActions', () => {
|
|
|
252
253
|
onStatusUpdate: expect.any(Function),
|
|
253
254
|
});
|
|
254
255
|
expect(refreshSpy).toHaveBeenCalled();
|
|
256
|
+
// Should auto-parse text files
|
|
257
|
+
expect(parseSpy).toHaveBeenCalledWith(['file-1'], { skipExist: false });
|
|
255
258
|
});
|
|
256
259
|
|
|
257
260
|
it('should upload files with knowledgeBaseId', async () => {
|
|
@@ -263,6 +266,7 @@ describe('FileManagerActions', () => {
|
|
|
263
266
|
.spyOn(result.current, 'uploadWithProgress')
|
|
264
267
|
.mockResolvedValue({ id: 'file-1', url: 'http://example.com/file-1' });
|
|
265
268
|
vi.spyOn(result.current, 'refreshFileList').mockResolvedValue();
|
|
269
|
+
vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue();
|
|
266
270
|
|
|
267
271
|
await act(async () => {
|
|
268
272
|
await result.current.pushDockFileList([file], 'kb-123');
|
|
@@ -287,6 +291,7 @@ describe('FileManagerActions', () => {
|
|
|
287
291
|
return { id: 'file-1', url: 'http://example.com/file-1' };
|
|
288
292
|
});
|
|
289
293
|
vi.spyOn(result.current, 'refreshFileList').mockResolvedValue();
|
|
294
|
+
vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue();
|
|
290
295
|
const dispatchSpy = vi.spyOn(result.current, 'dispatchDockFileList');
|
|
291
296
|
|
|
292
297
|
await act(async () => {
|
|
@@ -302,6 +307,7 @@ describe('FileManagerActions', () => {
|
|
|
302
307
|
|
|
303
308
|
const uploadSpy = vi.spyOn(result.current, 'uploadWithProgress');
|
|
304
309
|
const refreshSpy = vi.spyOn(result.current, 'refreshFileList');
|
|
310
|
+
const parseSpy = vi.spyOn(result.current, 'parseFilesToChunks');
|
|
305
311
|
|
|
306
312
|
await act(async () => {
|
|
307
313
|
await result.current.pushDockFileList([]);
|
|
@@ -309,6 +315,88 @@ describe('FileManagerActions', () => {
|
|
|
309
315
|
|
|
310
316
|
expect(uploadSpy).not.toHaveBeenCalled();
|
|
311
317
|
expect(refreshSpy).not.toHaveBeenCalled();
|
|
318
|
+
expect(parseSpy).not.toHaveBeenCalled();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should auto-embed files that support chunking', async () => {
|
|
322
|
+
const { result } = renderHook(() => useStore());
|
|
323
|
+
|
|
324
|
+
const textFile = new File(['text content'], 'doc.txt', { type: 'text/plain' });
|
|
325
|
+
const pdfFile = new File(['pdf content'], 'doc.pdf', { type: 'application/pdf' });
|
|
326
|
+
|
|
327
|
+
vi.spyOn(result.current, 'uploadWithProgress')
|
|
328
|
+
.mockResolvedValueOnce({ id: 'file-1', url: 'http://example.com/file-1' })
|
|
329
|
+
.mockResolvedValueOnce({ id: 'file-2', url: 'http://example.com/file-2' });
|
|
330
|
+
vi.spyOn(result.current, 'refreshFileList').mockResolvedValue();
|
|
331
|
+
const parseSpy = vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue();
|
|
332
|
+
|
|
333
|
+
await act(async () => {
|
|
334
|
+
await result.current.pushDockFileList([textFile, pdfFile]);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Should auto-parse both files that support chunking
|
|
338
|
+
expect(parseSpy).toHaveBeenCalledWith(['file-1', 'file-2'], { skipExist: false });
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should skip auto-embed for unsupported file types (images/videos/audio)', async () => {
|
|
342
|
+
const { result } = renderHook(() => useStore());
|
|
343
|
+
|
|
344
|
+
const imageFile = new File(['image content'], 'image.png', { type: 'image/png' });
|
|
345
|
+
const videoFile = new File(['video content'], 'video.mp4', { type: 'video/mp4' });
|
|
346
|
+
const audioFile = new File(['audio content'], 'audio.mp3', { type: 'audio/mpeg' });
|
|
347
|
+
|
|
348
|
+
vi.spyOn(result.current, 'uploadWithProgress')
|
|
349
|
+
.mockResolvedValueOnce({ id: 'file-1', url: 'http://example.com/file-1' })
|
|
350
|
+
.mockResolvedValueOnce({ id: 'file-2', url: 'http://example.com/file-2' })
|
|
351
|
+
.mockResolvedValueOnce({ id: 'file-3', url: 'http://example.com/file-3' });
|
|
352
|
+
vi.spyOn(result.current, 'refreshFileList').mockResolvedValue();
|
|
353
|
+
const parseSpy = vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue();
|
|
354
|
+
|
|
355
|
+
await act(async () => {
|
|
356
|
+
await result.current.pushDockFileList([imageFile, videoFile, audioFile]);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Should not auto-parse unsupported files
|
|
360
|
+
expect(parseSpy).not.toHaveBeenCalled();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should auto-embed only supported files in mixed upload', async () => {
|
|
364
|
+
const { result } = renderHook(() => useStore());
|
|
365
|
+
|
|
366
|
+
const textFile = new File(['text content'], 'doc.txt', { type: 'text/plain' });
|
|
367
|
+
const imageFile = new File(['image content'], 'image.png', { type: 'image/png' });
|
|
368
|
+
const pdfFile = new File(['pdf content'], 'doc.pdf', { type: 'application/pdf' });
|
|
369
|
+
|
|
370
|
+
vi.spyOn(result.current, 'uploadWithProgress')
|
|
371
|
+
.mockResolvedValueOnce({ id: 'file-1', url: 'http://example.com/file-1' })
|
|
372
|
+
.mockResolvedValueOnce({ id: 'file-2', url: 'http://example.com/file-2' })
|
|
373
|
+
.mockResolvedValueOnce({ id: 'file-3', url: 'http://example.com/file-3' });
|
|
374
|
+
vi.spyOn(result.current, 'refreshFileList').mockResolvedValue();
|
|
375
|
+
const parseSpy = vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue();
|
|
376
|
+
|
|
377
|
+
await act(async () => {
|
|
378
|
+
await result.current.pushDockFileList([textFile, imageFile, pdfFile]);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Should only auto-parse text and pdf files, skip image
|
|
382
|
+
expect(parseSpy).toHaveBeenCalledWith(['file-1', 'file-3'], { skipExist: false });
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should skip auto-embed when upload fails', async () => {
|
|
386
|
+
const { result } = renderHook(() => useStore());
|
|
387
|
+
|
|
388
|
+
const textFile = new File(['text content'], 'doc.txt', { type: 'text/plain' });
|
|
389
|
+
|
|
390
|
+
vi.spyOn(result.current, 'uploadWithProgress').mockResolvedValue(undefined);
|
|
391
|
+
vi.spyOn(result.current, 'refreshFileList').mockResolvedValue();
|
|
392
|
+
const parseSpy = vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue();
|
|
393
|
+
|
|
394
|
+
await act(async () => {
|
|
395
|
+
await result.current.pushDockFileList([textFile]);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Should not auto-parse when upload returns undefined
|
|
399
|
+
expect(parseSpy).not.toHaveBeenCalled();
|
|
312
400
|
});
|
|
313
401
|
});
|
|
314
402
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
uploadFileListReducer,
|
|
12
12
|
} from '@/store/file/reducers/uploadFileList';
|
|
13
13
|
import { FileListItem, QueryFileListParams } from '@/types/files';
|
|
14
|
+
import { isChunkingUnsupported } from '@/utils/isChunkingUnsupported';
|
|
14
15
|
|
|
15
16
|
import { FileStore } from '../../store';
|
|
16
17
|
import { fileManagerSelectors } from './selectors';
|
|
@@ -98,17 +99,28 @@ export const createFileManageSlice: StateCreator<
|
|
|
98
99
|
type: 'addFiles',
|
|
99
100
|
});
|
|
100
101
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
const uploadResults = await Promise.all(
|
|
103
|
+
files.map(async (file) => {
|
|
104
|
+
const result = await get().uploadWithProgress({
|
|
105
|
+
file,
|
|
106
|
+
knowledgeBaseId,
|
|
107
|
+
onStatusUpdate: dispatchDockFileList,
|
|
108
|
+
});
|
|
107
109
|
|
|
108
|
-
|
|
109
|
-
});
|
|
110
|
+
await get().refreshFileList();
|
|
110
111
|
|
|
111
|
-
|
|
112
|
+
return { file, fileId: result?.id, fileType: file.type };
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// 2. auto-embed files that support chunking
|
|
117
|
+
const fileIdsToEmbed = uploadResults
|
|
118
|
+
.filter(({ fileType, fileId }) => fileId && !isChunkingUnsupported(fileType))
|
|
119
|
+
.map(({ fileId }) => fileId!);
|
|
120
|
+
|
|
121
|
+
if (fileIdsToEmbed.length > 0) {
|
|
122
|
+
await get().parseFilesToChunks(fileIdsToEmbed, { skipExist: false });
|
|
123
|
+
}
|
|
112
124
|
},
|
|
113
125
|
|
|
114
126
|
reEmbeddingChunks: async (id) => {
|