'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 { updateSystemConfig, testSMTP } from '@/app/actions/admin-settings' import { getOllamaModels } from '@/app/actions/ollama' 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 = { // Removed hardcoded Ollama models in favor of dynamic fetching 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'] }, custom: { tags: ['gpt-4o-mini', 'gpt-4o', 'claude-3-haiku', 'claude-3-sonnet', 'llama-3.1-8b'], 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') // AI Provider state - separated for tags and embeddings const [tagsProvider, setTagsProvider] = useState((config.AI_PROVIDER_TAGS as AIProvider) || 'ollama') const [embeddingsProvider, setEmbeddingsProvider] = useState((config.AI_PROVIDER_EMBEDDING as AIProvider) || 'ollama') // Selected Models State (Controlled Inputs) const [selectedTagsModel, setSelectedTagsModel] = useState(config.AI_MODEL_TAGS || '') const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState(config.AI_MODEL_EMBEDDING || '') // Dynamic Models State const [ollamaTagsModels, setOllamaTagsModels] = useState([]) const [ollamaEmbeddingsModels, setOllamaEmbeddingsModels] = useState([]) const [isLoadingTagsModels, setIsLoadingTagsModels] = useState(false) const [isLoadingEmbeddingsModels, setIsLoadingEmbeddingsModels] = useState(false) // Sync state with config useEffect(() => { setAllowRegister(config.ALLOW_REGISTRATION !== 'false') setSmtpSecure(config.SMTP_SECURE === 'true') setSmtpIgnoreCert(config.SMTP_IGNORE_CERT === 'true') setTagsProvider((config.AI_PROVIDER_TAGS as AIProvider) || 'ollama') setEmbeddingsProvider((config.AI_PROVIDER_EMBEDDING as AIProvider) || 'ollama') setSelectedTagsModel(config.AI_MODEL_TAGS || '') setSelectedEmbeddingModel(config.AI_MODEL_EMBEDDING || '') }, [config]) // Fetch Ollama models const fetchOllamaModels = useCallback(async (type: 'tags' | 'embeddings', url: string) => { if (!url) return if (type === 'tags') setIsLoadingTagsModels(true) else setIsLoadingEmbeddingsModels(true) try { const result = await getOllamaModels(url) if (result.success) { if (type === 'tags') setOllamaTagsModels(result.models) else setOllamaEmbeddingsModels(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 setIsLoadingEmbeddingsModels(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) } }, [tagsProvider, config.OLLAMA_BASE_URL_TAGS, config.OLLAMA_BASE_URL, fetchOllamaModels]) useEffect(() => { if (embeddingsProvider === 'ollama') { const url = config.OLLAMA_BASE_URL_EMBEDDING || config.OLLAMA_BASE_URL || 'http://localhost:11434' fetchOllamaModels('embeddings', url) } }, [embeddingsProvider, config.OLLAMA_BASE_URL_EMBEDDING, config.OLLAMA_BASE_URL, fetchOllamaModels]) 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') 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') 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 } 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')) setTagsProvider(tagsProv) setEmbeddingsProvider(embedProv) // Refresh models after save if Ollama is selected if (tagsProv === 'ollama') { const url = data.OLLAMA_BASE_URL_TAGS || config.OLLAMA_BASE_URL || 'http://localhost:11434' fetchOllamaModels('tags', url) } if (embedProv === 'ollama') { const url = data.OLLAMA_BASE_URL_EMBEDDING || config.OLLAMA_BASE_URL || 'http://localhost:11434' fetchOllamaModels('embeddings', url) } } } catch (error: any) { setIsSaving(false) toast.error(t('general.error') + ': ' + error.message) } } const handleSaveSMTP = async (formData: FormData) => { setIsSaving(true) const data = { SMTP_HOST: formData.get('SMTP_HOST') as string, SMTP_PORT: formData.get('SMTP_PORT') as string, SMTP_USER: formData.get('SMTP_USER') as string, SMTP_PASS: formData.get('SMTP_PASS') as string, SMTP_FROM: formData.get('SMTP_FROM') as string, SMTP_IGNORE_CERT: smtpIgnoreCert ? 'true' : 'false', 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 testSMTP() if (result.success) { toast.success(t('admin.smtp.testSuccess')) } else { toast.error(t('admin.smtp.testFailed', { error: result.error })) } } catch (e: any) { toast.error(t('general.error') + ': ' + e.message) } finally { setIsTesting(false) } } return (
{t('admin.security.title')} {t('admin.security.description')}
setAllowRegister(!!c)} />

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

{t('admin.ai.title')} {t('admin.ai.description')}

🏷️ {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' && (

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

)}

🔍 {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' && (

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

)}
{t('admin.smtp.title')} {t('admin.smtp.description')}
setSmtpSecure(!!c)} />
setSmtpIgnoreCert(!!c)} />
) }