@ -7,7 +7,7 @@ import {
For ,
createEffect ,
} from 'solid-js' ;
import { Settings as SettingsIcon , Save , Plus , Trash2 , Info , Download , Upload } from 'lucide-solid' ;
import { Settings as SettingsIcon , Save , Plus , Trash2 , Info , Download , Upload , RefreshCw } from 'lucide-solid' ;
import { A } from '@solidjs/router' ;
import { settingsApi } from '~/api/settings' ;
import { configApi } from '~/api/config' ;
@ -18,6 +18,7 @@ import type { UserSettings, ProviderConfig } from '~/types';
import LoadingSpinner from '~/components/ui/LoadingSpinner' ;
import ApiKeyManager from '~/components/ApiKeyManager' ;
import { getProviderInfoKey , providerSupportsWebSearch } from '~/utils/providers' ;
import { useToast } from '~/components/ui/Toast' ;
/ * *
* Settings page for configuring the user ' s synthesis preferences .
@ -37,6 +38,7 @@ import { getProviderInfoKey, providerSupportsWebSearch } from '~/utils/providers
* /
const Settings : Component = ( ) = > {
const { t } = useI18n ( ) ;
const { addToast } = useToast ( ) ;
const [ settings , setSettings ] = createSignal < UserSettings > ( {
. . . DEFAULT_SETTINGS ,
@ -52,6 +54,13 @@ const Settings: Component = () => {
const [ providers ] = createResource ( ( ) = > configApi . listProviders ( ) ) ;
const [ providerWarning , setProviderWarning ] = createSignal ( false ) ;
// Brave Search key management
const [ apiKeys , { refetch : refetchApiKeys } ] = createResource ( ( ) = > apiKeysApi . list ( ) ) ;
const braveKey = ( ) = > apiKeys ( ) ? . find ( ( k ) = > k . provider_name === 'brave_search' ) ;
const [ braveKeyInput , setBraveKeyInput ] = createSignal ( '' ) ;
const [ braveSaving , setBraveSaving ] = createSignal ( false ) ;
const [ braveTesting , setBraveTesting ] = createSignal ( false ) ;
let fileInputRef : HTMLInputElement | undefined ;
onMount ( async ( ) = > {
@ -122,6 +131,53 @@ const Settings: Component = () => {
}
} ) ;
const handleBraveKeySave = async ( ) = > {
const key = braveKeyInput ( ) . trim ( ) ;
if ( ! key ) return ;
setBraveSaving ( true ) ;
try {
await apiKeysApi . create ( { provider_name : 'brave_search' , api_key : key } ) ;
addToast ( { type : 'success' , message : t ( 'settings.apiKeys.saved' ) , duration : 4000 } ) ;
setBraveKeyInput ( '' ) ;
refetchApiKeys ( ) ;
} catch ( err ) {
const msg = isApiError ( err ) ? err.message : t ( 'settings.apiKeys.saveError' ) ;
addToast ( { type : 'error' , message : msg , duration : 5000 } ) ;
} finally {
setBraveSaving ( false ) ;
}
} ;
const handleBraveKeyTest = async ( ) = > {
setBraveTesting ( true ) ;
try {
const result = await apiKeysApi . test ( 'brave_search' ) ;
if ( result . success ) {
addToast ( { type : 'success' , message : t ( 'settings.apiKeys.testSuccess' ) , duration : 4000 } ) ;
} else {
addToast ( { type : 'error' , message : t ( 'settings.apiKeys.testFailure' , { message : result.message } ) , duration : 6000 } ) ;
}
} catch ( err ) {
const msg = isApiError ( err ) ? err.message : t ( 'settings.apiKeys.testFailure' , { message : 'Erreur inconnue' } ) ;
addToast ( { type : 'error' , message : msg , duration : 5000 } ) ;
} finally {
setBraveTesting ( false ) ;
}
} ;
const handleBraveKeyDelete = async ( ) = > {
try {
await apiKeysApi . remove ( 'brave_search' ) ;
addToast ( { type : 'success' , message : t ( 'settings.apiKeys.deleted' ) , duration : 4000 } ) ;
// Auto-disable use_brave_search when the key is removed
setSettings ( ( prev ) = > ( { . . . prev , use_brave_search : false } ) ) ;
refetchApiKeys ( ) ;
} catch ( err ) {
const msg = isApiError ( err ) ? err.message : t ( 'settings.apiKeys.deleteError' ) ;
addToast ( { type : 'error' , message : msg , duration : 5000 } ) ;
}
} ;
const handleSave = async ( ) = > {
setSaving ( true ) ;
setMessage ( null ) ;
@ -513,6 +569,115 @@ const Settings: Component = () => {
< / div >
< / div >
{ /* Brave Search */ }
< div class = "mt-6" >
< h3 class = "text-lg font-medium text-gray-900 mb-1" >
{ t ( 'settings.braveSearch' ) }
< / h3 >
< p class = "text-sm text-gray-500 mb-4" >
{ t ( 'settings.braveSearchKeyHelp' ) }
< / p >
{ /* Key management */ }
< Show
when = { braveKey ( ) }
fallback = {
< div class = "flex items-center gap-2" >
< input
type = "text"
class = "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md py-2 px-3 border font-mono"
placeholder = { t ( 'settings.braveSearchKey' ) }
value = { braveKeyInput ( ) }
onInput = { ( e ) = > setBraveKeyInput ( e . currentTarget . value ) }
/ >
< button
type = "button"
onClick = { handleBraveKeySave }
disabled = { braveSaving ( ) || ! braveKeyInput ( ) . trim ( ) }
class = "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 whitespace-nowrap"
>
< Show when = { braveSaving ( ) } >
< div class = "animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" / >
< / Show >
{ braveSaving ( ) ? t ( 'settings.apiKeys.saving' ) : t ( 'settings.apiKeys.save' ) }
< / button >
< / div >
}
>
{ ( key ) = > (
< div class = "flex items-center justify-between" >
< div class = "flex items-center gap-3" >
< span class = "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800" >
{ t ( 'settings.apiKeys.configured' ) }
< / span >
< span class = "text-sm font-mono text-gray-400" >
{ t ( 'settings.apiKeys.keyPrefix' , { prefix : key ( ) . key_prefix } ) }
< / span >
< / div >
< div class = "flex items-center gap-2" >
< button
type = "button"
onClick = { handleBraveKeyTest }
disabled = { braveTesting ( ) }
class = "inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-md text-indigo-700 bg-indigo-50 hover:bg-indigo-100 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-indigo-500 disabled:opacity-50"
>
< Show
when = { ! braveTesting ( ) }
fallback = { < div class = "animate-spin rounded-full h-3 w-3 border-b-2 border-indigo-700 mr-1.5" / > }
>
< RefreshCw class = "h-3 w-3 mr-1.5" / >
< / Show >
{ braveTesting ( ) ? t ( 'settings.apiKeys.testing' ) : t ( 'settings.apiKeys.test' ) }
< / button >
< button
type = "button"
onClick = { handleBraveKeyDelete }
class = "inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-md text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-red-500"
>
< Trash2 class = "h-3 w-3 mr-1.5" / >
{ t ( 'settings.apiKeys.delete' ) }
< / button >
< / div >
< / div >
) }
< / Show >
{ /* use_brave_search toggle */ }
< div class = "mt-4 space-y-1" >
< div class = "flex items-center" >
< input
type = "checkbox"
id = "useBraveSearch"
checked = { settings ( ) . use_brave_search }
disabled = { ! braveKey ( ) }
onChange = { ( e ) = >
setSettings ( ( prev ) = > ( {
. . . prev ,
use_brave_search : e.currentTarget.checked ,
} ) )
}
class = "h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded disabled:opacity-50"
/ >
< label
for = "useBraveSearch"
class = { ` ml-2 block text-sm ${ ! braveKey ( ) ? 'text-gray-400' : 'text-gray-700' } ` }
>
{ t ( 'settings.useBraveSearch' ) }
< / label >
< / div >
< Show when = { ! braveKey ( ) } >
< p class = "text-xs text-gray-400 ml-6" >
{ t ( 'settings.braveSearchNotConfigured' ) }
< / p >
< / Show >
< Show when = { braveKey ( ) } >
< p class = "text-xs text-gray-500 ml-6" >
{ t ( 'settings.useBraveSearchHelp' ) }
< / p >
< / Show >
< / div >
< / div >
{ /* Search agent behavior */ }
< div >
< label