feat: AI provider testing page + multi-provider support + UX design spec
- 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
This commit is contained in:
258
keep-notes/app/(main)/admin/ai-test/ai-tester.tsx
Normal file
258
keep-notes/app/(main)/admin/ai-test/ai-tester.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { Loader2, CheckCircle2, XCircle, Clock, Zap, Info } from 'lucide-react'
|
||||
|
||||
interface TestResult {
|
||||
success: boolean
|
||||
provider?: string
|
||||
model?: string
|
||||
responseTime?: number
|
||||
tags?: Array<{ tag: string; confidence: number }>
|
||||
embeddingLength?: number
|
||||
firstValues?: number[]
|
||||
error?: string
|
||||
details?: any
|
||||
}
|
||||
|
||||
export function AI_TESTER({ type }: { type: 'tags' | 'embeddings' }) {
|
||||
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)
|
||||
|
||||
// Set previous result if available
|
||||
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 data = await response.json()
|
||||
|
||||
setResult({
|
||||
...data,
|
||||
responseTime: endTime - startTime
|
||||
})
|
||||
|
||||
if (data.success) {
|
||||
toast.success(
|
||||
`✅ ${type === 'tags' ? 'Tags' : 'Embeddings'} Test Successful!`,
|
||||
{
|
||||
description: `Provider: ${data.provider} | Time: ${endTime - startTime}ms`
|
||||
}
|
||||
)
|
||||
} else {
|
||||
toast.error(
|
||||
`❌ ${type === 'tags' ? 'Tags' : 'Embeddings'} Test Failed`,
|
||||
{
|
||||
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(`❌ Test Error: ${error.message}`)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getProviderInfo = () => {
|
||||
if (!config) return { provider: 'Loading...', model: 'Loading...' }
|
||||
|
||||
if (type === 'tags') {
|
||||
return {
|
||||
provider: config.AI_PROVIDER_TAGS || 'ollama',
|
||||
model: config.AI_MODEL_TAGS || '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-4">
|
||||
{/* Provider Info */}
|
||||
<div className="space-y-3 p-4 bg-muted/50 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Provider:</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{providerInfo.provider.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Model:</span>
|
||||
<span className="text-sm text-muted-foreground font-mono">
|
||||
{providerInfo.model}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Test Button */}
|
||||
<Button
|
||||
onClick={runTest}
|
||||
disabled={isLoading}
|
||||
className="w-full"
|
||||
variant={result?.success ? 'default' : result?.success === false ? 'destructive' : 'default'}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Testing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Zap className="mr-2 h-4 w-4" />
|
||||
Run Test
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Results */}
|
||||
{result && (
|
||||
<Card className={result.success ? 'border-green-200 dark:border-green-900' : 'border-red-200 dark:border-red-900'}>
|
||||
<CardContent className="pt-6">
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
{result.success ? (
|
||||
<>
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
||||
<span className="font-semibold text-green-600 dark:text-green-400">Test Passed</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XCircle className="h-5 w-5 text-red-600" />
|
||||
<span className="font-semibold text-red-600 dark:text-red-400">Test Failed</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Response Time */}
|
||||
{result.responseTime && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-4">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>Response time: {result.responseTime}ms</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags Results */}
|
||||
{type === 'tags' && result.success && result.tags && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Info className="h-4 w-4 text-blue-600" />
|
||||
<span className="text-sm font-medium">Generated Tags:</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{result.tags.map((tag, idx) => (
|
||||
<Badge
|
||||
key={idx}
|
||||
variant="secondary"
|
||||
className="text-sm"
|
||||
>
|
||||
{tag.tag}
|
||||
<span className="ml-1 text-xs opacity-70">
|
||||
({Math.round(tag.confidence * 100)}%)
|
||||
</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Embeddings Results */}
|
||||
{type === 'embeddings' && result.success && result.embeddingLength && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Info className="h-4 w-4 text-green-600" />
|
||||
<span className="text-sm font-medium">Embedding Dimensions:</span>
|
||||
</div>
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-2xl font-bold text-center">
|
||||
{result.embeddingLength}
|
||||
</div>
|
||||
<div className="text-xs text-center text-muted-foreground mt-1">
|
||||
vector dimensions
|
||||
</div>
|
||||
</div>
|
||||
{result.firstValues && result.firstValues.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs font-medium">First 5 values:</span>
|
||||
<div className="p-2 bg-muted rounded font-mono text-xs">
|
||||
[{result.firstValues.slice(0, 5).map((v, i) => v.toFixed(4)).join(', ')}]
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Details */}
|
||||
{!result.success && result.error && (
|
||||
<div className="mt-4 p-3 bg-red-50 dark:bg-red-950/20 rounded-lg border border-red-200 dark:border-red-900">
|
||||
<p className="text-sm font-medium text-red-900 dark:text-red-100">Error:</p>
|
||||
<p className="text-sm text-red-700 dark:text-red-300 mt-1">{result.error}</p>
|
||||
{result.details && (
|
||||
<details className="mt-2">
|
||||
<summary className="text-xs cursor-pointer text-red-600 dark:text-red-400">
|
||||
Technical details
|
||||
</summary>
|
||||
<pre className="mt-2 text-xs overflow-auto p-2 bg-red-100 dark:bg-red-900/30 rounded">
|
||||
{JSON.stringify(result.details, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Loading State */}
|
||||
{isLoading && (
|
||||
<div className="text-center py-4">
|
||||
<Loader2 className="h-8 w-8 animate-spin mx-auto text-blue-600" />
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
Testing {type === 'tags' ? 'tags generation' : 'embeddings'}...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
106
keep-notes/app/(main)/admin/ai-test/page.tsx
Normal file
106
keep-notes/app/(main)/admin/ai-test/page.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { auth } from '@/auth'
|
||||
import { redirect } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { ArrowLeft, TestTube } from 'lucide-react'
|
||||
import { AI_TESTER } from './ai-tester'
|
||||
|
||||
export default async function AITestPage() {
|
||||
const session = await auth()
|
||||
|
||||
if ((session?.user as any)?.role !== 'ADMIN') {
|
||||
redirect('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-10 px-4 max-w-6xl">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/admin/settings">
|
||||
<Button variant="outline" size="icon">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold flex items-center gap-2">
|
||||
<TestTube className="h-8 w-8" />
|
||||
AI Provider Testing
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Test your AI providers for tag generation and semantic search embeddings
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Tags Provider Test */}
|
||||
<Card className="border-blue-200 dark:border-blue-900">
|
||||
<CardHeader className="bg-blue-50/50 dark:bg-blue-950/20">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<span className="text-2xl">🏷️</span>
|
||||
Tags Generation Test
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Test the AI provider responsible for automatic tag suggestions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<AI_TESTER type="tags" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Embeddings Provider Test */}
|
||||
<Card className="border-green-200 dark:border-green-900">
|
||||
<CardHeader className="bg-green-50/50 dark:bg-green-950/20">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<span className="text-2xl">🔍</span>
|
||||
Embeddings Test
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Test the AI provider responsible for semantic search embeddings
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<AI_TESTER type="embeddings" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>ℹ️ How Testing Works</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 text-sm">
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">🏷️ Tags Generation Test:</h4>
|
||||
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
|
||||
<li>Sends a sample note to the AI provider</li>
|
||||
<li>Requests 3-5 relevant tags based on the content</li>
|
||||
<li>Displays the generated tags with confidence scores</li>
|
||||
<li>Measures response time</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">🔍 Embeddings Test:</h4>
|
||||
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
|
||||
<li>Sends a sample text to the embedding provider</li>
|
||||
<li>Generates a vector representation (list of numbers)</li>
|
||||
<li>Displays embedding dimensions and sample values</li>
|
||||
<li>Verifies the vector is valid and properly formatted</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-amber-50 dark:bg-amber-950/20 p-4 rounded-lg border border-amber-200 dark:border-amber-900">
|
||||
<p className="font-semibold text-amber-900 dark:text-amber-100">💡 Tip:</p>
|
||||
<p className="text-amber-800 dark:text-amber-200 mt-1">
|
||||
You can use different providers for tags and embeddings! For example, use Ollama (free) for tags
|
||||
and OpenAI (best quality) for embeddings to optimize costs and performance.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user