feat: split model dropdowns — scraping vs websearch in frontend

Replace the single `models` array in `ProviderConfig` and `AdminProvider`
with separate `models_scraping` / `models_websearch` lists. Rename
`ai_model_writing` → `ai_model_websearch` in `UserSettings` and all
references (Settings page, admin Providers page, E2E test, fixtures,
and unit tests). Update i18n label for the second dropdown to
"Modele d'IA (Recherche Web)".

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

@ -140,7 +140,7 @@ test.describe('Live generation with OpenAI', () => {
search_agent_behavior: '',
ai_provider: 'openai',
ai_model: 'gpt-4o-mini',
ai_model_writing: 'gpt-4o-mini',
ai_model_websearch: 'gpt-4o-mini',
use_llm_for_source_links: false,
article_history_days: 90,
});

@ -20,14 +20,20 @@ describe('Config API - Provider Config', () => {
{
provider_name: 'gemini',
display_name: 'Google Gemini',
models: [
models_scraping: [
{ model_id: 'gemini-3.1-pro', display_name: 'Gemini 3.1 Pro' },
],
models_websearch: [
{ model_id: 'gemini-3.1-pro', display_name: 'Gemini 3.1 Pro' },
],
},
{
provider_name: 'openai',
display_name: 'OpenAI',
models: [
models_scraping: [
{ model_id: 'gpt-4o', display_name: 'GPT-4o' },
],
models_websearch: [
{ model_id: 'gpt-4o', display_name: 'GPT-4o' },
],
},
@ -56,7 +62,7 @@ describe('Config API - Provider Config', () => {
expect(result).toEqual(mockProviders);
expect(result).toHaveLength(2);
expect(result[0].provider_name).toBe('gemini');
expect(result[0].models).toHaveLength(1);
expect(result[0].models_scraping).toHaveLength(1);
expect(result[1].provider_name).toBe('openai');
});
@ -131,7 +137,14 @@ describe('Admin API - Providers', () => {
id: '1',
provider_name: 'gemini',
display_name: 'Google Gemini',
models: [
models_scraping: [
{
model_id: 'gemini-3.1-pro',
display_name: 'Gemini 3.1 Pro',
is_default: true,
},
],
models_websearch: [
{
model_id: 'gemini-3.1-pro',
display_name: 'Gemini 3.1 Pro',
@ -160,14 +173,21 @@ describe('Admin API - Providers', () => {
expect(result).toEqual(mockProviders);
expect(result[0].is_enabled).toBe(true);
expect(result[0].models[0].is_default).toBe(true);
expect(result[0].models_scraping[0].is_default).toBe(true);
});
it('should call POST /api/v1/admin/providers with body', async () => {
const newProvider = {
provider_name: 'anthropic',
display_name: 'Anthropic',
models: [
models_scraping: [
{
model_id: 'claude-opus-4',
display_name: 'Claude Opus 4',
is_default: true,
},
],
models_websearch: [
{
model_id: 'claude-opus-4',
display_name: 'Claude Opus 4',

@ -53,13 +53,14 @@ export const MOCK_SOURCES: Source[] = [
// ---- Settings ----
export const MOCK_SETTINGS: UserSettings = {
...DEFAULT_SETTINGS, theme: 'Intelligence Artificielle', ai_provider: 'gemini', ai_model: 'gemini-2.5-pro', ai_model_writing: 'gemini-2.5-flash',
...DEFAULT_SETTINGS, theme: 'Intelligence Artificielle', ai_provider: 'gemini', ai_model: 'gemini-2.5-pro', ai_model_websearch: 'gemini-2.5-flash',
};
// ---- Providers ----
export const MOCK_PROVIDER_CONFIG: ProviderConfig = {
provider_name: 'gemini', display_name: 'Google Gemini',
models: [{ model_id: 'gemini-2.5-pro', display_name: 'Gemini 2.5 Pro' }, { model_id: 'gemini-2.5-flash', display_name: 'Gemini 2.5 Flash' }],
models_scraping: [{ model_id: 'gemini-2.5-pro', display_name: 'Gemini 2.5 Pro' }, { model_id: 'gemini-2.5-flash', display_name: 'Gemini 2.5 Flash' }],
models_websearch: [{ model_id: 'gemini-2.5-pro', display_name: 'Gemini 2.5 Pro' }, { model_id: 'gemini-2.5-flash', display_name: 'Gemini 2.5 Flash' }],
};
export const MOCK_PROVIDER_CONFIGS: ProviderConfig[] = [MOCK_PROVIDER_CONFIG];

@ -45,7 +45,11 @@ const mockProvidersWithOpenAI = [
{
provider_name: 'openai',
display_name: 'OpenAI',
models: [
models_scraping: [
{ model_id: 'gpt-4o', display_name: 'GPT-4o' },
{ model_id: 'gpt-4o-mini', display_name: 'GPT-4o Mini' },
],
models_websearch: [
{ model_id: 'gpt-4o', display_name: 'GPT-4o' },
{ model_id: 'gpt-4o-mini', display_name: 'GPT-4o Mini' },
],
@ -146,7 +150,7 @@ describe('Settings Page', () => {
});
});
it('should render two model dropdowns (research + writing)', async () => {
it('should render two model dropdowns (scraping + websearch)', async () => {
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI);
mockedListKeys.mockResolvedValue([]);
@ -159,7 +163,7 @@ describe('Settings Page', () => {
).toBeInTheDocument();
});
expect(
screen.getByLabelText("Modele d'IA (Redaction et Synthese)"),
screen.getByLabelText("Modele d'IA (Recherche Web)"),
).toBeInTheDocument();
});

@ -8,7 +8,7 @@ describe('Settings validation logic', () => {
expect(DEFAULT_SETTINGS.max_items_per_category).toBe(4);
expect(DEFAULT_SETTINGS.categories.length).toBeGreaterThan(0);
expect(DEFAULT_SETTINGS.ai_model).toBe('');
expect(DEFAULT_SETTINGS.ai_model_writing).toBe('');
expect(DEFAULT_SETTINGS.ai_model_websearch).toBe('');
expect(DEFAULT_SETTINGS.ai_provider).toBe('');
expect(DEFAULT_SETTINGS.rate_limit_max_requests).toBeNull();
expect(DEFAULT_SETTINGS.rate_limit_time_window_seconds).toBeNull();

@ -142,8 +142,8 @@ const fr = {
'settings.loadError': 'Erreur lors du chargement des parametres.',
'settings.modelResearch': "Modele d'IA (Recherche et Extraction)",
'settings.modelResearchHelp': "Choisissez le modele d'IA utilise pour rechercher et extraire les informations.",
'settings.modelWriting': "Modele d'IA (Redaction et Synthese)",
'settings.modelWritingHelp': "Choisissez le modele d'IA utilise pour le second agent, charge de rediger et structurer la synthese finale.",
'settings.modelWebsearch': "Modele d'IA (Recherche Web)",
'settings.modelWebsearchHelp': "Choisissez le modele d'IA utilise pour la recherche web et la generation des syntheses.",
'settings.rateLimitSection': 'Limitation de taux',
'settings.rateLimitMaxRequests': 'Requetes maximum',
'settings.rateLimitTimeWindow': 'Fenetre de temps (secondes)',
@ -266,6 +266,8 @@ const fr = {
'admin.providers.displayName': "Nom d'affichage",
'admin.providers.displayNamePlaceholder': 'ex: OpenAI, Google Gemini',
'admin.providers.models': 'Modeles disponibles',
'admin.providers.modelsScraping': 'Modeles (Extraction / Scraping)',
'admin.providers.modelsWebsearch': 'Modeles (Recherche Web)',
'admin.providers.modelId': 'Identifiant du modele',
'admin.providers.modelIdPlaceholder': 'ex: gpt-4o',
'admin.providers.modelDisplayName': "Nom d'affichage du modele",

@ -87,7 +87,7 @@ const GenerateSynthesis: Component = () => {
const modelDisplayName = (): string => {
const provider = selectedProvider();
if (!provider) return settings().ai_model;
const model = provider.models.find((m) => m.model_id === settings().ai_model);
const model = provider.models_scraping.find((m) => m.model_id === settings().ai_model);
return model?.display_name ?? settings().ai_model;
};

@ -31,9 +31,9 @@ import { getProviderInfoKey, providerSupportsWebSearch } from '~/utils/providers
* - **Rate limit null handling**: The `rate_limit_max_requests` and
* `rate_limit_time_window_seconds` fields accept `null` (meaning "use
* server defaults"). Empty inputs are stored as `null`, not zero.
* - **Dual model state**: Research model (`ai_model`) and writing model
* (`ai_model_writing`) are independently selectable from the same
* provider's model list.
* - **Dual model state**: Scraping model (`ai_model`) and websearch model
* (`ai_model_websearch`) are independently selectable from their
* respective provider model lists.
*/
const Settings: Component = () => {
const { t } = useI18n();
@ -106,7 +106,7 @@ const Settings: Component = () => {
setSettings((prev) => ({
...prev,
ai_provider: providerName,
ai_model: provider?.models[0]?.model_id ?? '',
ai_model: provider?.models_scraping[0]?.model_id ?? '',
}));
};
@ -117,7 +117,7 @@ const Settings: Component = () => {
setSettings((prev) => ({
...prev,
ai_provider: single.provider_name,
ai_model: prev.ai_model || single.models[0]?.model_id || '',
ai_model: prev.ai_model || single.models_scraping[0]?.model_id || '',
}));
}
});
@ -590,7 +590,7 @@ const Settings: Component = () => {
{t('settings.modelPlaceholder')}
</option>
</Show>
<For each={selectedProvider()?.models ?? []}>
<For each={selectedProvider()?.models_scraping ?? []}>
{(model) => (
<option value={model.model_id}>
{model.display_name}
@ -604,29 +604,29 @@ const Settings: Component = () => {
</p>
</div>
{/* Writing model dropdown */}
{/* Websearch model dropdown */}
<div>
<label
for="aiModelWriting"
for="aiModelWebsearch"
class="block text-sm font-medium text-gray-700"
>
{t('settings.modelWriting')}
{t('settings.modelWebsearch')}
</label>
<div class="mt-1">
<select
id="aiModelWriting"
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"
value={settings().ai_model_writing}
value={settings().ai_model_websearch}
onChange={(e) =>
setSettings((prev) => ({
...prev,
ai_model_writing: e.currentTarget.value,
ai_model_websearch: e.currentTarget.value,
}))
}
disabled={!selectedProvider()}
>
<option value="">{t('settings.modelPlaceholder')}</option>
<For each={selectedProvider()?.models ?? []}>
<For each={selectedProvider()?.models_websearch ?? []}>
{(model) => (
<option value={model.model_id}>
{model.display_name}
@ -636,7 +636,7 @@ const Settings: Component = () => {
</select>
</div>
<p class="mt-2 text-sm text-gray-500">
{t('settings.modelWritingHelp')}
{t('settings.modelWebsearchHelp')}
</p>
</div>

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

@ -48,7 +48,7 @@ export interface UserSettings {
article_history_days: number;
search_agent_behavior: string;
ai_model: string;
ai_model_writing: string;
ai_model_websearch: string;
ai_provider: string;
rate_limit_max_requests: number | null;
rate_limit_time_window_seconds: number | null;
@ -65,7 +65,7 @@ export const DEFAULT_SETTINGS: UserSettings = {
search_agent_behavior:
"Tu peux egalement utiliser d'autres sources pertinentes trouvees via la recherche Google.",
ai_model: '',
ai_model_writing: '',
ai_model_websearch: '',
ai_provider: '',
rate_limit_max_requests: null,
rate_limit_time_window_seconds: null,
@ -178,7 +178,8 @@ export interface AdminProvider {
id: string;
provider_name: string;
display_name: string;
models: AdminProviderModel[];
models_scraping: AdminProviderModel[];
models_websearch: AdminProviderModel[];
is_enabled: boolean;
created_at: string;
updated_at: string;
@ -187,13 +188,15 @@ export interface AdminProvider {
export interface CreateProviderRequest {
provider_name: string;
display_name: string;
models: AdminProviderModel[];
models_scraping: AdminProviderModel[];
models_websearch: AdminProviderModel[];
is_enabled: boolean;
}
export interface UpdateProviderRequest {
display_name: string;
models: AdminProviderModel[];
models_scraping: AdminProviderModel[];
models_websearch: AdminProviderModel[];
is_enabled: boolean;
}
@ -256,7 +259,8 @@ export interface ProviderConfigModel {
export interface ProviderConfig {
provider_name: string;
display_name: string;
models: ProviderConfigModel[];
models_scraping: ProviderConfigModel[];
models_websearch: ProviderConfigModel[];
}
// ---- Article History ----

Loading…
Cancel
Save