1- import { useMemo , useState } from ' react'
2- import { useTranslation } from ' react-i18next'
1+ import { useMemo , useState , useCallback } from " react" ;
2+ import { useTranslation } from " react-i18next" ;
33
44import { Modal , Select , Input , Button , Switch , Tooltip , App } from "antd" ;
55import {
@@ -11,6 +11,7 @@ import {
1111} from "@ant-design/icons" ;
1212
1313import { useConfig } from "@/hooks/useConfig" ;
14+ import { configService } from "@/services/configService" ;
1415import { getConnectivityMeta , ConnectivityStatusType } from "@/lib/utils" ;
1516import { modelService } from "@/services/modelService" ;
1617import { ModelType , SingleModelConfig } from "@/types/modelConfig" ;
@@ -40,68 +41,89 @@ interface ModelAddDialogProps {
4041// Connectivity status type comes from utils
4142
4243// Helper function to translate error messages from backend
43- const translateError = ( errorMessage : string , t : ( key : string , params ?: any ) => string ) : string => {
44- if ( ! errorMessage ) return errorMessage
44+ const translateError = (
45+ errorMessage : string ,
46+ t : ( key : string , params ?: any ) => string
47+ ) : string => {
48+ if ( ! errorMessage ) return errorMessage ;
4549
46- const errorLower = errorMessage . toLowerCase ( )
50+ const errorLower = errorMessage . toLowerCase ( ) ;
4751
4852 // Extract model name from patterns like "Name 'xxx' is already in use"
4953 // Matches: "Name 'xxx' is already in use" or "Name xxx is already in use"
50- const nameMatch = errorMessage . match ( / N a m e \s + (?: [ ' " ] ( [ ^ ' " ] + ) [ ' " ] | ( [ ^ \s , ] + ) ) \s + i s a l r e a d y i n u s e / i)
54+ const nameMatch = errorMessage . match (
55+ / N a m e \s + (?: [ ' " ] ( [ ^ ' " ] + ) [ ' " ] | ( [ ^ \s , ] + ) ) \s + i s a l r e a d y i n u s e / i
56+ ) ;
5157 if ( nameMatch ) {
52- const modelName = nameMatch [ 1 ] || nameMatch [ 2 ]
53- return t ( ' model.dialog.error.nameAlreadyInUse' , { name : modelName } )
58+ const modelName = nameMatch [ 1 ] || nameMatch [ 2 ] ;
59+ return t ( " model.dialog.error.nameAlreadyInUse" , { name : modelName } ) ;
5460 }
5561
5662 // Model not found pattern
57- if ( errorLower . includes ( 'model not found' ) || errorLower . includes ( 'not found' ) ) {
58- const modelNameMatch = errorMessage . match ( / (?: M o d e l n o t f o u n d | n o t f o u n d ) [: \s] + ( [ ^ \s , ] + ) / i)
63+ if (
64+ errorLower . includes ( "model not found" ) ||
65+ errorLower . includes ( "not found" )
66+ ) {
67+ const modelNameMatch = errorMessage . match (
68+ / (?: M o d e l n o t f o u n d | n o t f o u n d ) [: \s] + ( [ ^ \s , ] + ) / i
69+ ) ;
5970 if ( modelNameMatch ) {
60- return t ( ' model.dialog.error.modelNotFound' , { name : modelNameMatch [ 1 ] } )
71+ return t ( " model.dialog.error.modelNotFound" , { name : modelNameMatch [ 1 ] } ) ;
6172 }
62- return t ( ' model.dialog.error.modelNotFound' , { name : '' } )
73+ return t ( " model.dialog.error.modelNotFound" , { name : "" } ) ;
6374 }
6475
6576 // Unsupported model type
66- if ( errorLower . includes ( 'unsupported model type' ) ) {
67- const typeMatch = errorMessage . match ( / u n s u p p o r t e d m o d e l t y p e [: \s] + ( [ ^ \s , ] + ) / i)
77+ if ( errorLower . includes ( "unsupported model type" ) ) {
78+ const typeMatch = errorMessage . match (
79+ / u n s u p p o r t e d m o d e l t y p e [: \s] + ( [ ^ \s , ] + ) / i
80+ ) ;
6881 if ( typeMatch ) {
69- return t ( 'model.dialog.error.unsupportedModelType' , { type : typeMatch [ 1 ] } )
82+ return t ( "model.dialog.error.unsupportedModelType" , {
83+ type : typeMatch [ 1 ] ,
84+ } ) ;
7085 }
71- return t ( ' model.dialog.error.unsupportedModelType' , { type : ' unknown' } )
86+ return t ( " model.dialog.error.unsupportedModelType" , { type : " unknown" } ) ;
7287 }
7388
7489 // Connection failed patterns - extract model name and URL from backend error
75- if ( errorLower . includes ( 'failed to connect' ) || errorLower . includes ( 'connection failed' ) ||
76- errorLower . includes ( 'connection error' ) || errorLower . includes ( 'unable to connect' ) ) {
90+ if (
91+ errorLower . includes ( "failed to connect" ) ||
92+ errorLower . includes ( "connection failed" ) ||
93+ errorLower . includes ( "connection error" ) ||
94+ errorLower . includes ( "unable to connect" )
95+ ) {
7796 // Try to extract model name and URL from pattern: "Failed to connect to model 'xxx' at https://..."
7897 // Match URL that may end with period before the next sentence (e.g., "https://api.example.com. Please verify...")
7998 // Match URL pattern: http:// or https:// followed by domain (may contain dots) and optional path
8099 // Example: "Failed to connect to model 'qwen-plus' at https://api.siliconflow.cn. Please verify..."
81- const connectMatch = errorMessage . match ( / F a i l e d t o c o n n e c t t o m o d e l \s + [ ' " ] ( [ ^ ' " ] + ) [ ' " ] \s + a t \s + ( h t t p s ? : \/ \/ [ ^ \s ] + ?) (?: \. \s | \. $ | $ ) / i)
100+ const connectMatch = errorMessage . match (
101+ / F a i l e d t o c o n n e c t t o m o d e l \s + [ ' " ] ( [ ^ ' " ] + ) [ ' " ] \s + a t \s + ( h t t p s ? : \/ \/ [ ^ \s ] + ?) (?: \. \s | \. $ | $ ) / i
102+ ) ;
82103 if ( connectMatch ) {
83104 // Remove trailing period if present (URL might end with period before next sentence)
84- let url = connectMatch [ 2 ] . replace ( / \. $ / , '' )
105+ let url = connectMatch [ 2 ] . replace ( / \. $ / , "" ) ;
85106 // Return fully translated message with model name and URL
86- return t ( ' model.dialog.error.failedToConnect' , {
107+ return t ( " model.dialog.error.failedToConnect" , {
87108 modelName : connectMatch [ 1 ] ,
88- url : url
89- } )
109+ url : url ,
110+ } ) ;
90111 }
91112 // Fallback: return original error message (will be wrapped by connectivityFailed)
92- return errorMessage
113+ return errorMessage ;
93114 }
94115
95116 // Invalid configuration
96- if ( errorLower . includes ( ' invalid' ) && errorLower . includes ( ' config' ) ) {
117+ if ( errorLower . includes ( " invalid" ) && errorLower . includes ( " config" ) ) {
97118 // Extract the actual error description
98- const configError = errorMessage . replace ( / ^ .* ?i n v a l i d [ ^ : ] * : ? \s * / i, '' ) . trim ( ) || errorMessage
99- return t ( 'model.dialog.error.invalidConfiguration' , { error : configError } )
119+ const configError =
120+ errorMessage . replace ( / ^ .* ?i n v a l i d [ ^ : ] * : ? \s * / i, "" ) . trim ( ) || errorMessage ;
121+ return t ( "model.dialog.error.invalidConfiguration" , { error : configError } ) ;
100122 }
101123
102124 // Return original error if no pattern matches
103- return errorMessage
104- }
125+ return errorMessage ;
126+ } ;
105127
106128export const ModelAddDialog = ( {
107129 isOpen,
@@ -110,26 +132,30 @@ export const ModelAddDialog = ({
110132} : ModelAddDialogProps ) => {
111133 const { t } = useTranslation ( ) ;
112134 const { message } = App . useApp ( ) ;
113- const { updateModelConfig } = useConfig ( ) ;
135+ const { updateModelConfig, getConfig } = useConfig ( ) ;
114136
115137 // Parse backend error message and return i18n key with params
116- const parseModelError = ( errorMessage : string ) : { key : string ; params ?: Record < string , string > } => {
138+ const parseModelError = (
139+ errorMessage : string
140+ ) : { key : string ; params ?: Record < string , string > } => {
117141 if ( ! errorMessage ) {
118- return { key : ' model.dialog.error.addFailed' }
142+ return { key : " model.dialog.error.addFailed" } ;
119143 }
120144
121145 // Check for name conflict error
122- const nameConflictMatch = errorMessage . match ( / N a m e [ ' " ] ? ( [ ^ ' " ] + ) [ ' " ] ? i s a l r e a d y i n u s e / i)
146+ const nameConflictMatch = errorMessage . match (
147+ / N a m e [ ' " ] ? ( [ ^ ' " ] + ) [ ' " ] ? i s a l r e a d y i n u s e / i
148+ ) ;
123149 if ( nameConflictMatch ) {
124150 return {
125- key : ' model.dialog.error.nameConflict' ,
126- params : { name : nameConflictMatch [ 1 ] }
127- }
151+ key : " model.dialog.error.nameConflict" ,
152+ params : { name : nameConflictMatch [ 1 ] } ,
153+ } ;
128154 }
129155
130156 // For other errors, return generic error key without showing backend details
131- return { key : ' model.dialog.error.addFailed' }
132- }
157+ return { key : " model.dialog.error.addFailed" } ;
158+ } ;
133159 const [ form , setForm ] = useState ( {
134160 type : MODEL_TYPES . LLM as ModelType ,
135161 name : "" ,
@@ -166,6 +192,18 @@ export const ModelAddDialog = ({
166192 const [ showModelList , setShowModelList ] = useState ( false ) ;
167193 const [ loadingModelList , setLoadingModelList ] = useState ( false ) ;
168194
195+ const persistModelConfig = useCallback ( async ( ) => {
196+ try {
197+ const ok = await configService . saveConfigToBackend ( getConfig ( ) as any ) ;
198+ if ( ! ok ) {
199+ message . error ( t ( "setup.page.error.saveConfig" ) ) ;
200+ }
201+ } catch ( error ) {
202+ message . error ( t ( "setup.page.error.saveConfig" ) ) ;
203+ log . error ( "Failed to auto save model configuration" , error ) ;
204+ }
205+ } , [ getConfig , message , t ] ) ;
206+
169207 // Settings modal state
170208 const [ settingsModalVisible , setSettingsModalVisible ] = useState ( false ) ;
171209 const [ selectedModelForSettings , setSelectedModelForSettings ] =
@@ -192,22 +230,23 @@ export const ModelAddDialog = ({
192230 } ;
193231
194232 const filteredModelList = useMemo ( ( ) => {
195- const keyword = modelSearchTerm . trim ( ) . toLowerCase ( )
233+ const keyword = modelSearchTerm . trim ( ) . toLowerCase ( ) ;
196234 if ( ! keyword ) {
197- return modelList
235+ return modelList ;
198236 }
199237 return modelList . filter ( ( model : any ) => {
200238 const candidates = [
201239 model . id ,
202240 model . model_name ,
203241 model . model_tag ,
204242 model . description ,
205- ]
206- return candidates . some ( ( text ) =>
207- typeof text === "string" && text . toLowerCase ( ) . includes ( keyword )
208- )
209- } )
210- } , [ modelList , modelSearchTerm ] )
243+ ] ;
244+ return candidates . some (
245+ ( text ) =>
246+ typeof text === "string" && text . toLowerCase ( ) . includes ( keyword )
247+ ) ;
248+ } ) ;
249+ } , [ modelList , modelSearchTerm ] ) ;
211250
212251 // Handle model name change, automatically update the display name
213252 const handleModelNameChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
@@ -311,29 +350,40 @@ export const ModelAddDialog = ({
311350 // Set status to unavailable
312351 setConnectivityStatus ( {
313352 status : "unavailable" ,
314- message : t ( "model.dialog.connectivity.status.unavailable" )
353+ message : t ( "model.dialog.connectivity.status.unavailable" ) ,
315354 } ) ;
316355 // Show detailed error message using internationalized component (same as add failure)
317356 if ( result . error ) {
318- const translatedError = translateError ( result . error , t )
357+ const translatedError = translateError ( result . error , t ) ;
319358 // Ensure translatedError is a valid string, fallback to original error if needed
320- const errorText = ( translatedError && translatedError . length > 0 )
321- ? translatedError
322- : ( result . error || 'Unknown error' )
323- message . error ( t ( 'model.dialog.error.connectivityFailed' , { error : errorText } ) )
359+ const errorText =
360+ translatedError && translatedError . length > 0
361+ ? translatedError
362+ : result . error || "Unknown error" ;
363+ message . error (
364+ t ( "model.dialog.error.connectivityFailed" , { error : errorText } )
365+ ) ;
324366 }
325367 }
326368 } catch ( error ) {
327- const errorMessage = error instanceof Error ? error . message : String ( error )
369+ const errorMessage =
370+ error instanceof Error ? error . message : String ( error ) ;
328371 setConnectivityStatus ( {
329372 status : "unavailable" ,
330373 message : t ( "model.dialog.connectivity.status.unavailable" ) ,
331374 } ) ;
332375 // Show error message using internationalized component (same as add failure)
333- const translatedError = translateError ( errorMessage || t ( 'model.dialog.connectivity.status.unavailable' ) , t )
376+ const translatedError = translateError (
377+ errorMessage || t ( "model.dialog.connectivity.status.unavailable" ) ,
378+ t
379+ ) ;
334380 // Ensure translatedError is a valid string
335- const errorText = translatedError ? translatedError : ( errorMessage || t ( "model.dialog.connectivity.status.unavailable" ) )
336- message . error ( t ( 'model.dialog.error.connectivityFailed' , { error : errorText } ) )
381+ const errorText = translatedError
382+ ? translatedError
383+ : errorMessage || t ( "model.dialog.connectivity.status.unavailable" ) ;
384+ message . error (
385+ t ( "model.dialog.error.connectivityFailed" , { error : errorText } )
386+ ) ;
337387 } finally {
338388 setVerifyingConnectivity ( false ) ;
339389 }
@@ -370,9 +420,12 @@ export const ModelAddDialog = ({
370420 onSuccess ( ) ;
371421 }
372422 } catch ( error : any ) {
373- const errorMessage = error ?. message || t ( "model.dialog.error.addFailedLog" ) ;
374- const translatedError = translateError ( errorMessage , t )
375- message . error ( t ( "model.dialog.error.addFailed" , { error : translatedError } ) ) ;
423+ const errorMessage =
424+ error ?. message || t ( "model.dialog.error.addFailedLog" ) ;
425+ const translatedError = translateError ( errorMessage , t ) ;
426+ message . error (
427+ t ( "model.dialog.error.addFailed" , { error : translatedError } )
428+ ) ;
376429 }
377430
378431 setForm ( ( prev ) => ( {
@@ -409,9 +462,9 @@ export const ModelAddDialog = ({
409462 // Handle adding a model
410463 const handleAddModel = async ( ) => {
411464 // Check connectivity status before adding
412- if ( ! form . isBatchImport && connectivityStatus . status !== ' available' ) {
413- message . warning ( t ( ' model.dialog.error.connectivityRequired' ) )
414- return
465+ if ( ! form . isBatchImport && connectivityStatus . status !== " available" ) {
466+ message . warning ( t ( " model.dialog.error.connectivityRequired" ) ) ;
467+ return ;
415468 }
416469
417470 setLoading ( true ) ;
@@ -495,8 +548,9 @@ export const ModelAddDialog = ({
495548 break ;
496549 }
497550
498- // Save to localStorage
551+ // Save to localStorage and persist to backend
499552 updateModelConfig ( configUpdate ) ;
553+ await persistModelConfig ( ) ;
500554
501555 // Create the returned model information
502556 const addedModel : AddedModel = {
@@ -531,9 +585,12 @@ export const ModelAddDialog = ({
531585 // Close the dialog
532586 onClose ( ) ;
533587 } catch ( error ) {
534- const errorMessage = error instanceof Error ? error . message : String ( error )
535- const translatedError = translateError ( errorMessage , t )
536- message . error ( t ( "model.dialog.error.addFailed" , { error : translatedError } ) ) ;
588+ const errorMessage =
589+ error instanceof Error ? error . message : String ( error ) ;
590+ const translatedError = translateError ( errorMessage , t ) ;
591+ message . error (
592+ t ( "model.dialog.error.addFailed" , { error : translatedError } )
593+ ) ;
537594 log . error ( t ( "model.dialog.error.addFailedLog" ) , error ) ;
538595 } finally {
539596 setLoading ( false ) ;
@@ -838,7 +895,9 @@ export const ModelAddDialog = ({
838895 < Input
839896 allowClear
840897 size = "small"
841- placeholder = { t ( "model.dialog.modelList.searchPlaceholder" ) }
898+ placeholder = { t (
899+ "model.dialog.modelList.searchPlaceholder"
900+ ) }
842901 value = { modelSearchTerm }
843902 onChange = { ( event ) =>
844903 setModelSearchTerm ( event . target . value )
@@ -1079,7 +1138,10 @@ export const ModelAddDialog = ({
10791138 < Button
10801139 type = "primary"
10811140 onClick = { handleAddModel }
1082- disabled = { ! isFormValid ( ) || ( ! form . isBatchImport && connectivityStatus . status !== 'available' ) }
1141+ disabled = {
1142+ ! isFormValid ( ) ||
1143+ ( ! form . isBatchImport && connectivityStatus . status !== "available" )
1144+ }
10831145 loading = { loading }
10841146 >
10851147 { t ( "model.dialog.button.add" ) }
@@ -1113,4 +1175,4 @@ export const ModelAddDialog = ({
11131175 </ Modal >
11141176 </ Modal >
11151177 ) ;
1116- } ;
1178+ } ;
0 commit comments