@ -7,6 +7,7 @@ import {
For ,
For ,
createEffect ,
createEffect ,
} from 'solid-js' ;
} from 'solid-js' ;
import { A } from '@solidjs/router' ;
import { Settings as SettingsIcon , Save , Plus , Trash2 , Info , Download , Upload } from 'lucide-solid' ;
import { Settings as SettingsIcon , Save , Plus , Trash2 , Info , Download , Upload } from 'lucide-solid' ;
import Button from '~/components/ui/Button' ;
import Button from '~/components/ui/Button' ;
import { settingsApi } from '~/api/settings' ;
import { settingsApi } from '~/api/settings' ;
@ -19,7 +20,6 @@ import LoadingSpinner from '~/components/ui/LoadingSpinner';
import ApiKeyManager from '~/components/ApiKeyManager' ;
import ApiKeyManager from '~/components/ApiKeyManager' ;
import { getProviderInfoKey , providerSupportsWebSearch } from '~/utils/providers' ;
import { getProviderInfoKey , providerSupportsWebSearch } from '~/utils/providers' ;
import SettingsBraveSearch from '~/components/settings/SettingsBraveSearch' ;
import SettingsBraveSearch from '~/components/settings/SettingsBraveSearch' ;
import SettingsAdvanced from '~/components/settings/SettingsAdvanced' ;
import SettingsRateLimit from '~/components/settings/SettingsRateLimit' ;
import SettingsRateLimit from '~/components/settings/SettingsRateLimit' ;
/ * *
/ * *
@ -53,6 +53,7 @@ const Settings: Component = () => {
const [ includeApiKeys , setIncludeApiKeys ] = createSignal ( false ) ;
const [ includeApiKeys , setIncludeApiKeys ] = createSignal ( false ) ;
const [ providers ] = createResource ( ( ) = > configApi . listProviders ( ) ) ;
const [ providers ] = createResource ( ( ) = > configApi . listProviders ( ) ) ;
const [ apiKeys , { refetch : refetchApiKeys } ] = createResource ( ( ) = > apiKeysApi . list ( ) ) ;
const [ providerWarning , setProviderWarning ] = createSignal ( false ) ;
const [ providerWarning , setProviderWarning ] = createSignal ( false ) ;
let fileInputRef : HTMLInputElement | undefined ;
let fileInputRef : HTMLInputElement | undefined ;
@ -101,6 +102,10 @@ const Settings: Component = () => {
) ;
) ;
} ;
} ;
const getKeyForProvider = ( providerName : string ) = > {
return apiKeys ( ) ? . find ( ( k ) = > k . provider_name === providerName ) ;
} ;
const handleProviderChange = ( providerName : string ) = > {
const handleProviderChange = ( providerName : string ) = > {
setProviderWarning ( false ) ;
setProviderWarning ( false ) ;
const provider = providers ( ) ? . find (
const provider = providers ( ) ? . find (
@ -232,6 +237,7 @@ const Settings: Component = () => {
} ) ;
} ) ;
}
}
}
}
refetchApiKeys ( ) ;
}
}
setMessage ( { type : 'success' , text : t ( 'settings.importSuccess' ) } ) ;
setMessage ( { type : 'success' , text : t ( 'settings.importSuccess' ) } ) ;
@ -248,64 +254,13 @@ const Settings: Component = () => {
return (
return (
< Show when = { ! loading ( ) } fallback = { < LoadingSpinner / > } >
< Show when = { ! loading ( ) } fallback = { < LoadingSpinner / > } >
< div class = "max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8" >
< div class = "max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8" >
<div class = "flex items-center justify-between mb-8" >
{/* Page header */ }
< div class = "flex items-center ">
< div class = "flex items-center mb-8 ">
< SettingsIcon class = "h-8 w-8 text-indigo-600 mr-3" / >
< SettingsIcon class = "h-8 w-8 text-indigo-600 mr-3" / >
< h1 class = "text-3xl font-extrabold text-gray-900" >
< h1 class = "text-3xl font-extrabold text-gray-900" >
{ t ( 'settings.title' ) }
{ t ( 'settings.title' ) }
< / h1 >
< / h1 >
< / div >
< / div >
< div class = "flex items-center gap-2" >
{ /* Export button */ }
< button
type = "button"
onClick = { handleExport }
class = "inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
title = { t ( 'settings.export' ) }
>
< Download class = "h-4 w-4 mr-1" / >
{ t ( 'settings.export' ) }
< / button >
{ /* Import button */ }
< button
type = "button"
onClick = { ( ) = > fileInputRef ? . click ( ) }
class = "inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
title = { t ( 'settings.import' ) }
>
< Upload class = "h-4 w-4 mr-1" / >
{ t ( 'settings.import' ) }
< / button >
< input
ref = { fileInputRef }
type = "file"
accept = ".json"
class = "hidden"
onChange = { ( e ) = > {
const file = e . currentTarget . files ? . [ 0 ] ;
if ( file ) handleImport ( file ) ;
} }
/ >
< / div >
< / div >
{ /* Export: Include API keys checkbox */ }
< div class = "mb-6" >
< label class = "inline-flex items-center gap-2 cursor-pointer" >
< input
type = "checkbox"
checked = { includeApiKeys ( ) }
onChange = { ( e ) = > setIncludeApiKeys ( e . currentTarget . checked ) }
class = "rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/ >
< span class = "text-sm text-gray-700" > { t ( 'settings.exportIncludeKeys' ) } < / span >
< / label >
< Show when = { includeApiKeys ( ) } >
< p class = "mt-1 text-sm text-amber-600" >
{ t ( 'settings.exportKeysWarning' ) }
< / p >
< / Show >
< / div >
< Show when = { message ( ) } >
< Show when = { message ( ) } >
{ ( msg ) = > (
{ ( msg ) = > (
@ -328,8 +283,11 @@ const Settings: Component = () => {
< / div >
< / div >
< / Show >
< / Show >
< div class = "bg-white shadow overflow-hidden sm:rounded-lg border border-gray-200" >
{ /* ── Section 1: Contenu ── */ }
< div class = "px-4 py-5 sm:p-6 space-y-6" >
< div class = "mb-8" >
< h2 class = "text-xl font-semibold text-gray-900 mb-1" > { t ( 'settings.section.content' ) } < / h2 >
< p class = "text-sm text-gray-500 mb-4" > { t ( 'settings.section.contentDesc' ) } < / p >
< div class = "bg-white shadow-sm rounded-lg border border-gray-200 p-6 space-y-6" >
{ /* Theme */ }
{ /* Theme */ }
< div >
< div >
< label for = "theme" class = "block text-sm font-medium text-gray-700" >
< label for = "theme" class = "block text-sm font-medium text-gray-700" >
@ -352,6 +310,52 @@ const Settings: Component = () => {
< p class = "mt-2 text-sm text-gray-500" > { t ( 'settings.themeHelp' ) } < / p >
< p class = "mt-2 text-sm text-gray-500" > { t ( 'settings.themeHelp' ) } < / p >
< / div >
< / div >
{ /* Categories */ }
< div >
< div class = "flex justify-between items-center mb-4" >
< label class = "block text-sm font-medium text-gray-700" >
{ t ( 'settings.categories' ) }
< / label >
< button
type = "button"
onClick = { addCategory }
disabled = { settings ( ) . categories . length >= 20 }
class = "inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
< Plus class = "h-4 w-4 mr-1" / >
{ t ( 'settings.addCategory' ) }
< / button >
< / div >
< div class = "space-y-3" >
< For each = { settings ( ) . categories } >
{ ( category , index ) = > (
< div class = "flex items-center gap-2" >
< span class = "text-gray-500 font-medium w-6" >
{ index ( ) + 1 } .
< / span >
< 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"
value = { category }
onInput = { ( e ) = >
handleCategoryChange ( index ( ) , e . currentTarget . value )
}
/ >
< button
type = "button"
onClick = { ( ) = > removeCategory ( index ( ) ) }
disabled = { settings ( ) . categories . length <= 1 }
class = "p-2 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
title = { t ( 'settings.removeCategory' ) }
>
< Trash2 class = "h-5 w-5" / >
< / button >
< / div >
) }
< / For >
< / div >
< / div >
{ /* Max age days + Max items per category */ }
{ /* Max age days + Max items per category */ }
< div class = "grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-2" >
< div class = "grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-2" >
< div >
< div >
@ -367,7 +371,7 @@ const Settings: Component = () => {
id = "maxAgeDays"
id = "maxAgeDays"
min = "1"
min = "1"
max = "365"
max = "365"
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"
class = "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w- 24 sm:text-sm border-gray-300 rounded-md py-2 px-3 border"
value = { settings ( ) . max_age_days }
value = { settings ( ) . max_age_days }
onInput = { ( e ) = >
onInput = { ( e ) = >
setSettings ( ( prev ) = > ( {
setSettings ( ( prev ) = > ( {
@ -392,7 +396,7 @@ const Settings: Component = () => {
id = "maxItemsPerCategory"
id = "maxItemsPerCategory"
min = "1"
min = "1"
max = "20"
max = "20"
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"
class = "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w- 24 sm:text-sm border-gray-300 rounded-md py-2 px-3 border"
value = { settings ( ) . max_items_per_category }
value = { settings ( ) . max_items_per_category }
onInput = { ( e ) = >
onInput = { ( e ) = >
setSettings ( ( prev ) = > ( {
setSettings ( ( prev ) = > ( {
@ -404,7 +408,51 @@ const Settings: Component = () => {
/ >
/ >
< / div >
< / div >
< / div >
< / div >
< / div >
{ /* Summary length slider */ }
< div >
< label for = "summaryLength" class = "block text-sm font-medium text-gray-700" >
{ t ( 'settings.summaryLength' ) }
< / label >
< p class = "text-xs text-gray-500 mb-2" > { t ( 'settings.summaryLengthHelp' ) } < / p >
< div class = "flex items-center gap-4" >
< span class = "text-xs text-gray-500" > { t ( 'settings.summaryShort' ) } < / span >
< input
type = "range"
id = "summaryLength"
min = "1"
max = "3"
step = "1"
class = "flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600"
value = { settings ( ) . summary_length }
onInput = { ( e ) = >
setSettings ( ( prev ) = > ( {
. . . prev ,
summary_length : parseInt ( e . currentTarget . value ) || 3 ,
} ) )
}
/ >
< span class = "text-xs text-gray-500" > { t ( 'settings.summaryDetailed' ) } < / span >
< / div >
< div class = "text-center text-xs text-gray-500 mt-1" >
{ settings ( ) . summary_length === 1
? t ( 'settings.summaryShort' )
: settings ( ) . summary_length === 2
? t ( 'settings.summaryMedium' )
: t ( 'settings.summaryDetailed' ) }
< / div >
< / div >
< / div >
< / div >
{ /* ── Section 2: Sources ── */ }
< div class = "mb-8" >
< h2 class = "text-xl font-semibold text-gray-900 mb-1" > { t ( 'settings.section.sources' ) } < / h2 >
< p class = "text-sm text-gray-500 mb-4" > { t ( 'settings.section.sourcesDesc' ) } < / p >
< div class = "bg-white shadow-sm rounded-lg border border-gray-200 p-6 space-y-6" >
{ /* Max articles per source + Max links per source */ }
< div class = "grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-2" >
< div >
< div >
< label
< label
for = "maxArticlesPerSource"
for = "maxArticlesPerSource"
@ -418,7 +466,7 @@ const Settings: Component = () => {
id = "maxArticlesPerSource"
id = "maxArticlesPerSource"
min = "1"
min = "1"
max = "10"
max = "10"
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"
class = "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w- 24 sm:text-sm border-gray-300 rounded-md py-2 px-3 border"
value = { settings ( ) . max_articles_per_source }
value = { settings ( ) . max_articles_per_source }
onInput = { ( e ) = >
onInput = { ( e ) = >
setSettings ( ( prev ) = > ( {
setSettings ( ( prev ) = > ( {
@ -442,7 +490,7 @@ const Settings: Component = () => {
id = "maxLinksPerSource"
id = "maxLinksPerSource"
min = "1"
min = "1"
max = "30"
max = "30"
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"
class = "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w- 24 sm:text-sm border-gray-300 rounded-md py-2 px-3 border"
value = { settings ( ) . max_links_per_source }
value = { settings ( ) . max_links_per_source }
onInput = { ( e ) = >
onInput = { ( e ) = >
setSettings ( ( prev ) = > ( {
setSettings ( ( prev ) = > ( {
@ -454,47 +502,40 @@ const Settings: Component = () => {
< / div >
< / div >
< / div >
< / div >
< / div >
{ /* Summary length slider */ }
< div >
< div >
< label for = "s ummaryLength " class = "block text-sm font-medium text-gray-700" >
< label for = "sourceExtractionWindow" class = "block text-sm font-medium text-gray-700" >
{ t ( 'settings.summaryLength ') }
{ t ( 'settings.sourceExtractionWindow' ) }
< / label >
< / label >
< p class = "text-xs text-gray-500 mb-2" > { t ( 'settings.summaryLengthHelp' ) } < / p >
< p class = "text-xs text-gray-500 mb-1" > { t ( 'settings.sourceExtractionWindowHelp' ) } < / p >
< div class = "flex items-center gap-4" >
< div class = "mt-1" >
< span class = "text-xs text-gray-500" > { t ( 'settings.summaryShort' ) } < / span >
< input
< input
type = "range "
type = "number "
id = "summaryLength "
id = "sourceExtractionWindow "
min = "1"
min = "1"
max = "3"
max = "10"
step = "1"
class = "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-24 sm:text-sm border-gray-300 rounded-md py-2 px-3 border"
class = "flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600"
value = { settings ( ) . source_extraction_window }
value = { settings ( ) . summary_length }
onInput = { ( e ) = >
onInput = { ( e ) = >
setSettings ( ( prev ) = > ( {
setSettings ( ( prev ) = > ( {
. . . prev ,
. . . prev ,
summary_length : parseInt ( e . currentTarget . value ) || 3 ,
source_extraction_window : parseInt ( e . currentTarget . value ) || 3 ,
} ) )
} ) )
}
}
/ >
/ >
< span class = "text-xs text-gray-500" > { t ( 'settings.summaryDetailed' ) } < / span >
< / div >
< / div >
< div class = "text-center text-xs text-gray-500 mt-1" >
{ settings ( ) . summary_length === 1
? t ( 'settings.summaryShort' )
: settings ( ) . summary_length === 2
? t ( 'settings.summaryMedium' )
: t ( 'settings.summaryDetailed' ) }
< / div >
< / div >
< / div >
< / div >
< SettingsAdvanced settings = { settings } setSettings = { setSettings } / >
{ /* Brave Search */ }
< SettingsBraveSearch settings = { settings } setSettings = { setSettings } / >
< SettingsBraveSearch settings = { settings } setSettings = { setSettings } / >
< / div >
< / div >
{ /* AI Provider & Model - Dynamic selection */ }
{ /* ── Section 3: Intelligence Artificielle ── */ }
< div class = "mb-8" >
< h2 class = "text-xl font-semibold text-gray-900 mb-1" > { t ( 'settings.section.ai' ) } < / h2 >
< p class = "text-sm text-gray-500 mb-4" > { t ( 'settings.section.aiDesc' ) } < / p >
< div class = "bg-white shadow-sm rounded-lg border border-gray-200 p-6 space-y-6" >
< Show
< Show
when = { providers ( ) && providers ( ) ! . length > 0 }
when = { providers ( ) && providers ( ) ! . length > 0 }
fallback = {
fallback = {
@ -508,19 +549,22 @@ const Settings: Component = () => {
< / div >
< / div >
}
}
>
>
{ /* Provider dropdown - only if multiple providers */ }
{ /* Provider card with integrated key status */ }
< Show when = { multipleProviders ( ) } >
< div class = "border border-gray-200 rounded-lg p-4 bg-gray-50" >
< div >
< div class = "flex items-center justify-between mb-3" >
< label
< div class = "flex items-center gap-2" >
for = "aiProvider"
{ /* Provider dropdown or single provider name */ }
class = "block text-sm font-medium text-gray-700"
< Show
when = { multipleProviders ( ) }
fallback = {
< span class = "font-medium text-gray-900" >
{ selectedProvider ( ) ? . display_name }
< / span >
}
>
>
{ t ( 'settings.provider' ) }
< / label >
< div class = "mt-1" >
< select
< select
id = "aiProvider"
id = "aiProvider"
class = "mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md border"
class = "block pl-3 pr-10 py-1.5 text-sm font-medium border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md border bg-white"
value = { settings ( ) . ai_provider }
value = { settings ( ) . ai_provider }
onChange = { ( e ) = >
onChange = { ( e ) = >
handleProviderChange ( e . currentTarget . value )
handleProviderChange ( e . currentTarget . value )
@ -537,25 +581,70 @@ const Settings: Component = () => {
) }
) }
< / For >
< / For >
< / select >
< / select >
< / Show >
< Show when = { selectedProvider ( ) } >
{ ( provider ) = > (
< Show
when = { providerSupportsWebSearch ( provider ( ) . provider_name ) }
fallback = {
< span class = "px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600" >
{ t ( 'settings.provider.noWebSearchBadge' ) }
< / span >
}
>
< span class = "px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800" >
{ t ( 'settings.provider.webSearchBadge' ) }
< / span >
< / Show >
) }
< / Show >
< / div >
< / div >
< p class = "mt-2 text-sm text-gray-500" >
{ t ( 'settings.providerHelp' ) }
{ /* API key status badge */ }
< Show when = { selectedProvider ( ) } >
{ ( provider ) = > (
< Show
when = { getKeyForProvider ( provider ( ) . provider_name ) }
fallback = {
< span class = "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700" >
{ t ( 'settings.aiKeyNotConfigured' ) }
< / span >
}
>
< 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.aiKeyConfigured' ) }
< / span >
< / Show >
) }
< / Show >
< / div >
{ /* Provider info text */ }
< Show when = { selectedProvider ( ) } >
{ ( provider ) = > (
< div class = "flex items-start gap-2 mb-3" >
< Info class = "h-4 w-4 text-gray-400 mt-0.5 flex-shrink-0" / >
< p class = "text-sm text-gray-500" >
{ t ( getProviderInfoKey ( provider ( ) . provider_name ) ) }
< / p >
< / p >
< / div >
< / div >
) }
< / Show >
< / Show >
{ /* Model dropdowns inside the provider card */ }
< div class = "grid grid-cols-1 gap-y-4 gap-x-4 sm:grid-cols-2" >
{ /* Research model dropdown */ }
{ /* Research model dropdown */ }
< div >
< div >
< label
< label
for = "aiModel"
for = "aiModel"
class = "block text-sm font-medium text-gray-700"
class = "block text-xs font-medium text-gray-600 mb-1 "
>
>
{ t ( 'settings.modelResearch' ) }
{ t ( 'settings.modelResearch' ) }
< / label >
< / label >
< div class = "mt-1" >
< select
< select
id = "aiModel"
id = "aiModel"
class = "mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md border"
class = "block w-full pl-3 pr-10 py-2 text-sm border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md border bg-white "
value = { settings ( ) . ai_model }
value = { settings ( ) . ai_model }
onChange = { ( e ) = >
onChange = { ( e ) = >
setSettings ( ( prev ) = > ( {
setSettings ( ( prev ) = > ( {
@ -578,8 +667,7 @@ const Settings: Component = () => {
) }
) }
< / For >
< / For >
< / select >
< / select >
< / div >
< p class = "mt-1 text-xs text-gray-500" >
< p class = "mt-2 text-sm text-gray-500" >
{ t ( 'settings.modelResearchHelp' ) }
{ t ( 'settings.modelResearchHelp' ) }
< / p >
< / p >
< / div >
< / div >
@ -588,14 +676,13 @@ const Settings: Component = () => {
< div >
< div >
< label
< label
for = "aiModelWebsearch"
for = "aiModelWebsearch"
class = "block text-sm font-medium text-gray-700 "
class = "block text-xs font-medium text-gray-600 mb-1 "
>
>
{ t ( 'settings.modelWebsearch' ) }
{ t ( 'settings.modelWebsearch' ) }
< / label >
< / label >
< div class = "mt-1" >
< select
< select
id = "aiModelWebsearch"
id = "aiModelWebsearch"
class = "mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md border"
class = "block w-full pl-3 pr-10 py-2 text-sm border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md border bg-white "
value = { settings ( ) . ai_model_websearch }
value = { settings ( ) . ai_model_websearch }
onChange = { ( e ) = >
onChange = { ( e ) = >
setSettings ( ( prev ) = > ( {
setSettings ( ( prev ) = > ( {
@ -614,90 +701,180 @@ const Settings: Component = () => {
) }
) }
< / For >
< / For >
< / select >
< / select >
< / div >
< p class = "mt-1 text-xs text-gray-500" >
< p class = "mt-2 text-sm text-gray-500" >
{ t ( 'settings.modelWebsearchHelp' ) }
{ t ( 'settings.modelWebsearchHelp' ) }
< / p >
< / p >
< / div >
< / div >
< / div >
< / div >
{ /* Provider info text + web search badge */ }
{ /* Search agent behavior */ }
< Show when = { selectedProvider ( ) } >
< div >
{ ( provider ) = > (
< label
< div class = "mt-3 flex items-start gap-2" >
for = "searchAgentBehavior"
< Info class = "h-4 w-4 text-gray-400 mt-0.5 flex-shrink-0" / >
class = "block text-sm font-medium text-gray-700"
< div class = "space-y-1" >
< p class = "text-sm text-gray-500" >
{ t ( getProviderInfoKey ( provider ( ) . provider_name ) ) }
< / p >
< Show
when = { providerSupportsWebSearch ( provider ( ) . provider_name ) }
fallback = {
< span class = "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600" >
{ t ( 'settings.provider.noWebSearchBadge' ) }
< / span >
}
>
>
< span class = "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800" >
{ t ( 'settings.searchBehavior' ) }
{ t ( 'settings.provider.webSearchBadge' ) }
< / label >
< / span >
< div class = "mt-1" >
< / Show >
< textarea
id = "searchAgentBehavior"
rows = { 3 }
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"
value = { settings ( ) . search_agent_behavior }
onInput = { ( e ) = >
setSettings ( ( prev ) = > ( {
. . . prev ,
search_agent_behavior : e.currentTarget.value ,
} ) )
}
placeholder = { t ( 'settings.searchBehaviorPlaceholder' ) }
/ >
< / div >
< / div >
< p class = "mt-2 text-sm text-gray-500" >
{ t ( 'settings.searchBehaviorHelp' ) }
< / p >
< / div >
< / div >
) }
< / Show >
{ /* Full API Key Manager */ }
< ApiKeyManager providers = { providers ( ) ! } / >
< / Show >
< / Show >
< / div >
< / div >
{ /* Categories */ }
{ /* ── Section 4: Performance ── */ }
< div class = "mb-8" >
< h2 class = "text-xl font-semibold text-gray-900 mb-1" > { t ( 'settings.section.performance' ) } < / h2 >
< p class = "text-sm text-gray-500 mb-4" > { t ( 'settings.section.performanceDesc' ) } < / p >
< div class = "bg-white shadow-sm rounded-lg border border-gray-200 p-6 space-y-6" >
< div class = "grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-2" >
< div >
< div >
< div class = "flex justify-between items-center mb-4" >
< label
< label class = "block text-sm font-medium text-gray-700" >
for = "batchSize"
{ t ( 'settings.categories' ) }
class = "block text-sm font-medium text-gray-700"
< / label >
< button
type = "button"
onClick = { addCategory }
disabled = { settings ( ) . categories . length >= 20 }
class = "inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
>
< Plus class = "h-4 w-4 mr-1" / >
{ t ( 'settings.batchSize' ) }
{ t ( 'settings.addCategory' ) }
< / label >
< / button >
< p class = "text-xs text-gray-500 mb-1" > { t ( 'settings.batchSizeHelp' ) } < / p >
< div class = "mt-1" >
< input
type = "number"
id = "batchSize"
min = "1"
max = "20"
class = "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-24 sm:text-sm border-gray-300 rounded-md py-2 px-3 border"
value = { settings ( ) . batch_size }
onInput = { ( e ) = >
setSettings ( ( prev ) = > ( {
. . . prev ,
batch_size :
parseInt ( e . currentTarget . value ) || 5 ,
} ) )
}
/ >
< / div >
< / div >
< div class = "space-y-3" >
< / div >
< For each = { settings ( ) . categories } >
{ ( category , index ) = > (
< div >
< div class = "flex items-center gap-2" >
< label
< span class = "text-gray-500 font-medium w-6" >
for = "articleHistoryDays"
{ index ( ) + 1 } .
class = "block text-sm font-medium text-gray-700"
< / span >
>
{ t ( 'settings.articleHistoryDays' ) }
< / label >
< div class = "mt-1" >
< input
< input
type = "text"
type = "number"
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"
id = "articleHistoryDays"
value = { category }
min = "0"
max = "365"
class = "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-24 sm:text-sm border-gray-300 rounded-md py-2 px-3 border"
value = { settings ( ) . article_history_days }
onInput = { ( e ) = >
onInput = { ( e ) = >
handleCategoryChange ( index ( ) , e . currentTarget . value )
setSettings ( ( prev ) = > ( {
. . . prev ,
article_history_days :
parseInt ( e . currentTarget . value ) || 90 ,
} ) )
}
}
/ >
/ >
< / div >
< div class = "mt-2" >
< A href = "/article-history" class = "text-sm text-indigo-600 hover:text-indigo-800 underline" >
{ t ( 'articleHistory.viewHistory' ) }
< / A >
< / div >
< / div >
< / div >
< SettingsRateLimit settings = { settings } setSettings = { setSettings } / >
< / div >
< / div >
{ /* ── Section 5: Import / Export (collapsed) ── */ }
< div class = "mb-8" >
< details >
< summary class = "cursor-pointer" >
< h2 class = "text-xl font-semibold text-gray-900 mb-1 inline" > { t ( 'settings.section.importExport' ) } < / h2 >
< p class = "text-sm text-gray-500 mt-1 mb-4" > { t ( 'settings.section.importExportDesc' ) } < / p >
< / summary >
< div class = "bg-white shadow-sm rounded-lg border border-gray-200 p-6 space-y-4" >
< div class = "flex items-center gap-3" >
{ /* Export button */ }
< button
< button
type = "button"
type = "button"
onClick = { ( ) = > removeCategory ( index ( ) ) }
onClick = { handleExport }
disabled = { settings ( ) . categories . length <= 1 }
class = "inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
class = "p-2 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
title = { t ( 'settings.export' ) }
title = { t ( 'settings.removeCategory' ) }
>
>
< Trash2 class = "h-5 w-5" / >
< Download class = "h-4 w-4 mr-1" / >
{ t ( 'settings.export' ) }
< / button >
{ /* Import button */ }
< button
type = "button"
onClick = { ( ) = > fileInputRef ? . click ( ) }
class = "inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
title = { t ( 'settings.import' ) }
>
< Upload class = "h-4 w-4 mr-1" / >
{ t ( 'settings.import' ) }
< / button >
< / button >
< input
ref = { fileInputRef }
type = "file"
accept = ".json"
class = "hidden"
onChange = { ( e ) = > {
const file = e . currentTarget . files ? . [ 0 ] ;
if ( file ) handleImport ( file ) ;
} }
/ >
< / div >
< / div >
) }
< / For >
{ /* Include API keys checkbox */ }
< div >
< label class = "inline-flex items-center gap-2 cursor-pointer" >
< input
type = "checkbox"
checked = { includeApiKeys ( ) }
onChange = { ( e ) = > setIncludeApiKeys ( e . currentTarget . checked ) }
class = "rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/ >
< span class = "text-sm text-gray-700" > { t ( 'settings.exportIncludeKeys' ) } < / span >
< / label >
< Show when = { includeApiKeys ( ) } >
< p class = "mt-1 text-sm text-amber-600" >
{ t ( 'settings.exportKeysWarning' ) }
< / p >
< / Show >
< / div >
< / div >
< / div >
< / div >
< / details >
< SettingsRateLimit settings = { settings } setSettings = { setSettings } / >
< / div >
< / div >
{ /* Save button */ }
{ /* S ticky s ave button */}
< div class = "px-4 py-3 bg-gray-50 text-right sm:px-6" >
< div class = " sticky bottom-0 bg-white border-t border-gray-200 px-6 py-3 -mx-4 sm:-mx-6 lg:-mx-8 mt-8 flex justify-end ">
< Button
< Button
onClick = { handleSave }
onClick = { handleSave }
loading = { saving ( ) }
loading = { saving ( ) }
@ -707,15 +884,8 @@ const Settings: Component = () => {
< / Button >
< / Button >
< / div >
< / div >
< / div >
< / div >
{ /* API Key Management */ }
< Show when = { providers ( ) && providers ( ) ! . length > 0 } >
< ApiKeyManager providers = { providers ( ) ! } / >
< / Show >
< / div >
< / Show >
< / Show >
) ;
) ;
} ;
} ;
export default Settings ;
export default Settings ;