diff --git a/frontend/src/__tests__/settings-validation.test.ts b/frontend/src/__tests__/settings-validation.test.ts index 5cab679..555d4b7 100644 --- a/frontend/src/__tests__/settings-validation.test.ts +++ b/frontend/src/__tests__/settings-validation.test.ts @@ -8,7 +8,10 @@ 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_provider).toBe(''); + expect(DEFAULT_SETTINGS.rate_limit_max_requests).toBeNull(); + expect(DEFAULT_SETTINGS.rate_limit_time_window_seconds).toBeNull(); }); it('should filter empty categories before save', () => { diff --git a/frontend/src/api/apiKeys.ts b/frontend/src/api/apiKeys.ts index 5a96f91..9618c6b 100644 --- a/frontend/src/api/apiKeys.ts +++ b/frontend/src/api/apiKeys.ts @@ -13,4 +13,7 @@ export const apiKeysApi = { test: (provider: string): Promise => api.post(`/user/api-keys/${encodeURIComponent(provider)}/test`), + + exportKeys: (): Promise<{ provider_name: string; api_key: string }[]> => + api.post('/user/api-keys/export'), }; diff --git a/frontend/src/i18n/fr.ts b/frontend/src/i18n/fr.ts index 656bfce..738d84b 100644 --- a/frontend/src/i18n/fr.ts +++ b/frontend/src/i18n/fr.ts @@ -137,6 +137,22 @@ const fr = { 'settings.saved': 'Parametres enregistres avec succes.', 'settings.saveError': "Erreur lors de l'enregistrement des parametres.", '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.rateLimitSection': 'Limitation de taux', + 'settings.rateLimitMaxRequests': 'Requetes maximum', + 'settings.rateLimitTimeWindow': 'Fenetre de temps (secondes)', + 'settings.rateLimitHelp': "Configurez le nombre maximum de requetes autorisees pendant la fenetre de temps specifiee. Laissez vide pour utiliser les valeurs par defaut de l'administrateur.", + 'settings.rateLimitEffective': '{max} requetes / {window} secondes', + 'settings.rateLimitReset': 'Reinitialiser', + 'settings.export': 'Exporter', + 'settings.import': 'Importer', + 'settings.exportIncludeKeys': 'Inclure les cles API', + 'settings.exportKeysWarning': 'Les cles API seront incluses en clair dans le fichier. Ne partagez pas ce fichier.', + 'settings.importSuccess': "Configuration importee avec succes. N'oubliez pas d'enregistrer.", + 'settings.importError': "Erreur lors de l'importation du fichier JSON.", // Sources 'sources.title': 'Sources Personnalisees', diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 7efe373..c9dc5e6 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -7,9 +7,10 @@ import { For, createEffect, } from 'solid-js'; -import { Settings as SettingsIcon, Save, Plus, Trash2, Info } from 'lucide-solid'; +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'; import { useI18n } from '~/i18n'; import { DEFAULT_SETTINGS, isApiError } from '~/types'; import type { UserSettings, ProviderConfig } from '~/types'; @@ -29,10 +30,13 @@ const Settings: Component = () => { type: 'success' | 'error'; text: string; } | null>(null); + const [includeApiKeys, setIncludeApiKeys] = createSignal(false); const [providers] = createResource(() => configApi.listProviders()); const [providerWarning, setProviderWarning] = createSignal(false); + let fileInputRef: HTMLInputElement | undefined; + onMount(async () => { try { const data = await settingsApi.get(); @@ -153,6 +157,80 @@ const Settings: Component = () => { })); }; + const handleExport = async () => { + try { + const exportData: Record = { ...settings() }; + + if (includeApiKeys()) { + const keys = await apiKeysApi.exportKeys(); + exportData.api_keys = keys; + } + + const blob = new Blob([JSON.stringify(exportData, null, 2)], { + type: 'application/json', + }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'settings.json'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch (err) { + if (isApiError(err)) { + setMessage({ type: 'error', text: err.message }); + } else { + setMessage({ type: 'error', text: t('settings.saveError') }); + } + } + }; + + const handleImport = async (file: File) => { + try { + const text = await file.text(); + const data = JSON.parse(text); + + // Merge over DEFAULT_SETTINGS for missing fields + const merged: UserSettings = { + ...DEFAULT_SETTINGS, + theme: data.theme ?? DEFAULT_SETTINGS.theme, + max_age_days: data.max_age_days ?? DEFAULT_SETTINGS.max_age_days, + max_items_per_category: data.max_items_per_category ?? DEFAULT_SETTINGS.max_items_per_category, + search_agent_behavior: data.search_agent_behavior ?? DEFAULT_SETTINGS.search_agent_behavior, + ai_model: data.ai_model ?? DEFAULT_SETTINGS.ai_model, + ai_model_writing: data.ai_model_writing ?? DEFAULT_SETTINGS.ai_model_writing, + ai_provider: data.ai_provider ?? DEFAULT_SETTINGS.ai_provider, + rate_limit_max_requests: data.rate_limit_max_requests ?? DEFAULT_SETTINGS.rate_limit_max_requests, + rate_limit_time_window_seconds: data.rate_limit_time_window_seconds ?? DEFAULT_SETTINGS.rate_limit_time_window_seconds, + categories: Array.isArray(data.categories) ? data.categories : DEFAULT_SETTINGS.categories, + }; + + setSettings(merged); + + // Import API keys if present + if (Array.isArray(data.api_keys)) { + for (const key of data.api_keys) { + if (key.provider_name && key.api_key) { + await apiKeysApi.create({ + provider_name: key.provider_name, + api_key: key.api_key, + }); + } + } + } + + setMessage({ type: 'success', text: t('settings.importSuccess') }); + } catch { + setMessage({ type: 'error', text: t('settings.importError') }); + } + + // Reset file input + if (fileInputRef) { + fileInputRef.value = ''; + } + }; + return ( }>
@@ -163,6 +241,56 @@ const Settings: Component = () => { {t('settings.title')}
+
+ {/* Export button */} + + {/* Import button */} + + { + const file = e.currentTarget.files?.[0]; + if (file) handleImport(file); + }} + /> +
+ + + {/* Export: Include API keys checkbox */} +
+ + +

+ {t('settings.exportKeysWarning')} +

+
@@ -342,13 +470,13 @@ const Settings: Component = () => { - {/* Model dropdown */} + {/* Research model dropdown */}

- {t('settings.modelHelp')} + {t('settings.modelResearchHelp')} +

+
+ + {/* Writing model dropdown */} +
+ +
+ +
+

+ {t('settings.modelWritingHelp')}

@@ -454,6 +618,92 @@ const Settings: Component = () => { + + {/* Rate Limit Section */} +
+
+

+ {t('settings.rateLimitSection')} +

+
+
+ +
+ { + const val = e.currentTarget.value; + setSettings((prev) => ({ + ...prev, + rate_limit_max_requests: val === '' ? null : (parseInt(val) || null), + })); + }} + placeholder="" + /> +
+
+ +
+ +
+ { + const val = e.currentTarget.value; + setSettings((prev) => ({ + ...prev, + rate_limit_time_window_seconds: val === '' ? null : (parseInt(val) || null), + })); + }} + placeholder="" + /> +
+
+
+

+ {t('settings.rateLimitHelp')} +

+ +

+ {t('settings.rateLimitEffective') + .replace('{max}', String(settings().rate_limit_max_requests)) + .replace('{window}', String(settings().rate_limit_time_window_seconds))} +

+
+ + + +
{/* Save button */} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index b143ff4..15db2c1 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -45,7 +45,10 @@ export interface UserSettings { max_items_per_category: number; search_agent_behavior: string; ai_model: string; + ai_model_writing: string; ai_provider: string; + rate_limit_max_requests: number | null; + rate_limit_time_window_seconds: number | null; categories: string[]; } @@ -56,7 +59,10 @@ 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_provider: '', + rate_limit_max_requests: null, + rate_limit_time_window_seconds: null, categories: [ 'Annonces majeures / importantes', 'Entreprises des secteurs financiers (banques, assurances, etc.)',