'use client' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Checkbox } from '@/components/ui/checkbox' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { Label } from '@/components/ui/label' import { Combobox } from '@/components/ui/combobox' import { updateSystemConfig, testEmail } from '@/app/actions/admin-settings' import { getOllamaModels } from '@/app/actions/ollama' import { getCustomModels, getCustomEmbeddingModels } from '@/app/actions/custom-provider' import { toast } from 'sonner' import { useState, useEffect, useCallback } from 'react' import Link from 'next/link' import { TestTube, ExternalLink, RefreshCw } from 'lucide-react' import { useLanguage } from '@/lib/i18n' type AIProvider = 'ollama' | 'openai' | 'custom' interface AvailableModels { tags: string[] embeddings: string[] } const MODELS_2026 = { openai: { tags: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo'], embeddings: ['text-embedding-3-small', 'text-embedding-3-large', 'text-embedding-ada-002'] }, } export function AdminSettingsForm({ config }: { config: Record }) { const { t } = useLanguage() const [isSaving, setIsSaving] = useState(false) const [isTesting, setIsTesting] = useState(false) // Local state for Checkbox const [allowRegister, setAllowRegister] = useState(config.ALLOW_REGISTRATION !== 'false') const [smtpSecure, setSmtpSecure] = useState(config.SMTP_SECURE === 'true') const [smtpIgnoreCert, setSmtpIgnoreCert] = useState(config.SMTP_IGNORE_CERT === 'true') // Agent tools state const [webSearchProvider, setWebSearchProvider] = useState(config.WEB_SEARCH_PROVIDER || 'searxng') // Email provider state const [emailProvider, setEmailProvider] = useState<'resend' | 'smtp'>(config.EMAIL_PROVIDER as 'resend' | 'smtp' || (config.RESEND_API_KEY ? 'resend' : 'smtp')) const [emailTestResult, setEmailTestResult] = useState<{ provider: 'resend' | 'smtp'; success: boolean; message?: string } | null>(null) // AI Provider state - separated for tags, embeddings, and chat const [tagsProvider, setTagsProvider] = useState((config.AI_PROVIDER_TAGS as AIProvider) || 'ollama') const [embeddingsProvider, setEmbeddingsProvider] = useState((config.AI_PROVIDER_EMBEDDING as AIProvider) || 'ollama') const [chatProvider, setChatProvider] = useState((config.AI_PROVIDER_CHAT as AIProvider) || 'ollama') // Selected Models State (Controlled Inputs) const [selectedTagsModel, setSelectedTagsModel] = useState(config.AI_MODEL_TAGS || '') const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState(config.AI_MODEL_EMBEDDING || '') const [selectedChatModel, setSelectedChatModel] = useState(config.AI_MODEL_CHAT || '') // Dynamic Models State const [ollamaTagsModels, setOllamaTagsModels] = useState([]) const [ollamaEmbeddingsModels, setOllamaEmbeddingsModels] = useState([]) const [ollamaChatModels, setOllamaChatModels] = useState([]) const [isLoadingTagsModels, setIsLoadingTagsModels] = useState(false) const [isLoadingEmbeddingsModels, setIsLoadingEmbeddingsModels] = useState(false) const [isLoadingChatModels, setIsLoadingChatModels] = useState(false) // Custom provider dynamic models const [customTagsModels, setCustomTagsModels] = useState([]) const [customEmbeddingsModels, setCustomEmbeddingsModels] = useState([]) const [customChatModels, setCustomChatModels] = useState([]) const [isLoadingCustomTagsModels, setIsLoadingCustomTagsModels] = useState(false) const [isLoadingCustomEmbeddingsModels, setIsLoadingCustomEmbeddingsModels] = useState(false) const [isLoadingCustomChatModels, setIsLoadingCustomChatModels] = useState(false) // Fetch Ollama models const fetchOllamaModels = useCallback(async (type: 'tags' | 'embeddings' | 'chat', url: string) => { if (!url) return if (type === 'tags') setIsLoadingTagsModels(true) else if (type === 'embeddings') setIsLoadingEmbeddingsModels(true) else setIsLoadingChatModels(true) try { const result = await getOllamaModels(url) if (result.success) { if (type === 'tags') setOllamaTagsModels(result.models) else if (type === 'embeddings') setOllamaEmbeddingsModels(result.models) else setOllamaChatModels(result.models) } else { toast.error(`Failed to fetch Ollama models: ${result.error}`) } } catch (error) { console.error(error) toast.error('Failed to fetch Ollama models') } finally { if (type === 'tags') setIsLoadingTagsModels(false) else if (type === 'embeddings') setIsLoadingEmbeddingsModels(false) else setIsLoadingChatModels(false) } }, []) // Fetch Custom provider models — tags use /v1/models, embeddings use /v1/embeddings/models const fetchCustomModels = useCallback(async (type: 'tags' | 'embeddings' | 'chat', url: string, apiKey?: string) => { if (!url) return if (type === 'tags') setIsLoadingCustomTagsModels(true) else if (type === 'embeddings') setIsLoadingCustomEmbeddingsModels(true) else setIsLoadingCustomChatModels(true) try { const result = type === 'embeddings' ? await getCustomEmbeddingModels(url, apiKey) : await getCustomModels(url, apiKey) if (result.success && result.models.length > 0) { if (type === 'tags') setCustomTagsModels(result.models) else if (type === 'embeddings') setCustomEmbeddingsModels(result.models) else setCustomChatModels(result.models) } else { toast.error(`Impossible de récupérer les modèles : ${result.error}`) } } catch (error) { console.error(error) toast.error('Erreur lors de la récupération des modèles') } finally { if (type === 'tags') setIsLoadingCustomTagsModels(false) else if (type === 'embeddings') setIsLoadingCustomEmbeddingsModels(false) else setIsLoadingCustomChatModels(false) } }, []) // Initial fetch for Ollama models if provider is selected useEffect(() => { if (tagsProvider === 'ollama') { const url = config.OLLAMA_BASE_URL_TAGS || config.OLLAMA_BASE_URL || 'http://localhost:11434' fetchOllamaModels('tags', url) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [tagsProvider]) useEffect(() => { if (embeddingsProvider === 'ollama') { const url = config.OLLAMA_BASE_URL_EMBEDDING || config.OLLAMA_BASE_URL || 'http://localhost:11434' fetchOllamaModels('embeddings', url) } else if (embeddingsProvider === 'custom') { const url = config.CUSTOM_OPENAI_BASE_URL || '' const key = config.CUSTOM_OPENAI_API_KEY || '' if (url) fetchCustomModels('embeddings', url, key) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [embeddingsProvider]) useEffect(() => { if (tagsProvider === 'custom') { const url = config.CUSTOM_OPENAI_BASE_URL || '' const key = config.CUSTOM_OPENAI_API_KEY || '' if (url) fetchCustomModels('tags', url, key) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [tagsProvider]) useEffect(() => { if (chatProvider === 'ollama') { const url = config.OLLAMA_BASE_URL_CHAT || config.OLLAMA_BASE_URL || 'http://localhost:11434' fetchOllamaModels('chat', url) } else if (chatProvider === 'custom') { const url = config.CUSTOM_OPENAI_BASE_URL || '' const key = config.CUSTOM_OPENAI_API_KEY || '' if (url) fetchCustomModels('chat', url, key) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [chatProvider]) const handleSaveSecurity = async (formData: FormData) => { setIsSaving(true) const data = { ALLOW_REGISTRATION: allowRegister ? 'true' : 'false', } const result = await updateSystemConfig(data) setIsSaving(false) if (result.error) { toast.error(t('admin.security.updateFailed')) } else { toast.success(t('admin.security.updateSuccess')) } } const handleSaveAI = async (formData: FormData) => { setIsSaving(true) const data: Record = {} try { const tagsProv = formData.get('AI_PROVIDER_TAGS') as AIProvider if (!tagsProv) throw new Error(t('admin.ai.providerTagsRequired')) data.AI_PROVIDER_TAGS = tagsProv const tagsModel = formData.get(`AI_MODEL_TAGS_${tagsProv.toUpperCase()}`) as string if (tagsModel) data.AI_MODEL_TAGS = tagsModel if (tagsProv === 'ollama') { const ollamaUrl = formData.get('OLLAMA_BASE_URL_TAGS') as string if (ollamaUrl) data.OLLAMA_BASE_URL_TAGS = ollamaUrl } else if (tagsProv === 'openai') { const openaiKey = formData.get('OPENAI_API_KEY') as string if (openaiKey) data.OPENAI_API_KEY = openaiKey } else if (tagsProv === 'custom') { const customKey = formData.get('CUSTOM_OPENAI_API_KEY_TAGS') as string const customUrl = formData.get('CUSTOM_OPENAI_BASE_URL_TAGS') as string if (customKey) data.CUSTOM_OPENAI_API_KEY = customKey if (customUrl) data.CUSTOM_OPENAI_BASE_URL = customUrl } const embedProv = formData.get('AI_PROVIDER_EMBEDDING') as AIProvider if (!embedProv) throw new Error(t('admin.ai.providerEmbeddingRequired')) data.AI_PROVIDER_EMBEDDING = embedProv const embedModel = formData.get(`AI_MODEL_EMBEDDING_${embedProv.toUpperCase()}`) as string if (embedModel) data.AI_MODEL_EMBEDDING = embedModel if (embedProv === 'ollama') { const ollamaUrl = formData.get('OLLAMA_BASE_URL_EMBEDDING') as string if (ollamaUrl) data.OLLAMA_BASE_URL_EMBEDDING = ollamaUrl } else if (embedProv === 'openai') { const openaiKey = formData.get('OPENAI_API_KEY') as string if (openaiKey) data.OPENAI_API_KEY = openaiKey } else if (embedProv === 'custom') { const customKey = formData.get('CUSTOM_OPENAI_API_KEY_EMBEDDING') as string const customUrl = formData.get('CUSTOM_OPENAI_BASE_URL_EMBEDDING') as string if (customKey) data.CUSTOM_OPENAI_API_KEY = customKey if (customUrl) data.CUSTOM_OPENAI_BASE_URL = customUrl } // Chat provider config const chatProv = formData.get('AI_PROVIDER_CHAT') as AIProvider if (chatProv) { data.AI_PROVIDER_CHAT = chatProv const chatModel = formData.get(`AI_MODEL_CHAT_${chatProv.toUpperCase()}`) as string if (chatModel) data.AI_MODEL_CHAT = chatModel if (chatProv === 'ollama') { const ollamaUrl = formData.get('OLLAMA_BASE_URL_CHAT') as string if (ollamaUrl) data.OLLAMA_BASE_URL_CHAT = ollamaUrl } else if (chatProv === 'openai') { const openaiKey = formData.get('OPENAI_API_KEY') as string if (openaiKey) data.OPENAI_API_KEY = openaiKey } else if (chatProv === 'custom') { const customKey = formData.get('CUSTOM_OPENAI_API_KEY_CHAT') as string const customUrl = formData.get('CUSTOM_OPENAI_BASE_URL_CHAT') as string if (customKey) data.CUSTOM_OPENAI_API_KEY = customKey if (customUrl) data.CUSTOM_OPENAI_BASE_URL = customUrl } } const result = await updateSystemConfig(data) setIsSaving(false) if (result.error) { toast.error(t('admin.ai.updateFailed') + ': ' + result.error) } else { toast.success(t('admin.ai.updateSuccess')) } } catch (error: any) { setIsSaving(false) toast.error(t('general.error') + ': ' + error.message) } } const handleSaveEmail = async (formData: FormData) => { setIsSaving(true) const data: Record = { EMAIL_PROVIDER: emailProvider } if (emailProvider === 'resend') { const key = formData.get('RESEND_API_KEY') as string if (key) data.RESEND_API_KEY = key } else { data.SMTP_HOST = formData.get('SMTP_HOST') as string data.SMTP_PORT = formData.get('SMTP_PORT') as string data.SMTP_USER = formData.get('SMTP_USER') as string data.SMTP_PASS = formData.get('SMTP_PASS') as string data.SMTP_FROM = formData.get('SMTP_FROM') as string data.SMTP_IGNORE_CERT = smtpIgnoreCert ? 'true' : 'false' data.SMTP_SECURE = smtpSecure ? 'true' : 'false' } const result = await updateSystemConfig(data) setIsSaving(false) if (result.error) { toast.error(t('admin.smtp.updateFailed')) } else { toast.success(t('admin.smtp.updateSuccess')) } } const handleTestEmail = async () => { setIsTesting(true) try { const result: any = await testEmail(emailProvider) if (result.success) { toast.success(t('admin.smtp.testSuccess')) setEmailTestResult({ provider: emailProvider, success: true }) } else { toast.error(t('admin.smtp.testFailed', { error: result.error })) setEmailTestResult({ provider: emailProvider, success: false, message: result.error }) } } catch (e: any) { toast.error(t('general.error') + ': ' + e.message) setEmailTestResult({ provider: emailProvider, success: false, message: e.message }) } finally { setIsTesting(false) } } const handleSaveTools = async (formData: FormData) => { setIsSaving(true) const data = { WEB_SEARCH_PROVIDER: formData.get('WEB_SEARCH_PROVIDER') as string || 'searxng', SEARXNG_URL: formData.get('SEARXNG_URL') as string || '', BRAVE_SEARCH_API_KEY: formData.get('BRAVE_SEARCH_API_KEY') as string || '', JINA_API_KEY: formData.get('JINA_API_KEY') as string || '', } const result = await updateSystemConfig(data) setIsSaving(false) if (result.error) { toast.error(t('admin.tools.updateFailed')) } else { toast.success(t('admin.tools.updateSuccess')) } } return (
{t('admin.security.title')} {t('admin.security.description')}
{ e.preventDefault(); handleSaveSecurity(new FormData(e.currentTarget)) }}>
setAllowRegister(!!c)} />

{t('admin.security.allowPublicRegistrationDescription')}

{t('admin.ai.title')} {t('admin.ai.description')}
{ e.preventDefault(); handleSaveAI(new FormData(e.currentTarget)) }}>

🏷️ {t('admin.ai.tagsGenerationProvider')}

{t('admin.ai.tagsGenerationDescription')}

{tagsProvider === 'ollama' && (

{isLoadingTagsModels ? 'Fetching models...' : t('admin.ai.selectOllamaModel')}

)} {tagsProvider === 'openai' && (

{t('admin.ai.openAIKeyDescription')}

gpt-4o-mini = {t('admin.ai.bestValue')} • gpt-4o = {t('admin.ai.bestQuality')}

)} {tagsProvider === 'custom' && (
0 ? customTagsModels.map((m) => ({ value: m, label: m })) : selectedTagsModel ? [{ value: selectedTagsModel, label: selectedTagsModel }] : [] } value={selectedTagsModel} onChange={setSelectedTagsModel} placeholder={selectedTagsModel || 'Cliquez sur ↺ pour charger les modèles'} searchPlaceholder="Rechercher un modèle..." emptyMessage="Aucun modèle. Cliquez sur ↺" />

{isLoadingCustomTagsModels ? 'Récupération des modèles...' : customTagsModels.length > 0 ? `${customTagsModels.length} modèle(s) disponible(s)` : 'Renseignez l\'URL et cliquez sur ↺ pour charger les modèles'}

)}

🔍 {t('admin.ai.embeddingsProvider')}

{t('admin.ai.embeddingsDescription')}

Config value: {config.AI_PROVIDER_EMBEDDING || 'Not set (defaults to ollama)'}

{embeddingsProvider === 'ollama' && (

{isLoadingEmbeddingsModels ? 'Fetching models...' : t('admin.ai.selectEmbeddingModel')}

)} {embeddingsProvider === 'openai' && (

{t('admin.ai.openAIKeyDescription')}

text-embedding-3-small = {t('admin.ai.bestValue')} • text-embedding-3-large = {t('admin.ai.bestQuality')}

)} {embeddingsProvider === 'custom' && (
0 ? customEmbeddingsModels.map((m) => ({ value: m, label: m })) : selectedEmbeddingModel ? [{ value: selectedEmbeddingModel, label: selectedEmbeddingModel }] : [] } value={selectedEmbeddingModel} onChange={setSelectedEmbeddingModel} placeholder={selectedEmbeddingModel || 'Cliquez sur ↺ pour charger les modèles'} searchPlaceholder="Rechercher un modèle..." emptyMessage="Aucun modèle. Cliquez sur ↺" />

{isLoadingCustomEmbeddingsModels ? 'Récupération des modèles...' : customEmbeddingsModels.length > 0 ? `${customEmbeddingsModels.length} modèle(s) disponible(s)` : 'Renseignez l\'URL et cliquez sur ↺ pour charger les modèles'}

)}
{/* Chat Provider Section */}

💬 {t('admin.ai.chatProvider')}

{t('admin.ai.chatDescription')}

{chatProvider === 'ollama' && (

{isLoadingChatModels ? 'Fetching models...' : t('admin.ai.selectOllamaModel')}

)} {chatProvider === 'openai' && (

{t('admin.ai.openAIKeyDescription')}

gpt-4o-mini = {t('admin.ai.bestValue')} • gpt-4o = {t('admin.ai.bestQuality')}

)} {chatProvider === 'custom' && (
0 ? customChatModels.map((m) => ({ value: m, label: m })) : selectedChatModel ? [{ value: selectedChatModel, label: selectedChatModel }] : [] } value={selectedChatModel} onChange={setSelectedChatModel} placeholder={selectedChatModel || 'Cliquez sur ↺ pour charger les modèles'} searchPlaceholder="Rechercher un modèle..." emptyMessage="Aucun modèle. Cliquez sur ↺" />

{isLoadingCustomChatModels ? 'Récupération des modèles...' : customChatModels.length > 0 ? `${customChatModels.length} modèle(s) disponible(s)` : 'Renseignez l\'URL et cliquez sur ↺ pour charger les modèles'}

)}
{t('admin.email.title')} {t('admin.email.description')}
{ e.preventDefault(); handleSaveEmail(new FormData(e.currentTarget)) }}>
{/* Email service status */}
{t('admin.email.status')}
Resend {config.RESEND_API_KEY && ({t('admin.email.keySet')})}
SMTP {config.SMTP_HOST && ({config.SMTP_HOST}:{config.SMTP_PORT || '587'})}
{t('admin.email.activeProvider')}: {emailProvider === 'resend' ? 'Resend' : 'SMTP'}
{emailTestResult && (
{emailTestResult.provider === 'resend' ? 'Resend' : 'SMTP'} — {emailTestResult.success ? t('admin.email.testOk') : `${t('admin.email.testFail')}: ${emailTestResult.message || ''}`}
)}
{emailProvider === 'resend' ? (

{t('admin.resend.apiKeyHint')}

{config.RESEND_API_KEY && (
{t('admin.resend.configured')}
)}
) : (
setSmtpSecure(!!c)} />
setSmtpIgnoreCert(!!c)} />
)}
{t('admin.tools.title')} {t('admin.tools.description')}
{ e.preventDefault(); handleSaveTools(new FormData(e.currentTarget)) }}>
{(webSearchProvider === 'searxng' || webSearchProvider === 'both') && (
)} {(webSearchProvider === 'brave' || webSearchProvider === 'both') && (
)}

{t('admin.tools.jinaKeyDescription')}

) }