"use client"; import { useState, useEffect } from "react"; import { Settings, Save, Loader2, CheckCircle, XCircle, RefreshCw, FlaskConical, KeyRound, Mail } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useNotification } from "@/components/ui/notification"; import { ModelCombobox, ModelOption } from "@/components/ui/model-combobox"; import { useTranslationStore } from "@/lib/store"; import { API_BASE } from "@/lib/config"; interface ProviderConfig { enabled: boolean; api_key?: string; base_url?: string; model?: string; timeout?: number; max_retries?: number; } interface SmtpConfig { enabled: boolean; host?: string; port: number; username?: string; password?: string; from_email?: string; use_tls: boolean; } interface SettingsConfig { google: ProviderConfig; google_cloud: ProviderConfig; openai: ProviderConfig; ollama: ProviderConfig; openrouter: ProviderConfig; openrouter_premium: ProviderConfig; zai: ProviderConfig; mistral: ProviderConfig; smtp: SmtpConfig; fallback_chain: string; fallback_chain_classic: string; fallback_chain_llm: string; } interface EnvInfo { openai: boolean; openrouter: boolean; openrouter_premium: boolean; zai: boolean; mistral: boolean; ollama: boolean; google_cloud: boolean; smtp: boolean; } interface OllamaModel { name: string; size: number; modified_at: string; } const defaultConfig: SettingsConfig = { google: { enabled: true, timeout: 30, max_retries: 3 }, google_cloud: { enabled: false, api_key: "", timeout: 30, max_retries: 3 }, openai: { enabled: false, api_key: "", timeout: 60, max_retries: 3 }, ollama: { enabled: false, base_url: "http://localhost:11434", model: "llama3" }, openrouter: { enabled: false, api_key: "", model: "deepseek/deepseek-chat" }, openrouter_premium: { enabled: false, api_key: "", model: "openai/gpt-4o-mini" }, zai: { enabled: false, api_key: "", base_url: "https://api.x.ai/v1", model: "grok-2-1212" }, mistral: { enabled: false, api_key: "", model: "mistral-ocr-latest", timeout: 180 }, smtp: { enabled: false, host: "", port: 587, username: "", password: "", from_email: "", use_tls: true }, fallback_chain: "google,google_cloud,openrouter,openrouter_premium,openai,deepseek,zai", fallback_chain_classic: "google,google_cloud", fallback_chain_llm: "openrouter,openrouter_premium,openai,deepseek,zai", }; const defaultEnvInfo: EnvInfo = { openai: false, openrouter: false, openrouter_premium: false, zai: false, mistral: false, ollama: false, google_cloud: false, smtp: false, }; export default function AdminSettingsPage() { const [config, setConfig] = useState(defaultConfig); const [envInfo, setEnvInfo] = useState(defaultEnvInfo); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [testResults, setTestResults] = useState>({}); const [testMessages, setTestMessages] = useState>({}); const [ollamaModels, setOllamaModels] = useState([]); const [isLoadingModels, setIsLoadingModels] = useState(false); const [openaiModels, setOpenaiModels] = useState([]); const [openrouterModels, setOpenrouterModels] = useState([]); const [zaiModels, setZaiModels] = useState([]); const [loadingModelsProvider, setLoadingModelsProvider] = useState(null); const [isSendingTestEmail, setIsSendingTestEmail] = useState(false); const [testEmailResult, setTestEmailResult] = useState<"idle" | "ok" | "error">("idle"); const [testEmailMessage, setTestEmailMessage] = useState(""); const { success, error, info } = useNotification(); useEffect(() => { loadConfig(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const getToken = () => useTranslationStore.getState().settings.adminToken ?? ""; const loadConfig = async () => { setIsLoading(true); try { const response = await fetch(`${API_BASE}/api/v1/admin/settings`, { headers: { Authorization: `Bearer ${getToken()}` }, }); if (response.ok) { const envelope = await response.json(); // API returns { data: {...settings...}, env_info: {...}, meta: {} } const payload = envelope.data ?? envelope; setConfig({ ...defaultConfig, ...payload }); if (envelope.env_info) { setEnvInfo({ ...defaultEnvInfo, ...envelope.env_info }); } } else { error({ title: "Erreur de chargement", description: `HTTP ${response.status} — vérifiez votre token admin.` }); } } catch (e) { error({ title: "Erreur réseau", description: "Impossible de contacter le backend." }); console.error("Failed to load settings:", e); } finally { setIsLoading(false); } }; const saveConfig = async () => { setIsSaving(true); try { const response = await fetch(`${API_BASE}/api/v1/admin/settings`, { method: "PUT", headers: { Authorization: `Bearer ${getToken()}`, "Content-Type": "application/json", }, body: JSON.stringify(config), }); if (response.ok) { success({ title: "✅ Configuration sauvegardée", description: "Les paramètres ont été enregistrés avec succès." }); } else { const body = await response.json().catch(() => ({})); error({ title: "Erreur de sauvegarde", description: body.detail || `HTTP ${response.status}` }); } } catch (e) { error({ title: "Erreur réseau", description: "Impossible de contacter le backend pour la sauvegarde." }); } finally { setIsSaving(false); } }; const testProvider = async (provider: string) => { setTestResults((prev) => ({ ...prev, [provider]: "testing" })); setTestMessages((prev) => ({ ...prev, [provider]: "" })); try { // For SMTP, send current form values so unsaved changes are tested const smtpBody = provider === "smtp" ? { body: JSON.stringify(config.smtp) } : {}; const response = await fetch( `${API_BASE}/api/v1/admin/providers/${provider}/test`, { method: "POST", headers: { Authorization: `Bearer ${getToken()}`, ...(provider === "smtp" ? { "Content-Type": "application/json" } : {}), }, ...smtpBody, } ); const data = await response.json(); if (data.available) { setTestResults((prev) => ({ ...prev, [provider]: "ok" })); const detail = data.test_result || data.usage || data.models_count !== undefined ? `Connexion OK${data.models_count !== undefined ? ` — ${data.models_count} modèles` : ""}${data.test_result ? ` — "${data.test_result}"` : ""}` : "Connexion OK"; setTestMessages((prev) => ({ ...prev, [provider]: detail })); } else { setTestResults((prev) => ({ ...prev, [provider]: "error" })); setTestMessages((prev) => ({ ...prev, [provider]: data.error || "Échec" })); } } catch (e) { setTestResults((prev) => ({ ...prev, [provider]: "error" })); setTestMessages((prev) => ({ ...prev, [provider]: "Erreur réseau" })); } }; const fetchOllamaModels = async () => { setIsLoadingModels(true); try { const url = config.ollama.base_url ? `${API_BASE}/api/v1/admin/providers/ollama/models?base_url=${encodeURIComponent(config.ollama.base_url)}` : `${API_BASE}/api/v1/admin/providers/ollama/models`; const response = await fetch(url, { headers: { Authorization: `Bearer ${getToken()}` }, }); if (response.ok) { const data = await response.json(); setOllamaModels(data.data || []); if (data.data?.length > 0 && !config.ollama.model) { updateProvider("ollama", { model: data.data[0].name }); } info({ title: `${data.data?.length || 0} modèles Ollama trouvés` }); } else { error({ title: "Ollama inaccessible", description: "Vérifiez que Ollama tourne sur l'URL configurée." }); } } catch (e) { error({ title: "Erreur Ollama", description: "Impossible de contacter Ollama." }); } finally { setIsLoadingModels(false); } }; const fetchModels = async (provider: string) => { setLoadingModelsProvider(provider); try { const response = await fetch( `${API_BASE}/api/v1/admin/providers/${provider}/models`, { headers: { Authorization: `Bearer ${getToken()}` } } ); if (response.ok) { const data = await response.json(); const models: ModelOption[] = data.data || []; if (provider === "openai") setOpenaiModels(models); else if (provider === "openrouter") setOpenrouterModels(models); else if (provider === "zai") setZaiModels(models); info({ title: `${models.length} modèles ${provider} trouvés` }); } else { const body = await response.json().catch(() => ({})); error({ title: `Erreur ${provider}`, description: body.message || `HTTP ${response.status}` }); } } catch (e) { error({ title: `Erreur ${provider}`, description: "Impossible de contacter le serveur." }); } finally { setLoadingModelsProvider(null); } }; type ProviderKey = keyof Omit; const updateProvider = (provider: ProviderKey, updates: Partial) => { setConfig((prev) => ({ ...prev, [provider]: { ...prev[provider], ...updates } as ProviderConfig, })); }; const updateSmtp = (updates: Partial) => { setConfig((prev) => ({ ...prev, smtp: { ...prev.smtp, ...updates }, })); }; const sendTestEmail = async () => { setIsSendingTestEmail(true); setTestEmailResult("idle"); setTestEmailMessage(""); try { const response = await fetch( `${API_BASE}/api/v1/admin/providers/smtp/test-send`, { method: "POST", headers: { Authorization: `Bearer ${getToken()}`, "Content-Type": "application/json", }, body: JSON.stringify(config.smtp), } ); const data = await response.json(); if (data.available) { setTestEmailResult("ok"); setTestEmailMessage(data.test_result || "Email envoyé avec succès"); } else { setTestEmailResult("error"); setTestEmailMessage(data.error || "Échec de l'envoi"); } } catch { setTestEmailResult("error"); setTestEmailMessage("Erreur réseau"); } finally { setIsSendingTestEmail(false); } }; if (isLoading) { return (
); } return (

Paramètres des providers

Configurez les clés API. Les clés peuvent aussi être définies dans le fichier .env.

updateProvider("google", { enabled })} onTest={() => testProvider("google")} testResult={testResults.google ?? "idle"} testMessage={testMessages.google} noApiKey /> updateProvider("google_cloud", { enabled })} onTest={() => testProvider("google_cloud")} testResult={testResults.google_cloud ?? "idle"} testMessage={testMessages.google_cloud} envKeySet={envInfo.google_cloud} >
updateProvider("google_cloud", { api_key: e.target.value })} />

Activez Cloud Translation API dans Google Cloud Console, puis créez une clé API restreinte à cette API.

updateProvider("openai", { enabled })} onTest={() => testProvider("openai")} testResult={testResults.openai ?? "idle"} testMessage={testMessages.openai} envKeySet={envInfo.openai} >
updateProvider("openai", { api_key: e.target.value })} />
updateProvider("openai", { model: v })} models={openaiModels} isLoading={loadingModelsProvider === "openai"} onFetchModels={() => fetchModels("openai")} providerLabel="OpenAI" placeholder="gpt-4o" />
updateProvider("ollama", { enabled })} onTest={() => testProvider("ollama")} testResult={testResults.ollama ?? "idle"} testMessage={testMessages.ollama} envKeySet={envInfo.ollama} >
updateProvider("ollama", { base_url: e.target.value })} />
{ollamaModels.length > 0 ? ( ) : ( updateProvider("ollama", { model: e.target.value })} /> )} {ollamaModels.length === 0 && (

Cliquez sur "Récupérer les modèles" pour charger la liste depuis Ollama.

)}
updateProvider("openrouter", { enabled })} onTest={() => testProvider("openrouter")} testResult={testResults.openrouter ?? "idle"} testMessage={testMessages.openrouter} envKeySet={envInfo.openrouter} >
updateProvider("openrouter", { api_key: e.target.value })} />
updateProvider("openrouter", { model: v })} models={openrouterModels} isLoading={loadingModelsProvider === "openrouter"} onFetchModels={() => fetchModels("openrouter")} providerLabel="OpenRouter" placeholder="deepseek/deepseek-chat" />

Recommandé : deepseek/deepseek-chat (~€0.04/doc)

updateProvider("openrouter_premium", { enabled })} onTest={() => testProvider("openrouter_premium")} testResult={testResults.openrouter_premium ?? "idle"} testMessage={testMessages.openrouter_premium} envKeySet={envInfo.openrouter_premium} >
updateProvider("openrouter_premium", { model: v })} models={openrouterModels} isLoading={loadingModelsProvider === "openrouter"} onFetchModels={() => fetchModels("openrouter")} providerLabel="OpenRouter" placeholder="anthropic/claude-sonnet-4.6" />

Recommandé : anthropic/claude-sonnet-4.6 (~€0.20/doc) ou openai/gpt-4o (~€0.30/doc)

updateProvider("zai", { enabled })} onTest={() => testProvider("zai")} testResult={testResults.zai ?? "idle"} testMessage={testMessages.zai} envKeySet={envInfo.zai} >
updateProvider("zai", { api_key: e.target.value })} />
updateProvider("zai", { model: v })} models={zaiModels} isLoading={loadingModelsProvider === "zai"} onFetchModels={() => fetchModels("zai")} providerLabel="xAI" placeholder="grok-2-1212" />
updateProvider("zai", { base_url: e.target.value })} />

Par défaut : https://api.x.ai/v1 — à changer uniquement si vous utilisez un proxy.

updateProvider("mistral", { enabled })} onTest={() => testProvider("mistral")} testResult={testResults.mistral ?? "idle"} testMessage={testMessages.mistral} envKeySet={envInfo.mistral} >
updateProvider("mistral", { api_key: e.target.value })} />
updateProvider("mistral", { model: e.target.value })} />

Recommandé : mistral-ocr-latest

Chaîne de fallback Ordre de priorité pour la sélection des providers
setConfig((prev) => ({ ...prev, fallback_chain_classic: e.target.value }))} placeholder="google,google_cloud" />
setConfig((prev) => ({ ...prev, fallback_chain_llm: e.target.value }))} placeholder="openrouter,openai,deepseek,zai" />
Email SMTP {config.smtp.enabled ? "Activé" : "Désactivé"} {envInfo.smtp && ( Config dans .env )}
updateSmtp({ enabled })} />
Configuration du serveur SMTP pour l'envoi d'emails (mot de passe oublié, notifications, etc.) {testMessages.smtp && (

{testMessages.smtp}

)}
updateSmtp({ host: e.target.value })} />
updateSmtp({ port: parseInt(e.target.value) || 587 })} />
updateSmtp({ username: e.target.value })} />
updateSmtp({ password: e.target.value })} />
updateSmtp({ from_email: e.target.value })} />
updateSmtp({ use_tls: checked })} />
{testEmailMessage && (

{testEmailMessage}

)}
); } function ProviderCard({ title, description, enabled, onToggle, onTest, testResult, testMessage, noApiKey = false, envKeySet = false, children, }: { title: string; description: string; enabled: boolean; onToggle: (enabled: boolean) => void; onTest: () => void; testResult: "ok" | "error" | "testing" | "idle"; testMessage?: string; noApiKey?: boolean; envKeySet?: boolean; children?: React.ReactNode; }) { return (
{title} {enabled ? "Activé" : "Désactivé"} {envKeySet && !noApiKey && ( Clé dans .env )}
{description} {testMessage && (

{testMessage}

)}
{!noApiKey && children && {children}}
); }