@@ -34,6 +34,7 @@ import orderBy from 'lodash/orderBy'
3434
3535
3636import ConceptIcon from '../concepts/ConceptIcon'
37+ import APIService from '../../services/APIService' ;
3738
3839/**
3940 * MultiAlgoSelector (MUI5)
@@ -78,6 +79,7 @@ export default function MultiAlgoSelector({
7879 const { t } = useTranslation ( )
7980 const [ expanded , setExpanded ] = useState ( ( ) => new Map ( ) ) ;
8081 const [ errors , setErrors ] = React . useState ( { } )
82+ const [ customAlgoMeta , setCustomAlgoMeta ] = React . useState ( { } )
8183
8284 const normalizedValue = useMemo ( ( ) => {
8385 let changed = false ;
@@ -152,6 +154,7 @@ export default function MultiAlgoSelector({
152154 const removeSelected = ( key ) => {
153155 const next = ( value || [ ] ) . filter ( ( v ) => v . __key !== key ) ;
154156 onChange ( next ) ;
157+ setCustomAlgoMeta ( prev => omit ( prev , [ key ] ) ) ;
155158
156159 setExpanded ( ( prev ) => {
157160 const n = new Map ( prev ) ;
@@ -219,6 +222,68 @@ export default function MultiAlgoSelector({
219222 return < TuneRoundedIcon sx = { { fontSize : '1.5rem' , color : 'warning.main' } } />
220223
221224 } ;
225+
226+ const setCustomAlgoState = ( key , patch ) => {
227+ setCustomAlgoMeta ( prev => ( { ...prev , [ key ] : { ...prev [ key ] , ...patch } } ) ) ;
228+ } ;
229+
230+ const verifyCustomAlgorithm = async ( key , tokenOverride ) => {
231+ const selected = normalizedValue . find ( v => v . __key === key ) ;
232+ if ( ! selected ?. url )
233+ return ;
234+
235+ const token = tokenOverride ?? selected ?. token ;
236+ setCustomAlgoState ( key , { isLoading : true , requestError : '' , requestSuccess : '' , tokenRequired : false } ) ;
237+
238+ const service = APIService . new ( ) ;
239+ service . URL = selected . url ;
240+
241+ const response = await service . get ( token || false , { } , undefined , true ) ;
242+ const status = response ?. response ?. status || response ?. status ;
243+
244+ if ( [ 401 , 403 ] . includes ( status ) ) {
245+ setCustomAlgoState ( key , {
246+ isLoading : false ,
247+ tokenRequired : true ,
248+ requestError : '' ,
249+ requestSuccess : '' ,
250+ } ) ;
251+ return ;
252+ }
253+
254+ if ( response ?. response || response ?. detail || response ?. message === 'Network Error' || typeof response === 'string' ) {
255+ setCustomAlgoState ( key , {
256+ isLoading : false ,
257+ tokenRequired : false ,
258+ requestError : response ?. response ?. data ?. detail || response ?. detail || response ?. message || response || t ( 'unknown_error' ) ,
259+ requestSuccess : '' ,
260+ } ) ;
261+ return ;
262+ }
263+
264+ const data = response ?. data || response ;
265+ if ( ! data ?. name || ! ( data ?. ID || data ?. id ) ) {
266+ setCustomAlgoState ( key , {
267+ isLoading : false ,
268+ tokenRequired : false ,
269+ requestError : t ( 'unknown_error' ) ,
270+ requestSuccess : '' ,
271+ } ) ;
272+ return ;
273+ }
274+
275+ updateSelected ( key , {
276+ id : data . ID || data . id || '' ,
277+ name : data . name || '' ,
278+ description : data . description || '' ,
279+ } ) ;
280+ setCustomAlgoState ( key , {
281+ isLoading : false ,
282+ tokenRequired : false ,
283+ requestError : '' ,
284+ requestSuccess : t ( 'common.saved' ) ,
285+ } ) ;
286+ } ;
222287 return (
223288 < Box sx = { { width : "100%" } } >
224289 < Stack spacing = { 1.5 } >
@@ -228,6 +293,7 @@ export default function MultiAlgoSelector({
228293
229294 const isOpen = expanded . get ( sel . __key ) ?? false ;
230295 const hasErrors = errors [ sel . __key ] ?. id || errors [ sel . __key ] ?. name
296+ const customMeta = customAlgoMeta [ sel . __key ] || { }
231297
232298 return (
233299 < Paper
@@ -320,6 +386,51 @@ export default function MultiAlgoSelector({
320386 borderLeftStyle : "solid" ,
321387 } }
322388 >
389+ < Stack spacing = { 1.5 } >
390+ < TextField
391+ fullWidth
392+ required
393+ label = { t ( 'map_project.api_url' ) }
394+ value = { sel . url || "" }
395+ onChange = { ( e ) => {
396+ updateSelected ( sel . __key , { url : e . target . value } ) ;
397+ if ( customMeta . requestError || customMeta . requestSuccess || customMeta . tokenRequired ) {
398+ setCustomAlgoState ( sel . __key , {
399+ requestError : '' ,
400+ requestSuccess : '' ,
401+ tokenRequired : false ,
402+ } ) ;
403+ }
404+ } }
405+ onBlur = { ( ) => verifyCustomAlgorithm ( sel . __key ) }
406+ placeholder = "https://example.com/match"
407+ error = { Boolean ( customMeta . requestError ) }
408+ helperText = { customMeta . requestError || customMeta . requestSuccess || '' }
409+ />
410+ < TextField
411+ fullWidth
412+ required = { Boolean ( customMeta . tokenRequired ) }
413+ type = 'password'
414+ label = { t ( 'map_project.api_token' ) }
415+ value = { sel . token || "" }
416+ onChange = { ( e ) => {
417+ updateSelected ( sel . __key , { token : e . target . value } ) ;
418+ if ( customMeta . requestError || customMeta . requestSuccess ) {
419+ setCustomAlgoState ( sel . __key , {
420+ requestError : '' ,
421+ requestSuccess : '' ,
422+ } ) ;
423+ }
424+ } }
425+ onBlur = { ( ) => {
426+ if ( sel . url && eHasValue ( sel . token || '' ) ) {
427+ verifyCustomAlgorithm ( sel . __key , sel . token ) ;
428+ }
429+ } }
430+ placeholder = "••••••••"
431+ error = { Boolean ( customMeta . tokenRequired && ! sel . token ) }
432+ helperText = { customMeta . tokenRequired && ! sel . token ? 'Authentication is required for this algorithm.' : '' }
433+ />
323434 < Stack direction = { { xs : "column" , sm : "row" } } spacing = { 1.5 } >
324435 < TextField
325436 sx = { { width : '50%' } }
@@ -346,34 +457,14 @@ export default function MultiAlgoSelector({
346457 helperText = { errors [ sel . __key ] ?. name ? t ( 'map_project.algo_conflicting_name' ) : '' }
347458 />
348459 </ Stack >
349- < Stack direction = { { xs : "column" , sm : "row" } } sx = { { marginTop : '12px' } } spacing = { 1.5 } >
350460 < TextField
351461 fullWidth
352462 label = { t ( 'common.description' ) }
353463 value = { sel . description || '' }
354464 onChange = { ( e ) =>
355- updateSelected ( sel . __key , { id : e . target . value || '' } )
465+ updateSelected ( sel . __key , { description : e . target . value || '' } )
356466 }
357467 />
358- </ Stack >
359- < Stack spacing = { 1.5 } sx = { { marginTop : '12px' } } >
360- < TextField
361- fullWidth
362- required
363- label = { t ( 'map_project.api_url' ) }
364- value = { sel . url || "" }
365- onChange = { ( e ) => updateSelected ( sel . __key , { url : e . target . value } ) }
366- placeholder = "https://example.com/match"
367- />
368- < TextField
369- fullWidth
370- type = 'password'
371- label = { t ( 'map_project.api_token' ) }
372- value = { sel . token || "" }
373- onChange = { ( e ) => updateSelected ( sel . __key , { token : e . target . value } ) }
374- placeholder = "••••••••"
375- />
376-
377468 < Stack direction = { { xs : "column" , sm : "row" } } spacing = { 1.5 } >
378469 < TextField
379470 label = { t ( 'map_project.batch_size' ) }
@@ -511,3 +602,7 @@ function clampInt(value, min, max) {
511602 if ( Number . isNaN ( n ) ) return min ;
512603 return Math . max ( min , Math . min ( max , n ) ) ;
513604}
605+
606+ function eHasValue ( value ) {
607+ return Boolean ( value && String ( value ) . trim ( ) ) ;
608+ }
0 commit comments