feat: embedding dimension validation + migration system
- Add /api/admin/embeddings/dimension (GET column dim, POST test model dim) - Add /api/admin/embeddings/migrate (alter column, clear, re-index) - Admin form warns on dimension mismatch after save, offers migrate button - Remove hardcoded 1536 from validate endpoint and embedding service - Add validateDimension() utility to EmbeddingService - Fix health route: import prisma correctly, use router instead of missing registry - i18n keys for dimension warning (EN/FR)
This commit is contained in:
@@ -178,6 +178,9 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
const [embedFallbackProvider, setEmbedFallbackProvider] = useState<string>(config.AI_PROVIDER_EMBEDDING_FALLBACK || '')
|
||||
const [chatFallbackProvider, setChatFallbackProvider] = useState<string>(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<Record<ModelPurpose, string[]>>({
|
||||
tags: [],
|
||||
@@ -412,6 +415,18 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
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)
|
||||
@@ -419,6 +434,24 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
}
|
||||
}
|
||||
|
||||
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<string, string> = { EMAIL_PROVIDER: emailProvider }
|
||||
@@ -426,6 +459,8 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
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
|
||||
@@ -772,6 +807,33 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">{t('admin.ai.embeddingsDescription')}</p>
|
||||
|
||||
{dimensionWarning && (
|
||||
<div className="rounded-lg border border-amber-500/50 bg-amber-50 dark:bg-amber-950/20 p-3 space-y-2">
|
||||
<p className="text-sm font-medium text-amber-800 dark:text-amber-200">
|
||||
{t('admin.ai.dimensionMismatch')}
|
||||
</p>
|
||||
<p className="text-xs text-amber-700 dark:text-amber-300">
|
||||
{t('admin.ai.dimensionMismatchDetail', {
|
||||
dbDimension: dimensionWarning.dbDimension,
|
||||
modelDimension: dimensionWarning.modelDimension,
|
||||
})}
|
||||
</p>
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400">
|
||||
{t('admin.ai.dimensionMismatchAction')}
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-amber-500 text-amber-800 hover:bg-amber-100 dark:text-amber-200 dark:hover:bg-amber-900"
|
||||
disabled={isMigrating}
|
||||
onClick={handleMigrateEmbeddings}
|
||||
>
|
||||
{isMigrating ? t('admin.ai.migrating') : t('admin.ai.migrateButton')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="AI_PROVIDER_EMBEDDING">
|
||||
{t('admin.ai.provider')}
|
||||
@@ -972,6 +1034,15 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
<Input id="RESEND_API_KEY" name="RESEND_API_KEY" type="password" defaultValue={config.RESEND_API_KEY || ''} placeholder="re_..." />
|
||||
<p className="text-xs text-muted-foreground">{t('admin.resend.apiKeyHint')}</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="SMTP_FROM" className="text-sm font-medium">{t('admin.smtp.fromEmail')}</label>
|
||||
<Input
|
||||
id="SMTP_FROM"
|
||||
name="SMTP_FROM"
|
||||
defaultValue={config.SMTP_FROM || 'noreply@memento-note.com'}
|
||||
placeholder="noreply@memento-note.com"
|
||||
/>
|
||||
</div>
|
||||
{config.RESEND_API_KEY && (
|
||||
<div className="flex items-center gap-2 text-xs text-green-600">
|
||||
<span className="inline-block w-2 h-2 rounded-full bg-green-500" />
|
||||
|
||||
Reference in New Issue
Block a user