feat: add Brave Search section to Settings page

Adds a dedicated Brave Search section after the Advanced Extraction section,
including inline API key management (save/test/delete) and a use_brave_search
toggle that auto-disables when the key is removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
master
oabrivard 3 months ago
parent f124b056fe
commit c5d23ecd10

@ -7,7 +7,7 @@ import {
For, For,
createEffect, createEffect,
} from 'solid-js'; } 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 { A } from '@solidjs/router';
import { settingsApi } from '~/api/settings'; import { settingsApi } from '~/api/settings';
import { configApi } from '~/api/config'; import { configApi } from '~/api/config';
@ -18,6 +18,7 @@ import type { UserSettings, ProviderConfig } from '~/types';
import LoadingSpinner from '~/components/ui/LoadingSpinner'; 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 { useToast } from '~/components/ui/Toast';
/** /**
* Settings page for configuring the user's synthesis preferences. * Settings page for configuring the user's synthesis preferences.
@ -37,6 +38,7 @@ import { getProviderInfoKey, providerSupportsWebSearch } from '~/utils/providers
*/ */
const Settings: Component = () => { const Settings: Component = () => {
const { t } = useI18n(); const { t } = useI18n();
const { addToast } = useToast();
const [settings, setSettings] = createSignal<UserSettings>({ const [settings, setSettings] = createSignal<UserSettings>({
...DEFAULT_SETTINGS, ...DEFAULT_SETTINGS,
@ -52,6 +54,13 @@ const Settings: Component = () => {
const [providers] = createResource(() => configApi.listProviders()); const [providers] = createResource(() => configApi.listProviders());
const [providerWarning, setProviderWarning] = createSignal(false); 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; let fileInputRef: HTMLInputElement | undefined;
onMount(async () => { 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 () => { const handleSave = async () => {
setSaving(true); setSaving(true);
setMessage(null); setMessage(null);
@ -513,6 +569,115 @@ const Settings: Component = () => {
</div> </div>
</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 */} {/* Search agent behavior */}
<div> <div>
<label <label

Loading…
Cancel
Save