Keep/keep-notes/app/(main)/admin/settings/admin-settings-form.tsx
sepehr 640fcb26f7 fix: improve note interactions and markdown LaTeX support
## Bug Fixes

### Note Card Actions
- Fix broken size change functionality (missing state declaration)
- Implement React 19 useOptimistic for instant UI feedback
- Add startTransition for non-blocking updates
- Ensure smooth animations without page refresh
- All note actions now work: pin, archive, color, size, checklist

### Markdown LaTeX Rendering
- Add remark-math and rehype-katex plugins
- Support inline equations with dollar sign syntax
- Support block equations with double dollar sign syntax
- Import KaTeX CSS for proper styling
- Equations now render correctly instead of showing raw LaTeX

## Technical Details

- Replace undefined currentNote references with optimistic state
- Add optimistic updates before server actions for instant feedback
- Use router.refresh() in transitions for smart cache invalidation
- Install remark-math, rehype-katex, and katex packages

## Testing

- Build passes successfully with no TypeScript errors
- Dev server hot-reloads changes correctly
2026-01-09 22:13:49 +01:00

246 lines
9.7 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 { updateSystemConfig, testSMTP } from '@/app/actions/admin-settings'
import { toast } from 'sonner'
import { useState, useEffect } from 'react'
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')
// Sync state with config when server revalidates
useEffect(() => {
setAllowRegister(config.ALLOW_REGISTRATION !== 'false')
setSmtpSecure(config.SMTP_SECURE === 'true')
setSmtpIgnoreCert(config.SMTP_IGNORE_CERT === 'true')
}, [config])
const handleSaveSecurity = async (formData: FormData) => {
setIsSaving(true)
// We override the formData get because the hidden input might be tricky
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 = {
AI_PROVIDER: formData.get('AI_PROVIDER') as string,
OLLAMA_BASE_URL: formData.get('OLLAMA_BASE_URL') as string,
AI_MODEL_EMBEDDING: formData.get('AI_MODEL_EMBEDDING') as string,
OPENAI_API_KEY: formData.get('OPENAI_API_KEY') 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')
}
}
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 the AI provider for auto-tagging and semantic search.</CardDescription>
</CardHeader>
<form action={handleSaveAI}>
<CardContent className="space-y-4">
<div className="space-y-2">
<label htmlFor="AI_PROVIDER" className="text-sm font-medium">Provider</label>
<select
id="AI_PROVIDER"
name="AI_PROVIDER"
defaultValue={config.AI_PROVIDER || 'ollama'}
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)</option>
<option value="openai">OpenAI</option>
</select>
</div>
<div className="space-y-2">
<label htmlFor="OLLAMA_BASE_URL" className="text-sm font-medium">Ollama Base URL</label>
<Input id="OLLAMA_BASE_URL" name="OLLAMA_BASE_URL" defaultValue={config.OLLAMA_BASE_URL || 'http://localhost:11434'} placeholder="http://localhost:11434" />
</div>
<div className="space-y-2">
<label htmlFor="AI_MODEL_EMBEDDING" className="text-sm font-medium">Embedding Model</label>
<Input id="AI_MODEL_EMBEDDING" name="AI_MODEL_EMBEDDING" defaultValue={config.AI_MODEL_EMBEDDING || 'embeddinggemma:latest'} placeholder="embeddinggemma:latest" />
</div>
<div className="space-y-2">
<label htmlFor="OPENAI_API_KEY" className="text-sm font-medium">OpenAI API Key (if using OpenAI)</label>
<Input id="OPENAI_API_KEY" name="OPENAI_API_KEY" type="password" defaultValue={config.OPENAI_API_KEY || ''} placeholder="sk-..." />
</div>
</CardContent>
<CardFooter>
<Button type="submit" disabled={isSaving}>Save AI Settings</Button>
</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>
)
}