refactor: decompose Settings.tsx into sub-components

Extract three self-contained sections from Settings.tsx (1025 lines)
into dedicated components under frontend/src/components/settings/:
- SettingsBraveSearch: Brave API key lifecycle + use_brave_search toggle
- SettingsAdvanced: article_history_days, batch_size, use_llm_for_source_links, search_agent_behavior
- SettingsRateLimit: rate limit inputs + effective rate display + reset

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
master
oabrivard 3 months ago
parent f44aa44c48
commit ee6d833d70

@ -0,0 +1,143 @@
import { type Component } from 'solid-js';
import { A } from '@solidjs/router';
import { useI18n } from '~/i18n';
import type { UserSettings } from '~/types';
interface SettingsAdvancedProps {
settings: () => UserSettings;
setSettings: (updater: (prev: UserSettings) => UserSettings) => void;
}
/**
* Advanced settings section on the Settings page.
*
* Groups fields that control extraction and pipeline behaviour:
* - `article_history_days` deduplication window
* - `batch_size` number of sources processed per LLM batch
* - `use_llm_for_source_links` whether to use LLM to extract links
* - `search_agent_behavior` free-text prompt injection for the search agent
*/
const SettingsAdvanced: Component<SettingsAdvancedProps> = (props) => {
const { t } = useI18n();
return (
<>
{/* articleHistoryDays + batchSize grid */}
<div class="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-2">
<div>
<label
for="articleHistoryDays"
class="block text-sm font-medium text-gray-700"
>
{t('settings.articleHistoryDays')}
</label>
<div class="mt-1">
<input
type="number"
id="articleHistoryDays"
min="0"
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"
value={props.settings().article_history_days}
onInput={(e) =>
props.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>
<label
for="batchSize"
class="block text-sm font-medium text-gray-700"
>
{t('settings.batchSize')}
</label>
<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-full sm:text-sm border-gray-300 rounded-md py-2 px-3 border"
value={props.settings().batch_size}
onInput={(e) =>
props.setSettings((prev) => ({
...prev,
batch_size:
parseInt(e.currentTarget.value) || 5,
}))
}
/>
</div>
</div>
</div>
{/* Advanced extraction */}
<div class="mt-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">
{t('settings.advancedExtraction')}
</h3>
<div class="space-y-4">
<div class="flex items-center">
<input
type="checkbox"
id="useLlmSourceLinks"
checked={props.settings().use_llm_for_source_links}
onChange={(e) =>
props.setSettings((prev) => ({
...prev,
use_llm_for_source_links: e.currentTarget.checked,
}))
}
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
/>
<label for="useLlmSourceLinks" class="ml-2 block text-sm text-gray-700">
{t('settings.useLlmForSourceLinks')}
</label>
</div>
</div>
</div>
{/* Search agent behavior */}
<div>
<label
for="searchAgentBehavior"
class="block text-sm font-medium text-gray-700"
>
{t('settings.searchBehavior')}
</label>
<div class="mt-1">
<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={props.settings().search_agent_behavior}
onInput={(e) =>
props.setSettings((prev) => ({
...prev,
search_agent_behavior: e.currentTarget.value,
}))
}
placeholder={t('settings.searchBehaviorPlaceholder')}
/>
</div>
<p class="mt-2 text-sm text-gray-500">
{t('settings.searchBehaviorHelp')}
</p>
</div>
</>
);
};
export default SettingsAdvanced;

@ -0,0 +1,189 @@
import { type Component, createSignal, createResource, Show } from 'solid-js';
import { Trash2, RefreshCw } from 'lucide-solid';
import { apiKeysApi } from '~/api/apiKeys';
import { useI18n } from '~/i18n';
import { isApiError } from '~/types';
import type { UserSettings } from '~/types';
import { useToast } from '~/components/ui/Toast';
interface SettingsBraveSearchProps {
settings: () => UserSettings;
setSettings: (updater: (prev: UserSettings) => UserSettings) => void;
}
/**
* Brave Search configuration section on the Settings page.
*
* Manages the Brave Search API key lifecycle (save, test, delete) and the
* `use_brave_search` toggle. The key resource is owned here so that
* refetching after mutations is self-contained.
*/
const SettingsBraveSearch: Component<SettingsBraveSearchProps> = (props) => {
const { t } = useI18n();
const { addToast } = useToast();
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);
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
props.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 });
}
};
return (
<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={props.settings().use_brave_search}
disabled={!braveKey()}
onChange={(e) =>
props.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>
);
};
export default SettingsBraveSearch;

@ -0,0 +1,111 @@
import { type Component, Show } from 'solid-js';
import { useI18n } from '~/i18n';
import type { UserSettings } from '~/types';
interface SettingsRateLimitProps {
settings: () => UserSettings;
setSettings: (updater: (prev: UserSettings) => UserSettings) => void;
}
/**
* Rate limit configuration section on the Settings page.
*
* Exposes `rate_limit_max_requests` and `rate_limit_time_window_seconds`.
* Both fields accept `null` (meaning "use server defaults"); empty inputs
* are stored as `null`, not zero. A reset button clears both fields back
* to `null` at once.
*/
const SettingsRateLimit: Component<SettingsRateLimitProps> = (props) => {
const { t } = useI18n();
return (
<>
<hr class="border-gray-200" />
<div>
<h3 class="text-sm font-medium text-gray-700 mb-4">
{t('settings.rateLimitSection')}
</h3>
<div class="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-2">
<div>
<label
for="rateLimitMaxRequests"
class="block text-sm font-medium text-gray-700"
>
{t('settings.rateLimitMaxRequests')}
</label>
<div class="mt-1">
<input
type="number"
id="rateLimitMaxRequests"
min="1"
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={props.settings().rate_limit_max_requests ?? ''}
onInput={(e) => {
const val = e.currentTarget.value;
props.setSettings((prev) => ({
...prev,
rate_limit_max_requests: val === '' ? null : (parseInt(val) || null),
}));
}}
placeholder=""
/>
</div>
</div>
<div>
<label
for="rateLimitTimeWindow"
class="block text-sm font-medium text-gray-700"
>
{t('settings.rateLimitTimeWindow')}
</label>
<div class="mt-1">
<input
type="number"
id="rateLimitTimeWindow"
min="1"
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={props.settings().rate_limit_time_window_seconds ?? ''}
onInput={(e) => {
const val = e.currentTarget.value;
props.setSettings((prev) => ({
...prev,
rate_limit_time_window_seconds: val === '' ? null : (parseInt(val) || null),
}));
}}
placeholder=""
/>
</div>
</div>
</div>
<p class="mt-2 text-sm text-gray-500">
{t('settings.rateLimitHelp')}
</p>
<Show when={props.settings().rate_limit_max_requests !== null && props.settings().rate_limit_time_window_seconds !== null}>
<p class="mt-2 text-sm text-indigo-600 font-medium">
{t('settings.rateLimitEffective')
.replace('{max}', String(props.settings().rate_limit_max_requests))
.replace('{window}', String(props.settings().rate_limit_time_window_seconds))}
</p>
</Show>
<Show when={props.settings().rate_limit_max_requests !== null || props.settings().rate_limit_time_window_seconds !== null}>
<button
type="button"
onClick={() =>
props.setSettings((prev) => ({
...prev,
rate_limit_max_requests: null,
rate_limit_time_window_seconds: null,
}))
}
class="mt-2 text-sm text-indigo-600 hover:text-indigo-800 underline"
>
{t('settings.rateLimitReset')}
</button>
</Show>
</div>
</>
);
};
export default SettingsRateLimit;

@ -7,8 +7,7 @@ import {
For,
createEffect,
} from 'solid-js';
import { Settings as SettingsIcon, Save, Plus, Trash2, Info, Download, Upload, RefreshCw } from 'lucide-solid';
import { A } from '@solidjs/router';
import { Settings as SettingsIcon, Save, Plus, Trash2, Info, Download, Upload } from 'lucide-solid';
import { settingsApi } from '~/api/settings';
import { configApi } from '~/api/config';
import { apiKeysApi } from '~/api/apiKeys';
@ -18,7 +17,9 @@ 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';
import SettingsBraveSearch from '~/components/settings/SettingsBraveSearch';
import SettingsAdvanced from '~/components/settings/SettingsAdvanced';
import SettingsRateLimit from '~/components/settings/SettingsRateLimit';
/**
* Settings page for configuring the user's synthesis preferences.
@ -38,7 +39,6 @@ import { useToast } from '~/components/ui/Toast';
*/
const Settings: Component = () => {
const { t } = useI18n();
const { addToast } = useToast();
const [settings, setSettings] = createSignal<UserSettings>({
...DEFAULT_SETTINGS,
@ -54,13 +54,6 @@ 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 () => {
@ -131,53 +124,6 @@ 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);
@ -484,227 +430,11 @@ const Settings: Component = () => {
</div>
</div>
<div>
<label
for="articleHistoryDays"
class="block text-sm font-medium text-gray-700"
>
{t('settings.articleHistoryDays')}
</label>
<div class="mt-1">
<input
type="number"
id="articleHistoryDays"
min="0"
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"
value={settings().article_history_days}
onInput={(e) =>
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>
<label
for="batchSize"
class="block text-sm font-medium text-gray-700"
>
{t('settings.batchSize')}
</label>
<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-full 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>
{/* Advanced extraction */}
<div class="mt-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">
{t('settings.advancedExtraction')}
</h3>
<div class="space-y-4">
<div class="flex items-center">
<input
type="checkbox"
id="useLlmSourceLinks"
checked={settings().use_llm_for_source_links}
onChange={(e) =>
setSettings((prev) => ({
...prev,
use_llm_for_source_links: e.currentTarget.checked,
}))
}
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
/>
<label for="useLlmSourceLinks" class="ml-2 block text-sm text-gray-700">
{t('settings.useLlmForSourceLinks')}
</label>
</div>
</div>
</div>
<SettingsAdvanced settings={settings} setSettings={setSettings} />
{/* 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
for="searchAgentBehavior"
class="block text-sm font-medium text-gray-700"
>
{t('settings.searchBehavior')}
</label>
<div class="mt-1">
<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>
<p class="mt-2 text-sm text-gray-500">
{t('settings.searchBehaviorHelp')}
</p>
</div>
<SettingsBraveSearch settings={settings} setSettings={setSettings} />
{/* AI Provider & Model - Dynamic selection */}
<Show
@ -905,91 +635,7 @@ const Settings: Component = () => {
</div>
</div>
{/* Rate Limit Section */}
<hr class="border-gray-200" />
<div>
<h3 class="text-sm font-medium text-gray-700 mb-4">
{t('settings.rateLimitSection')}
</h3>
<div class="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-2">
<div>
<label
for="rateLimitMaxRequests"
class="block text-sm font-medium text-gray-700"
>
{t('settings.rateLimitMaxRequests')}
</label>
<div class="mt-1">
<input
type="number"
id="rateLimitMaxRequests"
min="1"
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().rate_limit_max_requests ?? ''}
onInput={(e) => {
const val = e.currentTarget.value;
setSettings((prev) => ({
...prev,
rate_limit_max_requests: val === '' ? null : (parseInt(val) || null),
}));
}}
placeholder=""
/>
</div>
</div>
<div>
<label
for="rateLimitTimeWindow"
class="block text-sm font-medium text-gray-700"
>
{t('settings.rateLimitTimeWindow')}
</label>
<div class="mt-1">
<input
type="number"
id="rateLimitTimeWindow"
min="1"
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().rate_limit_time_window_seconds ?? ''}
onInput={(e) => {
const val = e.currentTarget.value;
setSettings((prev) => ({
...prev,
rate_limit_time_window_seconds: val === '' ? null : (parseInt(val) || null),
}));
}}
placeholder=""
/>
</div>
</div>
</div>
<p class="mt-2 text-sm text-gray-500">
{t('settings.rateLimitHelp')}
</p>
<Show when={settings().rate_limit_max_requests !== null && settings().rate_limit_time_window_seconds !== null}>
<p class="mt-2 text-sm text-indigo-600 font-medium">
{t('settings.rateLimitEffective')
.replace('{max}', String(settings().rate_limit_max_requests))
.replace('{window}', String(settings().rate_limit_time_window_seconds))}
</p>
</Show>
<Show when={settings().rate_limit_max_requests !== null || settings().rate_limit_time_window_seconds !== null}>
<button
type="button"
onClick={() =>
setSettings((prev) => ({
...prev,
rate_limit_max_requests: null,
rate_limit_time_window_seconds: null,
}))
}
class="mt-2 text-sm text-indigo-600 hover:text-indigo-800 underline"
>
{t('settings.rateLimitReset')}
</button>
</Show>
</div>
<SettingsRateLimit settings={settings} setSettings={setSettings} />
</div>
{/* Save button */}

Loading…
Cancel
Save