11import { useT } from '@open-codesign/i18n' ;
22import { type WireApi , canonicalBaseUrl , detectWireFromBaseUrl } from '@open-codesign/shared' ;
33import { Button } from '@open-codesign/ui' ;
4- import { AlertCircle , Check , CheckCircle , Loader2 , X } from 'lucide-react' ;
5- import { useRef , useState } from 'react' ;
4+ import { AlertCircle , CheckCircle , Loader2 , X } from 'lucide-react' ;
5+ import { useState } from 'react' ;
66
77interface Props {
88 onSave : ( ) => void ;
@@ -48,28 +48,6 @@ type TestState =
4848 | { kind : 'ok' ; modelCount : number }
4949 | { kind : 'error' ; message : string } ;
5050
51- type DiscoveryState =
52- | { kind : 'idle' }
53- | { kind : 'discovering' }
54- | { kind : 'found' ; models : string [ ] }
55- | { kind : 'failed' } ;
56-
57- /** Priority-ordered model selection after a successful discovery. */
58- function pickBestModel ( models : string [ ] ) : string {
59- const priorities : RegExp [ ] = [
60- / ^ c l a u d e - s o n n e t - 4 - 5 / ,
61- / ^ c l a u d e - o p u s / ,
62- / ^ c l a u d e - s o n n e t / ,
63- / ^ g e m i n i - 2 \. 5 - p r o $ | ^ g e m i n i - 3 .* p r o / ,
64- / ^ g p t - 5 / ,
65- ] ;
66- for ( const pattern of priorities ) {
67- const match = models . find ( ( m ) => pattern . test ( m ) ) ;
68- if ( match !== undefined ) return match ;
69- }
70- return models [ 0 ] ?? '' ;
71- }
72-
7351/**
7452 * Minimal Custom Provider form — wire-agnostic endpoint onboarding.
7553 * Deliberately barebones (native form + FormData-ish accessors, no schema),
@@ -101,74 +79,15 @@ export function AddCustomProviderModal({
10179 const [ saving , setSaving ] = useState ( false ) ;
10280 const [ error , setError ] = useState < string | null > ( null ) ;
10381
104- const [ discovery , setDiscovery ] = useState < DiscoveryState > ( { kind : 'idle' } ) ;
105- // When true, user explicitly chose to type a model name instead of picking from the dropdown.
106- const [ manualModel , setManualModel ] = useState ( false ) ;
107- // Track whether user has explicitly typed/picked a model so auto-pick doesn't override it.
108- const userPickedModel = useRef ( false ) ;
109-
110- const debounceTimer = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
111-
112- function scheduleDiscovery ( currentBaseUrl : string , currentApiKey : string , currentWire : WireApi ) {
113- if ( debounceTimer . current !== null ) clearTimeout ( debounceTimer . current ) ;
114- if ( ! currentBaseUrl . trim ( ) . match ( / ^ h t t p s ? : \/ \/ / ) ) {
115- setDiscovery ( { kind : 'idle' } ) ;
116- return ;
117- }
118- debounceTimer . current = setTimeout ( ( ) => {
119- void runDiscovery ( currentBaseUrl , currentApiKey , currentWire ) ;
120- } , 500 ) ;
121- }
122-
123- async function runDiscovery ( currentBaseUrl : string , currentApiKey : string , currentWire : WireApi ) {
124- if ( ! window . codesign ?. config ) return ;
125- setDiscovery ( { kind : 'discovering' } ) ;
126- try {
127- const res = await window . codesign . config . testEndpoint ( {
128- wire : currentWire ,
129- baseUrl : currentBaseUrl . trim ( ) ,
130- apiKey : currentApiKey . trim ( ) ,
131- } ) ;
132- if ( res . ok && res . models . length > 0 ) {
133- setDiscovery ( { kind : 'found' , models : res . models } ) ;
134- if ( ! userPickedModel . current ) {
135- const best = pickBestModel ( res . models ) ;
136- setDefaultModel ( best ) ;
137- }
138- } else {
139- setDiscovery ( { kind : 'failed' } ) ;
140- }
141- } catch {
142- setDiscovery ( { kind : 'failed' } ) ;
143- }
144- }
145-
14682 function handleBaseUrlChange ( v : string ) {
14783 setBaseUrl ( v ) ;
14884 if ( wireAuto ) setWire ( detectWireFromBaseUrl ( v ) ) ;
14985 setTest ( { kind : 'idle' } ) ;
150- scheduleDiscovery ( v , apiKey , wireAuto ? detectWireFromBaseUrl ( v ) : wire ) ;
151- }
152-
153- function handleApiKeyChange ( v : string ) {
154- setApiKey ( v ) ;
155- scheduleDiscovery ( baseUrl , v , wire ) ;
15686 }
15787
15888 function handleWireChange ( v : WireApi ) {
15989 setWire ( v ) ;
16090 setWireAuto ( false ) ;
161- scheduleDiscovery ( baseUrl , apiKey , v ) ;
162- }
163-
164- function handleModelSelect ( v : string ) {
165- setDefaultModel ( v ) ;
166- userPickedModel . current = true ;
167- }
168-
169- function handleModelTextChange ( v : string ) {
170- setDefaultModel ( v ) ;
171- userPickedModel . current = v . length > 0 ;
17291 }
17392
17493 async function handleTest ( ) {
@@ -249,10 +168,6 @@ export function AddCustomProviderModal({
249168 ? t ( 'settings.providers.custom.editTitle' )
250169 : t ( 'settings.providers.custom.title' ) ;
251170
252- // Show the model dropdown when discovery found models AND user hasn't switched to manual entry.
253- const showModelDropdown =
254- ! manualModel && discovery . kind === 'found' && discovery . models . length > 0 ;
255-
256171 return (
257172 < div
258173 role = "dialog"
@@ -328,7 +243,7 @@ export function AddCustomProviderModal({
328243 < Field label = { t ( 'settings.providers.custom.apiKey' ) } >
329244 < TextInput
330245 value = { apiKey }
331- onChange = { handleApiKeyChange }
246+ onChange = { setApiKey }
332247 type = "password"
333248 placeholder = {
334249 isEdit && editTarget ?. keyMask !== undefined && editTarget . keyMask . length > 0
@@ -340,68 +255,8 @@ export function AddCustomProviderModal({
340255 />
341256 </ Field >
342257
343- < Field
344- label = { t ( 'settings.providers.custom.defaultModel' ) }
345- inline = {
346- discovery . kind === 'discovering' ? (
347- < span className = "inline-flex items-center gap-1 text-[var(--text-xs)] text-[var(--color-text-muted)]" >
348- < Loader2 className = "w-3 h-3 animate-spin" />
349- { t ( 'settings.providers.cliProxyApi.discoveringModels' ) }
350- </ span >
351- ) : discovery . kind === 'found' ? (
352- < span className = "inline-flex items-center gap-1 text-[var(--text-xs)] text-[var(--color-success)]" >
353- < Check className = "w-3 h-3" />
354- { t ( 'settings.providers.cliProxyApi.discoveredModels' , {
355- count : discovery . models . length ,
356- } ) }
357- </ span >
358- ) : discovery . kind === 'failed' ? (
359- < span className = "inline-flex items-center gap-1 text-[var(--text-xs)] text-[var(--color-text-muted)]" >
360- < AlertCircle className = "w-3 h-3" />
361- { t ( 'settings.providers.cliProxyApi.discoveryFailed' ) }
362- </ span >
363- ) : null
364- }
365- >
366- { showModelDropdown ? (
367- < div className = "flex items-center gap-2" >
368- < select
369- value = { defaultModel }
370- onChange = { ( e ) => handleModelSelect ( e . target . value ) }
371- className = "flex-1 h-8 px-3 rounded-[var(--radius-md)] bg-[var(--color-surface)] border border-[var(--color-border)] text-[var(--text-sm)] text-[var(--color-text-primary)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]"
372- >
373- { discovery . models . map ( ( m ) => (
374- < option key = { m } value = { m } >
375- { m }
376- </ option >
377- ) ) }
378- </ select >
379- < button
380- type = "button"
381- onClick = { ( ) => setManualModel ( true ) }
382- className = "shrink-0 text-[var(--text-xs)] text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] underline"
383- >
384- { t ( 'settings.providers.custom.switchToManual' ) }
385- </ button >
386- </ div >
387- ) : (
388- < div className = "flex items-center gap-2" >
389- < TextInput
390- value = { defaultModel }
391- onChange = { handleModelTextChange }
392- placeholder = "model-name"
393- />
394- { manualModel && discovery . kind === 'found' && discovery . models . length > 0 && (
395- < button
396- type = "button"
397- onClick = { ( ) => setManualModel ( false ) }
398- className = "shrink-0 text-[var(--text-xs)] text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] underline"
399- >
400- { t ( 'settings.providers.custom.switchToDropdown' ) }
401- </ button >
402- ) }
403- </ div >
404- ) }
258+ < Field label = { t ( 'settings.providers.custom.defaultModel' ) } >
259+ < TextInput value = { defaultModel } onChange = { setDefaultModel } placeholder = "model-name" />
405260 </ Field >
406261
407262 < div className = "flex items-center gap-2" >
@@ -449,23 +304,12 @@ export function AddCustomProviderModal({
449304 ) ;
450305}
451306
452- function Field ( {
453- label,
454- inline,
455- children,
456- } : {
457- label : string ;
458- inline ?: React . ReactNode ;
459- children : React . ReactNode ;
460- } ) {
307+ function Field ( { label, children } : { label : string ; children : React . ReactNode } ) {
461308 return (
462309 < div >
463- < div className = "flex items-center justify-between mb-1.5" >
464- < p className = "block text-[var(--text-xs)] font-medium text-[var(--color-text-secondary)]" >
465- { label }
466- </ p >
467- { inline !== undefined && < span > { inline } </ span > }
468- </ div >
310+ < p className = "block text-[var(--text-xs)] font-medium text-[var(--color-text-secondary)] mb-1.5" >
311+ { label }
312+ </ p >
469313 { children }
470314 </ div >
471315 ) ;
0 commit comments