@ -17,7 +17,8 @@ import Button from '~/components/ui/Button';
/** Local editable state for an existing provider card. */
/** Local editable state for an existing provider card. */
interface ProviderFormState {
interface ProviderFormState {
display_name : string ;
display_name : string ;
models : AdminProviderModel [ ] ;
models_scraping : AdminProviderModel [ ] ;
models_websearch : AdminProviderModel [ ] ;
is_enabled : boolean ;
is_enabled : boolean ;
}
}
@ -62,7 +63,8 @@ const Providers: Component = () => {
const [ newProvider , setNewProvider ] = createSignal ( {
const [ newProvider , setNewProvider ] = createSignal ( {
provider_name : '' ,
provider_name : '' ,
display_name : '' ,
display_name : '' ,
models : [ emptyModel ( ) ] as AdminProviderModel [ ] ,
models_scraping : [ emptyModel ( ) ] as AdminProviderModel [ ] ,
models_websearch : [ emptyModel ( ) ] as AdminProviderModel [ ] ,
is_enabled : true ,
is_enabled : true ,
} ) ;
} ) ;
@ -71,7 +73,8 @@ const Providers: Component = () => {
if ( existing ) return existing ;
if ( existing ) return existing ;
return {
return {
display_name : provider.display_name ,
display_name : provider.display_name ,
models : [ . . . provider . models ] ,
models_scraping : [ . . . provider . models_scraping ] ,
models_websearch : [ . . . provider . models_websearch ] ,
is_enabled : provider.is_enabled ,
is_enabled : provider.is_enabled ,
} ;
} ;
} ;
} ;
@ -89,7 +92,8 @@ const Providers: Component = () => {
. . . prev ,
. . . prev ,
[ provider . id ] : {
[ provider . id ] : {
display_name : provider.display_name ,
display_name : provider.display_name ,
models : provider.models.map ( ( m ) = > ( { . . . m } ) ) ,
models_scraping : provider.models_scraping.map ( ( m ) = > ( { . . . m } ) ) ,
models_websearch : provider.models_websearch.map ( ( m ) = > ( { . . . m } ) ) ,
is_enabled : provider.is_enabled ,
is_enabled : provider.is_enabled ,
} ,
} ,
} ) ) ;
} ) ) ;
@ -103,7 +107,8 @@ const Providers: Component = () => {
try {
try {
await adminProvidersApi . update ( provider . id , {
await adminProvidersApi . update ( provider . id , {
display_name : state.display_name ,
display_name : state.display_name ,
models : state.models.filter ( ( m ) = > m . model_id . trim ( ) !== '' ) ,
models_scraping : state.models_scraping.filter ( ( m ) = > m . model_id . trim ( ) !== '' ) ,
models_websearch : state.models_websearch.filter ( ( m ) = > m . model_id . trim ( ) !== '' ) ,
is_enabled : state.is_enabled ,
is_enabled : state.is_enabled ,
} ) ;
} ) ;
addToast ( {
addToast ( {
@ -152,7 +157,8 @@ const Providers: Component = () => {
await adminProvidersApi . create ( {
await adminProvidersApi . create ( {
provider_name : data.provider_name.trim ( ) ,
provider_name : data.provider_name.trim ( ) ,
display_name : data.display_name.trim ( ) ,
display_name : data.display_name.trim ( ) ,
models : data.models.filter ( ( m ) = > m . model_id . trim ( ) !== '' ) ,
models_scraping : data.models_scraping.filter ( ( m ) = > m . model_id . trim ( ) !== '' ) ,
models_websearch : data.models_websearch.filter ( ( m ) = > m . model_id . trim ( ) !== '' ) ,
is_enabled : data.is_enabled ,
is_enabled : data.is_enabled ,
} ) ;
} ) ;
addToast ( {
addToast ( {
@ -164,7 +170,8 @@ const Providers: Component = () => {
setNewProvider ( {
setNewProvider ( {
provider_name : '' ,
provider_name : '' ,
display_name : '' ,
display_name : '' ,
models : [ emptyModel ( ) ] ,
models_scraping : [ emptyModel ( ) ] ,
models_websearch : [ emptyModel ( ) ] ,
is_enabled : true ,
is_enabled : true ,
} ) ;
} ) ;
refetch ( ) ;
refetch ( ) ;
@ -183,28 +190,29 @@ const Providers: Component = () => {
} ) ) ;
} ) ) ;
} ;
} ;
const addModelToProvider = ( id : string ) = > {
const addModelToProvider = ( id : string , listKey : 'models_scraping' | 'models_websearch' ) = > {
updateEdit ( id , ( prev ) = > ( {
updateEdit ( id , ( prev ) = > ( {
. . . prev ,
. . . prev ,
mode ls: [ . . . prev .models , emptyModel ( ) ] ,
[ li stKey] : [ . . . prev [listKey ] , emptyModel ( ) ] ,
} ) ) ;
} ) ) ;
} ;
} ;
const removeModelFromProvider = ( id : string , modelIndex: number ) = > {
const removeModelFromProvider = ( id : string , listKey: 'models_scraping' | 'models_websearch' , modelIndex: number ) = > {
updateEdit ( id , ( prev ) = > ( {
updateEdit ( id , ( prev ) = > ( {
. . . prev ,
. . . prev ,
models : prev.models. filter( ( _ , i ) = > i !== modelIndex ) ,
[ listKey ] : prev [ listKey ] . filter( ( _ , i ) = > i !== modelIndex ) ,
} ) ) ;
} ) ) ;
} ;
} ;
const updateModelField = (
const updateModelField = (
id : string ,
id : string ,
listKey : 'models_scraping' | 'models_websearch' ,
modelIndex : number ,
modelIndex : number ,
field : keyof AdminProviderModel ,
field : keyof AdminProviderModel ,
value : string | boolean ,
value : string | boolean ,
) = > {
) = > {
updateEdit ( id , ( prev ) = > {
updateEdit ( id , ( prev ) = > {
const models = prev .models . map ( ( m , i ) = > {
const models = prev [listKey ] . map ( ( m , i ) = > {
if ( i !== modelIndex ) {
if ( i !== modelIndex ) {
// If setting a new default, unset others
// If setting a new default, unset others
if ( field === 'is_default' && value === true ) {
if ( field === 'is_default' && value === true ) {
@ -214,14 +222,14 @@ const Providers: Component = () => {
}
}
return { . . . m , [ field ] : value } ;
return { . . . m , [ field ] : value } ;
} ) ;
} ) ;
return { . . . prev , models } ;
return { . . . prev , [ listKey ] : models } ;
} ) ;
} ) ;
} ;
} ;
const setDefaultModel = ( id : string , modelIndex: number ) = > {
const setDefaultModel = ( id : string , listKey: 'models_scraping' | 'models_websearch' , modelIndex: number ) = > {
updateEdit ( id , ( prev ) = > ( {
updateEdit ( id , ( prev ) = > ( {
. . . prev ,
. . . prev ,
models : prev.models. map( ( m , i ) = > ( {
[ listKey ] : prev [ listKey ] . map( ( m , i ) = > ( {
. . . m ,
. . . m ,
is_default : i === modelIndex ,
is_default : i === modelIndex ,
} ) ) ,
} ) ) ,
@ -229,28 +237,29 @@ const Providers: Component = () => {
} ;
} ;
// New provider form helpers
// New provider form helpers
const addModelToNew = ( ) = > {
const addModelToNew = ( listKey : 'models_scraping' | 'models_websearch' ) = > {
setNewProvider ( ( prev ) = > ( {
setNewProvider ( ( prev ) = > ( {
. . . prev ,
. . . prev ,
mode ls: [ . . . prev .models , emptyModel ( ) ] ,
[ li stKey] : [ . . . prev [listKey ] , emptyModel ( ) ] ,
} ) ) ;
} ) ) ;
} ;
} ;
const removeModelFromNew = ( index: number ) = > {
const removeModelFromNew = ( listKey: 'models_scraping' | 'models_websearch' , index: number ) = > {
setNewProvider ( ( prev ) = > ( {
setNewProvider ( ( prev ) = > ( {
. . . prev ,
. . . prev ,
models : prev.models. filter( ( _ , i ) = > i !== index ) ,
[ listKey ] : prev [ listKey ] . filter( ( _ , i ) = > i !== index ) ,
} ) ) ;
} ) ) ;
} ;
} ;
const updateNewModelField = (
const updateNewModelField = (
listKey : 'models_scraping' | 'models_websearch' ,
index : number ,
index : number ,
field : keyof AdminProviderModel ,
field : keyof AdminProviderModel ,
value : string | boolean ,
value : string | boolean ,
) = > {
) = > {
setNewProvider ( ( prev ) = > ( {
setNewProvider ( ( prev ) = > ( {
. . . prev ,
. . . prev ,
models : prev.models. map( ( m , i ) = > {
[ listKey ] : prev [ listKey ] . map( ( m , i ) = > {
if ( i !== index ) {
if ( i !== index ) {
if ( field === 'is_default' && value === true ) {
if ( field === 'is_default' && value === true ) {
return { . . . m , is_default : false } ;
return { . . . m , is_default : false } ;
@ -262,10 +271,10 @@ const Providers: Component = () => {
} ) ) ;
} ) ) ;
} ;
} ;
const setNewDefaultModel = ( index: number ) = > {
const setNewDefaultModel = ( listKey: 'models_scraping' | 'models_websearch' , index: number ) = > {
setNewProvider ( ( prev ) = > ( {
setNewProvider ( ( prev ) = > ( {
. . . prev ,
. . . prev ,
models : prev.models. map( ( m , i ) = > ( {
[ listKey ] : prev [ listKey ] . map( ( m , i ) = > ( {
. . . m ,
. . . m ,
is_default : i === index ,
is_default : i === index ,
} ) ) ,
} ) ) ,
@ -341,15 +350,15 @@ const Providers: Component = () => {
< / div >
< / div >
< / div >
< / div >
{ /* Models for new provider */}
{ /* Models for new provider - Scraping */}
< div >
< div >
< div class = "flex items-center justify-between mb-2" >
< div class = "flex items-center justify-between mb-2" >
< label class = "block text-sm font-medium text-gray-700" >
< label class = "block text-sm font-medium text-gray-700" >
{ t ( 'admin.providers.models ') }
{ t ( 'admin.providers.models Scraping ') }
< / label >
< / label >
< button
< button
type = "button"
type = "button"
onClick = { addModelToNew }
onClick = { ( ) = > addModelToNew ( 'models_scraping' ) }
class = "text-sm text-indigo-600 hover:text-indigo-800 flex items-center"
class = "text-sm text-indigo-600 hover:text-indigo-800 flex items-center"
>
>
< Plus class = "h-4 w-4 mr-1" / >
< Plus class = "h-4 w-4 mr-1" / >
@ -357,7 +366,7 @@ const Providers: Component = () => {
< / button >
< / button >
< / div >
< / div >
< div class = "space-y-3" >
< div class = "space-y-3" >
< For each = { newProvider ( ) . models } >
< For each = { newProvider ( ) . models _scraping } >
{ ( model , index ) = > (
{ ( model , index ) = > (
< div class = "flex items-center gap-3 bg-gray-50 rounded-md p-3" >
< div class = "flex items-center gap-3 bg-gray-50 rounded-md p-3" >
< input
< input
@ -366,6 +375,7 @@ const Providers: Component = () => {
value = { model . model_id }
value = { model . model_id }
onInput = { ( e ) = >
onInput = { ( e ) = >
updateNewModelField (
updateNewModelField (
'models_scraping' ,
index ( ) ,
index ( ) ,
'model_id' ,
'model_id' ,
e . currentTarget . value ,
e . currentTarget . value ,
@ -379,6 +389,7 @@ const Providers: Component = () => {
value = { model . display_name }
value = { model . display_name }
onInput = { ( e ) = >
onInput = { ( e ) = >
updateNewModelField (
updateNewModelField (
'models_scraping' ,
index ( ) ,
index ( ) ,
'display_name' ,
'display_name' ,
e . currentTarget . value ,
e . currentTarget . value ,
@ -391,16 +402,89 @@ const Providers: Component = () => {
< label class = "flex items-center text-sm text-gray-600 whitespace-nowrap cursor-pointer" >
< label class = "flex items-center text-sm text-gray-600 whitespace-nowrap cursor-pointer" >
< input
< input
type = "radio"
type = "radio"
name = "new-default-model "
name = "new-default-model -scraping "
checked = { model . is_default }
checked = { model . is_default }
onChange = { ( ) = > setNewDefaultModel ( index ( ) ) }
onChange = { ( ) = > setNewDefaultModel ( 'models_scraping' , index ( ) ) }
class = "mr-1.5 text-indigo-600 focus:ring-indigo-500"
class = "mr-1.5 text-indigo-600 focus:ring-indigo-500"
/ >
/ >
{ t ( 'admin.providers.defaultModel' ) }
{ t ( 'admin.providers.defaultModel' ) }
< / label >
< / label >
< button
< button
type = "button"
type = "button"
onClick = { ( ) = > removeModelFromNew ( index ( ) ) }
onClick = { ( ) = > removeModelFromNew ( 'models_scraping' , index ( ) ) }
class = "p-1.5 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-md"
title = { t ( 'admin.providers.removeModel' ) }
>
< Trash2 class = "h-4 w-4" / >
< / button >
< / div >
) }
< / For >
< / div >
< / div >
{ /* Models for new provider - Websearch */ }
< div >
< div class = "flex items-center justify-between mb-2" >
< label class = "block text-sm font-medium text-gray-700" >
{ t ( 'admin.providers.modelsWebsearch' ) }
< / label >
< button
type = "button"
onClick = { ( ) = > addModelToNew ( 'models_websearch' ) }
class = "text-sm text-indigo-600 hover:text-indigo-800 flex items-center"
>
< Plus class = "h-4 w-4 mr-1" / >
{ t ( 'admin.providers.addModel' ) }
< / button >
< / div >
< div class = "space-y-3" >
< For each = { newProvider ( ) . models_websearch } >
{ ( model , index ) = > (
< div class = "flex items-center gap-3 bg-gray-50 rounded-md p-3" >
< input
type = "text"
class = "flex-1 shadow-sm sm:text-sm border-gray-300 rounded-md py-1.5 px-3 border focus:ring-indigo-500 focus:border-indigo-500"
value = { model . model_id }
onInput = { ( e ) = >
updateNewModelField (
'models_websearch' ,
index ( ) ,
'model_id' ,
e . currentTarget . value ,
)
}
placeholder = { t ( 'admin.providers.modelIdPlaceholder' ) }
/ >
< input
type = "text"
class = "flex-1 shadow-sm sm:text-sm border-gray-300 rounded-md py-1.5 px-3 border focus:ring-indigo-500 focus:border-indigo-500"
value = { model . display_name }
onInput = { ( e ) = >
updateNewModelField (
'models_websearch' ,
index ( ) ,
'display_name' ,
e . currentTarget . value ,
)
}
placeholder = { t (
'admin.providers.modelDisplayNamePlaceholder' ,
) }
/ >
< label class = "flex items-center text-sm text-gray-600 whitespace-nowrap cursor-pointer" >
< input
type = "radio"
name = "new-default-model-websearch"
checked = { model . is_default }
onChange = { ( ) = > setNewDefaultModel ( 'models_websearch' , index ( ) ) }
class = "mr-1.5 text-indigo-600 focus:ring-indigo-500"
/ >
{ t ( 'admin.providers.defaultModel' ) }
< / label >
< button
type = "button"
onClick = { ( ) = > removeModelFromNew ( 'models_websearch' , index ( ) ) }
class = "p-1.5 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-md"
class = "p-1.5 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-md"
title = { t ( 'admin.providers.removeModel' ) }
title = { t ( 'admin.providers.removeModel' ) }
>
>
@ -420,7 +504,8 @@ const Providers: Component = () => {
setNewProvider ( {
setNewProvider ( {
provider_name : '' ,
provider_name : '' ,
display_name : '' ,
display_name : '' ,
models : [ emptyModel ( ) ] ,
models_scraping : [ emptyModel ( ) ] ,
models_websearch : [ emptyModel ( ) ] ,
is_enabled : true ,
is_enabled : true ,
} ) ;
} ) ;
} }
} }
@ -533,15 +618,106 @@ const Providers: Component = () => {
/ >
/ >
< / div >
< / div >
{ /* Models */ }
{ /* Models - Scraping */ }
< div >
< div class = "flex items-center justify-between mb-2" >
< label class = "block text-sm font-medium text-gray-700" >
{ t ( 'admin.providers.modelsScraping' ) }
< / label >
< button
type = "button"
onClick = { ( ) = > addModelToProvider ( provider . id , 'models_scraping' ) }
class = "text-sm text-indigo-600 hover:text-indigo-800 flex items-center"
>
< Plus class = "h-4 w-4 mr-1" / >
{ t ( 'admin.providers.addModel' ) }
< / button >
< / div >
< Show
when = { state ( ) . models_scraping . length > 0 }
fallback = {
< p class = "text-sm text-gray-400 italic" >
{ t ( 'admin.providers.noModels' ) }
< / p >
}
>
< div class = "space-y-3" >
< For each = { state ( ) . models_scraping } >
{ ( model , index ) = > (
< div class = "flex items-center gap-3 bg-gray-50 rounded-md p-3" >
< input
type = "text"
class = "flex-1 shadow-sm sm:text-sm border-gray-300 rounded-md py-1.5 px-3 border focus:ring-indigo-500 focus:border-indigo-500"
value = { model . model_id }
onInput = { ( e ) = >
updateModelField (
provider . id ,
'models_scraping' ,
index ( ) ,
'model_id' ,
e . currentTarget . value ,
)
}
placeholder = { t (
'admin.providers.modelIdPlaceholder' ,
) }
/ >
< input
type = "text"
class = "flex-1 shadow-sm sm:text-sm border-gray-300 rounded-md py-1.5 px-3 border focus:ring-indigo-500 focus:border-indigo-500"
value = { model . display_name }
onInput = { ( e ) = >
updateModelField (
provider . id ,
'models_scraping' ,
index ( ) ,
'display_name' ,
e . currentTarget . value ,
)
}
placeholder = { t (
'admin.providers.modelDisplayNamePlaceholder' ,
) }
/ >
< label class = "flex items-center text-sm text-gray-600 whitespace-nowrap cursor-pointer" >
< input
type = "radio"
name = { ` default-model-scraping- ${ provider . id } ` }
checked = { model . is_default }
onChange = { ( ) = >
setDefaultModel ( provider . id , 'models_scraping' , index ( ) )
}
class = "mr-1.5 text-indigo-600 focus:ring-indigo-500"
/ >
{ t ( 'admin.providers.defaultModel' ) }
< / label >
< button
type = "button"
onClick = { ( ) = >
removeModelFromProvider ( provider . id , 'models_scraping' , index ( ) )
}
class = "p-1.5 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-md"
title = { t ( 'admin.providers.removeModel' ) }
>
< Trash2 class = "h-4 w-4" / >
< / button >
< / div >
) }
< / For >
< / div >
< / Show >
< / div >
{ /* Models - Websearch */ }
< div >
< div >
< div class = "flex items-center justify-between mb-2" >
< div class = "flex items-center justify-between mb-2" >
< label class = "block text-sm font-medium text-gray-700" >
< label class = "block text-sm font-medium text-gray-700" >
{ t ( 'admin.providers.models' ) }
{ t ( 'admin.providers.models Websearch ') }
< / label >
< / label >
< button
< button
type = "button"
type = "button"
onClick = { ( ) = > addModelToProvider ( provider . id ) }
onClick = { ( ) = > addModelToProvider ( provider . id , 'models_websearch' )}
class = "text-sm text-indigo-600 hover:text-indigo-800 flex items-center"
class = "text-sm text-indigo-600 hover:text-indigo-800 flex items-center"
>
>
< Plus class = "h-4 w-4 mr-1" / >
< Plus class = "h-4 w-4 mr-1" / >
@ -550,7 +726,7 @@ const Providers: Component = () => {
< / div >
< / div >
< Show
< Show
when = { state ( ) . models . length > 0 }
when = { state ( ) . models _websearch . length > 0 }
fallback = {
fallback = {
< p class = "text-sm text-gray-400 italic" >
< p class = "text-sm text-gray-400 italic" >
{ t ( 'admin.providers.noModels' ) }
{ t ( 'admin.providers.noModels' ) }
@ -558,7 +734,7 @@ const Providers: Component = () => {
}
}
>
>
< div class = "space-y-3" >
< div class = "space-y-3" >
< For each = { state ( ) . models } >
< For each = { state ( ) . models _websearch } >
{ ( model , index ) = > (
{ ( model , index ) = > (
< div class = "flex items-center gap-3 bg-gray-50 rounded-md p-3" >
< div class = "flex items-center gap-3 bg-gray-50 rounded-md p-3" >
< input
< input
@ -568,6 +744,7 @@ const Providers: Component = () => {
onInput = { ( e ) = >
onInput = { ( e ) = >
updateModelField (
updateModelField (
provider . id ,
provider . id ,
'models_websearch' ,
index ( ) ,
index ( ) ,
'model_id' ,
'model_id' ,
e . currentTarget . value ,
e . currentTarget . value ,
@ -584,6 +761,7 @@ const Providers: Component = () => {
onInput = { ( e ) = >
onInput = { ( e ) = >
updateModelField (
updateModelField (
provider . id ,
provider . id ,
'models_websearch' ,
index ( ) ,
index ( ) ,
'display_name' ,
'display_name' ,
e . currentTarget . value ,
e . currentTarget . value ,
@ -596,10 +774,10 @@ const Providers: Component = () => {
< label class = "flex items-center text-sm text-gray-600 whitespace-nowrap cursor-pointer" >
< label class = "flex items-center text-sm text-gray-600 whitespace-nowrap cursor-pointer" >
< input
< input
type = "radio"
type = "radio"
name = { ` default-model- ${ provider . id } ` }
name = { ` default-model- websearch- ${ provider . id } ` }
checked = { model . is_default }
checked = { model . is_default }
onChange = { ( ) = >
onChange = { ( ) = >
setDefaultModel ( provider . id , index ( ) )
setDefaultModel ( provider . id , 'models_websearch' , index ( ) )
}
}
class = "mr-1.5 text-indigo-600 focus:ring-indigo-500"
class = "mr-1.5 text-indigo-600 focus:ring-indigo-500"
/ >
/ >
@ -608,7 +786,7 @@ const Providers: Component = () => {
< button
< button
type = "button"
type = "button"
onClick = { ( ) = >
onClick = { ( ) = >
removeModelFromProvider ( provider . id , index ( ) )
removeModelFromProvider ( provider . id , 'models_websearch' , index ( ) )
}
}
class = "p-1.5 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-md"
class = "p-1.5 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-md"
title = { t ( 'admin.providers.removeModel' ) }
title = { t ( 'admin.providers.removeModel' ) }