@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/ar/chat.json +4 -4
  4. package/locales/ar/file.json +1 -0
  5. package/locales/ar/models.json +1 -1
  6. package/locales/bg-BG/chat.json +4 -4
  7. package/locales/bg-BG/file.json +1 -0
  8. package/locales/bg-BG/models.json +1 -1
  9. package/locales/de-DE/chat.json +4 -4
  10. package/locales/de-DE/file.json +1 -0
  11. package/locales/de-DE/models.json +1 -1
  12. package/locales/en-US/chat.json +4 -4
  13. package/locales/en-US/file.json +1 -0
  14. package/locales/en-US/models.json +1 -1
  15. package/locales/es-ES/chat.json +4 -4
  16. package/locales/es-ES/file.json +1 -0
  17. package/locales/es-ES/models.json +1 -1
  18. package/locales/fa-IR/chat.json +4 -4
  19. package/locales/fa-IR/file.json +1 -0
  20. package/locales/fa-IR/models.json +1 -1
  21. package/locales/fr-FR/chat.json +4 -4
  22. package/locales/fr-FR/file.json +1 -0
  23. package/locales/fr-FR/models.json +1 -1
  24. package/locales/it-IT/chat.json +4 -4
  25. package/locales/it-IT/file.json +1 -0
  26. package/locales/ja-JP/chat.json +4 -4
  27. package/locales/ja-JP/file.json +1 -0
  28. package/locales/ja-JP/models.json +1 -1
  29. package/locales/ko-KR/chat.json +4 -4
  30. package/locales/ko-KR/file.json +1 -0
  31. package/locales/ko-KR/models.json +1 -1
  32. package/locales/nl-NL/chat.json +4 -4
  33. package/locales/nl-NL/file.json +1 -0
  34. package/locales/nl-NL/models.json +1 -1
  35. package/locales/pl-PL/chat.json +4 -4
  36. package/locales/pl-PL/file.json +1 -0
  37. package/locales/pl-PL/models.json +1 -1
  38. package/locales/pt-BR/chat.json +4 -4
  39. package/locales/pt-BR/file.json +1 -0
  40. package/locales/ru-RU/chat.json +4 -4
  41. package/locales/ru-RU/file.json +1 -0
  42. package/locales/ru-RU/models.json +1 -1
  43. package/locales/tr-TR/chat.json +4 -4
  44. package/locales/tr-TR/file.json +1 -0
  45. package/locales/tr-TR/models.json +1 -1
  46. package/locales/vi-VN/chat.json +4 -4
  47. package/locales/vi-VN/file.json +1 -0
  48. package/locales/vi-VN/models.json +1 -1
  49. package/locales/zh-CN/chat.json +4 -4
  50. package/locales/zh-CN/file.json +1 -0
  51. package/locales/zh-TW/chat.json +4 -4
  52. package/locales/zh-TW/file.json +1 -0
  53. package/locales/zh-TW/models.json +1 -1
  54. package/package.json +3 -2
  55. package/packages/const/src/file.ts +2 -0
  56. package/packages/obervability-otel/package.json +5 -5
  57. package/src/app/[variants]/(main)/settings/provider/features/ModelList/CreateNewModelModal/index.tsx +7 -0
  58. package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelConfigModal/index.tsx +7 -0
  59. package/src/features/FileManager/FileList/FileListItem/index.tsx +3 -2
  60. package/src/features/FileManager/FileList/MasonryFileItem/MasonryItemWrapper.tsx +1 -1
  61. package/src/features/FileManager/FileList/MasonryFileItem/index.tsx +2 -4
  62. package/src/features/FileManager/FileList/MasonrySkeleton.tsx +11 -5
  63. package/src/features/FileManager/FileList/ToolBar/MultiSelectActions.tsx +1 -1
  64. package/src/features/FileManager/FileList/index.tsx +51 -9
  65. package/src/locales/default/chat.ts +4 -4
  66. package/src/locales/default/file.ts +1 -0
  67. package/src/store/file/slices/fileManager/action.test.ts +136 -1
  68. package/src/store/file/slices/fileManager/action.ts +30 -8
  69. package/src/utils/unzipFile.test.ts +128 -0
  70. package/src/utils/unzipFile.ts +122 -0
  71. package/vitest.config.mts +1 -0
@@ -1,7 +1,8 @@
1
+ import pMap from 'p-map';
1
2
  import { SWRResponse, mutate } from 'swr';
2
3
  import { StateCreator } from 'zustand/vanilla';
3
4
 
4
- import { FILE_UPLOAD_BLACKLIST } from '@/const/file';
5
+ import { FILE_UPLOAD_BLACKLIST, MAX_UPLOAD_FILE_COUNT } from '@/const/file';
5
6
  import { useClientDataSWR } from '@/libs/swr';
6
7
  import { fileService } from '@/services/file';
7
8
  import { ServerService } from '@/services/file/server';
@@ -12,6 +13,7 @@ import {
12
13
  } from '@/store/file/reducers/uploadFileList';
13
14
  import { FileListItem, QueryFileListParams } from '@/types/files';
14
15
  import { isChunkingUnsupported } from '@/utils/isChunkingUnsupported';
16
+ import { unzipFile } from '@/utils/unzipFile';
15
17
 
16
18
  import { FileStore } from '../../store';
17
19
  import { fileManagerSelectors } from './selectors';
@@ -89,18 +91,37 @@ export const createFileManageSlice: StateCreator<
89
91
  pushDockFileList: async (rawFiles, knowledgeBaseId) => {
90
92
  const { dispatchDockFileList } = get();
91
93
 
92
- // 0. skip file in blacklist
93
- const files = rawFiles.filter((file) => !FILE_UPLOAD_BLACKLIST.includes(file.name));
94
+ // 0. Process ZIP files and extract their contents
95
+ const filesToUpload: File[] = [];
96
+ for (const file of rawFiles) {
97
+ if (file.type === 'application/zip' || file.name.endsWith('.zip')) {
98
+ try {
99
+ const extractedFiles = await unzipFile(file);
100
+ filesToUpload.push(...extractedFiles);
101
+ } catch (error) {
102
+ console.error('Failed to extract ZIP file:', error);
103
+ // If extraction fails, treat it as a regular file
104
+ filesToUpload.push(file);
105
+ }
106
+ } else {
107
+ filesToUpload.push(file);
108
+ }
109
+ }
94
110
 
95
- // 1. add files
111
+ // 1. skip file in blacklist
112
+ const files = filesToUpload.filter((file) => !FILE_UPLOAD_BLACKLIST.includes(file.name));
113
+
114
+ // 2. Add all files to dock
96
115
  dispatchDockFileList({
97
116
  atStart: true,
98
117
  files: files.map((file) => ({ file, id: file.name, status: 'pending' })),
99
118
  type: 'addFiles',
100
119
  });
101
120
 
102
- const uploadResults = await Promise.all(
103
- files.map(async (file) => {
121
+ // 3. Upload files with concurrency limit using p-map
122
+ const uploadResults = await pMap(
123
+ files,
124
+ async (file) => {
104
125
  const result = await get().uploadWithProgress({
105
126
  file,
106
127
  knowledgeBaseId,
@@ -110,10 +131,11 @@ export const createFileManageSlice: StateCreator<
110
131
  await get().refreshFileList();
111
132
 
112
133
  return { file, fileId: result?.id, fileType: file.type };
113
- }),
134
+ },
135
+ { concurrency: MAX_UPLOAD_FILE_COUNT },
114
136
  );
115
137
 
116
- // 2. auto-embed files that support chunking
138
+ // 4. auto-embed files that support chunking
117
139
  const fileIdsToEmbed = uploadResults
118
140
  .filter(({ fileType, fileId }) => fileId && !isChunkingUnsupported(fileType))
119
141
  .map(({ fileId }) => fileId!);
@@ -0,0 +1,128 @@
1
+ import { zip } from 'fflate';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { unzipFile } from './unzipFile';
5
+
6
+ describe('unzipFile', () => {
7
+ it('should extract files from a ZIP archive', async () => {
8
+ // Create a mock ZIP file with test data
9
+ const testFiles = {
10
+ 'test.txt': new TextEncoder().encode('Hello, World!'),
11
+ 'folder/nested.txt': new TextEncoder().encode('Nested file content'),
12
+ };
13
+
14
+ const zipped = await new Promise<Uint8Array>((resolve, reject) => {
15
+ zip(testFiles, (error, data) => {
16
+ if (error) reject(error);
17
+ else resolve(data);
18
+ });
19
+ });
20
+
21
+ const zipFile = new File([new Uint8Array(zipped)], 'test.zip', { type: 'application/zip' });
22
+
23
+ const extractedFiles = await unzipFile(zipFile);
24
+
25
+ expect(extractedFiles).toHaveLength(2);
26
+ expect(extractedFiles[0].name).toBe('test.txt');
27
+ expect(extractedFiles[1].name).toBe('nested.txt');
28
+
29
+ // Verify file contents
30
+ const content1 = await extractedFiles[0].text();
31
+ expect(content1).toBe('Hello, World!');
32
+
33
+ const content2 = await extractedFiles[1].text();
34
+ expect(content2).toBe('Nested file content');
35
+ });
36
+
37
+ it('should skip directories in ZIP archive', async () => {
38
+ const testFiles = {
39
+ 'file.txt': new TextEncoder().encode('File content'),
40
+ 'folder/': new Uint8Array(0), // Directory entry
41
+ };
42
+
43
+ const zipped = await new Promise<Uint8Array>((resolve, reject) => {
44
+ zip(testFiles, (error, data) => {
45
+ if (error) reject(error);
46
+ else resolve(data);
47
+ });
48
+ });
49
+
50
+ const zipFile = new File([new Uint8Array(zipped)], 'test.zip', { type: 'application/zip' });
51
+
52
+ const extractedFiles = await unzipFile(zipFile);
53
+
54
+ expect(extractedFiles).toHaveLength(1);
55
+ expect(extractedFiles[0].name).toBe('file.txt');
56
+ });
57
+
58
+ it('should skip hidden files and __MACOSX directories', async () => {
59
+ const testFiles = {
60
+ '.hidden': new TextEncoder().encode('Hidden file'),
61
+ '__MACOSX/._file.txt': new TextEncoder().encode('Mac metadata'),
62
+ 'visible.txt': new TextEncoder().encode('Visible file'),
63
+ };
64
+
65
+ const zipped = await new Promise<Uint8Array>((resolve, reject) => {
66
+ zip(testFiles, (error, data) => {
67
+ if (error) reject(error);
68
+ else resolve(data);
69
+ });
70
+ });
71
+
72
+ const zipFile = new File([new Uint8Array(zipped)], 'test.zip', { type: 'application/zip' });
73
+
74
+ const extractedFiles = await unzipFile(zipFile);
75
+
76
+ expect(extractedFiles).toHaveLength(1);
77
+ expect(extractedFiles[0].name).toBe('visible.txt');
78
+ });
79
+
80
+ it('should set correct MIME types for extracted files', async () => {
81
+ const testFiles = {
82
+ 'document.pdf': new TextEncoder().encode('PDF content'),
83
+ 'image.png': new TextEncoder().encode('PNG content'),
84
+ 'code.ts': new TextEncoder().encode('TypeScript code'),
85
+ };
86
+
87
+ const zipped = await new Promise<Uint8Array>((resolve, reject) => {
88
+ zip(testFiles, (error, data) => {
89
+ if (error) reject(error);
90
+ else resolve(data);
91
+ });
92
+ });
93
+
94
+ const zipFile = new File([new Uint8Array(zipped)], 'test.zip', { type: 'application/zip' });
95
+
96
+ const extractedFiles = await unzipFile(zipFile);
97
+
98
+ expect(extractedFiles).toHaveLength(3);
99
+ expect(extractedFiles.find((f) => f.name === 'document.pdf')?.type).toBe('application/pdf');
100
+ expect(extractedFiles.find((f) => f.name === 'image.png')?.type).toBe('image/png');
101
+ expect(extractedFiles.find((f) => f.name === 'code.ts')?.type).toBe('text/typescript');
102
+ });
103
+
104
+ it('should handle empty ZIP files', async () => {
105
+ const testFiles = {};
106
+
107
+ const zipped = await new Promise<Uint8Array>((resolve, reject) => {
108
+ zip(testFiles, (error, data) => {
109
+ if (error) reject(error);
110
+ else resolve(data);
111
+ });
112
+ });
113
+
114
+ const zipFile = new File([new Uint8Array(zipped)], 'empty.zip', { type: 'application/zip' });
115
+
116
+ const extractedFiles = await unzipFile(zipFile);
117
+
118
+ expect(extractedFiles).toHaveLength(0);
119
+ });
120
+
121
+ it('should reject on invalid ZIP file', async () => {
122
+ const invalidFile = new File([new Uint8Array([1, 2, 3, 4])], 'invalid.zip', {
123
+ type: 'application/zip',
124
+ });
125
+
126
+ await expect(unzipFile(invalidFile)).rejects.toThrow();
127
+ });
128
+ });
@@ -0,0 +1,122 @@
1
+ import { unzip } from 'fflate';
2
+
3
+ /**
4
+ * Determines the MIME type based on file extension
5
+ */
6
+ const getFileType = (fileName: string): string => {
7
+ const extension = fileName.split('.').pop()?.toLowerCase() || '';
8
+
9
+ const mimeTypes: Record<string, string> = {
10
+ // Images
11
+ bmp: 'image/bmp',
12
+
13
+ // Code files
14
+ c: 'text/x-c',
15
+
16
+ cpp: 'text/x-c++',
17
+
18
+ cs: 'text/x-csharp',
19
+
20
+ css: 'text/css',
21
+
22
+ // Documents
23
+ csv: 'text/csv',
24
+
25
+ doc: 'application/msword',
26
+
27
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
28
+
29
+ gif: 'image/gif',
30
+
31
+ go: 'text/x-go',
32
+
33
+ html: 'text/html',
34
+
35
+ java: 'text/x-java',
36
+
37
+ jpeg: 'image/jpeg',
38
+
39
+ jpg: 'image/jpeg',
40
+
41
+ js: 'text/javascript',
42
+
43
+ json: 'application/json',
44
+
45
+ jsx: 'text/javascript',
46
+
47
+ md: 'text/markdown',
48
+
49
+ pdf: 'application/pdf',
50
+
51
+ php: 'application/x-httpd-php',
52
+
53
+ png: 'image/png',
54
+
55
+ ppt: 'application/vnd.ms-powerpoint',
56
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
57
+ py: 'text/x-python',
58
+ rb: 'text/x-ruby',
59
+ rs: 'text/x-rust',
60
+ rtf: 'application/rtf',
61
+ sh: 'application/x-sh',
62
+ svg: 'image/svg+xml',
63
+ ts: 'text/typescript',
64
+ tsx: 'text/typescript',
65
+ txt: 'text/plain',
66
+ webp: 'image/webp',
67
+ xls: 'application/vnd.ms-excel',
68
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
69
+ xml: 'application/xml',
70
+ };
71
+
72
+ return mimeTypes[extension] || 'application/octet-stream';
73
+ };
74
+
75
+ /**
76
+ * Extracts files from a ZIP archive
77
+ * @param zipFile - The ZIP file to extract
78
+ * @returns Promise that resolves to an array of extracted Files
79
+ */
80
+ export const unzipFile = async (zipFile: File): Promise<File[]> => {
81
+ return new Promise((resolve, reject) => {
82
+ zipFile
83
+ .arrayBuffer()
84
+ .then((arrayBuffer) => {
85
+ const uint8Array = new Uint8Array(arrayBuffer);
86
+
87
+ unzip(uint8Array, (error, unzipped) => {
88
+ if (error) {
89
+ reject(error);
90
+ return;
91
+ }
92
+
93
+ const extractedFiles: File[] = [];
94
+
95
+ for (const [path, data] of Object.entries(unzipped)) {
96
+ // Skip directories and hidden files
97
+ if (path.endsWith('/') || path.includes('__MACOSX') || path.startsWith('.')) {
98
+ continue;
99
+ }
100
+
101
+ // Get the filename from the path
102
+ const fileName = path.split('/').pop() || path;
103
+
104
+ // Create a File object from the extracted data
105
+ const blob = new Blob([new Uint8Array(data)], {
106
+ type: getFileType(fileName),
107
+ });
108
+ const file = new File([blob], fileName, {
109
+ type: getFileType(fileName),
110
+ });
111
+
112
+ extractedFiles.push(file);
113
+ }
114
+
115
+ resolve(extractedFiles);
116
+ });
117
+ })
118
+ .catch(() => {
119
+ reject(new Error('Failed to read ZIP file'));
120
+ });
121
+ });
122
+ };
package/vitest.config.mts CHANGED
@@ -15,6 +15,7 @@ export default defineConfig({
15
15
  '@/const/locale': resolve(__dirname, './src/const/locale'),
16
16
  // TODO: after refactor the errorResponse, we can remove it
17
17
  '@/utils/errorResponse': resolve(__dirname, './src/utils/errorResponse'),
18
+ '@/utils/unzipFile': resolve(__dirname, './src/utils/unzipFile'),
18
19
  '@/utils': resolve(__dirname, './packages/utils/src'),
19
20
  '@/types': resolve(__dirname, './packages/types/src'),
20
21
  '@/const': resolve(__dirname, './packages/const/src'),