Files
Momento/memento-note/app/(admin)/admin/ai-test/ai-tester.tsx
Antigravity 5b794d6449
Some checks failed
CI / Lint, Test & Build (push) Failing after 7m46s
CI / Deploy production (on server) (push) Has been cancelled
feat(auth): restore Google sign-in and AI admin test routes
Google OAuth was implemented locally but never deployed; the login button
only renders when AUTH_GOOGLE_ID and AUTH_GOOGLE_SECRET are set. Also
restores /api/ai/test-* endpoints removed by mistake and wires Google
credentials into deploy workflows.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-17 17:17:42 +00:00

322 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { useState, useEffect } from 'react'
import { toast } from 'sonner'
import { Loader2, CheckCircle2, XCircle, Clock, Zap, Info, Shield, Brain, MessageSquare, Search } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
interface TestResult {
success: boolean
provider?: string
model?: string
responseTime?: number
tags?: Array<{ tag: string; confidence: number }>
embeddingLength?: number
firstValues?: number[]
chatResponse?: string
error?: string
details?: any
}
export function AI_TESTER({ type }: { type: 'tags' | 'embeddings' | 'chat' }) {
const { t } = useLanguage()
const [isLoading, setIsLoading] = useState(false)
const [result, setResult] = useState<TestResult | null>(null)
const [config, setConfig] = useState<any>(null)
useEffect(() => {
fetchConfig()
}, [])
const fetchConfig = async () => {
try {
const response = await fetch('/api/ai/config')
const data = await response.json()
setConfig(data)
if (data.previousTest) {
setResult(data.previousTest[type] || null)
}
} catch (error) {
console.error('Failed to fetch config:', error)
}
}
const runTest = async () => {
setIsLoading(true)
const startTime = Date.now()
try {
const response = await fetch(`/api/ai/test-${type}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
const endTime = Date.now()
const contentType = response.headers.get('content-type') || ''
if (!contentType.includes('application/json')) {
const bodyPreview = (await response.text()).slice(0, 200)
throw new Error(
response.status === 404
? `Route API introuvable (/api/ai/test-${type}). Redéployez lapplication.`
: `Réponse non-JSON (HTTP ${response.status}): ${bodyPreview}`,
)
}
const data = await response.json()
setResult({
...data,
responseTime: endTime - startTime
})
if (data.success) {
toast.success(
`${t('admin.aiTest.testSuccessToast', { type: type === 'tags' ? 'Tags' : type === 'chat' ? 'Chat' : 'Embeddings' })}`,
{
description: `Provider: ${data.provider} | Time: ${endTime - startTime}ms`
}
)
} else {
toast.error(
`${t('admin.aiTest.testFailedToast', { type: type === 'tags' ? 'Tags' : type === 'chat' ? 'Chat' : 'Embeddings' })}`,
{
description: data.error || 'Unknown error'
}
)
}
} catch (error: any) {
const endTime = Date.now()
const errorResult = {
success: false,
error: error.message || 'Network error',
responseTime: endTime - startTime
}
setResult(errorResult)
toast.error(t('admin.aiTest.testError', { error: error.message }))
} finally {
setIsLoading(false)
}
}
const getProviderInfo = () => {
if (!config) return { provider: '...', model: '...' }
if (type === 'tags') {
return {
provider: config.AI_PROVIDER_TAGS || 'ollama',
model: config.AI_MODEL_TAGS || 'granite4:latest'
}
} else if (type === 'chat') {
return {
provider: config.AI_PROVIDER_CHAT || config.AI_PROVIDER_TAGS || 'ollama',
model: config.AI_MODEL_CHAT || 'granite4:latest'
}
} else {
return {
provider: config.AI_PROVIDER_EMBEDDING || 'ollama',
model: config.AI_MODEL_EMBEDDING || 'embeddinggemma:latest'
}
}
}
const providerInfo = getProviderInfo()
return (
<div className="space-y-8">
{/* Current Config Summary */}
<div className="rounded-2xl border border-border bg-muted/20 overflow-hidden shadow-inner">
<div className="px-4 py-2.5 border-b border-border/50 bg-muted/40 flex items-center gap-2">
{type === 'tags' ? <Brain className="h-4 w-4 text-primary" /> :
type === 'embeddings' ? <Search className="h-4 w-4 text-green-600" /> :
<MessageSquare className="h-4 w-4 text-violet-600" />}
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">{t('admin.aiTest.provider')}</span>
</div>
<div className="p-6 space-y-5">
<div className="flex items-center justify-between gap-6">
<span className="text-sm font-bold text-muted-foreground truncate">{t('admin.aiTest.provider')}</span>
<Badge variant="secondary" className="font-mono text-xs py-1 h-7 px-4 bg-background border-border rounded-xl shadow-sm">
{providerInfo.provider}
</Badge>
</div>
<div className="flex items-center justify-between gap-6">
<span className="text-sm font-bold text-muted-foreground truncate">{t('admin.aiTest.model')}</span>
<span className="text-xs font-mono font-bold text-foreground truncate bg-background/80 px-3 py-1 rounded-lg border border-border/50" title={providerInfo.model}>
{providerInfo.model}
</span>
</div>
</div>
</div>
{/* Run Test Action */}
<Button
onClick={runTest}
disabled={isLoading}
className={`w-full shadow-lg py-7 h-auto rounded-2xl transition-all duration-300 relative overflow-hidden group/btn ${
isLoading ? 'opacity-90' : 'hover:scale-[1.02] active:scale-[0.98] hover:shadow-xl'
} ${
result?.success ? 'bg-primary' : result?.success === false ? 'bg-destructive hover:bg-destructive/90' : 'bg-primary hover:bg-primary/90'
}`}
>
<div className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/10 to-white/0 -translate-x-full group-hover/btn:animate-[shimmer_2s_infinite] pointer-events-none" />
{isLoading ? (
<div className="flex flex-col items-center gap-2">
<Loader2 className="h-6 w-6 animate-spin" />
<span className="text-xs font-bold tracking-widest uppercase">{t('admin.aiTest.testing')}...</span>
</div>
) : (
<div className="flex items-center justify-center gap-4">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center transition-all ${
result?.success ? 'bg-white/20' : 'bg-white/20 group-hover/btn:scale-110'
}`}>
<Zap className="h-5 w-5" />
</div>
<span className="font-black tracking-tight text-lg py-1">
{t('admin.aiTest.runTest')}
</span>
</div>
)}
</Button>
{/* Result Display */}
{result && (
<div className={`rounded-xl border transition-all duration-300 animate-in fade-in slide-in-from-top-2 ${
result.success
? 'bg-green-500/[0.03] border-green-500/20 shadow-green-500/[0.02] shadow-lg'
: 'bg-destructive/[0.03] border-destructive/20 shadow-destructive/[0.02] shadow-lg'
}`}>
<div className="p-5 space-y-5">
{/* Status Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{result.success ? (
<div className="w-8 h-8 rounded-full bg-green-500/20 flex items-center justify-center text-green-600">
<CheckCircle2 className="h-5 w-5" />
</div>
) : (
<div className="w-8 h-8 rounded-full bg-destructive/20 flex items-center justify-center text-destructive">
<XCircle className="h-5 w-5" />
</div>
)}
<span className={`font-bold tracking-tight ${result.success ? 'text-green-700 dark:text-green-400' : 'text-destructive'}`}>
{result.success ? t('admin.aiTest.testPassed') : t('admin.aiTest.testFailed')}
</span>
</div>
{result.responseTime && (
<div className="flex items-center gap-1.5 px-2 py-1 bg-background/50 rounded-md border border-border/50 text-[10px] font-bold text-muted-foreground">
<Clock className="h-3 w-3" />
{result.responseTime}ms
</div>
)}
</div>
{/* Response Content - Tags */}
{type === 'tags' && result.success && result.tags && (
<div className="space-y-3 pt-3 border-t border-border/10">
<div className="flex items-center gap-2 text-xs font-semibold text-muted-foreground uppercase tracking-widest">
<Info className="h-3.5 w-3.5" />
{t('admin.aiTest.generatedTags')}
</div>
<div className="flex flex-wrap gap-2">
{result.tags.map((tag, idx) => (
<div
key={idx}
className="group relative flex items-center gap-2 px-3 py-1.5 bg-background border border-border rounded-lg text-sm font-medium hover:border-primary/30 hover:bg-primary/[0.02] transition-colors"
>
<span className="text-foreground">{tag.tag}</span>
<span className="text-[10px] text-muted-foreground bg-muted px-1 rounded font-mono">
{Math.round(tag.confidence * 100)}%
</span>
</div>
))}
</div>
</div>
)}
{/* Response Content - Chat */}
{type === 'chat' && result.success && result.chatResponse && (
<div className="space-y-3 pt-3 border-t border-border/10">
<div className="flex items-center gap-2 text-xs font-semibold text-muted-foreground uppercase tracking-widest">
<MessageSquare className="h-3.5 w-3.5" />
Modèle Réponse
</div>
<div className="p-4 bg-background/50 border border-border/50 rounded-xl relative">
<div className="absolute -left-1.5 top-4 w-3 h-3 bg-background border-l border-t border-border/50 rotate-[-45deg] rounded-sm" />
<p className="text-sm leading-relaxed text-foreground italic">
&quot;{result.chatResponse}&quot;
</p>
</div>
</div>
)}
{/* Response Content - Embeddings */}
{type === 'embeddings' && result.success && result.embeddingLength && (
<div className="space-y-4 pt-3 border-t border-border/10">
<div className="grid grid-cols-2 gap-4">
<div className="bg-background/50 border border-border/50 rounded-xl p-4 text-center">
<div className="text-2xl font-black tracking-tighter text-foreground">
{result.embeddingLength}
</div>
<div className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mt-1">
{t('admin.aiTest.vectorDimensions')}
</div>
</div>
<div className="bg-background/50 border border-border/50 rounded-xl p-4 text-center flex flex-col justify-center">
<div className="flex items-center justify-center gap-1">
<div className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
<span className="text-xs font-bold text-foreground">Active Vector</span>
</div>
</div>
</div>
{result.firstValues && result.firstValues.length > 0 && (
<div className="space-y-2">
<div className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
{t('admin.aiTest.first5Values')}
</div>
<div className="p-3 bg-muted/50 rounded-lg font-mono text-[11px] leading-none flex justify-between overflow-x-auto whitespace-nowrap gap-2 scrollbar-hide border border-border/50">
{result.firstValues.slice(0, 5).map((v, i) => (
<span key={i} className="text-foreground tabular-nums">
{v.toFixed(4)}
</span>
))}
</div>
</div>
)}
</div>
)}
{/* Error Details */}
{!result.success && result.error && (
<div className="space-y-3 pt-3 border-t border-border/10">
<div className="p-4 bg-destructive/[0.05] border border-destructive/20 rounded-xl">
<div className="flex items-center gap-2 mb-2">
<Info className="h-4 w-4 text-destructive" />
<span className="text-xs font-bold text-destructive uppercase tracking-widest">Détails de l&apos;erreur</span>
</div>
<p className="text-sm text-destructive font-medium leading-relaxed">{result.error}</p>
{result.details && (
<details className="mt-4 group">
<summary className="text-[10px] font-bold uppercase tracking-widest cursor-pointer text-muted-foreground hover:text-destructive transition-colors">
{t('admin.aiTest.technicalDetails')}
</summary>
<div className="mt-2 p-3 bg-black/5 dark:bg-black/20 rounded-lg overflow-x-auto border border-border/50">
<pre className="text-[10px] font-mono text-muted-foreground leading-tight">
{JSON.stringify(result.details, null, 2)}
</pre>
</div>
</details>
)}
</div>
</div>
)}
</div>
</div>
)}
</div>
)
}