'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 { toast } from 'sonner' import { useState, useEffect, useCallback } from 'react' import { TestTube, ExternalLink, RefreshCw, Shield, Brain, Mail, Wrench } from 'lucide-react' import { useLanguage } from '@/lib/i18n' type AIProvider = | 'ollama' | 'openai' | 'anthropic' | 'anthropic_custom' | 'custom' | 'deepseek' | 'openrouter' | 'mistral' | 'zai' | 'lmstudio' | 'google' | 'minimax' | 'glm' /** Providers that cannot be used for embeddings in Memento (no embedding API wired). */ const PROVIDERS_WITHOUT_EMBEDDINGS: AIProvider[] = ['anthropic', 'anthropic_custom'] // Provider config metadata const PROVIDER_META: Record = { ollama: { apiKeyLabel: '', baseUrlLabel: 'admin.ai.baseUrl', hasApiKey: false, hasBaseUrl: true, isLocal: true }, openai: { apiKeyLabel: 'OPENAI_API_KEY', baseUrlLabel: '', hasApiKey: true, hasBaseUrl: false, isLocal: false }, anthropic: { apiKeyLabel: 'ANTHROPIC_API_KEY', baseUrlLabel: '', hasApiKey: true, hasBaseUrl: false, isLocal: false }, anthropic_custom: { apiKeyLabel: 'ANTHROPIC_CUSTOM_API_KEY', baseUrlLabel: 'admin.ai.baseUrl', hasApiKey: true, hasBaseUrl: true, isLocal: false, }, deepseek: { apiKeyLabel: 'DEEPSEEK_API_KEY', baseUrlLabel: '', hasApiKey: true, hasBaseUrl: false, isLocal: false }, openrouter:{ apiKeyLabel: 'OPENROUTER_API_KEY', baseUrlLabel: '', hasApiKey: true, hasBaseUrl: false, isLocal: false }, mistral: { apiKeyLabel: 'MISTRAL_API_KEY', baseUrlLabel: '', hasApiKey: true, hasBaseUrl: false, isLocal: false }, zai: { apiKeyLabel: 'ZAI_API_KEY', baseUrlLabel: '', hasApiKey: true, hasBaseUrl: false, isLocal: false }, lmstudio: { apiKeyLabel: '', baseUrlLabel: 'admin.ai.baseUrl', hasApiKey: false, hasBaseUrl: true, isLocal: true }, custom: { apiKeyLabel: 'CUSTOM_OPENAI_API_KEY', baseUrlLabel: 'admin.ai.baseUrl', hasApiKey: true, hasBaseUrl: true, isLocal: false }, google: { apiKeyLabel: 'GOOGLE_GENERATIVE_AI_API_KEY', baseUrlLabel: '', hasApiKey: true, hasBaseUrl: false, isLocal: false }, minimax: { apiKeyLabel: 'MINIMAX_API_KEY', baseUrlLabel: '', hasApiKey: true, hasBaseUrl: false, isLocal: false }, glm: { apiKeyLabel: 'GLM_API_KEY', baseUrlLabel: '', hasApiKey: true, hasBaseUrl: false, isLocal: false }, } // Config key names for each provider's API key (used to read/save from config) const API_KEY_CONFIG: Record = { ollama: '', openai: 'OPENAI_API_KEY', anthropic: 'ANTHROPIC_API_KEY', anthropic_custom: 'ANTHROPIC_CUSTOM_API_KEY', deepseek: 'DEEPSEEK_API_KEY', openrouter: 'OPENROUTER_API_KEY', mistral: 'MISTRAL_API_KEY', zai: 'ZAI_API_KEY', lmstudio: 'LMSTUDIO_API_KEY', custom: 'CUSTOM_OPENAI_API_KEY', google: 'GOOGLE_GENERATIVE_AI_API_KEY', minimax: 'MINIMAX_API_KEY', glm: 'GLM_API_KEY', } const BASE_URL_CONFIG: Record = { ollama: 'OLLAMA_BASE_URL', openai: '', anthropic: '', anthropic_custom: 'ANTHROPIC_CUSTOM_BASE_URL', deepseek: '', openrouter: '', mistral: '', zai: '', lmstudio: 'LMSTUDIO_BASE_URL', custom: 'CUSTOM_OPENAI_BASE_URL', google: '', minimax: '', glm: '', } const DEFAULT_BASE_URLS: Record = { ollama: 'http://localhost:11434', openai: '', anthropic: '', anthropic_custom: '', deepseek: 'https://api.deepseek.com/v1', openrouter: 'https://openrouter.ai/api/v1', mistral: 'https://api.mistral.ai/v1', zai: 'https://api.zukijourney.com/v1', lmstudio: 'http://localhost:1234/v1', custom: '', google: '', minimax: 'https://api.minimax.io/v1', glm: 'https://open.bigmodel.ai/api/paas/v4', } // Suggested models per provider (shown as hints in Combobox - user can always type a custom name) const SUGGESTED_MODELS: Record = { openai: ['gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-4o', 'gpt-4o-mini', 'o3-mini', 'o4-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'], anthropic: [ 'claude-sonnet-4-20250514', 'claude-sonnet-4-5', 'claude-opus-4-20250514', 'claude-opus-4-5', 'claude-haiku-4-5', 'claude-3-haiku-20240307', ], anthropic_custom: [ 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed', 'MiniMax-M2.5', 'MiniMax-M2.5-highspeed', 'MiniMax-M2.1', 'MiniMax-M2.1-highspeed', 'MiniMax-M2', 'claude-sonnet-4-20250514', ], openrouter: ['openai/gpt-4o-mini', 'openai/gpt-4.1-mini', 'anthropic/claude-sonnet-4', 'google/gemini-2.5-flash-preview', 'google/gemma-4-26b-a4b-it', 'meta-llama/llama-4-maverick', 'deepseek/deepseek-chat-v3-0324'], deepseek: ['deepseek-chat', 'deepseek-reasoner'], mistral: ['mistral-small-latest', 'mistral-medium-latest', 'mistral-large-latest', 'codestral-latest', 'mistral-embed'], zai: ['gpt-4.1', 'gpt-4.1-mini', 'gpt-4o', 'gpt-4o-mini', 'claude-sonnet-4', 'gemini-2.5-flash'], google: ['gemini-2.0-flash', 'gemini-1.5-flash', 'gemini-1.5-pro', 'gemini-1.0-pro'], minimax: ['abab6.5-chat', 'abab6.5s-chat', 'abab5.5-chat'], glm: ['glm-4', 'glm-4-air', 'glm-4-flash', 'glm-3-turbo'], } const SUGGESTED_EMBEDDINGS: Record = { openai: ['text-embedding-3-small', 'text-embedding-3-large', 'text-embedding-ada-002'], openrouter: ['openai/text-embedding-3-small'], mistral: ['mistral-embed'], zai: ['text-embedding-3-small', 'text-embedding-3-large'], } type ModelPurpose = 'tags' | 'embeddings' | 'chat' export function AdminSettingsForm({ config }: { config: Record }) { const { t } = useLanguage() const [activeAiTab, setActiveAiTab] = useState<'tags' | 'embeddings' | 'chat'>('tags') 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) const [isTestingSearch, setIsTestingSearch] = useState(false) const [searchTestResult, setSearchTestResult] = useState<{ 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(() => { const v = (config.AI_PROVIDER_EMBEDDING as AIProvider) || 'ollama' return PROVIDERS_WITHOUT_EMBEDDINGS.includes(v) ? 'ollama' : v }) const [chatProvider, setChatProvider] = useState((config.AI_PROVIDER_CHAT as AIProvider) || 'ollama') // Selected Models State const [selectedTagsModel, setSelectedTagsModel] = useState(config.AI_MODEL_TAGS || '') const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState(config.AI_MODEL_EMBEDDING || '') const [selectedChatModel, setSelectedChatModel] = useState(config.AI_MODEL_CHAT || '') const [tagsFallbackProvider, setTagsFallbackProvider] = useState(config.AI_PROVIDER_TAGS_FALLBACK || '') const [embedFallbackProvider, setEmbedFallbackProvider] = useState(config.AI_PROVIDER_EMBEDDING_FALLBACK || '') const [chatFallbackProvider, setChatFallbackProvider] = useState(config.AI_PROVIDER_CHAT_FALLBACK || '') const [dimensionWarning, setDimensionWarning] = useState<{ dbDimension: number; modelDimension: number } | null>(null) const [isMigrating, setIsMigrating] = useState(false) // Dynamic Models State (for local providers with /api/tags or /v1/models endpoints) const [dynamicModels, setDynamicModels] = useState>({ tags: [], embeddings: [], chat: [], }) const [isLoadingModels, setIsLoadingModels] = useState>({ tags: false, embeddings: false, chat: false, }) // Fetch models from local providers (Ollama, LM Studio) or cloud providers with /v1/models const fetchModels = useCallback(async (purpose: ModelPurpose, provider: AIProvider, url: string, apiKey?: string) => { setIsLoadingModels(prev => ({ ...prev, [purpose]: true })) try { const params = new URLSearchParams({ type: provider === 'ollama' ? 'ollama' : 'custom', url, kind: purpose }) if (apiKey) params.set('key', apiKey) const res = await fetch(`/api/admin/models?${params}`) const result = await res.json() if (result.success && result.models.length > 0) { setDynamicModels(prev => ({ ...prev, [purpose]: result.models })) } else if (!result.success) { toast.error(`${t('admin.ai.fetchModelsFailed')}: ${result.error}`) } } catch (error) { console.error(error) toast.error(t('admin.ai.fetchModelsFailed')) } finally { setIsLoadingModels(prev => ({ ...prev, [purpose]: false })) } }, [t]) // Get the API URL for fetching models for a given provider const getModelFetchUrl = useCallback((provider: AIProvider): string => { if (provider === 'ollama') { const input = document.getElementById(`BASE_URL_${provider}`) as HTMLInputElement return input?.value || config.OLLAMA_BASE_URL || 'http://localhost:11434' } if (provider === 'lmstudio') { const input = document.getElementById('BASE_URL_lmstudio') as HTMLInputElement return input?.value || config.LMSTUDIO_BASE_URL || 'http://localhost:1234/v1' } // Cloud providers have known URLs return DEFAULT_BASE_URLS[provider] || '' }, [config]) const getApiKey = useCallback((provider: AIProvider, purpose: ModelPurpose): string => { const configKey = API_KEY_CONFIG[provider] if (!configKey) return '' const input = document.getElementById(`API_KEY_${provider}_${purpose}`) as HTMLInputElement return input?.value || config[configKey] || '' }, [config]) // Initial model fetch useEffect(() => { const fetchInitial = async () => { if (tagsProvider === 'ollama') { await fetchModels('tags', 'ollama', config.OLLAMA_BASE_URL_TAGS || config.OLLAMA_BASE_URL || 'http://localhost:11434') } else if (tagsProvider === 'lmstudio') { await fetchModels('tags', 'lmstudio', config.LMSTUDIO_BASE_URL || 'http://localhost:1234/v1') } else if (PROVIDER_META[tagsProvider]?.hasApiKey && tagsProvider !== 'anthropic_custom') { const url = DEFAULT_BASE_URLS[tagsProvider] const key = config[API_KEY_CONFIG[tagsProvider]] || '' if (url && key) await fetchModels('tags', tagsProvider, url, key) } if (embeddingsProvider === 'ollama') { await fetchModels('embeddings', 'ollama', config.OLLAMA_BASE_URL_EMBEDDING || config.OLLAMA_BASE_URL || 'http://localhost:11434') } else if (embeddingsProvider === 'lmstudio') { await fetchModels('embeddings', 'lmstudio', config.LMSTUDIO_BASE_URL || 'http://localhost:1234/v1') } else if (PROVIDER_META[embeddingsProvider]?.hasApiKey && embeddingsProvider !== 'anthropic_custom') { const url = DEFAULT_BASE_URLS[embeddingsProvider] const key = config[API_KEY_CONFIG[embeddingsProvider]] || '' if (url && key) await fetchModels('embeddings', embeddingsProvider, url, key) } if (chatProvider === 'ollama') { await fetchModels('chat', 'ollama', config.OLLAMA_BASE_URL_CHAT || config.OLLAMA_BASE_URL || 'http://localhost:11434') } else if (chatProvider === 'lmstudio') { await fetchModels('chat', 'lmstudio', config.LMSTUDIO_BASE_URL || 'http://localhost:1234/v1') } else if (PROVIDER_META[chatProvider]?.hasApiKey && chatProvider !== 'anthropic_custom') { const url = DEFAULT_BASE_URLS[chatProvider] const key = config[API_KEY_CONFIG[chatProvider]] || '' if (url && key) await fetchModels('chat', chatProvider, url, key) } } fetchInitial() }, []) // Build model options for Combobox: dynamic models + current saved + suggested const buildModelOptions = useCallback((purpose: ModelPurpose, provider: AIProvider, currentValue: string) => { const dynamic = dynamicModels[purpose] || [] const suggested = purpose === 'embeddings' ? (SUGGESTED_EMBEDDINGS[provider] || []) : (SUGGESTED_MODELS[provider] || []) const allModels = new Set() dynamic.forEach(m => allModels.add(m)) suggested.forEach(m => allModels.add(m)) if (currentValue) allModels.add(currentValue) const options = Array.from(allModels).sort().map(m => ({ value: m, label: m })) return options }, [dynamicModels]) 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 { // Tags provider 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') as string if (tagsModel) data.AI_MODEL_TAGS = tagsModel const tagsFallbackProv = formData.get('AI_PROVIDER_TAGS_FALLBACK') as string data.AI_PROVIDER_TAGS_FALLBACK = tagsFallbackProv?.trim() ?? '' const tagsFallbackModel = formData.get('AI_MODEL_TAGS_FALLBACK') as string if (tagsFallbackModel) data.AI_MODEL_TAGS_FALLBACK = tagsFallbackModel // Save provider-specific config for tags if (tagsProv === 'ollama') { const ollamaUrl = formData.get('BASE_URL_ollama_tags') as string if (ollamaUrl) data.OLLAMA_BASE_URL_TAGS = ollamaUrl } else if (tagsProv === 'lmstudio') { const url = formData.get('BASE_URL_lmstudio_tags') as string if (url) data.LMSTUDIO_BASE_URL = url } else { const apiKeyConfigKey = API_KEY_CONFIG[tagsProv] if (apiKeyConfigKey) { const apiKey = formData.get(`API_KEY_${tagsProv}_tags`) as string if (apiKey) data[apiKeyConfigKey] = apiKey } const baseUrlConfigKey = BASE_URL_CONFIG[tagsProv] if (baseUrlConfigKey) { const baseUrl = formData.get(`BASE_URL_${tagsProv}_tags`) as string if (baseUrl) data[baseUrlConfigKey] = baseUrl } } // Embeddings provider 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') as string if (embedModel) data.AI_MODEL_EMBEDDING = embedModel const embedFallbackProv = formData.get('AI_PROVIDER_EMBEDDING_FALLBACK') as string data.AI_PROVIDER_EMBEDDING_FALLBACK = embedFallbackProv?.trim() ?? '' const embedFallbackModel = formData.get('AI_MODEL_EMBEDDING_FALLBACK') as string if (embedFallbackModel) data.AI_MODEL_EMBEDDING_FALLBACK = embedFallbackModel // Save provider-specific config for embeddings if (embedProv === 'ollama') { const ollamaUrl = formData.get('BASE_URL_ollama_embeddings') as string if (ollamaUrl) data.OLLAMA_BASE_URL_EMBEDDING = ollamaUrl } else if (embedProv === 'lmstudio') { const url = formData.get('BASE_URL_lmstudio_embeddings') as string if (url) data.LMSTUDIO_BASE_URL = url } else { const apiKeyConfigKey = API_KEY_CONFIG[embedProv] if (apiKeyConfigKey) { const apiKey = formData.get(`API_KEY_${embedProv}_embeddings`) as string if (apiKey) data[apiKeyConfigKey] = apiKey } const baseUrlConfigKey = BASE_URL_CONFIG[embedProv] if (baseUrlConfigKey) { const baseUrl = formData.get(`BASE_URL_${embedProv}_embeddings`) as string if (baseUrl) data[baseUrlConfigKey] = baseUrl } } // Chat provider const chatProv = formData.get('AI_PROVIDER_CHAT') as AIProvider if (chatProv) { data.AI_PROVIDER_CHAT = chatProv const chatModel = formData.get('AI_MODEL_CHAT') as string if (chatModel) data.AI_MODEL_CHAT = chatModel const chatFallbackProv = formData.get('AI_PROVIDER_CHAT_FALLBACK') as string data.AI_PROVIDER_CHAT_FALLBACK = chatFallbackProv?.trim() ?? '' const chatFallbackModel = formData.get('AI_MODEL_CHAT_FALLBACK') as string if (chatFallbackModel) data.AI_MODEL_CHAT_FALLBACK = chatFallbackModel // Save provider-specific config for chat if (chatProv === 'ollama') { const ollamaUrl = formData.get('BASE_URL_ollama_chat') as string if (ollamaUrl) data.OLLAMA_BASE_URL_CHAT = ollamaUrl } else if (chatProv === 'lmstudio') { const url = formData.get('BASE_URL_lmstudio_chat') as string if (url) data.LMSTUDIO_BASE_URL = url } else { const apiKeyConfigKey = API_KEY_CONFIG[chatProv] if (apiKeyConfigKey) { const apiKey = formData.get(`API_KEY_${chatProv}_chat`) as string if (apiKey) data[apiKeyConfigKey] = apiKey } const baseUrlConfigKey = BASE_URL_CONFIG[chatProv] if (baseUrlConfigKey) { const baseUrl = formData.get(`BASE_URL_${chatProv}_chat`) as string if (baseUrl) data[baseUrlConfigKey] = baseUrl } } } 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')) setDimensionWarning(null) try { const dimRes = await fetch('/api/admin/embeddings/dimension', { method: 'POST' }) const dimData = await dimRes.json() if (dimData.success && !dimData.match) { setDimensionWarning({ dbDimension: dimData.dbDimension, modelDimension: dimData.modelDimension, }) } } catch {} } } catch (error: any) { setIsSaving(false) toast.error(t('general.error') + ': ' + error.message) } } const handleMigrateEmbeddings = async () => { setIsMigrating(true) try { const res = await fetch('/api/admin/embeddings/migrate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }) const data = await res.json() if (data.success) { toast.success(data.message) setDimensionWarning(null) } else { toast.error(data.error || 'Migration failed') } } catch (error: any) { toast.error(error.message) } finally { setIsMigrating(false) } } 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 const from = formData.get('SMTP_FROM') as string if (from) data.SMTP_FROM = from } 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 handleTestSearch = async () => { setIsTestingSearch(true) setSearchTestResult(null) try { const url = (document.getElementById('SEARXNG_URL') as HTMLInputElement)?.value || config.SEARXNG_URL || 'http://localhost:8080' const apiKey = (document.getElementById('BRAVE_SEARCH_API_KEY') as HTMLInputElement)?.value || config.BRAVE_SEARCH_API_KEY || '' const res = await fetch('/api/admin/test-search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider: webSearchProvider, searxngUrl: url, braveApiKey: apiKey }), }) const data = await res.json() setSearchTestResult(data) } catch (e: any) { setSearchTestResult({ success: false, message: e.message }) } finally { setIsTestingSearch(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')) } } // Renders the provider config fields (API key, base URL) for a given provider and purpose const renderProviderConfig = (provider: AIProvider, purpose: ModelPurpose, currentValue: string, onModelChange: (v: string) => void) => { const meta = PROVIDER_META[provider] const loading = isLoadingModels[purpose] return (
{/* API Key field */} {meta.hasApiKey && (
)} {/* Base URL field */} {meta.hasBaseUrl && (
)} {/* API Key field for cloud providers without base URL - refresh button inline */} {meta.hasApiKey && !meta.hasBaseUrl && (
{/* API key already rendered above */}
)} {/* Model selection - always editable Combobox */}

{loading ? t('admin.ai.fetchingModels') : dynamicModels[purpose].length > 0 ? t('admin.ai.modelsAvailable', { count: dynamicModels[purpose].length }) : provider === 'anthropic' ? t('admin.ai.anthropicModelHint') : provider === 'anthropic_custom' ? t('admin.ai.anthropicCustomModelHint') : provider === 'ollama' || provider === 'lmstudio' ? t('admin.ai.selectOllamaModel') : t('admin.ai.enterUrlToLoad')}

) } // Provider dropdown options const providerOptions = [ { value: 'ollama', label: t('admin.ai.providerOllamaOption') }, { value: 'openai', label: t('admin.ai.providerOpenAIOption') }, { value: 'anthropic', label: t('admin.ai.providerAnthropicOption') }, { value: 'anthropic_custom', label: t('admin.ai.providerAnthropicCustomOption') }, { value: 'deepseek', label: t('admin.ai.providerDeepSeekOption') }, { value: 'openrouter', label: t('admin.ai.providerOpenRouterOption') }, { value: 'mistral', label: t('admin.ai.providerMistralOption') }, { value: 'zai', label: t('admin.ai.providerZAIOption') }, { value: 'lmstudio', label: t('admin.ai.providerLMStudioOption') }, { value: 'google', label: 'Google Gemini' }, { value: 'minimax', label: 'MiniMax' }, { value: 'glm', label: 'Zhipu GLM' }, { value: 'custom', label: t('admin.ai.providerCustomOption') }, ] const embeddingsProviderOptions = providerOptions.filter( (opt) => !PROVIDERS_WITHOUT_EMBEDDINGS.includes(opt.value as AIProvider) ) 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)) }}>
{/* Tags Generation Provider */}

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

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

{renderProviderConfig(tagsProvider, 'tags', selectedTagsModel, setSelectedTagsModel)}

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

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

{/* Embeddings Provider */}

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

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

{dimensionWarning && (

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

{t('admin.ai.dimensionMismatchDetail', { dbDimension: dimensionWarning.dbDimension, modelDimension: dimensionWarning.modelDimension, })}

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

)}
{renderProviderConfig(embeddingsProvider, 'embeddings', selectedEmbeddingModel, setSelectedEmbeddingModel)}

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

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

{/* Chat Provider */}

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

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

{renderProviderConfig(chatProvider, 'chat', selectedChatModel, setSelectedChatModel)}

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

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

{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')}

{/* Search test result */} {searchTestResult && (
{searchTestResult.message}
)}
) }