@lobehub/chat 1.114.5 → 1.114.6

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.
@@ -13,6 +13,7 @@ import { FileUploadStatus } from '@/types/files/upload';
13
13
 
14
14
  import { CONFIG_PANEL_WIDTH } from '../../constants';
15
15
  import { useDragAndDrop } from '../../hooks/useDragAndDrop';
16
+ import { useUploadFilesValidation } from '../../hooks/useUploadFilesValidation';
16
17
  import { useConfigPanelStyles } from '../../style';
17
18
  import ImageManageModal, { type ImageItem } from './ImageManageModal';
18
19
 
@@ -38,6 +39,8 @@ interface DisplayItem {
38
39
  export interface MultiImagesUploadProps {
39
40
  // Callback when URLs change
40
41
  className?: string; // Array of image URLs
42
+ maxCount?: number;
43
+ maxFileSize?: number;
41
44
  onChange?: (urls: string[]) => void;
42
45
  style?: React.CSSProperties;
43
46
  value?: string[];
@@ -311,7 +314,7 @@ const ImageUploadPlaceholder: FC<ImageUploadPlaceholderProps> = memo(({ isDragOv
311
314
 
312
315
  ImageUploadPlaceholder.displayName = 'ImageUploadPlaceholder';
313
316
 
314
- // ======== 圆形进度组件 ======== //
317
+ // ======== Circular Progress Component ======== //
315
318
 
316
319
  interface CircularProgressProps {
317
320
  className?: string;
@@ -466,7 +469,7 @@ const ImageThumbnails: FC<ImageThumbnailsProps> = memo(
466
469
  const showOverlay = isLastItem && remainingCount > 1;
467
470
 
468
471
  return (
469
- <div className={styles.imageItem} key={imageUrl}>
472
+ <div className={styles.imageItem} key={`${imageUrl}-${index}`}>
470
473
  <Image
471
474
  alt={`Uploaded image ${index + 1}`}
472
475
  fill
@@ -559,12 +562,13 @@ SingleImageDisplay.displayName = 'SingleImageDisplay';
559
562
  // ======== Main Component ======== //
560
563
 
561
564
  const MultiImagesUpload: FC<MultiImagesUploadProps> = memo(
562
- ({ value, onChange, style, className }) => {
565
+ ({ value, onChange, style, className, maxCount, maxFileSize }) => {
563
566
  const inputRef = useRef<HTMLInputElement>(null);
564
567
  const uploadWithProgress = useFileStore((s) => s.uploadWithProgress);
565
568
  const [displayItems, setDisplayItems] = useState<DisplayItem[]>([]);
566
569
  const [modalOpen, setModalOpen] = useState(false);
567
570
  const { styles: configStyles } = useConfigPanelStyles();
571
+ const { validateFiles } = useUploadFilesValidation(maxCount, maxFileSize);
568
572
 
569
573
  // Cleanup blob URLs to prevent memory leaks
570
574
  useEffect(() => {
@@ -601,6 +605,11 @@ const MultiImagesUpload: FC<MultiImagesUploadProps> = memo(
601
605
 
602
606
  const currentUrls = baseUrls !== undefined ? baseUrls : value || [];
603
607
 
608
+ // Validate files, pass current image count
609
+ if (!validateFiles(files, currentUrls.length)) {
610
+ return;
611
+ }
612
+
604
613
  // Create initial display items with blob URLs for immediate preview
605
614
  const newDisplayItems: DisplayItem[] = files.map((file) => ({
606
615
  file,
@@ -716,17 +725,17 @@ const MultiImagesUpload: FC<MultiImagesUploadProps> = memo(
716
725
  onDrop: handleDrop,
717
726
  });
718
727
 
719
- // 处理 Modal 完成回调
728
+ // Handle Modal completion callback
720
729
  const handleModalComplete = async (imageItems: ImageItem[]) => {
721
- // 分离现有URL和新文件
730
+ // Separate existing URLs and new files
722
731
  const existingUrls = imageItems.filter((item) => item.url).map((item) => item.url!);
723
732
 
724
733
  const newFiles = imageItems.filter((item) => item.file).map((item) => item.file!);
725
734
 
726
- // 立即更新现有URL(删除的图片会被过滤掉)
735
+ // Immediately update existing URLs (deleted images will be filtered out)
727
736
  onChange?.(existingUrls);
728
737
 
729
- // 如果有新文件需要上传,基于 existingUrls 启动上传流程
738
+ // If there are new files to upload, start upload process based on existingUrls
730
739
  if (newFiles.length > 0) {
731
740
  await handleFilesSelected(newFiles, existingUrls);
732
741
  }
@@ -793,6 +802,7 @@ const MultiImagesUpload: FC<MultiImagesUploadProps> = memo(
793
802
  {/* Image Management Modal */}
794
803
  <ImageManageModal
795
804
  images={value || []}
805
+ maxCount={maxCount}
796
806
  onClose={handleCloseModal}
797
807
  onComplete={handleModalComplete}
798
808
  open={modalOpen}
@@ -0,0 +1,77 @@
1
+ import { App } from 'antd';
2
+ import { useCallback } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ import { formatFileSize, validateImageFiles } from '../utils/imageValidation';
6
+
7
+ /**
8
+ * File upload validation hook
9
+ * Encapsulates file size and count validation logic, provides user-friendly error messages
10
+ */
11
+ export const useUploadFilesValidation = (maxCount?: number, maxFileSize?: number) => {
12
+ const { t } = useTranslation('components');
13
+ const { message } = App.useApp();
14
+
15
+ const validateFiles = useCallback(
16
+ (files: File[], currentCount: number = 0): boolean => {
17
+ const validationResult = validateImageFiles(files, {
18
+ maxAddedFiles: maxCount ? maxCount - currentCount : undefined,
19
+ maxFileSize,
20
+ });
21
+
22
+ if (!validationResult.valid) {
23
+ // Display user-friendly error messages
24
+ validationResult.errors.forEach((error) => {
25
+ if (error === 'fileSizeExceeded') {
26
+ // Collect all failed files info
27
+ const fileSizeFailures =
28
+ validationResult.failedFiles?.filter(
29
+ (failedFile) =>
30
+ failedFile.error === 'fileSizeExceeded' &&
31
+ failedFile.actualSize &&
32
+ failedFile.maxSize,
33
+ ) || [];
34
+
35
+ if (fileSizeFailures.length === 1) {
36
+ // Single file error - show detailed message
37
+ const failedFile = fileSizeFailures[0];
38
+ const actualSizeStr = formatFileSize(failedFile.actualSize!);
39
+ const maxSizeStr = formatFileSize(failedFile.maxSize!);
40
+ const fileName = failedFile.fileName || 'File';
41
+ message.error(
42
+ t('MultiImagesUpload.validation.fileSizeExceededDetail', {
43
+ actualSize: actualSizeStr,
44
+ fileName,
45
+ maxSize: maxSizeStr,
46
+ }),
47
+ );
48
+ } else if (fileSizeFailures.length > 1) {
49
+ // Multiple files error - show summary message
50
+ const maxSizeStr = formatFileSize(fileSizeFailures[0].maxSize!);
51
+ const fileList = fileSizeFailures
52
+ .map((f) => `${f.fileName || 'File'} (${formatFileSize(f.actualSize!)})`)
53
+ .join(', ');
54
+ message.error(
55
+ t('MultiImagesUpload.validation.fileSizeExceededMultiple', {
56
+ count: fileSizeFailures.length,
57
+ fileList,
58
+ maxSize: maxSizeStr,
59
+ }),
60
+ );
61
+ }
62
+ } else if (error === 'imageCountExceeded') {
63
+ message.error(t('MultiImagesUpload.validation.imageCountExceeded'));
64
+ }
65
+ });
66
+ return false;
67
+ }
68
+
69
+ return true;
70
+ },
71
+ [maxCount, maxFileSize, message, t],
72
+ );
73
+
74
+ return {
75
+ validateFiles,
76
+ };
77
+ };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Image file validation utility functions
3
+ */
4
+
5
+ /**
6
+ * Format file size to human readable format
7
+ * @param bytes - File size in bytes
8
+ * @returns Formatted string like "1.5 MB"
9
+ */
10
+ export const formatFileSize = (bytes: number): string => {
11
+ if (bytes === 0) return '0 B';
12
+
13
+ const k = 1024;
14
+ const sizes = ['B', 'KB', 'MB', 'GB'];
15
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
16
+
17
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
18
+ };
19
+
20
+ export interface ValidationResult {
21
+ // Additional details for error messages
22
+ actualSize?: number;
23
+ error?: string;
24
+ fileName?: string;
25
+ maxSize?: number;
26
+ valid: boolean;
27
+ }
28
+
29
+ /**
30
+ * Validate single image file size
31
+ * @param file - File to validate
32
+ * @param maxSize - Maximum file size in bytes, defaults to 10MB if not provided
33
+ * @returns Validation result
34
+ */
35
+ export const validateImageFileSize = (file: File, maxSize?: number): ValidationResult => {
36
+ const defaultMaxSize = 10 * 1024 * 1024; // 10MB default limit
37
+ const actualMaxSize = maxSize ?? defaultMaxSize;
38
+
39
+ if (file.size > actualMaxSize) {
40
+ return {
41
+ actualSize: file.size,
42
+ error: 'fileSizeExceeded',
43
+ fileName: file.name,
44
+ maxSize: actualMaxSize,
45
+ valid: false,
46
+ };
47
+ }
48
+
49
+ return { valid: true };
50
+ };
51
+
52
+ /**
53
+ * Validate image count
54
+ * @param count - Current image count
55
+ * @param maxCount - Maximum allowed count, skip validation if not provided
56
+ * @returns Validation result
57
+ */
58
+ export const validateImageCount = (count: number, maxCount?: number): ValidationResult => {
59
+ if (!maxCount) return { valid: true };
60
+
61
+ if (count > maxCount) {
62
+ return {
63
+ error: 'imageCountExceeded',
64
+ valid: false,
65
+ };
66
+ }
67
+
68
+ return { valid: true };
69
+ };
70
+
71
+ /**
72
+ * Validate image file list
73
+ * @param files - File list
74
+ * @param constraints - Constraint configuration
75
+ * @returns Validation result, including validation result for each file
76
+ */
77
+ export const validateImageFiles = (
78
+ files: File[],
79
+ constraints: {
80
+ maxAddedFiles?: number;
81
+ maxFileSize?: number;
82
+ },
83
+ ): {
84
+ errors: string[];
85
+ // Additional details for error messages
86
+ failedFiles?: ValidationResult[];
87
+ fileResults: ValidationResult[];
88
+ valid: boolean;
89
+ } => {
90
+ const errors: string[] = [];
91
+ const fileResults: ValidationResult[] = [];
92
+ const failedFiles: ValidationResult[] = [];
93
+
94
+ // Validate file count
95
+ const countResult = validateImageCount(files.length, constraints.maxAddedFiles);
96
+ if (!countResult.valid && countResult.error) {
97
+ errors.push(countResult.error);
98
+ }
99
+
100
+ // Validate each file
101
+ files.forEach((file) => {
102
+ const fileSizeResult = validateImageFileSize(file, constraints.maxFileSize);
103
+ fileResults.push(fileSizeResult);
104
+
105
+ if (!fileSizeResult.valid && fileSizeResult.error) {
106
+ errors.push(fileSizeResult.error);
107
+ failedFiles.push(fileSizeResult);
108
+ }
109
+ });
110
+
111
+ return {
112
+ errors: Array.from(new Set(errors)), // Remove duplicates
113
+ failedFiles,
114
+ fileResults,
115
+ valid: errors.length === 0,
116
+ };
117
+ };
@@ -40,6 +40,7 @@ export const ModelParamsMetaSchema = z.object({
40
40
  .object({
41
41
  default: z.string().nullable().optional(),
42
42
  description: z.string().optional(),
43
+ maxFileSize: z.number().optional(),
43
44
  type: z.tuple([z.literal('string'), z.literal('null')]).optional(),
44
45
  })
45
46
  .optional(),
@@ -48,6 +49,8 @@ export const ModelParamsMetaSchema = z.object({
48
49
  .object({
49
50
  default: z.array(z.string()),
50
51
  description: z.string().optional(),
52
+ maxCount: z.number().optional(),
53
+ maxFileSize: z.number().optional(),
51
54
  type: z.literal('array').optional(),
52
55
  })
53
56
  .optional(),
@@ -133,6 +133,14 @@ export default {
133
133
  progress: {
134
134
  uploadingWithCount: '{{completed}}/{{total}} 已上传',
135
135
  },
136
+ validation: {
137
+ fileSizeExceeded: 'File size exceeded limit',
138
+ fileSizeExceededDetail:
139
+ '{{fileName}} ({{actualSize}}) exceeds the maximum size limit of {{maxSize}}',
140
+ fileSizeExceededMultiple:
141
+ '{{count}} files exceed the maximum size limit of {{maxSize}}: {{fileList}}',
142
+ imageCountExceeded: 'Image count exceeded limit',
143
+ },
136
144
  },
137
145
  OllamaSetupGuide: {
138
146
  action: {
@@ -6,7 +6,7 @@ import { getModelListByType } from '../action';
6
6
 
7
7
  // Mock getModelPropertyWithFallback
8
8
  vi.mock('@/utils/getFallbackModelProperty', () => ({
9
- getModelPropertyWithFallback: vi.fn().mockReturnValue({ size: '1024x1024' }),
9
+ getModelPropertyWithFallback: vi.fn().mockResolvedValue({ size: '1024x1024' }),
10
10
  }));
11
11
 
12
12
  describe('getModelListByType', () => {
@@ -48,9 +48,9 @@ describe('getModelListByType', () => {
48
48
  abilities: {} as ModelAbilities,
49
49
  displayName: 'DALL-E 3',
50
50
  enabled: true,
51
- parameters: {
51
+ parameters: {
52
52
  prompt: { default: '' },
53
- size: { default: '1024x1024', enum: ['512x512', '1024x1024', '1536x1536'] }
53
+ size: { default: '1024x1024', enum: ['512x512', '1024x1024', '1536x1536'] },
54
54
  },
55
55
  },
56
56
  {
@@ -66,15 +66,15 @@ describe('getModelListByType', () => {
66
66
  const allModels = [...mockChatModels, ...mockImageModels];
67
67
 
68
68
  describe('basic functionality', () => {
69
- it('should filter models by providerId and type correctly', () => {
70
- const result = getModelListByType(allModels, 'openai', 'chat');
69
+ it('should filter models by providerId and type correctly', async () => {
70
+ const result = await getModelListByType(allModels, 'openai', 'chat');
71
71
 
72
72
  expect(result).toHaveLength(2);
73
73
  expect(result.map((m) => m.id)).toEqual(['gpt-4', 'gpt-3.5-turbo']);
74
74
  });
75
75
 
76
- it('should return correct model structure', () => {
77
- const result = getModelListByType(allModels, 'openai', 'chat');
76
+ it('should return correct model structure', async () => {
77
+ const result = await getModelListByType(allModels, 'openai', 'chat');
78
78
 
79
79
  expect(result[0]).toEqual({
80
80
  abilities: { functionCall: true, files: true },
@@ -84,23 +84,23 @@ describe('getModelListByType', () => {
84
84
  });
85
85
  });
86
86
 
87
- it('should add parameters field for image models', () => {
88
- const result = getModelListByType(allModels, 'openai', 'image');
87
+ it('should add parameters field for image models', async () => {
88
+ const result = await getModelListByType(allModels, 'openai', 'image');
89
89
 
90
90
  expect(result[0]).toEqual({
91
91
  abilities: {},
92
92
  contextWindowTokens: undefined,
93
93
  displayName: 'DALL-E 3',
94
94
  id: 'dall-e-3',
95
- parameters: {
95
+ parameters: {
96
96
  prompt: { default: '' },
97
- size: { default: '1024x1024', enum: ['512x512', '1024x1024', '1536x1536'] }
97
+ size: { default: '1024x1024', enum: ['512x512', '1024x1024', '1536x1536'] },
98
98
  },
99
99
  });
100
100
  });
101
101
 
102
- it('should use fallback parameters for image models without parameters', () => {
103
- const result = getModelListByType(allModels, 'midjourney', 'image');
102
+ it('should use fallback parameters for image models without parameters', async () => {
103
+ const result = await getModelListByType(allModels, 'midjourney', 'image');
104
104
 
105
105
  expect(result[0]).toEqual({
106
106
  abilities: {},
@@ -113,22 +113,22 @@ describe('getModelListByType', () => {
113
113
  });
114
114
 
115
115
  describe('edge cases', () => {
116
- it('should handle empty model list', () => {
117
- const result = getModelListByType([], 'openai', 'chat');
116
+ it('should handle empty model list', async () => {
117
+ const result = await getModelListByType([], 'openai', 'chat');
118
118
  expect(result).toEqual([]);
119
119
  });
120
120
 
121
- it('should handle non-existent providerId', () => {
122
- const result = getModelListByType(allModels, 'nonexistent', 'chat');
121
+ it('should handle non-existent providerId', async () => {
122
+ const result = await getModelListByType(allModels, 'nonexistent', 'chat');
123
123
  expect(result).toEqual([]);
124
124
  });
125
125
 
126
- it('should handle non-existent type', () => {
127
- const result = getModelListByType(allModels, 'openai', 'nonexistent');
126
+ it('should handle non-existent type', async () => {
127
+ const result = await getModelListByType(allModels, 'openai', 'nonexistent');
128
128
  expect(result).toEqual([]);
129
129
  });
130
130
 
131
- it('should handle missing displayName', () => {
131
+ it('should handle missing displayName', async () => {
132
132
  const modelsWithoutDisplayName: EnabledAiModel[] = [
133
133
  {
134
134
  id: 'test-model',
@@ -139,11 +139,11 @@ describe('getModelListByType', () => {
139
139
  },
140
140
  ];
141
141
 
142
- const result = getModelListByType(modelsWithoutDisplayName, 'test', 'chat');
142
+ const result = await getModelListByType(modelsWithoutDisplayName, 'test', 'chat');
143
143
  expect(result[0].displayName).toBe('');
144
144
  });
145
145
 
146
- it('should handle missing abilities', () => {
146
+ it('should handle missing abilities', async () => {
147
147
  const modelsWithoutAbilities: EnabledAiModel[] = [
148
148
  {
149
149
  id: 'test-model',
@@ -153,13 +153,13 @@ describe('getModelListByType', () => {
153
153
  } as EnabledAiModel,
154
154
  ];
155
155
 
156
- const result = getModelListByType(modelsWithoutAbilities, 'test', 'chat');
156
+ const result = await getModelListByType(modelsWithoutAbilities, 'test', 'chat');
157
157
  expect(result[0].abilities).toEqual({});
158
158
  });
159
159
  });
160
160
 
161
161
  describe('deduplication', () => {
162
- it('should remove duplicate model IDs', () => {
162
+ it('should remove duplicate model IDs', async () => {
163
163
  const duplicateModels: EnabledAiModel[] = [
164
164
  {
165
165
  id: 'gpt-4',
@@ -179,7 +179,7 @@ describe('getModelListByType', () => {
179
179
  },
180
180
  ];
181
181
 
182
- const result = getModelListByType(duplicateModels, 'openai', 'chat');
182
+ const result = await getModelListByType(duplicateModels, 'openai', 'chat');
183
183
 
184
184
  expect(result).toHaveLength(1);
185
185
  expect(result[0].displayName).toBe('GPT-4 Version 1');
@@ -187,7 +187,7 @@ describe('getModelListByType', () => {
187
187
  });
188
188
 
189
189
  describe('type casting', () => {
190
- it('should handle image model type casting correctly', () => {
190
+ it('should handle image model type casting correctly', async () => {
191
191
  const imageModel: EnabledAiModel[] = [
192
192
  {
193
193
  id: 'dall-e-3',
@@ -200,14 +200,14 @@ describe('getModelListByType', () => {
200
200
  } as any, // Simulate AIImageModelCard type
201
201
  ];
202
202
 
203
- const result = getModelListByType(imageModel, 'openai', 'image');
203
+ const result = await getModelListByType(imageModel, 'openai', 'image');
204
204
 
205
205
  expect(result[0]).toHaveProperty('parameters');
206
206
  expect(result[0].parameters).toEqual({ size: '1024x1024' });
207
207
  });
208
208
 
209
- it('should not add parameters field for non-image models', () => {
210
- const result = getModelListByType(mockChatModels, 'openai', 'chat');
209
+ it('should not add parameters field for non-image models', async () => {
210
+ const result = await getModelListByType(mockChatModels, 'openai', 'chat');
211
211
 
212
212
  result.forEach((model) => {
213
213
  expect(model).not.toHaveProperty('parameters');
@@ -6,7 +6,12 @@ import { isDeprecatedEdition, isDesktop, isUsePgliteDB } from '@/const/version';
6
6
  import { useClientDataSWR } from '@/libs/swr';
7
7
  import { aiProviderService } from '@/services/aiProvider';
8
8
  import { AiInfraStore } from '@/store/aiInfra/store';
9
- import { AIImageModelCard, LobeDefaultAiModelListItem, ModelAbilities } from '@/types/aiModel';
9
+ import {
10
+ AIImageModelCard,
11
+ EnabledAiModel,
12
+ LobeDefaultAiModelListItem,
13
+ ModelAbilities,
14
+ } from '@/types/aiModel';
10
15
  import {
11
16
  AiProviderDetailItem,
12
17
  AiProviderListItem,
@@ -15,6 +20,7 @@ import {
15
20
  AiProviderSourceEnum,
16
21
  CreateAiProviderParams,
17
22
  EnabledProvider,
23
+ EnabledProviderWithModels,
18
24
  UpdateAiProviderConfigParams,
19
25
  UpdateAiProviderParams,
20
26
  } from '@/types/aiProvider';
@@ -23,10 +29,17 @@ import { getModelPropertyWithFallback } from '@/utils/getFallbackModelProperty';
23
29
  /**
24
30
  * Get models by provider ID and type, with proper formatting and deduplication
25
31
  */
26
- export const getModelListByType = (enabledAiModels: any[], providerId: string, type: string) => {
27
- const models = enabledAiModels
28
- .filter((model) => model.providerId === providerId && model.type === type)
29
- .map((model) => ({
32
+ export const getModelListByType = async (
33
+ enabledAiModels: EnabledAiModel[],
34
+ providerId: string,
35
+ type: string,
36
+ ) => {
37
+ const filteredModels = enabledAiModels.filter(
38
+ (model) => model.providerId === providerId && model.type === type,
39
+ );
40
+
41
+ const models = await Promise.all(
42
+ filteredModels.map(async (model) => ({
30
43
  abilities: (model.abilities || {}) as ModelAbilities,
31
44
  contextWindowTokens: model.contextWindowTokens,
32
45
  displayName: model.displayName ?? '',
@@ -34,13 +47,31 @@ export const getModelListByType = (enabledAiModels: any[], providerId: string, t
34
47
  ...(model.type === 'image' && {
35
48
  parameters:
36
49
  (model as AIImageModelCard).parameters ||
37
- getModelPropertyWithFallback(model.id, 'parameters'),
50
+ (await getModelPropertyWithFallback(model.id, 'parameters')),
38
51
  }),
39
- }));
52
+ })),
53
+ );
40
54
 
41
55
  return uniqBy(models, 'id');
42
56
  };
43
57
 
58
+ /**
59
+ * Build provider model lists with proper async handling
60
+ */
61
+ const buildProviderModelLists = async (
62
+ providers: EnabledProvider[],
63
+ enabledAiModels: EnabledAiModel[],
64
+ type: 'chat' | 'image',
65
+ ) => {
66
+ return Promise.all(
67
+ providers.map(async (provider) => ({
68
+ ...provider,
69
+ children: await getModelListByType(enabledAiModels, provider.id, type),
70
+ name: provider.name || provider.id,
71
+ })),
72
+ );
73
+ };
74
+
44
75
  enum AiProviderSwrKey {
45
76
  fetchAiProviderItem = 'FETCH_AI_PROVIDER_ITEM',
46
77
  fetchAiProviderList = 'FETCH_AI_PROVIDER',
@@ -49,6 +80,8 @@ enum AiProviderSwrKey {
49
80
 
50
81
  type AiProviderRuntimeStateWithBuiltinModels = AiProviderRuntimeState & {
51
82
  builtinAiModelList: LobeDefaultAiModelListItem[];
83
+ enabledChatModelList?: EnabledProviderWithModels[];
84
+ enabledImageModelList?: EnabledProviderWithModels[];
52
85
  };
53
86
 
54
87
  export interface AiProviderAction {
@@ -203,31 +236,54 @@ export const createAiProviderSlice: StateCreator<
203
236
 
204
237
  if (isLogin) {
205
238
  const data = await aiProviderService.getAiProviderRuntimeState();
239
+
240
+ // Build model lists with proper async handling
241
+ const [enabledChatModelList, enabledImageModelList] = await Promise.all([
242
+ buildProviderModelLists(data.enabledChatAiProviders, data.enabledAiModels, 'chat'),
243
+ buildProviderModelLists(data.enabledImageAiProviders, data.enabledAiModels, 'image'),
244
+ ]);
245
+
206
246
  return {
207
247
  ...data,
208
248
  builtinAiModelList,
249
+ enabledChatModelList,
250
+ enabledImageModelList,
209
251
  };
210
252
  }
211
253
 
212
254
  const enabledAiProviders: EnabledProvider[] = DEFAULT_MODEL_PROVIDER_LIST.filter(
213
255
  (provider) => provider.enabled,
214
- ).map((item) => ({ id: item.id, name: item.name, source: 'builtin' }));
215
- return {
216
- builtinAiModelList,
217
- enabledAiModels: builtinAiModelList.filter((m) => m.enabled),
218
- enabledAiProviders: enabledAiProviders,
219
- enabledChatAiProviders: enabledAiProviders.filter((provider) => {
256
+ ).map((item) => ({ id: item.id, name: item.name, source: AiProviderSourceEnum.Builtin }));
257
+
258
+ const enabledChatAiProviders = enabledAiProviders.filter((provider) => {
259
+ return builtinAiModelList.some(
260
+ (model) => model.providerId === provider.id && model.type === 'chat',
261
+ );
262
+ });
263
+
264
+ const enabledImageAiProviders = enabledAiProviders
265
+ .filter((provider) => {
220
266
  return builtinAiModelList.some(
221
- (model) => model.providerId === provider.id && model.type === 'chat',
267
+ (model) => model.providerId === provider.id && model.type === 'image',
222
268
  );
223
- }),
224
- enabledImageAiProviders: enabledAiProviders
225
- .filter((provider) => {
226
- return builtinAiModelList.some(
227
- (model) => model.providerId === provider.id && model.type === 'image',
228
- );
229
- })
230
- .map((item) => ({ id: item.id, name: item.name, source: 'builtin' })),
269
+ })
270
+ .map((item) => ({ id: item.id, name: item.name, source: AiProviderSourceEnum.Builtin }));
271
+
272
+ // Build model lists for non-login state as well
273
+ const enabledAiModels = builtinAiModelList.filter((m) => m.enabled);
274
+ const [enabledChatModelList, enabledImageModelList] = await Promise.all([
275
+ buildProviderModelLists(enabledChatAiProviders, enabledAiModels, 'chat'),
276
+ buildProviderModelLists(enabledImageAiProviders, enabledAiModels, 'image'),
277
+ ]);
278
+
279
+ return {
280
+ builtinAiModelList,
281
+ enabledAiModels,
282
+ enabledAiProviders,
283
+ enabledChatAiProviders,
284
+ enabledChatModelList,
285
+ enabledImageAiProviders,
286
+ enabledImageModelList,
231
287
  runtimeConfig: {},
232
288
  };
233
289
  },
@@ -236,26 +292,14 @@ export const createAiProviderSlice: StateCreator<
236
292
  onSuccess: (data) => {
237
293
  if (!data) return;
238
294
 
239
- const enabledChatModelList = data.enabledChatAiProviders.map((provider) => ({
240
- ...provider,
241
- children: getModelListByType(data.enabledAiModels, provider.id, 'chat'),
242
- name: provider.name || provider.id,
243
- }));
244
-
245
- const enabledImageModelList = data.enabledImageAiProviders.map((provider) => ({
246
- ...provider,
247
- children: getModelListByType(data.enabledAiModels, provider.id, 'image'),
248
- name: provider.name || provider.id,
249
- }));
250
-
251
295
  set(
252
296
  {
253
297
  aiProviderRuntimeConfig: data.runtimeConfig,
254
298
  builtinAiModelList: data.builtinAiModelList,
255
299
  enabledAiModels: data.enabledAiModels,
256
300
  enabledAiProviders: data.enabledAiProviders,
257
- enabledChatModelList,
258
- enabledImageModelList,
301
+ enabledChatModelList: data.enabledChatModelList || [],
302
+ enabledImageModelList: data.enabledImageModelList || [],
259
303
  },
260
304
  false,
261
305
  'useFetchAiProviderRuntimeState',