- Add AI Provider Testing page (/admin/ai-test) with Tags and Embeddings tests - Add new AI providers: CustomOpenAI, DeepSeek, OpenRouter - Add API routes for AI config, models listing, and testing endpoints - Add UX Design Specification document for Phase 1 MVP AI - Add PRD Phase 1 MVP AI planning document - Update admin settings and sidebar navigation - Fix AI factory for multi-provider support
480 lines
23 KiB
TypeScript
480 lines
23 KiB
TypeScript
'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 { toast } from 'sonner'
|
||
import { useState, useEffect } from 'react'
|
||
import Link from 'next/link'
|
||
import { TestTube, ExternalLink } from 'lucide-react'
|
||
|
||
type AIProvider = 'ollama' | 'openai' | 'custom'
|
||
|
||
interface AvailableModels {
|
||
tags: string[]
|
||
embeddings: string[]
|
||
}
|
||
|
||
const MODELS_2026 = {
|
||
ollama: {
|
||
tags: ['llama3:latest', 'llama3.2:latest', 'granite4:latest', 'mistral:latest', 'mixtral:latest', 'phi3:latest', 'gemma2:latest', 'qwen2:latest'],
|
||
embeddings: ['embeddinggemma:latest', 'mxbai-embed-large:latest', 'nomic-embed-text:latest']
|
||
},
|
||
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<string, string> }) {
|
||
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<AIProvider>((config.AI_PROVIDER_TAGS as AIProvider) || 'ollama')
|
||
const [embeddingsProvider, setEmbeddingsProvider] = useState<AIProvider>((config.AI_PROVIDER_EMBEDDING as AIProvider) || 'ollama')
|
||
|
||
// Sync state with config when server revalidates
|
||
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')
|
||
}, [config])
|
||
|
||
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('Failed to update security settings')
|
||
} else {
|
||
toast.success('Security Settings updated')
|
||
}
|
||
}
|
||
|
||
const handleSaveAI = async (formData: FormData) => {
|
||
setIsSaving(true)
|
||
const data: Record<string, string> = {}
|
||
|
||
// Tags provider configuration
|
||
const tagsProv = formData.get('AI_PROVIDER_TAGS') as AIProvider
|
||
data.AI_PROVIDER_TAGS = tagsProv
|
||
data.AI_MODEL_TAGS = formData.get('AI_MODEL_TAGS') as string
|
||
|
||
if (tagsProv === 'ollama') {
|
||
data.OLLAMA_BASE_URL = formData.get('OLLAMA_BASE_URL_TAGS') as string
|
||
} else if (tagsProv === 'openai') {
|
||
data.OPENAI_API_KEY = formData.get('OPENAI_API_KEY') as string
|
||
} else if (tagsProv === 'custom') {
|
||
data.CUSTOM_OPENAI_API_KEY = formData.get('CUSTOM_OPENAI_API_KEY_TAGS') as string
|
||
data.CUSTOM_OPENAI_BASE_URL = formData.get('CUSTOM_OPENAI_BASE_URL_TAGS') as string
|
||
}
|
||
|
||
// Embeddings provider configuration
|
||
const embedProv = formData.get('AI_PROVIDER_EMBEDDING') as AIProvider
|
||
data.AI_PROVIDER_EMBEDDING = embedProv
|
||
data.AI_MODEL_EMBEDDING = formData.get('AI_MODEL_EMBEDDING') as string
|
||
|
||
if (embedProv === 'ollama') {
|
||
data.OLLAMA_BASE_URL = formData.get('OLLAMA_BASE_URL_EMBEDDING') as string
|
||
} else if (embedProv === 'openai') {
|
||
data.OPENAI_API_KEY = formData.get('OPENAI_API_KEY') as string
|
||
} else if (embedProv === 'custom') {
|
||
data.CUSTOM_OPENAI_API_KEY = formData.get('CUSTOM_OPENAI_API_KEY_EMBEDDING') as string
|
||
data.CUSTOM_OPENAI_BASE_URL = formData.get('CUSTOM_OPENAI_BASE_URL_EMBEDDING') as string
|
||
}
|
||
|
||
const result = await updateSystemConfig(data)
|
||
setIsSaving(false)
|
||
|
||
if (result.error) {
|
||
toast.error('Failed to update AI settings')
|
||
} else {
|
||
toast.success('AI Settings updated')
|
||
setTagsProvider(tagsProv)
|
||
setEmbeddingsProvider(embedProv)
|
||
}
|
||
}
|
||
|
||
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('Failed to update SMTP settings')
|
||
} else {
|
||
toast.success('SMTP Settings updated')
|
||
}
|
||
}
|
||
|
||
const handleTestEmail = async () => {
|
||
setIsTesting(true)
|
||
try {
|
||
const result: any = await testSMTP()
|
||
if (result.success) {
|
||
toast.success('Test email sent successfully!')
|
||
} else {
|
||
toast.error(`Failed: ${result.error}`)
|
||
}
|
||
} catch (e: any) {
|
||
toast.error(`Error: ${e.message}`)
|
||
} finally {
|
||
setIsTesting(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>Security Settings</CardTitle>
|
||
<CardDescription>Manage access control and registration policies.</CardDescription>
|
||
</CardHeader>
|
||
<form action={handleSaveSecurity}>
|
||
<CardContent className="space-y-4">
|
||
<div className="flex items-center space-x-2">
|
||
<Checkbox
|
||
id="ALLOW_REGISTRATION"
|
||
checked={allowRegister}
|
||
onCheckedChange={(c) => setAllowRegister(!!c)}
|
||
/>
|
||
<label
|
||
htmlFor="ALLOW_REGISTRATION"
|
||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||
>
|
||
Allow Public Registration
|
||
</label>
|
||
</div>
|
||
<p className="text-xs text-muted-foreground">
|
||
If disabled, new users can only be added by an Administrator via the User Management page.
|
||
</p>
|
||
</CardContent>
|
||
<CardFooter>
|
||
<Button type="submit" disabled={isSaving}>Save Security Settings</Button>
|
||
</CardFooter>
|
||
</form>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>AI Configuration</CardTitle>
|
||
<CardDescription>Configure AI providers for auto-tagging and semantic search. Use different providers for optimal performance.</CardDescription>
|
||
</CardHeader>
|
||
<form action={handleSaveAI}>
|
||
<CardContent className="space-y-6">
|
||
{/* Tags Generation Section */}
|
||
<div className="space-y-4 p-4 border rounded-lg bg-blue-50/50 dark:bg-blue-950/20">
|
||
<h3 className="text-base font-semibold flex items-center gap-2">
|
||
<span className="text-blue-600">🏷️</span> Tags Generation Provider
|
||
</h3>
|
||
<p className="text-xs text-muted-foreground">AI provider for automatic tag suggestions. Recommended: Ollama (free, local).</p>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="AI_PROVIDER_TAGS">Provider</Label>
|
||
<select
|
||
id="AI_PROVIDER_TAGS"
|
||
name="AI_PROVIDER_TAGS"
|
||
value={tagsProvider}
|
||
onChange={(e) => setTagsProvider(e.target.value as AIProvider)}
|
||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||
>
|
||
<option value="ollama">🦙 Ollama (Local & Free)</option>
|
||
<option value="openai">🤖 OpenAI (GPT-5, GPT-4)</option>
|
||
<option value="custom">🔧 Custom OpenAI-Compatible</option>
|
||
</select>
|
||
</div>
|
||
|
||
{/* Ollama Tags Config */}
|
||
{tagsProvider === 'ollama' && (
|
||
<div className="space-y-3">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="OLLAMA_BASE_URL_TAGS">Base URL</Label>
|
||
<Input id="OLLAMA_BASE_URL_TAGS" name="OLLAMA_BASE_URL_TAGS" defaultValue={config.OLLAMA_BASE_URL || 'http://localhost:11434'} placeholder="http://localhost:11434" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="AI_MODEL_TAGS_OLLAMA">Model</Label>
|
||
<select
|
||
id="AI_MODEL_TAGS_OLLAMA"
|
||
name="AI_MODEL_TAGS"
|
||
defaultValue={config.AI_MODEL_TAGS || 'granite4:latest'}
|
||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||
>
|
||
{MODELS_2026.ollama.tags.map((model) => (
|
||
<option key={model} value={model}>{model}</option>
|
||
))}
|
||
</select>
|
||
<p className="text-xs text-muted-foreground">Select an Ollama model installed on your system</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* OpenAI Tags Config */}
|
||
{tagsProvider === 'openai' && (
|
||
<div className="space-y-3">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="OPENAI_API_KEY">API Key</Label>
|
||
<Input id="OPENAI_API_KEY" name="OPENAI_API_KEY" type="password" defaultValue={config.OPENAI_API_KEY || ''} placeholder="sk-..." />
|
||
<p className="text-xs text-muted-foreground">Your OpenAI API key from <a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline">platform.openai.com</a></p>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="AI_MODEL_TAGS_OPENAI">Model</Label>
|
||
<select
|
||
id="AI_MODEL_TAGS_OPENAI"
|
||
name="AI_MODEL_TAGS"
|
||
defaultValue={config.AI_MODEL_TAGS || 'gpt-4o-mini'}
|
||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||
>
|
||
{MODELS_2026.openai.tags.map((model) => (
|
||
<option key={model} value={model}>{model}</option>
|
||
))}
|
||
</select>
|
||
<p className="text-xs text-muted-foreground"><strong className="text-green-600">gpt-4o-mini</strong> = Best value • <strong className="text-blue-600">gpt-4o</strong> = Best quality</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Custom OpenAI Tags Config */}
|
||
{tagsProvider === 'custom' && (
|
||
<div className="space-y-3">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="CUSTOM_OPENAI_BASE_URL_TAGS">Base URL</Label>
|
||
<Input id="CUSTOM_OPENAI_BASE_URL_TAGS" name="CUSTOM_OPENAI_BASE_URL_TAGS" defaultValue={config.CUSTOM_OPENAI_BASE_URL || ''} placeholder="https://api.example.com/v1" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="CUSTOM_OPENAI_API_KEY_TAGS">API Key</Label>
|
||
<Input id="CUSTOM_OPENAI_API_KEY_TAGS" name="CUSTOM_OPENAI_API_KEY_TAGS" type="password" defaultValue={config.CUSTOM_OPENAI_API_KEY || ''} placeholder="sk-..." />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="AI_MODEL_TAGS_CUSTOM">Model</Label>
|
||
<select
|
||
id="AI_MODEL_TAGS_CUSTOM"
|
||
name="AI_MODEL_TAGS"
|
||
defaultValue={config.AI_MODEL_TAGS || 'gpt-4o-mini'}
|
||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||
>
|
||
{MODELS_2026.custom.tags.map((model) => (
|
||
<option key={model} value={model}>{model}</option>
|
||
))}
|
||
</select>
|
||
<p className="text-xs text-muted-foreground">Common models for OpenAI-compatible APIs</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Embeddings Section */}
|
||
<div className="space-y-4 p-4 border rounded-lg bg-green-50/50 dark:bg-green-950/20">
|
||
<h3 className="text-base font-semibold flex items-center gap-2">
|
||
<span className="text-green-600">🔍</span> Embeddings Provider
|
||
</h3>
|
||
<p className="text-xs text-muted-foreground">AI provider for semantic search embeddings. Recommended: OpenAI (best quality).</p>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="AI_PROVIDER_EMBEDDING">Provider</Label>
|
||
<select
|
||
id="AI_PROVIDER_EMBEDDING"
|
||
name="AI_PROVIDER_EMBEDDING"
|
||
value={embeddingsProvider}
|
||
onChange={(e) => setEmbeddingsProvider(e.target.value as AIProvider)}
|
||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||
>
|
||
<option value="ollama">🦙 Ollama (Local & Free)</option>
|
||
<option value="openai">🤖 OpenAI (text-embedding-4)</option>
|
||
<option value="custom">🔧 Custom OpenAI-Compatible</option>
|
||
</select>
|
||
</div>
|
||
|
||
{/* Ollama Embeddings Config */}
|
||
{embeddingsProvider === 'ollama' && (
|
||
<div className="space-y-3">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="OLLAMA_BASE_URL_EMBEDDING">Base URL</Label>
|
||
<Input id="OLLAMA_BASE_URL_EMBEDDING" name="OLLAMA_BASE_URL_EMBEDDING" defaultValue={config.OLLAMA_BASE_URL || 'http://localhost:11434'} placeholder="http://localhost:11434" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="AI_MODEL_EMBEDDING_OLLAMA">Model</Label>
|
||
<select
|
||
id="AI_MODEL_EMBEDDING_OLLAMA"
|
||
name="AI_MODEL_EMBEDDING"
|
||
defaultValue={config.AI_MODEL_EMBEDDING || 'embeddinggemma:latest'}
|
||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||
>
|
||
{MODELS_2026.ollama.embeddings.map((model) => (
|
||
<option key={model} value={model}>{model}</option>
|
||
))}
|
||
</select>
|
||
<p className="text-xs text-muted-foreground">Select an embedding model installed on your system</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* OpenAI Embeddings Config */}
|
||
{embeddingsProvider === 'openai' && (
|
||
<div className="space-y-3">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="OPENAI_API_KEY">API Key</Label>
|
||
<Input id="OPENAI_API_KEY" name="OPENAI_API_KEY" type="password" defaultValue={config.OPENAI_API_KEY || ''} placeholder="sk-..." />
|
||
<p className="text-xs text-muted-foreground">Your OpenAI API key from <a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline">platform.openai.com</a></p>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="AI_MODEL_EMBEDDING_OPENAI">Model</Label>
|
||
<select
|
||
id="AI_MODEL_EMBEDDING_OPENAI"
|
||
name="AI_MODEL_EMBEDDING"
|
||
defaultValue={config.AI_MODEL_EMBEDDING || 'text-embedding-3-small'}
|
||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||
>
|
||
{MODELS_2026.openai.embeddings.map((model) => (
|
||
<option key={model} value={model}>{model}</option>
|
||
))}
|
||
</select>
|
||
<p className="text-xs text-muted-foreground"><strong className="text-green-600">text-embedding-3-small</strong> = Best value • <strong className="text-blue-600">text-embedding-3-large</strong> = Best quality</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Custom OpenAI Embeddings Config */}
|
||
{embeddingsProvider === 'custom' && (
|
||
<div className="space-y-3">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="CUSTOM_OPENAI_BASE_URL_EMBEDDING">Base URL</Label>
|
||
<Input id="CUSTOM_OPENAI_BASE_URL_EMBEDDING" name="CUSTOM_OPENAI_BASE_URL_EMBEDDING" defaultValue={config.CUSTOM_OPENAI_BASE_URL || ''} placeholder="https://api.example.com/v1" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="CUSTOM_OPENAI_API_KEY_EMBEDDING">API Key</Label>
|
||
<Input id="CUSTOM_OPENAI_API_KEY_EMBEDDING" name="CUSTOM_OPENAI_API_KEY_EMBEDDING" type="password" defaultValue={config.CUSTOM_OPENAI_API_KEY || ''} placeholder="sk-..." />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="AI_MODEL_EMBEDDING_CUSTOM">Model</Label>
|
||
<select
|
||
id="AI_MODEL_EMBEDDING_CUSTOM"
|
||
name="AI_MODEL_EMBEDDING"
|
||
defaultValue={config.AI_MODEL_EMBEDDING || 'text-embedding-3-small'}
|
||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||
>
|
||
{MODELS_2026.custom.embeddings.map((model) => (
|
||
<option key={model} value={model}>{model}</option>
|
||
))}
|
||
</select>
|
||
<p className="text-xs text-muted-foreground">Common embedding models for OpenAI-compatible APIs</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
<CardFooter className="flex justify-between">
|
||
<Button type="submit" disabled={isSaving}>{isSaving ? 'Saving...' : 'Save AI Settings'}</Button>
|
||
<Link href="/admin/ai-test">
|
||
<Button type="button" variant="outline" className="gap-2">
|
||
<TestTube className="h-4 w-4" />
|
||
Open AI Test Panel
|
||
<ExternalLink className="h-3 w-3" />
|
||
</Button>
|
||
</Link>
|
||
</CardFooter>
|
||
</form>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>SMTP Configuration</CardTitle>
|
||
<CardDescription>Configure email server for password resets.</CardDescription>
|
||
</CardHeader>
|
||
<form action={handleSaveSMTP}>
|
||
<CardContent className="space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<label htmlFor="SMTP_HOST" className="text-sm font-medium">Host</label>
|
||
<Input id="SMTP_HOST" name="SMTP_HOST" defaultValue={config.SMTP_HOST || ''} placeholder="smtp.example.com" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label htmlFor="SMTP_PORT" className="text-sm font-medium">Port</label>
|
||
<Input id="SMTP_PORT" name="SMTP_PORT" defaultValue={config.SMTP_PORT || '587'} placeholder="587" />
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label htmlFor="SMTP_USER" className="text-sm font-medium">Username</label>
|
||
<Input id="SMTP_USER" name="SMTP_USER" defaultValue={config.SMTP_USER || ''} />
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label htmlFor="SMTP_PASS" className="text-sm font-medium">Password</label>
|
||
<Input id="SMTP_PASS" name="SMTP_PASS" type="password" defaultValue={config.SMTP_PASS || ''} />
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label htmlFor="SMTP_FROM" className="text-sm font-medium">From Email</label>
|
||
<Input id="SMTP_FROM" name="SMTP_FROM" defaultValue={config.SMTP_FROM || 'noreply@memento.app'} />
|
||
</div>
|
||
|
||
<div className="flex items-center space-x-2">
|
||
<Checkbox
|
||
id="SMTP_SECURE"
|
||
checked={smtpSecure}
|
||
onCheckedChange={(c) => setSmtpSecure(!!c)}
|
||
/>
|
||
<label
|
||
htmlFor="SMTP_SECURE"
|
||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||
>
|
||
Force SSL/TLS (usually for port 465)
|
||
</label>
|
||
</div>
|
||
|
||
<div className="flex items-center space-x-2">
|
||
<Checkbox
|
||
id="SMTP_IGNORE_CERT"
|
||
checked={smtpIgnoreCert}
|
||
onCheckedChange={(c) => setSmtpIgnoreCert(!!c)}
|
||
/>
|
||
<label
|
||
htmlFor="SMTP_IGNORE_CERT"
|
||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-yellow-600"
|
||
>
|
||
Ignore Certificate Errors (Self-hosted/Dev only)
|
||
</label>
|
||
</div>
|
||
</CardContent>
|
||
<CardFooter className="flex justify-between">
|
||
<Button type="submit" disabled={isSaving}>Save SMTP Settings</Button>
|
||
<Button type="button" variant="secondary" onClick={handleTestEmail} disabled={isTesting}>
|
||
{isTesting ? 'Sending...' : 'Test Email'}
|
||
</Button>
|
||
</CardFooter>
|
||
</form>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|