@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.
Files changed (89) hide show
  1. package/.github/workflows/sync.yml +2 -2
  2. package/CHANGELOG.md +50 -0
  3. package/README.md +1 -1
  4. package/README.zh-CN.md +1 -1
  5. package/changelog/v1.json +18 -0
  6. package/locales/ar/error.json +12 -0
  7. package/locales/ar/modelProvider.json +52 -0
  8. package/locales/ar/models.json +33 -0
  9. package/locales/ar/providers.json +3 -3
  10. package/locales/bg-BG/error.json +12 -0
  11. package/locales/bg-BG/modelProvider.json +52 -0
  12. package/locales/bg-BG/models.json +33 -0
  13. package/locales/bg-BG/providers.json +3 -3
  14. package/locales/de-DE/error.json +12 -0
  15. package/locales/de-DE/modelProvider.json +52 -0
  16. package/locales/de-DE/models.json +33 -0
  17. package/locales/de-DE/providers.json +3 -3
  18. package/locales/en-US/error.json +12 -0
  19. package/locales/en-US/modelProvider.json +14 -14
  20. package/locales/en-US/models.json +33 -0
  21. package/locales/en-US/providers.json +3 -3
  22. package/locales/es-ES/error.json +12 -0
  23. package/locales/es-ES/modelProvider.json +52 -0
  24. package/locales/es-ES/models.json +33 -0
  25. package/locales/es-ES/providers.json +3 -3
  26. package/locales/fa-IR/error.json +12 -0
  27. package/locales/fa-IR/modelProvider.json +52 -0
  28. package/locales/fa-IR/models.json +33 -0
  29. package/locales/fa-IR/providers.json +3 -3
  30. package/locales/fr-FR/error.json +12 -0
  31. package/locales/fr-FR/modelProvider.json +52 -0
  32. package/locales/fr-FR/models.json +33 -0
  33. package/locales/fr-FR/providers.json +3 -3
  34. package/locales/it-IT/error.json +12 -0
  35. package/locales/it-IT/modelProvider.json +52 -0
  36. package/locales/it-IT/models.json +33 -0
  37. package/locales/it-IT/providers.json +3 -3
  38. package/locales/ja-JP/error.json +12 -0
  39. package/locales/ja-JP/modelProvider.json +52 -0
  40. package/locales/ja-JP/models.json +33 -0
  41. package/locales/ja-JP/providers.json +3 -3
  42. package/locales/ko-KR/error.json +12 -0
  43. package/locales/ko-KR/modelProvider.json +52 -0
  44. package/locales/ko-KR/models.json +33 -0
  45. package/locales/ko-KR/providers.json +3 -3
  46. package/locales/nl-NL/error.json +12 -0
  47. package/locales/nl-NL/modelProvider.json +52 -0
  48. package/locales/nl-NL/models.json +33 -0
  49. package/locales/nl-NL/providers.json +3 -3
  50. package/locales/pl-PL/error.json +12 -0
  51. package/locales/pl-PL/modelProvider.json +52 -0
  52. package/locales/pl-PL/models.json +33 -0
  53. package/locales/pl-PL/providers.json +3 -3
  54. package/locales/pt-BR/error.json +12 -0
  55. package/locales/pt-BR/modelProvider.json +52 -0
  56. package/locales/pt-BR/models.json +33 -0
  57. package/locales/pt-BR/providers.json +3 -3
  58. package/locales/ru-RU/error.json +12 -0
  59. package/locales/ru-RU/modelProvider.json +52 -0
  60. package/locales/ru-RU/models.json +33 -0
  61. package/locales/ru-RU/providers.json +3 -0
  62. package/locales/tr-TR/error.json +12 -0
  63. package/locales/tr-TR/modelProvider.json +52 -0
  64. package/locales/tr-TR/models.json +33 -0
  65. package/locales/tr-TR/providers.json +3 -3
  66. package/locales/vi-VN/error.json +12 -0
  67. package/locales/vi-VN/modelProvider.json +52 -0
  68. package/locales/vi-VN/models.json +33 -0
  69. package/locales/vi-VN/providers.json +3 -3
  70. package/locales/zh-CN/chat.json +1 -1
  71. package/locales/zh-CN/components.json +4 -4
  72. package/locales/zh-CN/error.json +12 -0
  73. package/locales/zh-CN/modelProvider.json +14 -14
  74. package/locales/zh-CN/models.json +8 -27
  75. package/locales/zh-TW/error.json +12 -0
  76. package/locales/zh-TW/modelProvider.json +52 -0
  77. package/locales/zh-TW/models.json +33 -0
  78. package/locales/zh-TW/providers.json +3 -3
  79. package/package.json +1 -1
  80. package/packages/agent-runtime/src/core/runtime.ts +23 -12
  81. package/packages/agent-runtime/src/types/instruction.ts +4 -0
  82. package/packages/const/src/currency.ts +2 -2
  83. package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.test.ts +205 -12
  84. package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.ts +47 -11
  85. package/src/app/__tests__/desktop.routes.test.ts +18 -0
  86. package/src/features/FileManager/FileList/MasonryFileItem/index.tsx +56 -25
  87. package/src/features/FileManager/FileList/index.tsx +12 -3
  88. package/src/store/file/slices/fileManager/action.test.ts +88 -0
  89. 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={{ knowledgeBaseId, selectFileIds, setSelectedFileIds }}
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 pools = files.map(async (file) => {
102
- await get().uploadWithProgress({
103
- file,
104
- knowledgeBaseId,
105
- onStatusUpdate: dispatchDockFileList,
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
- await get().refreshFileList();
109
- });
110
+ await get().refreshFileList();
110
111
 
111
- await Promise.all(pools);
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) => {