feat: embedding dimension validation + migration system
Some checks failed
CI / Lint, Test & Build (push) Failing after 8m1s
CI / Deploy production (on server) (push) Has been cancelled

- 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:
Antigravity
2026-05-19 18:45:50 +00:00
parent 28f46860c1
commit 6a8d0eb0a5
10 changed files with 297 additions and 20 deletions

View File

@@ -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" />