refactor: LLM Compare komplett entfernt, Video/Voice/Alerts Sidebar hinzugefuegt
- LLM Compare Seiten, Configs und alle Referenzen geloescht - Kommunikation-Kategorie in Sidebar mit Video & Chat, Voice Service, Alerts - Compliance SDK Kategorie aus Sidebar entfernt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -179,7 +179,6 @@ export default function GPUInfrastructurePage() {
|
|||||||
databases: ['PostgreSQL (Logs)'],
|
databases: ['PostgreSQL (Logs)'],
|
||||||
}}
|
}}
|
||||||
relatedPages={[
|
relatedPages={[
|
||||||
{ name: 'LLM Vergleich', href: '/ai/llm-compare', description: 'KI-Provider testen' },
|
|
||||||
{ name: 'Test Quality (BQAS)', href: '/ai/test-quality', description: 'Golden Suite & Tests' },
|
{ name: 'Test Quality (BQAS)', href: '/ai/test-quality', description: 'Golden Suite & Tests' },
|
||||||
{ name: 'Magic Help', href: '/ai/magic-help', description: 'TrOCR Testing' },
|
{ name: 'Magic Help', href: '/ai/magic-help', description: 'TrOCR Testing' },
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -1,503 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LLM Comparison Tool
|
|
||||||
*
|
|
||||||
* Vergleicht Antworten von verschiedenen LLM-Providern:
|
|
||||||
* - OpenAI/ChatGPT
|
|
||||||
* - Claude
|
|
||||||
* - Self-hosted + Tavily
|
|
||||||
* - Self-hosted + EduSearch
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
|
||||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
|
||||||
import { AIToolsSidebarResponsive } from '@/components/ai/AIToolsSidebar'
|
|
||||||
|
|
||||||
interface LLMResponse {
|
|
||||||
provider: string
|
|
||||||
model: string
|
|
||||||
response: string
|
|
||||||
latency_ms: number
|
|
||||||
tokens_used?: number
|
|
||||||
search_results?: Array<{
|
|
||||||
title: string
|
|
||||||
url: string
|
|
||||||
content: string
|
|
||||||
score?: number
|
|
||||||
}>
|
|
||||||
error?: string
|
|
||||||
timestamp: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ComparisonResult {
|
|
||||||
comparison_id: string
|
|
||||||
prompt: string
|
|
||||||
system_prompt?: string
|
|
||||||
responses: LLMResponse[]
|
|
||||||
created_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerColors: Record<string, { bg: string; border: string; text: string }> = {
|
|
||||||
openai: { bg: 'bg-emerald-50', border: 'border-emerald-300', text: 'text-emerald-700' },
|
|
||||||
claude: { bg: 'bg-orange-50', border: 'border-orange-300', text: 'text-orange-700' },
|
|
||||||
selfhosted_tavily: { bg: 'bg-blue-50', border: 'border-blue-300', text: 'text-blue-700' },
|
|
||||||
selfhosted_edusearch: { bg: 'bg-purple-50', border: 'border-purple-300', text: 'text-purple-700' },
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerLabels: Record<string, string> = {
|
|
||||||
openai: 'OpenAI GPT-4o-mini',
|
|
||||||
claude: 'Claude 3.5 Sonnet',
|
|
||||||
selfhosted_tavily: 'Self-hosted + Tavily',
|
|
||||||
selfhosted_edusearch: 'Self-hosted + EduSearch',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LLMComparePage() {
|
|
||||||
// State
|
|
||||||
const [prompt, setPrompt] = useState('')
|
|
||||||
const [systemPrompt, setSystemPrompt] = useState('Du bist ein hilfreicher Assistent fuer Lehrkraefte in Deutschland.')
|
|
||||||
|
|
||||||
// Provider toggles
|
|
||||||
const [enableOpenAI, setEnableOpenAI] = useState(true)
|
|
||||||
const [enableClaude, setEnableClaude] = useState(true)
|
|
||||||
const [enableTavily, setEnableTavily] = useState(true)
|
|
||||||
const [enableEduSearch, setEnableEduSearch] = useState(true)
|
|
||||||
|
|
||||||
// Parameters
|
|
||||||
const [model, setModel] = useState('llama3.2:3b')
|
|
||||||
const [temperature, setTemperature] = useState(0.7)
|
|
||||||
const [maxTokens, setMaxTokens] = useState(2048)
|
|
||||||
|
|
||||||
// Results
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [result, setResult] = useState<ComparisonResult | null>(null)
|
|
||||||
const [history, setHistory] = useState<ComparisonResult[]>([])
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
// UI State
|
|
||||||
const [showSettings, setShowSettings] = useState(false)
|
|
||||||
const [showHistory, setShowHistory] = useState(false)
|
|
||||||
|
|
||||||
// API Base URL
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_LLM_GATEWAY_URL || 'http://localhost:8082'
|
|
||||||
const API_KEY = process.env.NEXT_PUBLIC_LLM_API_KEY || 'dev-key'
|
|
||||||
|
|
||||||
// Load history
|
|
||||||
const loadHistory = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_URL}/v1/comparison/history?limit=20`, {
|
|
||||||
headers: { Authorization: `Bearer ${API_KEY}` },
|
|
||||||
})
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json()
|
|
||||||
setHistory(data.comparisons || [])
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load history:', e)
|
|
||||||
}
|
|
||||||
}, [API_URL, API_KEY])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadHistory()
|
|
||||||
}, [loadHistory])
|
|
||||||
|
|
||||||
const runComparison = async () => {
|
|
||||||
if (!prompt.trim()) {
|
|
||||||
setError('Bitte geben Sie einen Prompt ein')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
setError(null)
|
|
||||||
setResult(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_URL}/v1/comparison/run`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${API_KEY}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt,
|
|
||||||
system_prompt: systemPrompt || undefined,
|
|
||||||
enable_openai: enableOpenAI,
|
|
||||||
enable_claude: enableClaude,
|
|
||||||
enable_selfhosted_tavily: enableTavily,
|
|
||||||
enable_selfhosted_edusearch: enableEduSearch,
|
|
||||||
selfhosted_model: model,
|
|
||||||
temperature,
|
|
||||||
max_tokens: maxTokens,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`API Error: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
setResult(data)
|
|
||||||
loadHistory()
|
|
||||||
} catch (e) {
|
|
||||||
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ResponseCard = ({ response }: { response: LLMResponse }) => {
|
|
||||||
const colors = providerColors[response.provider] || {
|
|
||||||
bg: 'bg-slate-50',
|
|
||||||
border: 'border-slate-300',
|
|
||||||
text: 'text-slate-700',
|
|
||||||
}
|
|
||||||
const label = providerLabels[response.provider] || response.provider
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`rounded-xl border-2 ${colors.border} ${colors.bg} overflow-hidden`}>
|
|
||||||
<div className={`px-4 py-3 border-b ${colors.border} flex items-center justify-between`}>
|
|
||||||
<div>
|
|
||||||
<h3 className={`font-semibold ${colors.text}`}>{label}</h3>
|
|
||||||
<p className="text-xs text-slate-500">{response.model}</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-right text-xs text-slate-500">
|
|
||||||
<div>{response.latency_ms}ms</div>
|
|
||||||
{response.tokens_used && <div>{response.tokens_used} tokens</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4">
|
|
||||||
{response.error ? (
|
|
||||||
<div className="text-red-600 text-sm">
|
|
||||||
<strong>Fehler:</strong> {response.error}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<pre className="whitespace-pre-wrap text-sm text-slate-700 font-sans">
|
|
||||||
{response.response}
|
|
||||||
</pre>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{response.search_results && response.search_results.length > 0 && (
|
|
||||||
<div className="px-4 pb-4">
|
|
||||||
<details className="text-xs">
|
|
||||||
<summary className="cursor-pointer text-slate-500 hover:text-slate-700">
|
|
||||||
{response.search_results.length} Suchergebnisse anzeigen
|
|
||||||
</summary>
|
|
||||||
<ul className="mt-2 space-y-2">
|
|
||||||
{response.search_results.map((sr, idx) => (
|
|
||||||
<li key={idx} className="bg-white rounded p-2 border border-slate-200">
|
|
||||||
<a
|
|
||||||
href={sr.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-blue-600 hover:underline font-medium"
|
|
||||||
>
|
|
||||||
{sr.title || 'Untitled'}
|
|
||||||
</a>
|
|
||||||
<p className="text-slate-500 truncate">{sr.content}</p>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* Page Purpose */}
|
|
||||||
<PagePurpose
|
|
||||||
title="LLM Vergleich"
|
|
||||||
purpose="Vergleichen Sie Antworten verschiedener KI-Provider (OpenAI, Claude, Self-hosted) fuer Qualitaetssicherung. Optimieren Sie Parameter und System Prompts fuer beste Ergebnisse. Standalone-Werkzeug ohne direkten Datenfluss zur KI-Pipeline."
|
|
||||||
audience={['Entwickler', 'Data Scientists', 'QA']}
|
|
||||||
architecture={{
|
|
||||||
services: ['llm-gateway (Python)', 'Ollama', 'OpenAI API', 'Claude API'],
|
|
||||||
databases: ['PostgreSQL (History)', 'Qdrant (RAG)'],
|
|
||||||
}}
|
|
||||||
relatedPages={[
|
|
||||||
{ name: 'Test Quality (BQAS)', href: '/ai/test-quality', description: 'Golden Suite & Synthetic Tests' },
|
|
||||||
{ name: 'GPU Infrastruktur', href: '/ai/gpu', description: 'GPU-Ressourcen verwalten' },
|
|
||||||
{ name: 'Agent Management', href: '/ai/agents', description: 'Multi-Agent System' },
|
|
||||||
]}
|
|
||||||
collapsible={true}
|
|
||||||
defaultCollapsed={true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* KI-Werkzeuge Sidebar */}
|
|
||||||
<AIToolsSidebarResponsive currentTool="llm-compare" />
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
{/* Left Column: Input & Settings */}
|
|
||||||
<div className="lg:col-span-1 space-y-4">
|
|
||||||
{/* Prompt Input */}
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
|
||||||
<h2 className="font-semibold text-slate-900 mb-3">Prompt</h2>
|
|
||||||
|
|
||||||
{/* System Prompt */}
|
|
||||||
<div className="mb-3">
|
|
||||||
<label className="block text-sm text-slate-600 mb-1">System Prompt</label>
|
|
||||||
<textarea
|
|
||||||
value={systemPrompt}
|
|
||||||
onChange={(e) => setSystemPrompt(e.target.value)}
|
|
||||||
rows={3}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm resize-none"
|
|
||||||
placeholder="System Prompt (optional)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* User Prompt */}
|
|
||||||
<div className="mb-3">
|
|
||||||
<label className="block text-sm text-slate-600 mb-1">User Prompt</label>
|
|
||||||
<textarea
|
|
||||||
value={prompt}
|
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
|
||||||
rows={4}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm resize-none"
|
|
||||||
placeholder="z.B.: Erstelle ein Arbeitsblatt zum Thema Bruchrechnung fuer Klasse 6..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Provider Toggles */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="block text-sm text-slate-600 mb-2">Provider</label>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<label className="flex items-center gap-2 text-sm">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={enableOpenAI}
|
|
||||||
onChange={(e) => setEnableOpenAI(e.target.checked)}
|
|
||||||
className="rounded"
|
|
||||||
/>
|
|
||||||
OpenAI
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 text-sm">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={enableClaude}
|
|
||||||
onChange={(e) => setEnableClaude(e.target.checked)}
|
|
||||||
className="rounded"
|
|
||||||
/>
|
|
||||||
Claude
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 text-sm">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={enableTavily}
|
|
||||||
onChange={(e) => setEnableTavily(e.target.checked)}
|
|
||||||
className="rounded"
|
|
||||||
/>
|
|
||||||
Self + Tavily
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 text-sm">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={enableEduSearch}
|
|
||||||
onChange={(e) => setEnableEduSearch(e.target.checked)}
|
|
||||||
className="rounded"
|
|
||||||
/>
|
|
||||||
Self + EduSearch
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Run Button */}
|
|
||||||
<button
|
|
||||||
onClick={runComparison}
|
|
||||||
disabled={isLoading || !prompt.trim()}
|
|
||||||
className="w-full py-3 bg-teal-600 text-white rounded-lg font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<span className="flex items-center justify-center gap-2">
|
|
||||||
<svg className="animate-spin w-5 h-5" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
||||||
</svg>
|
|
||||||
Vergleiche...
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
'Vergleich starten'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Settings Panel */}
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowSettings(!showSettings)}
|
|
||||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-slate-50"
|
|
||||||
>
|
|
||||||
<span className="font-semibold text-slate-900">Parameter</span>
|
|
||||||
<svg
|
|
||||||
className={`w-5 h-5 transition-transform ${showSettings ? 'rotate-180' : ''}`}
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showSettings && (
|
|
||||||
<div className="p-4 border-t border-slate-200 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-slate-600 mb-1">Self-hosted Modell</label>
|
|
||||||
<select
|
|
||||||
value={model}
|
|
||||||
onChange={(e) => setModel(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
|
||||||
>
|
|
||||||
<option value="llama3.2:3b">Llama 3.2 3B</option>
|
|
||||||
<option value="llama3.1:8b">Llama 3.1 8B</option>
|
|
||||||
<option value="mistral:7b">Mistral 7B</option>
|
|
||||||
<option value="qwen2.5:7b">Qwen 2.5 7B</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-slate-600 mb-1">
|
|
||||||
Temperature: {temperature.toFixed(2)}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="2"
|
|
||||||
step="0.1"
|
|
||||||
value={temperature}
|
|
||||||
onChange={(e) => setTemperature(parseFloat(e.target.value))}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-slate-600 mb-1">Max Tokens: {maxTokens}</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="256"
|
|
||||||
max="4096"
|
|
||||||
step="256"
|
|
||||||
value={maxTokens}
|
|
||||||
onChange={(e) => setMaxTokens(parseInt(e.target.value))}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* History Panel */}
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowHistory(!showHistory)}
|
|
||||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-slate-50"
|
|
||||||
>
|
|
||||||
<span className="font-semibold text-slate-900">Verlauf ({history.length})</span>
|
|
||||||
<svg
|
|
||||||
className={`w-5 h-5 transition-transform ${showHistory ? 'rotate-180' : ''}`}
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showHistory && history.length > 0 && (
|
|
||||||
<div className="border-t border-slate-200 max-h-64 overflow-y-auto">
|
|
||||||
{history.map((h) => (
|
|
||||||
<button
|
|
||||||
key={h.comparison_id}
|
|
||||||
onClick={() => {
|
|
||||||
setResult(h)
|
|
||||||
setPrompt(h.prompt)
|
|
||||||
if (h.system_prompt) setSystemPrompt(h.system_prompt)
|
|
||||||
}}
|
|
||||||
className="w-full px-4 py-2 text-left hover:bg-slate-50 border-b border-slate-100 last:border-0"
|
|
||||||
>
|
|
||||||
<div className="text-sm text-slate-700 truncate">{h.prompt}</div>
|
|
||||||
<div className="text-xs text-slate-400">
|
|
||||||
{new Date(h.created_at).toLocaleString('de-DE')}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column: Results */}
|
|
||||||
<div className="lg:col-span-2">
|
|
||||||
{result ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="font-semibold text-slate-900">Ergebnisse</h2>
|
|
||||||
<p className="text-sm text-slate-500">ID: {result.comparison_id}</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-slate-500">
|
|
||||||
{new Date(result.created_at).toLocaleString('de-DE')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 p-3 bg-slate-50 rounded-lg">
|
|
||||||
<p className="text-sm text-slate-700">{result.prompt}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
|
||||||
{result.responses.map((response, idx) => (
|
|
||||||
<ResponseCard key={`${response.provider}-${idx}`} response={response} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 p-12 text-center">
|
|
||||||
<svg
|
|
||||||
className="w-16 h-16 mx-auto text-slate-300 mb-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<h3 className="text-lg font-medium text-slate-700 mb-2">LLM-Vergleich starten</h3>
|
|
||||||
<p className="text-slate-500 max-w-md mx-auto">
|
|
||||||
Geben Sie einen Prompt ein und klicken Sie auf "Vergleich starten", um
|
|
||||||
die Antworten verschiedener LLM-Provider zu vergleichen.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Info Box */}
|
|
||||||
<div className="mt-8 bg-teal-50 border border-teal-200 rounded-xl p-6">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<svg className="w-6 h-6 text-teal-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-teal-900">Qualitaetssicherung</h3>
|
|
||||||
<p className="text-sm text-teal-800 mt-1">
|
|
||||||
Dieses Tool dient zur Qualitaetssicherung der KI-Antworten. Vergleichen Sie verschiedene Provider,
|
|
||||||
um die optimalen Parameter und System Prompts zu finden. Die Ergebnisse werden fuer Audits gespeichert.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -685,7 +685,6 @@ export default function OCRComparePage() {
|
|||||||
databases: ['PostgreSQL (Sessions)'],
|
databases: ['PostgreSQL (Sessions)'],
|
||||||
}}
|
}}
|
||||||
relatedPages={[
|
relatedPages={[
|
||||||
{ name: 'LLM Vergleich', href: '/ai/llm-compare', description: 'KI-Provider vergleichen' },
|
|
||||||
{ name: 'OCR-Labeling', href: '/ai/ocr-labeling', description: 'Ground Truth erstellen' },
|
{ name: 'OCR-Labeling', href: '/ai/ocr-labeling', description: 'Ground Truth erstellen' },
|
||||||
]}
|
]}
|
||||||
collapsible={true}
|
collapsible={true}
|
||||||
|
|||||||
@@ -1430,7 +1430,6 @@ export default function TestQualityPage() {
|
|||||||
databases: ['Qdrant', 'PostgreSQL'],
|
databases: ['Qdrant', 'PostgreSQL'],
|
||||||
}}
|
}}
|
||||||
relatedPages={[
|
relatedPages={[
|
||||||
{ name: 'LLM Vergleich', href: '/ai/llm-compare', description: 'Provider-Vergleich' },
|
|
||||||
{ name: 'GPU Infrastruktur', href: '/ai/gpu', description: 'GPU-Ressourcen verwalten' },
|
{ name: 'GPU Infrastruktur', href: '/ai/gpu', description: 'GPU-Ressourcen verwalten' },
|
||||||
{ name: 'RAG Management', href: '/ai/rag', description: 'Training Data & RAG Pipelines' },
|
{ name: 'RAG Management', href: '/ai/rag', description: 'Training Data & RAG Pipelines' },
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -141,7 +141,6 @@ export default function VoiceMatrixPage() {
|
|||||||
}}
|
}}
|
||||||
relatedPages={[
|
relatedPages={[
|
||||||
{ name: 'Matrix & Jitsi', href: '/communication/matrix', description: 'Kommunikation Monitoring' },
|
{ name: 'Matrix & Jitsi', href: '/communication/matrix', description: 'Kommunikation Monitoring' },
|
||||||
{ name: 'LLM Vergleich', href: '/ai/llm-compare', description: 'KI-Provider vergleichen' },
|
|
||||||
{ name: 'GPU Infrastruktur', href: '/infrastructure/gpu', description: 'GPU fuer Voice-Service' },
|
{ name: 'GPU Infrastruktur', href: '/infrastructure/gpu', description: 'GPU fuer Voice-Service' },
|
||||||
]}
|
]}
|
||||||
collapsible={true}
|
collapsible={true}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export default function DevelopmentPage() {
|
|||||||
}}
|
}}
|
||||||
relatedPages={[
|
relatedPages={[
|
||||||
{ name: 'GPU Infrastruktur', href: '/infrastructure/gpu', description: 'GPU fuer Voice/Game' },
|
{ name: 'GPU Infrastruktur', href: '/infrastructure/gpu', description: 'GPU fuer Voice/Game' },
|
||||||
{ name: 'LLM Vergleich', href: '/ai/llm-compare', description: 'LLM fuer Voice/Game' },
|
|
||||||
]}
|
]}
|
||||||
collapsible={true}
|
collapsible={true}
|
||||||
defaultCollapsed={false}
|
defaultCollapsed={false}
|
||||||
|
|||||||
@@ -149,7 +149,6 @@ const ADMIN_SCREENS: ScreenDefinition[] = [
|
|||||||
{ id: 'admin-obligations', name: 'Pflichten', description: 'NIS2, DSGVO, AI Act', category: 'sdk', icon: '⚡', url: '/sdk/obligations' },
|
{ id: 'admin-obligations', name: 'Pflichten', description: 'NIS2, DSGVO, AI Act', category: 'sdk', icon: '⚡', url: '/sdk/obligations' },
|
||||||
|
|
||||||
// === KI & AUTOMATISIERUNG (Teal #14b8a6) ===
|
// === KI & AUTOMATISIERUNG (Teal #14b8a6) ===
|
||||||
{ id: 'admin-llm-compare', name: 'LLM Vergleich', description: 'KI-Provider Vergleich', category: 'ai', icon: '🤖', url: '/ai/llm-compare' },
|
|
||||||
{ id: 'admin-rag', name: 'Daten & RAG', description: 'Training Data & RAG', category: 'ai', icon: '🗄️', url: '/ai/rag' },
|
{ id: 'admin-rag', name: 'Daten & RAG', description: 'Training Data & RAG', category: 'ai', icon: '🗄️', url: '/ai/rag' },
|
||||||
{ id: 'admin-ocr-labeling', name: 'OCR-Labeling', description: 'Handschrift-Training', category: 'ai', icon: '✍️', url: '/ai/ocr-labeling' },
|
{ id: 'admin-ocr-labeling', name: 'OCR-Labeling', description: 'Handschrift-Training', category: 'ai', icon: '✍️', url: '/ai/ocr-labeling' },
|
||||||
{ id: 'admin-magic-help', name: 'Magic Help', description: 'TrOCR Handschrift-OCR', category: 'ai', icon: '🪄', url: '/ai/magic-help' },
|
{ id: 'admin-magic-help', name: 'Magic Help', description: 'TrOCR Handschrift-OCR', category: 'ai', icon: '🪄', url: '/ai/magic-help' },
|
||||||
@@ -196,7 +195,6 @@ const ADMIN_CONNECTIONS: ConnectionDef[] = [
|
|||||||
{ source: 'admin-dashboard', target: 'admin-backlog', label: 'Go-Live' },
|
{ source: 'admin-dashboard', target: 'admin-backlog', label: 'Go-Live' },
|
||||||
{ source: 'admin-dashboard', target: 'admin-compliance-hub', label: 'Compliance' },
|
{ source: 'admin-dashboard', target: 'admin-compliance-hub', label: 'Compliance' },
|
||||||
{ source: 'admin-onboarding', target: 'admin-consent' },
|
{ source: 'admin-onboarding', target: 'admin-consent' },
|
||||||
{ source: 'admin-onboarding', target: 'admin-llm-compare' },
|
|
||||||
{ source: 'admin-rbac', target: 'admin-consent' },
|
{ source: 'admin-rbac', target: 'admin-consent' },
|
||||||
|
|
||||||
// === DSGVO FLOW ===
|
// === DSGVO FLOW ===
|
||||||
@@ -224,7 +222,6 @@ const ADMIN_CONNECTIONS: ConnectionDef[] = [
|
|||||||
{ source: 'admin-dsms', target: 'admin-compliance-workflow' },
|
{ source: 'admin-dsms', target: 'admin-compliance-workflow' },
|
||||||
|
|
||||||
// === KI & AUTOMATISIERUNG FLOW ===
|
// === KI & AUTOMATISIERUNG FLOW ===
|
||||||
{ source: 'admin-llm-compare', target: 'admin-rag', label: 'Daten' },
|
|
||||||
{ source: 'admin-rag', target: 'admin-quality' },
|
{ source: 'admin-rag', target: 'admin-quality' },
|
||||||
{ source: 'admin-rag', target: 'admin-agents' },
|
{ source: 'admin-rag', target: 'admin-agents' },
|
||||||
{ source: 'admin-ocr-labeling', target: 'admin-magic-help', label: 'Training' },
|
{ source: 'admin-ocr-labeling', target: 'admin-magic-help', label: 'Training' },
|
||||||
|
|||||||
@@ -177,7 +177,6 @@ export default function GPUInfrastructurePage() {
|
|||||||
databases: ['PostgreSQL (Logs)'],
|
databases: ['PostgreSQL (Logs)'],
|
||||||
}}
|
}}
|
||||||
relatedPages={[
|
relatedPages={[
|
||||||
{ name: 'LLM Vergleich', href: '/ai/llm-compare', description: 'KI-Provider testen' },
|
|
||||||
{ name: 'Security', href: '/infrastructure/security', description: 'DevSecOps Dashboard' },
|
{ name: 'Security', href: '/infrastructure/security', description: 'DevSecOps Dashboard' },
|
||||||
{ name: 'Builds', href: '/infrastructure/builds', description: 'CI/CD Pipeline' },
|
{ name: 'Builds', href: '/infrastructure/builds', description: 'CI/CD Pipeline' },
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -335,7 +335,6 @@ export default function RBACPage() {
|
|||||||
}}
|
}}
|
||||||
relatedPages={[
|
relatedPages={[
|
||||||
{ name: 'Audit Trail', href: '/sdk/audit-report', description: 'LLM-Operationen protokollieren' },
|
{ name: 'Audit Trail', href: '/sdk/audit-report', description: 'LLM-Operationen protokollieren' },
|
||||||
{ name: 'LLM Vergleich', href: '/ai/llm-compare', description: 'KI-Provider testen' },
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
export type AIToolId = 'llm-compare' | 'test-quality' | 'gpu' | 'ocr-compare' | 'ocr-labeling' | 'rag-pipeline' | 'magic-help'
|
export type AIToolId = 'test-quality' | 'gpu' | 'ocr-compare' | 'ocr-labeling' | 'rag-pipeline' | 'magic-help'
|
||||||
|
|
||||||
export interface AIToolModule {
|
export interface AIToolModule {
|
||||||
id: AIToolId
|
id: AIToolId
|
||||||
@@ -25,13 +25,6 @@ export interface AIToolModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AI_TOOLS_MODULES: AIToolModule[] = [
|
export const AI_TOOLS_MODULES: AIToolModule[] = [
|
||||||
{
|
|
||||||
id: 'llm-compare',
|
|
||||||
name: 'LLM Vergleich',
|
|
||||||
href: '/ai/llm-compare',
|
|
||||||
description: 'KI-Provider vergleichen',
|
|
||||||
icon: '⚖️',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'test-quality',
|
id: 'test-quality',
|
||||||
name: 'Test Quality (BQAS)',
|
name: 'Test Quality (BQAS)',
|
||||||
@@ -93,13 +86,6 @@ export interface AIToolsSidebarResponsiveProps extends AIToolsSidebarProps {
|
|||||||
// Icons für die Tools
|
// Icons für die Tools
|
||||||
const ToolIcon = ({ id }: { id: string }) => {
|
const ToolIcon = ({ id }: { id: string }) => {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case 'llm-compare':
|
|
||||||
return (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
||||||
d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
case 'test-quality':
|
case 'test-quality':
|
||||||
return (
|
return (
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -228,8 +214,6 @@ export function AIToolsSidebar({
|
|||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<span title="GPU Infrastruktur">🖥️</span>
|
<span title="GPU Infrastruktur">🖥️</span>
|
||||||
<span className="text-slate-400">→</span>
|
<span className="text-slate-400">→</span>
|
||||||
<span title="LLM Vergleich">⚖️</span>
|
|
||||||
<span className="text-slate-400">→</span>
|
|
||||||
<span title="Test Quality">🧪</span>
|
<span title="Test Quality">🧪</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,9 +225,6 @@ export function AIToolsSidebar({
|
|||||||
{/* Quick Info zum aktuellen Tool */}
|
{/* Quick Info zum aktuellen Tool */}
|
||||||
<div className="pt-2 border-t border-slate-200 dark:border-gray-700">
|
<div className="pt-2 border-t border-slate-200 dark:border-gray-700">
|
||||||
<div className="text-xs text-slate-500 dark:text-slate-400 px-1">
|
<div className="text-xs text-slate-500 dark:text-slate-400 px-1">
|
||||||
{currentTool === 'llm-compare' && (
|
|
||||||
<span>Vergleichen Sie LLM-Antworten verschiedener Provider</span>
|
|
||||||
)}
|
|
||||||
{currentTool === 'test-quality' && (
|
{currentTool === 'test-quality' && (
|
||||||
<span>Ueberwachen Sie die Qualitaet der KI-Ausgaben</span>
|
<span>Ueberwachen Sie die Qualitaet der KI-Ausgaben</span>
|
||||||
)}
|
)}
|
||||||
@@ -387,11 +368,6 @@ export function AIToolsSidebarResponsive({
|
|||||||
<span className="text-xs text-slate-500 mt-1">GPU</span>
|
<span className="text-xs text-slate-500 mt-1">GPU</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-slate-400">→</span>
|
<span className="text-slate-400">→</span>
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<span className="text-2xl">⚖️</span>
|
|
||||||
<span className="text-xs text-slate-500 mt-1">LLM</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-slate-400">→</span>
|
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<span className="text-2xl">🧪</span>
|
<span className="text-2xl">🧪</span>
|
||||||
<span className="text-xs text-slate-500 mt-1">BQAS</span>
|
<span className="text-xs text-slate-500 mt-1">BQAS</span>
|
||||||
@@ -405,11 +381,6 @@ export function AIToolsSidebarResponsive({
|
|||||||
{/* Quick Info */}
|
{/* Quick Info */}
|
||||||
<div className="pt-4 border-t border-slate-200 dark:border-gray-700">
|
<div className="pt-4 border-t border-slate-200 dark:border-gray-700">
|
||||||
<div className="text-sm text-slate-600 dark:text-slate-400 p-3 bg-slate-50 dark:bg-gray-800 rounded-xl">
|
<div className="text-sm text-slate-600 dark:text-slate-400 p-3 bg-slate-50 dark:bg-gray-800 rounded-xl">
|
||||||
{currentTool === 'llm-compare' && (
|
|
||||||
<>
|
|
||||||
<strong className="text-slate-700 dark:text-slate-300">Aktuell:</strong> LLM-Antworten verschiedener Provider vergleichen
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{currentTool === 'test-quality' && (
|
{currentTool === 'test-quality' && (
|
||||||
<>
|
<>
|
||||||
<strong className="text-slate-700 dark:text-slate-300">Aktuell:</strong> Qualitaet der KI-Ausgaben ueberwachen
|
<strong className="text-slate-700 dark:text-slate-300">Aktuell:</strong> Qualitaet der KI-Ausgaben ueberwachen
|
||||||
|
|||||||
@@ -234,28 +234,6 @@ export const MODULE_REGISTRY: BackendModule[] = [
|
|||||||
},
|
},
|
||||||
priority: 'high'
|
priority: 'high'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'llm-compare',
|
|
||||||
name: 'LLM Vergleich',
|
|
||||||
description: 'Vergleich verschiedener KI-Modelle und Provider',
|
|
||||||
category: 'ai',
|
|
||||||
backend: {
|
|
||||||
service: 'python-backend',
|
|
||||||
port: 8000,
|
|
||||||
basePath: '/api/llm',
|
|
||||||
endpoints: [
|
|
||||||
{ path: '/providers', method: 'GET', description: 'Verfuegbare Provider' },
|
|
||||||
{ path: '/compare', method: 'POST', description: 'Modelle vergleichen' },
|
|
||||||
{ path: '/benchmark', method: 'POST', description: 'Benchmark ausfuehren' },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
frontend: {
|
|
||||||
adminV2Page: '/ai/llm-compare',
|
|
||||||
oldAdminPage: '/admin/llm-compare',
|
|
||||||
status: 'connected'
|
|
||||||
},
|
|
||||||
priority: 'medium'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'magic-help',
|
id: 'magic-help',
|
||||||
name: 'Magic Help (TrOCR)',
|
name: 'Magic Help (TrOCR)',
|
||||||
|
|||||||
@@ -124,16 +124,6 @@ export const navigation: NavCategory[] = [
|
|||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// KI-Werkzeuge: Standalone-Tools fuer Entwicklung & QA
|
// KI-Werkzeuge: Standalone-Tools fuer Entwicklung & QA
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
{
|
|
||||||
id: 'llm-compare',
|
|
||||||
name: 'LLM Vergleich',
|
|
||||||
href: '/ai/llm-compare',
|
|
||||||
description: 'KI-Provider Vergleich',
|
|
||||||
purpose: 'Vergleichen Sie verschiedene LLM-Anbieter (Ollama, OpenAI, Anthropic) hinsichtlich Qualitaet, Geschwindigkeit und Kosten. Standalone-Werkzeug fuer Modell-Evaluation.',
|
|
||||||
audience: ['Entwickler', 'Data Scientists'],
|
|
||||||
oldAdminPath: '/admin/llm-compare',
|
|
||||||
subgroup: 'KI-Werkzeuge',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'ocr-compare',
|
id: 'ocr-compare',
|
||||||
name: 'OCR Vergleich',
|
name: 'OCR Vergleich',
|
||||||
|
|||||||
@@ -119,13 +119,6 @@ export const AI_PIPELINE_MODULES: AIModuleLink[] = [
|
|||||||
* Kein direkter Datenfluss zur Pipeline.
|
* Kein direkter Datenfluss zur Pipeline.
|
||||||
*/
|
*/
|
||||||
export const AI_TOOLS_MODULES: AIModuleLink[] = [
|
export const AI_TOOLS_MODULES: AIModuleLink[] = [
|
||||||
{
|
|
||||||
id: 'llm-compare',
|
|
||||||
name: 'LLM Vergleich',
|
|
||||||
href: '/ai/llm-compare',
|
|
||||||
description: 'KI-Provider Vergleich & Evaluation',
|
|
||||||
icon: '⚖️',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'test-quality',
|
id: 'test-quality',
|
||||||
name: 'Test Quality (BQAS)',
|
name: 'Test Quality (BQAS)',
|
||||||
@@ -212,27 +205,7 @@ export const AI_MODULE_RELATIONS: Record<string, AIModuleLink[]> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
// KI-Werkzeuge Relations (Standalone-Tools)
|
// KI-Werkzeuge Relations (Standalone-Tools)
|
||||||
'llm-compare': [
|
|
||||||
{
|
|
||||||
id: 'test-quality',
|
|
||||||
name: 'Test Quality (BQAS)',
|
|
||||||
href: '/ai/test-quality',
|
|
||||||
description: 'Golden Suite & Synthetic Tests',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'agents',
|
|
||||||
name: 'Agent Management',
|
|
||||||
href: '/ai/agents',
|
|
||||||
description: 'Multi-Agent System',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'test-quality': [
|
'test-quality': [
|
||||||
{
|
|
||||||
id: 'llm-compare',
|
|
||||||
name: 'LLM Vergleich',
|
|
||||||
href: '/ai/llm-compare',
|
|
||||||
description: 'KI-Provider vergleichen',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'klausur-korrektur',
|
id: 'klausur-korrektur',
|
||||||
name: 'Klausur-Korrektur',
|
name: 'Klausur-Korrektur',
|
||||||
|
|||||||
@@ -1,761 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LLM Comparison Tool
|
|
||||||
*
|
|
||||||
* Vergleicht Antworten von verschiedenen LLM-Providern:
|
|
||||||
* - OpenAI/ChatGPT
|
|
||||||
* - Claude
|
|
||||||
* - Self-hosted + Tavily
|
|
||||||
* - Self-hosted + EduSearch
|
|
||||||
*
|
|
||||||
* Ermoeglicht Parameter-Tuning und System Prompt Management
|
|
||||||
*/
|
|
||||||
|
|
||||||
import AdminLayout from '@/components/admin/AdminLayout'
|
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
|
|
||||||
interface LLMResponse {
|
|
||||||
provider: string
|
|
||||||
model: string
|
|
||||||
response: string
|
|
||||||
latency_ms: number
|
|
||||||
tokens_used?: number
|
|
||||||
search_results?: Array<{
|
|
||||||
title: string
|
|
||||||
url: string
|
|
||||||
content: string
|
|
||||||
score?: number
|
|
||||||
}>
|
|
||||||
error?: string
|
|
||||||
timestamp: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ComparisonResult {
|
|
||||||
comparison_id: string
|
|
||||||
prompt: string
|
|
||||||
system_prompt?: string
|
|
||||||
responses: LLMResponse[]
|
|
||||||
created_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SystemPrompt {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
prompt: string
|
|
||||||
created_at: string
|
|
||||||
updated_at?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerColors: Record<string, { bg: string; border: string; text: string }> = {
|
|
||||||
openai: { bg: 'bg-emerald-50', border: 'border-emerald-300', text: 'text-emerald-700' },
|
|
||||||
claude: { bg: 'bg-orange-50', border: 'border-orange-300', text: 'text-orange-700' },
|
|
||||||
selfhosted_tavily: { bg: 'bg-blue-50', border: 'border-blue-300', text: 'text-blue-700' },
|
|
||||||
selfhosted_edusearch: { bg: 'bg-purple-50', border: 'border-purple-300', text: 'text-purple-700' },
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerLabels: Record<string, string> = {
|
|
||||||
openai: 'OpenAI GPT-4o-mini',
|
|
||||||
claude: 'Claude 3.5 Sonnet',
|
|
||||||
selfhosted_tavily: 'Self-hosted + Tavily',
|
|
||||||
selfhosted_edusearch: 'Self-hosted + EduSearch',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LLMComparePage() {
|
|
||||||
// State
|
|
||||||
const [prompt, setPrompt] = useState('')
|
|
||||||
const [systemPrompt, setSystemPrompt] = useState('')
|
|
||||||
const [selectedSystemPromptId, setSelectedSystemPromptId] = useState('default')
|
|
||||||
const [systemPrompts, setSystemPrompts] = useState<SystemPrompt[]>([])
|
|
||||||
|
|
||||||
// Provider toggles
|
|
||||||
const [enableOpenAI, setEnableOpenAI] = useState(true)
|
|
||||||
const [enableClaude, setEnableClaude] = useState(true)
|
|
||||||
const [enableTavily, setEnableTavily] = useState(true)
|
|
||||||
const [enableEduSearch, setEnableEduSearch] = useState(true)
|
|
||||||
|
|
||||||
// Parameters
|
|
||||||
const [model, setModel] = useState('llama3.2:3b')
|
|
||||||
const [temperature, setTemperature] = useState(0.7)
|
|
||||||
const [topP, setTopP] = useState(0.9)
|
|
||||||
const [maxTokens, setMaxTokens] = useState(2048)
|
|
||||||
const [searchResultsCount, setSearchResultsCount] = useState(5)
|
|
||||||
|
|
||||||
// EduSearch Filters
|
|
||||||
const [eduSearchLanguage, setEduSearchLanguage] = useState('de')
|
|
||||||
const [eduSearchDocType, setEduSearchDocType] = useState('')
|
|
||||||
const [eduSearchSchoolLevel, setEduSearchSchoolLevel] = useState('')
|
|
||||||
|
|
||||||
// Results
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [result, setResult] = useState<ComparisonResult | null>(null)
|
|
||||||
const [history, setHistory] = useState<ComparisonResult[]>([])
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
// UI State
|
|
||||||
const [showSettings, setShowSettings] = useState(false)
|
|
||||||
const [showHistory, setShowHistory] = useState(false)
|
|
||||||
const [editingPrompt, setEditingPrompt] = useState<SystemPrompt | null>(null)
|
|
||||||
|
|
||||||
// API Base URL
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_LLM_GATEWAY_URL || 'http://localhost:8082'
|
|
||||||
const API_KEY = process.env.NEXT_PUBLIC_LLM_API_KEY || 'dev-key'
|
|
||||||
|
|
||||||
// Load system prompts
|
|
||||||
useEffect(() => {
|
|
||||||
loadSystemPrompts()
|
|
||||||
loadHistory()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Update system prompt when selection changes
|
|
||||||
useEffect(() => {
|
|
||||||
const selected = systemPrompts.find((p) => p.id === selectedSystemPromptId)
|
|
||||||
if (selected) {
|
|
||||||
setSystemPrompt(selected.prompt)
|
|
||||||
}
|
|
||||||
}, [selectedSystemPromptId, systemPrompts])
|
|
||||||
|
|
||||||
const loadSystemPrompts = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_URL}/v1/comparison/prompts`, {
|
|
||||||
headers: { Authorization: `Bearer ${API_KEY}` },
|
|
||||||
})
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json()
|
|
||||||
setSystemPrompts(data.prompts || [])
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load system prompts:', e)
|
|
||||||
// Fallback prompts
|
|
||||||
setSystemPrompts([
|
|
||||||
{
|
|
||||||
id: 'default',
|
|
||||||
name: 'Standard Lehrer-Assistent',
|
|
||||||
prompt: 'Du bist ein hilfreicher Assistent fuer Lehrkraefte in Deutschland.',
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadHistory = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_URL}/v1/comparison/history?limit=20`, {
|
|
||||||
headers: { Authorization: `Bearer ${API_KEY}` },
|
|
||||||
})
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json()
|
|
||||||
setHistory(data.comparisons || [])
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load history:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const runComparison = async () => {
|
|
||||||
if (!prompt.trim()) {
|
|
||||||
setError('Bitte geben Sie einen Prompt ein')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
setError(null)
|
|
||||||
setResult(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const filters: Record<string, unknown> = {}
|
|
||||||
if (eduSearchLanguage) filters.language = [eduSearchLanguage]
|
|
||||||
if (eduSearchDocType) filters.doc_type = [eduSearchDocType]
|
|
||||||
if (eduSearchSchoolLevel) filters.school_level = [eduSearchSchoolLevel]
|
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}/v1/comparison/run`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${API_KEY}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt,
|
|
||||||
system_prompt: systemPrompt || undefined,
|
|
||||||
enable_openai: enableOpenAI,
|
|
||||||
enable_claude: enableClaude,
|
|
||||||
enable_selfhosted_tavily: enableTavily,
|
|
||||||
enable_selfhosted_edusearch: enableEduSearch,
|
|
||||||
selfhosted_model: model,
|
|
||||||
temperature,
|
|
||||||
top_p: topP,
|
|
||||||
max_tokens: maxTokens,
|
|
||||||
search_results_count: searchResultsCount,
|
|
||||||
edu_search_filters: Object.keys(filters).length > 0 ? filters : undefined,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`API Error: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
setResult(data)
|
|
||||||
|
|
||||||
// Refresh history
|
|
||||||
loadHistory()
|
|
||||||
} catch (e) {
|
|
||||||
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveSystemPrompt = async () => {
|
|
||||||
if (!editingPrompt) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const isNew = !editingPrompt.id || editingPrompt.id.startsWith('new-')
|
|
||||||
const url = isNew
|
|
||||||
? `${API_URL}/v1/comparison/prompts`
|
|
||||||
: `${API_URL}/v1/comparison/prompts/${editingPrompt.id}`
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`${url}?name=${encodeURIComponent(editingPrompt.name)}&prompt=${encodeURIComponent(editingPrompt.prompt)}`,
|
|
||||||
{
|
|
||||||
method: isNew ? 'POST' : 'PUT',
|
|
||||||
headers: { Authorization: `Bearer ${API_KEY}` },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setEditingPrompt(null)
|
|
||||||
loadSystemPrompts()
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to save prompt:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ResponseCard = ({ response }: { response: LLMResponse }) => {
|
|
||||||
const colors = providerColors[response.provider] || {
|
|
||||||
bg: 'bg-slate-50',
|
|
||||||
border: 'border-slate-300',
|
|
||||||
text: 'text-slate-700',
|
|
||||||
}
|
|
||||||
const label = providerLabels[response.provider] || response.provider
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`rounded-xl border-2 ${colors.border} ${colors.bg} overflow-hidden`}>
|
|
||||||
{/* Header */}
|
|
||||||
<div className={`px-4 py-3 border-b ${colors.border} flex items-center justify-between`}>
|
|
||||||
<div>
|
|
||||||
<h3 className={`font-semibold ${colors.text}`}>{label}</h3>
|
|
||||||
<p className="text-xs text-slate-500">{response.model}</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-right text-xs text-slate-500">
|
|
||||||
<div>{response.latency_ms}ms</div>
|
|
||||||
{response.tokens_used && <div>{response.tokens_used} tokens</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="p-4">
|
|
||||||
{response.error ? (
|
|
||||||
<div className="text-red-600 text-sm">
|
|
||||||
<strong>Fehler:</strong> {response.error}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="prose prose-sm max-w-none">
|
|
||||||
<pre className="whitespace-pre-wrap text-sm text-slate-700 font-sans">
|
|
||||||
{response.response}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search Results (for self-hosted) */}
|
|
||||||
{response.search_results && response.search_results.length > 0 && (
|
|
||||||
<div className="px-4 pb-4">
|
|
||||||
<details className="text-xs">
|
|
||||||
<summary className="cursor-pointer text-slate-500 hover:text-slate-700">
|
|
||||||
{response.search_results.length} Suchergebnisse anzeigen
|
|
||||||
</summary>
|
|
||||||
<ul className="mt-2 space-y-2">
|
|
||||||
{response.search_results.map((sr, idx) => (
|
|
||||||
<li key={idx} className="bg-white rounded p-2 border border-slate-200">
|
|
||||||
<a
|
|
||||||
href={sr.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-blue-600 hover:underline font-medium"
|
|
||||||
>
|
|
||||||
{sr.title || 'Untitled'}
|
|
||||||
</a>
|
|
||||||
<p className="text-slate-500 truncate">{sr.content}</p>
|
|
||||||
{sr.score !== undefined && (
|
|
||||||
<span className="text-slate-400">Score: {sr.score.toFixed(2)}</span>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AdminLayout title="LLM Vergleich" description="Qualitaetssicherung durch Provider-Vergleich">
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
{/* Left Column: Input & Settings */}
|
|
||||||
<div className="lg:col-span-1 space-y-4">
|
|
||||||
{/* Prompt Input */}
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
|
||||||
<h2 className="font-semibold text-slate-900 mb-3">Prompt</h2>
|
|
||||||
|
|
||||||
{/* System Prompt Selector */}
|
|
||||||
<div className="mb-3">
|
|
||||||
<label className="block text-sm text-slate-600 mb-1">System Prompt</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<select
|
|
||||||
value={selectedSystemPromptId}
|
|
||||||
onChange={(e) => setSelectedSystemPromptId(e.target.value)}
|
|
||||||
className="flex-1 px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
|
||||||
>
|
|
||||||
{systemPrompts.map((p) => (
|
|
||||||
<option key={p.id} value={p.id}>
|
|
||||||
{p.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
setEditingPrompt({
|
|
||||||
id: `new-${Date.now()}`,
|
|
||||||
name: '',
|
|
||||||
prompt: '',
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="px-3 py-2 text-sm bg-slate-100 rounded-lg hover:bg-slate-200"
|
|
||||||
title="Neuer System Prompt"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* System Prompt Preview */}
|
|
||||||
<div className="mb-3">
|
|
||||||
<textarea
|
|
||||||
value={systemPrompt}
|
|
||||||
onChange={(e) => setSystemPrompt(e.target.value)}
|
|
||||||
rows={3}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm resize-none"
|
|
||||||
placeholder="System Prompt (optional)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* User Prompt */}
|
|
||||||
<div className="mb-3">
|
|
||||||
<label className="block text-sm text-slate-600 mb-1">User Prompt (Lehrer-Frage)</label>
|
|
||||||
<textarea
|
|
||||||
value={prompt}
|
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
|
||||||
rows={4}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm resize-none"
|
|
||||||
placeholder="z.B.: Erstelle ein Arbeitsblatt zum Thema Bruchrechnung fuer Klasse 6..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Provider Toggles */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="block text-sm text-slate-600 mb-2">Provider</label>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<label className="flex items-center gap-2 text-sm">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={enableOpenAI}
|
|
||||||
onChange={(e) => setEnableOpenAI(e.target.checked)}
|
|
||||||
className="rounded"
|
|
||||||
/>
|
|
||||||
OpenAI
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 text-sm">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={enableClaude}
|
|
||||||
onChange={(e) => setEnableClaude(e.target.checked)}
|
|
||||||
className="rounded"
|
|
||||||
/>
|
|
||||||
Claude
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 text-sm">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={enableTavily}
|
|
||||||
onChange={(e) => setEnableTavily(e.target.checked)}
|
|
||||||
className="rounded"
|
|
||||||
/>
|
|
||||||
Self + Tavily
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 text-sm">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={enableEduSearch}
|
|
||||||
onChange={(e) => setEnableEduSearch(e.target.checked)}
|
|
||||||
className="rounded"
|
|
||||||
/>
|
|
||||||
Self + EduSearch
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Run Button */}
|
|
||||||
<button
|
|
||||||
onClick={runComparison}
|
|
||||||
disabled={isLoading || !prompt.trim()}
|
|
||||||
className="w-full py-3 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<span className="flex items-center justify-center gap-2">
|
|
||||||
<svg className="animate-spin w-5 h-5" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle
|
|
||||||
className="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="4"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Vergleiche...
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
'Vergleich starten'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Settings Panel */}
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowSettings(!showSettings)}
|
|
||||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-slate-50"
|
|
||||||
>
|
|
||||||
<span className="font-semibold text-slate-900">Parameter-Einstellungen</span>
|
|
||||||
<svg
|
|
||||||
className={`w-5 h-5 transition-transform ${showSettings ? 'rotate-180' : ''}`}
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showSettings && (
|
|
||||||
<div className="p-4 border-t border-slate-200 space-y-4">
|
|
||||||
{/* Model */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-slate-600 mb-1">Self-hosted Modell</label>
|
|
||||||
<select
|
|
||||||
value={model}
|
|
||||||
onChange={(e) => setModel(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
|
||||||
>
|
|
||||||
<option value="llama3.2:3b">Llama 3.2 3B</option>
|
|
||||||
<option value="llama3.1:8b">Llama 3.1 8B</option>
|
|
||||||
<option value="mistral:7b">Mistral 7B</option>
|
|
||||||
<option value="qwen2.5:7b">Qwen 2.5 7B</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Temperature */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-slate-600 mb-1">
|
|
||||||
Temperature: {temperature.toFixed(2)}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="2"
|
|
||||||
step="0.1"
|
|
||||||
value={temperature}
|
|
||||||
onChange={(e) => setTemperature(parseFloat(e.target.value))}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Top-P */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-slate-600 mb-1">Top-P: {topP.toFixed(2)}</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
step="0.05"
|
|
||||||
value={topP}
|
|
||||||
onChange={(e) => setTopP(parseFloat(e.target.value))}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Max Tokens */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-slate-600 mb-1">Max Tokens: {maxTokens}</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="256"
|
|
||||||
max="4096"
|
|
||||||
step="256"
|
|
||||||
value={maxTokens}
|
|
||||||
onChange={(e) => setMaxTokens(parseInt(e.target.value))}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search Results Count */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-slate-600 mb-1">
|
|
||||||
Suchergebnisse: {searchResultsCount}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="1"
|
|
||||||
max="20"
|
|
||||||
step="1"
|
|
||||||
value={searchResultsCount}
|
|
||||||
onChange={(e) => setSearchResultsCount(parseInt(e.target.value))}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* EduSearch Filters */}
|
|
||||||
<div className="pt-3 border-t border-slate-200">
|
|
||||||
<h4 className="text-sm font-medium text-slate-700 mb-2">EduSearch Filter</h4>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<select
|
|
||||||
value={eduSearchLanguage}
|
|
||||||
onChange={(e) => setEduSearchLanguage(e.target.value)}
|
|
||||||
className="w-full px-2 py-1 border border-slate-300 rounded text-sm"
|
|
||||||
>
|
|
||||||
<option value="de">Deutsch</option>
|
|
||||||
<option value="en">Englisch</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select
|
|
||||||
value={eduSearchDocType}
|
|
||||||
onChange={(e) => setEduSearchDocType(e.target.value)}
|
|
||||||
className="w-full px-2 py-1 border border-slate-300 rounded text-sm"
|
|
||||||
>
|
|
||||||
<option value="">Alle Dokumenttypen</option>
|
|
||||||
<option value="Lehrplan">Lehrplan</option>
|
|
||||||
<option value="Arbeitsblatt">Arbeitsblatt</option>
|
|
||||||
<option value="Unterrichtsentwurf">Unterrichtsentwurf</option>
|
|
||||||
<option value="Pruefung_Abitur">Pruefung/Abitur</option>
|
|
||||||
<option value="Studie_Bericht">Studie/Bericht</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select
|
|
||||||
value={eduSearchSchoolLevel}
|
|
||||||
onChange={(e) => setEduSearchSchoolLevel(e.target.value)}
|
|
||||||
className="w-full px-2 py-1 border border-slate-300 rounded text-sm"
|
|
||||||
>
|
|
||||||
<option value="">Alle Schulstufen</option>
|
|
||||||
<option value="Grundschule">Grundschule</option>
|
|
||||||
<option value="Sek_I">Sekundarstufe I</option>
|
|
||||||
<option value="Gymnasium">Gymnasium</option>
|
|
||||||
<option value="Berufsschule">Berufsschule</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* History Panel */}
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowHistory(!showHistory)}
|
|
||||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-slate-50"
|
|
||||||
>
|
|
||||||
<span className="font-semibold text-slate-900">
|
|
||||||
Verlauf ({history.length})
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
className={`w-5 h-5 transition-transform ${showHistory ? 'rotate-180' : ''}`}
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showHistory && history.length > 0 && (
|
|
||||||
<div className="border-t border-slate-200 max-h-64 overflow-y-auto">
|
|
||||||
{history.map((h) => (
|
|
||||||
<button
|
|
||||||
key={h.comparison_id}
|
|
||||||
onClick={() => {
|
|
||||||
setResult(h)
|
|
||||||
setPrompt(h.prompt)
|
|
||||||
if (h.system_prompt) setSystemPrompt(h.system_prompt)
|
|
||||||
}}
|
|
||||||
className="w-full px-4 py-2 text-left hover:bg-slate-50 border-b border-slate-100 last:border-0"
|
|
||||||
>
|
|
||||||
<div className="text-sm text-slate-700 truncate">{h.prompt}</div>
|
|
||||||
<div className="text-xs text-slate-400">
|
|
||||||
{new Date(h.created_at).toLocaleString('de-DE')}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column: Results */}
|
|
||||||
<div className="lg:col-span-2">
|
|
||||||
{result ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Results Header */}
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="font-semibold text-slate-900">Ergebnisse</h2>
|
|
||||||
<p className="text-sm text-slate-500">ID: {result.comparison_id}</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-slate-500">
|
|
||||||
{new Date(result.created_at).toLocaleString('de-DE')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 p-3 bg-slate-50 rounded-lg">
|
|
||||||
<p className="text-sm text-slate-700">{result.prompt}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Response Grid */}
|
|
||||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
|
||||||
{result.responses.map((response, idx) => (
|
|
||||||
<ResponseCard key={`${response.provider}-${idx}`} response={response} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 p-12 text-center">
|
|
||||||
<svg
|
|
||||||
className="w-16 h-16 mx-auto text-slate-300 mb-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<h3 className="text-lg font-medium text-slate-700 mb-2">
|
|
||||||
LLM-Vergleich starten
|
|
||||||
</h3>
|
|
||||||
<p className="text-slate-500 max-w-md mx-auto">
|
|
||||||
Geben Sie einen Prompt ein und klicken Sie auf "Vergleich starten", um
|
|
||||||
die Antworten verschiedener LLM-Provider zu vergleichen.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Edit System Prompt Modal */}
|
|
||||||
{editingPrompt && (
|
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white rounded-xl p-6 w-full max-w-lg mx-4">
|
|
||||||
<h3 className="text-lg font-semibold mb-4">
|
|
||||||
{editingPrompt.id.startsWith('new-') ? 'Neuer' : 'Bearbeite'} System Prompt
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-slate-600 mb-1">Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={editingPrompt.name}
|
|
||||||
onChange={(e) => setEditingPrompt({ ...editingPrompt, name: e.target.value })}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg"
|
|
||||||
placeholder="z.B. Lehrplan-Experte"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-slate-600 mb-1">Prompt</label>
|
|
||||||
<textarea
|
|
||||||
value={editingPrompt.prompt}
|
|
||||||
onChange={(e) => setEditingPrompt({ ...editingPrompt, prompt: e.target.value })}
|
|
||||||
rows={8}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg resize-none"
|
|
||||||
placeholder="System Prompt Text..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 mt-6">
|
|
||||||
<button
|
|
||||||
onClick={() => setEditingPrompt(null)}
|
|
||||||
className="px-4 py-2 text-slate-600 hover:text-slate-900"
|
|
||||||
>
|
|
||||||
Abbrechen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={saveSystemPrompt}
|
|
||||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700"
|
|
||||||
>
|
|
||||||
Speichern
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Info Box */}
|
|
||||||
<div className="mt-8 bg-blue-50 border border-blue-200 rounded-xl p-6">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<svg
|
|
||||||
className="w-6 h-6 text-blue-600 flex-shrink-0 mt-0.5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-blue-900">Qualitaetssicherung</h3>
|
|
||||||
<p className="text-sm text-blue-800 mt-1">
|
|
||||||
Dieses Tool dient zur Qualitaetssicherung der KI-Antworten. Vergleichen Sie
|
|
||||||
verschiedene Provider, um die optimalen Parameter und System Prompts fuer
|
|
||||||
BreakPilot zu finden. Die Ergebnisse koennen fuer Audits und Dokumentation
|
|
||||||
gespeichert werden.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AdminLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,342 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useState } from 'react'
|
|
||||||
import AdminLayout from '@/components/admin/AdminLayout'
|
|
||||||
import {
|
|
||||||
WizardStepper,
|
|
||||||
WizardNavigation,
|
|
||||||
EducationCard,
|
|
||||||
ArchitectureContext,
|
|
||||||
TestRunner,
|
|
||||||
TestSummary,
|
|
||||||
type WizardStep,
|
|
||||||
type TestCategoryResult,
|
|
||||||
type FullTestResults,
|
|
||||||
type EducationContent,
|
|
||||||
type ArchitectureContextType,
|
|
||||||
} from '@/components/wizard'
|
|
||||||
|
|
||||||
// ==============================================
|
|
||||||
// Constants
|
|
||||||
// ==============================================
|
|
||||||
|
|
||||||
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
|
|
||||||
|
|
||||||
const STEPS: WizardStep[] = [
|
|
||||||
{ id: 'welcome', name: 'Willkommen', icon: '👋', status: 'pending' },
|
|
||||||
{ id: 'gateway', name: 'LLM Gateway', icon: '🌐', status: 'pending', category: 'gateway' },
|
|
||||||
{ id: 'providers', name: 'Provider', icon: '🤖', status: 'pending', category: 'providers' },
|
|
||||||
{ id: 'summary', name: 'Zusammenfassung', icon: '📊', status: 'pending' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const EDUCATION_CONTENT: Record<string, EducationContent> = {
|
|
||||||
'welcome': {
|
|
||||||
title: 'Willkommen zum LLM Compare Wizard',
|
|
||||||
content: [
|
|
||||||
'Large Language Models (LLMs) sind das Herzstueck moderner KI.',
|
|
||||||
'',
|
|
||||||
'BreakPilot unterstuetzt mehrere Provider:',
|
|
||||||
'• OpenAI: GPT-4o, GPT-4, GPT-3.5-turbo',
|
|
||||||
'• Anthropic: Claude 3.5 Sonnet, Claude 3 Opus',
|
|
||||||
'• Lokale Modelle: Ollama (Llama 3, Mistral)',
|
|
||||||
'',
|
|
||||||
'Das LLM Gateway abstrahiert die Provider:',
|
|
||||||
'• Einheitliche API fuer alle Modelle',
|
|
||||||
'• Automatisches Fallback bei Ausfaellen',
|
|
||||||
'• Token-Counting und Kosten-Tracking',
|
|
||||||
'• Playbooks fuer vordefinierte Prompts',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'gateway': {
|
|
||||||
title: 'LLM Gateway - Zentrale Schnittstelle',
|
|
||||||
content: [
|
|
||||||
'Das Gateway routet Anfragen an die passenden Provider.',
|
|
||||||
'',
|
|
||||||
'Features:',
|
|
||||||
'• /llm/v1/chat - Unified Chat API',
|
|
||||||
'• /llm/playbooks - Vordefinierte Prompts',
|
|
||||||
'• /llm/health - Provider-Status',
|
|
||||||
'',
|
|
||||||
'Vorteile:',
|
|
||||||
'• Provider-Wechsel ohne Code-Aenderung',
|
|
||||||
'• Caching fuer haeufige Anfragen',
|
|
||||||
'• Rate-Limiting pro Benutzer',
|
|
||||||
'• Audit-Log aller Anfragen',
|
|
||||||
'',
|
|
||||||
'Aktivierung: LLM_GATEWAY_ENABLED=true',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'providers': {
|
|
||||||
title: 'LLM Provider - Modell-Auswahl',
|
|
||||||
content: [
|
|
||||||
'Verschiedene Provider fuer verschiedene Anforderungen.',
|
|
||||||
'',
|
|
||||||
'OpenAI:',
|
|
||||||
'• Beste allgemeine Leistung',
|
|
||||||
'• Hoechste Geschwindigkeit',
|
|
||||||
'• ~$0.01-0.03 pro 1K Tokens',
|
|
||||||
'',
|
|
||||||
'Anthropic (Claude):',
|
|
||||||
'• Beste lange Kontexte (200K)',
|
|
||||||
'• Sehr sicher und aligned',
|
|
||||||
'• ~$0.01-0.015 pro 1K Tokens',
|
|
||||||
'',
|
|
||||||
'Ollama (Lokal):',
|
|
||||||
'• Kostenlos nach Hardware',
|
|
||||||
'• Volle Datenkontrolle',
|
|
||||||
'• Langsamer ohne GPU',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'summary': {
|
|
||||||
title: 'Test-Zusammenfassung',
|
|
||||||
content: [
|
|
||||||
'Hier sehen Sie eine Uebersicht aller durchgefuehrten Tests:',
|
|
||||||
'• Gateway-Verfuegbarkeit',
|
|
||||||
'• Provider-Konnektivitaet',
|
|
||||||
'• Lokale LLM-Optionen',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const ARCHITECTURE_CONTEXTS: Record<string, ArchitectureContextType> = {
|
|
||||||
'gateway': {
|
|
||||||
layer: 'api',
|
|
||||||
services: ['backend'],
|
|
||||||
dependencies: ['OpenAI API', 'Anthropic API', 'Ollama'],
|
|
||||||
dataFlow: ['Browser', 'FastAPI', 'LLM Gateway', 'Provider API'],
|
|
||||||
},
|
|
||||||
'providers': {
|
|
||||||
layer: 'service',
|
|
||||||
services: ['backend'],
|
|
||||||
dependencies: ['API Keys', 'Rate Limits', 'Token Counter'],
|
|
||||||
dataFlow: ['LLM Gateway', 'Provider Selection', 'API Call', 'Response'],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==============================================
|
|
||||||
// Main Component
|
|
||||||
// ==============================================
|
|
||||||
|
|
||||||
export default function LLMCompareWizardPage() {
|
|
||||||
const [currentStep, setCurrentStep] = useState(0)
|
|
||||||
const [steps, setSteps] = useState<WizardStep[]>(STEPS)
|
|
||||||
const [categoryResults, setCategoryResults] = useState<Record<string, TestCategoryResult>>({})
|
|
||||||
const [fullResults, setFullResults] = useState<FullTestResults | null>(null)
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const currentStepData = steps[currentStep]
|
|
||||||
const isTestStep = currentStepData?.category !== undefined
|
|
||||||
const isWelcome = currentStepData?.id === 'welcome'
|
|
||||||
const isSummary = currentStepData?.id === 'summary'
|
|
||||||
|
|
||||||
const runCategoryTest = async (category: string) => {
|
|
||||||
setIsLoading(true)
|
|
||||||
setError(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${BACKEND_URL}/api/admin/llm-tests/${category}`, {
|
|
||||||
method: 'POST',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: TestCategoryResult = await response.json()
|
|
||||||
setCategoryResults((prev) => ({ ...prev, [category]: result }))
|
|
||||||
|
|
||||||
setSteps((prev) =>
|
|
||||||
prev.map((step) =>
|
|
||||||
step.category === category
|
|
||||||
? { ...step, status: result.failed === 0 ? 'completed' : 'failed' }
|
|
||||||
: step
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const runAllTests = async () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
setError(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${BACKEND_URL}/api/admin/llm-tests/run-all`, {
|
|
||||||
method: 'POST',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const results: FullTestResults = await response.json()
|
|
||||||
setFullResults(results)
|
|
||||||
|
|
||||||
setSteps((prev) =>
|
|
||||||
prev.map((step) => {
|
|
||||||
if (step.category) {
|
|
||||||
const catResult = results.categories.find((c) => c.category === step.category)
|
|
||||||
if (catResult) {
|
|
||||||
return { ...step, status: catResult.failed === 0 ? 'completed' : 'failed' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return step
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const newCategoryResults: Record<string, TestCategoryResult> = {}
|
|
||||||
results.categories.forEach((cat) => {
|
|
||||||
newCategoryResults[cat.category] = cat
|
|
||||||
})
|
|
||||||
setCategoryResults(newCategoryResults)
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const goToNext = () => {
|
|
||||||
if (currentStep < steps.length - 1) {
|
|
||||||
setSteps((prev) =>
|
|
||||||
prev.map((step, idx) =>
|
|
||||||
idx === currentStep && step.status === 'pending'
|
|
||||||
? { ...step, status: 'completed' }
|
|
||||||
: step
|
|
||||||
)
|
|
||||||
)
|
|
||||||
setCurrentStep((prev) => prev + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const goToPrev = () => {
|
|
||||||
if (currentStep > 0) {
|
|
||||||
setCurrentStep((prev) => prev - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleStepClick = (index: number) => {
|
|
||||||
if (index <= currentStep || steps[index - 1]?.status !== 'pending') {
|
|
||||||
setCurrentStep(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AdminLayout
|
|
||||||
title="LLM Compare Wizard"
|
|
||||||
description="Interaktives Lernen und Testen der LLM Provider"
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="bg-white rounded-lg shadow p-4 mb-6 flex items-center justify-between">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="text-3xl mr-3">🤖</span>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-bold text-gray-800">LLM Compare Wizard</h2>
|
|
||||||
<p className="text-sm text-gray-600">OpenAI, Anthropic & Ollama</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="/admin/llm-compare" className="text-blue-600 hover:text-blue-800 text-sm">
|
|
||||||
← Zurueck zu LLM Compare
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stepper */}
|
|
||||||
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
|
||||||
<WizardStepper steps={steps} currentStep={currentStep} onStepClick={handleStepClick} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
|
||||||
<div className="flex items-center mb-6">
|
|
||||||
<span className="text-3xl mr-3">{currentStepData?.icon}</span>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-bold text-gray-800">
|
|
||||||
Schritt {currentStep + 1}: {currentStepData?.name}
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-500 text-sm">
|
|
||||||
{currentStep + 1} von {steps.length}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EducationCard content={EDUCATION_CONTENT[currentStepData?.id || '']} />
|
|
||||||
|
|
||||||
{isTestStep && currentStepData?.category && ARCHITECTURE_CONTEXTS[currentStepData.category] && (
|
|
||||||
<ArchitectureContext
|
|
||||||
context={ARCHITECTURE_CONTEXTS[currentStepData.category]}
|
|
||||||
currentStep={currentStepData.name}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="bg-red-50 border border-red-200 text-red-700 rounded-lg p-4 mb-6">
|
|
||||||
<strong>Fehler:</strong> {error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isWelcome && (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<button
|
|
||||||
onClick={goToNext}
|
|
||||||
className="bg-blue-600 text-white px-8 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
Wizard starten
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isTestStep && currentStepData?.category && (
|
|
||||||
<TestRunner
|
|
||||||
category={currentStepData.category}
|
|
||||||
categoryResult={categoryResults[currentStepData.category]}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onRunTests={() => runCategoryTest(currentStepData.category!)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isSummary && (
|
|
||||||
<div>
|
|
||||||
{!fullResults ? (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<p className="text-gray-600 mb-4">
|
|
||||||
Fuehren Sie alle Tests aus um eine Zusammenfassung zu sehen.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={runAllTests}
|
|
||||||
disabled={isLoading}
|
|
||||||
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
|
|
||||||
isLoading
|
|
||||||
? 'bg-gray-400 cursor-not-allowed'
|
|
||||||
: 'bg-blue-600 text-white hover:bg-blue-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Alle Tests laufen...' : 'Alle Tests ausfuehren'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<TestSummary results={fullResults} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<WizardNavigation
|
|
||||||
currentStep={currentStep}
|
|
||||||
totalSteps={steps.length}
|
|
||||||
onPrev={goToPrev}
|
|
||||||
onNext={goToNext}
|
|
||||||
showNext={!isSummary}
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center text-gray-500 text-sm mt-6">
|
|
||||||
Diese Tests pruefen die LLM-Integration.
|
|
||||||
Bei Fragen wenden Sie sich an das KI-Team.
|
|
||||||
</div>
|
|
||||||
</AdminLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -126,7 +126,6 @@ const ADMIN_SCREENS: ScreenDefinition[] = [
|
|||||||
{ id: 'admin-dsms', name: 'DSMS', description: 'Datenschutz-Management', category: 'compliance', icon: '🛡️', url: '/admin/dsms' },
|
{ id: 'admin-dsms', name: 'DSMS', description: 'Datenschutz-Management', category: 'compliance', icon: '🛡️', url: '/admin/dsms' },
|
||||||
{ id: 'admin-compliance', name: 'Compliance', description: 'GRC & Audit', category: 'compliance', icon: '✅', url: '/admin/compliance' },
|
{ id: 'admin-compliance', name: 'Compliance', description: 'GRC & Audit', category: 'compliance', icon: '✅', url: '/admin/compliance' },
|
||||||
{ id: 'admin-docs-audit', name: 'DSGVO-Audit', description: 'Audit-Dokumentation', category: 'compliance', icon: '📋', url: '/admin/docs/audit' },
|
{ id: 'admin-docs-audit', name: 'DSGVO-Audit', description: 'Audit-Dokumentation', category: 'compliance', icon: '📋', url: '/admin/docs/audit' },
|
||||||
{ id: 'admin-llm-compare', name: 'LLM Vergleich', description: 'KI-Provider Vergleich', category: 'ai', icon: '🤖', url: '/admin/llm-compare' },
|
|
||||||
{ id: 'admin-rag', name: 'Daten & RAG', description: 'Training Data & RAG', category: 'ai', icon: '🗄️', url: '/admin/rag' },
|
{ id: 'admin-rag', name: 'Daten & RAG', description: 'Training Data & RAG', category: 'ai', icon: '🗄️', url: '/admin/rag' },
|
||||||
{ id: 'admin-ocr-labeling', name: 'OCR-Labeling', description: 'Handschrift-Training', category: 'ai', icon: '🏷️', url: '/admin/ocr-labeling' },
|
{ id: 'admin-ocr-labeling', name: 'OCR-Labeling', description: 'Handschrift-Training', category: 'ai', icon: '🏷️', url: '/admin/ocr-labeling' },
|
||||||
{ id: 'admin-magic-help', name: 'Magic Help (TrOCR)', description: 'Handschrift-OCR', category: 'ai', icon: '✨', url: '/admin/magic-help' },
|
{ id: 'admin-magic-help', name: 'Magic Help (TrOCR)', description: 'Handschrift-OCR', category: 'ai', icon: '✨', url: '/admin/magic-help' },
|
||||||
@@ -155,12 +154,10 @@ const ADMIN_CONNECTIONS: ConnectionDef[] = [
|
|||||||
{ source: 'admin-dashboard', target: 'admin-compliance' },
|
{ source: 'admin-dashboard', target: 'admin-compliance' },
|
||||||
{ source: 'admin-onboarding', target: 'admin-gpu' },
|
{ source: 'admin-onboarding', target: 'admin-gpu' },
|
||||||
{ source: 'admin-onboarding', target: 'admin-consent' },
|
{ source: 'admin-onboarding', target: 'admin-consent' },
|
||||||
{ source: 'admin-onboarding', target: 'admin-llm-compare' },
|
|
||||||
{ source: 'admin-consent', target: 'admin-dsr' },
|
{ source: 'admin-consent', target: 'admin-dsr' },
|
||||||
{ source: 'admin-dsr', target: 'admin-dsms' },
|
{ source: 'admin-dsr', target: 'admin-dsms' },
|
||||||
{ source: 'admin-dsms', target: 'admin-compliance' },
|
{ source: 'admin-dsms', target: 'admin-compliance' },
|
||||||
{ source: 'admin-compliance', target: 'admin-docs-audit' },
|
{ source: 'admin-compliance', target: 'admin-docs-audit' },
|
||||||
{ source: 'admin-llm-compare', target: 'admin-rag' },
|
|
||||||
{ source: 'admin-rag', target: 'admin-ocr-labeling' },
|
{ source: 'admin-rag', target: 'admin-ocr-labeling' },
|
||||||
{ source: 'admin-ocr-labeling', target: 'admin-magic-help' },
|
{ source: 'admin-ocr-labeling', target: 'admin-magic-help' },
|
||||||
{ source: 'admin-magic-help', target: 'admin-companion' },
|
{ source: 'admin-magic-help', target: 'admin-companion' },
|
||||||
|
|||||||
@@ -108,16 +108,6 @@ const navigation: NavItem[] = [
|
|||||||
),
|
),
|
||||||
description: 'Universitaets-Crawling Orchestrator',
|
description: 'Universitaets-Crawling Orchestrator',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'LLM Vergleich',
|
|
||||||
href: '/admin/llm-compare',
|
|
||||||
icon: (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
description: 'KI-Provider Vergleich',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Daten & RAG',
|
name: 'Daten & RAG',
|
||||||
href: '/admin/rag',
|
href: '/admin/rag',
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { dashboardConfig } from './dashboard-config'
|
|||||||
import { gpuConfig } from './gpu-config'
|
import { gpuConfig } from './gpu-config'
|
||||||
import { consentConfig } from './consent-config'
|
import { consentConfig } from './consent-config'
|
||||||
import { dsrConfig } from './dsr-config'
|
import { dsrConfig } from './dsr-config'
|
||||||
import { llmCompareConfig } from './llm-compare-config'
|
|
||||||
import { securityConfig } from './security-config'
|
import { securityConfig } from './security-config'
|
||||||
import { middlewareConfig } from './middleware-config'
|
import { middlewareConfig } from './middleware-config'
|
||||||
import { communicationConfig } from './communication-config'
|
import { communicationConfig } from './communication-config'
|
||||||
@@ -50,7 +49,6 @@ export const SYSTEM_INFO_CONFIGS: Record<string, SystemInfoConfig> = {
|
|||||||
dsr: dsrConfig,
|
dsr: dsrConfig,
|
||||||
|
|
||||||
// AI & ML
|
// AI & ML
|
||||||
llmCompare: llmCompareConfig,
|
|
||||||
training: trainingConfig,
|
training: trainingConfig,
|
||||||
rag: ragConfig,
|
rag: ragConfig,
|
||||||
|
|
||||||
@@ -90,7 +88,6 @@ export type { SystemInfoConfig } from './types'
|
|||||||
* ├── gpu-config.ts (~200 lines)
|
* ├── gpu-config.ts (~200 lines)
|
||||||
* ├── consent-config.ts (~200 lines)
|
* ├── consent-config.ts (~200 lines)
|
||||||
* ├── dsr-config.ts (~200 lines)
|
* ├── dsr-config.ts (~200 lines)
|
||||||
* ├── llm-compare-config.ts (~200 lines)
|
|
||||||
* ├── security-config.ts (~250 lines)
|
* ├── security-config.ts (~250 lines)
|
||||||
* ├── middleware-config.ts (~250 lines)
|
* ├── middleware-config.ts (~250 lines)
|
||||||
* ├── communication-config.ts (~200 lines)
|
* ├── communication-config.ts (~200 lines)
|
||||||
|
|||||||
@@ -1,164 +0,0 @@
|
|||||||
import type { SystemInfoConfig } from './types'
|
|
||||||
|
|
||||||
export const llmCompareConfig: SystemInfoConfig = {
|
|
||||||
title: 'LLM Vergleich System-Info',
|
|
||||||
description: 'Vergleich und Benchmarking verschiedener KI-Provider und Modelle.',
|
|
||||||
version: '1.0',
|
|
||||||
architecture: {
|
|
||||||
layers: [
|
|
||||||
{ title: 'Vergleichs-UI', components: ['Provider-Auswahl', 'Prompt-Editor', 'Ergebnis-Vergleich'], color: '#3b82f6' },
|
|
||||||
{ title: 'Provider Adapters', components: ['OpenAI', 'Anthropic', 'Google', 'Local'], color: '#8b5cf6' },
|
|
||||||
{ title: 'Evaluation Engine', components: ['Latenz-Messung', 'Qualitaets-Scoring', 'Cost Calculator'], color: '#10b981' },
|
|
||||||
{ title: 'Logging', components: ['Request Logs', 'Token Tracking', 'Error Logs'], color: '#f59e0b' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
features: [
|
|
||||||
{ name: 'Multi-Provider Vergleich', status: 'active', description: 'Parallele Anfragen an mehrere LLMs' },
|
|
||||||
{ name: 'Latenz-Tracking', status: 'active', description: 'Echtzeit-Performance-Messung' },
|
|
||||||
{ name: 'Kosten-Kalkulation', status: 'active', description: 'Token-basierte Kostenberechnung' },
|
|
||||||
{ name: 'Qualitaets-Bewertung', status: 'planned', description: 'Automatisches Scoring der Antworten' },
|
|
||||||
{ name: 'A/B Testing', status: 'planned', description: 'Statistische Signifikanz-Tests' },
|
|
||||||
],
|
|
||||||
roadmap: [
|
|
||||||
{ phase: 'Phase 1: Provider (Q1)', priority: 'high', items: ['Mistral Integration', 'Llama 3 Integration', 'Gemini Pro Integration', 'Rate Limiting'] },
|
|
||||||
{ phase: 'Phase 2: Evaluation (Q2)', priority: 'high', items: ['Automatisches Scoring', 'Benchmark-Suite', 'Domain-spezifische Tests', 'Halluzinations-Erkennung'] },
|
|
||||||
{ phase: 'Phase 3: Optimierung (Q3)', priority: 'medium', items: ['Prompt-Optimierung', 'Modell-Routing', 'Fallback-Strategien', 'Caching'] },
|
|
||||||
],
|
|
||||||
technicalDetails: [
|
|
||||||
{ component: 'OpenAI', technology: 'GPT-4o / o1', description: 'Haupt-Provider' },
|
|
||||||
{ component: 'Anthropic', technology: 'Claude 3.5', description: 'Alternative' },
|
|
||||||
{ component: 'Google', technology: 'Gemini 2.0', description: 'Multimodal' },
|
|
||||||
{ component: 'Local', technology: 'Ollama', description: 'Self-hosted' },
|
|
||||||
],
|
|
||||||
auditInfo: [
|
|
||||||
{
|
|
||||||
category: 'Provider-Status',
|
|
||||||
items: [
|
|
||||||
{ label: 'OpenAI', value: 'Aktiv', status: 'ok' },
|
|
||||||
{ label: 'Anthropic', value: 'Aktiv', status: 'ok' },
|
|
||||||
{ label: 'Google Gemini', value: 'Aktiv', status: 'ok' },
|
|
||||||
{ label: 'Ollama (Local)', value: 'Verfuegbar', status: 'ok' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: 'Kosten & Limits',
|
|
||||||
items: [
|
|
||||||
{ label: 'Monatliches Budget', value: 'Konfigurierbar', status: 'ok' },
|
|
||||||
{ label: 'Rate Limiting', value: 'Pro Provider', status: 'ok' },
|
|
||||||
{ label: 'Token Tracking', value: 'Aktiviert', status: 'ok' },
|
|
||||||
{ label: 'Cost Alerts', value: 'E-Mail', status: 'ok' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: 'Datenschutz',
|
|
||||||
items: [
|
|
||||||
{ label: 'Prompt-Logging', value: 'Optional', status: 'ok' },
|
|
||||||
{ label: 'PII Detection', value: 'Geplant', status: 'warning' },
|
|
||||||
{ label: 'Data Residency', value: 'EU verfuegbar', status: 'ok' },
|
|
||||||
{ label: 'Audit-Log', value: 'Aktiviert', status: 'ok' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
fullDocumentation: `
|
|
||||||
<h2>LLM Provider Vergleich & Benchmarking</h2>
|
|
||||||
|
|
||||||
<h3>1. Uebersicht</h3>
|
|
||||||
<p>Das LLM-Vergleichsmodul ermoeglicht den direkten Vergleich verschiedener KI-Provider hinsichtlich Qualitaet, Latenz und Kosten. Es dient der Auswahl des optimalen Modells fuer spezifische Use Cases.</p>
|
|
||||||
|
|
||||||
<h3>2. Unterstuetzte Provider</h3>
|
|
||||||
<table>
|
|
||||||
<tr><th>Provider</th><th>Modelle</th><th>Staerken</th><th>Preisbereich</th></tr>
|
|
||||||
<tr><td>OpenAI</td><td>GPT-4o, GPT-4o-mini, o1</td><td>Allrounder, Coding</td><td>$0.15-15/1M Token</td></tr>
|
|
||||||
<tr><td>Anthropic</td><td>Claude 3.5 Sonnet/Haiku</td><td>Lange Kontexte, Safety</td><td>$0.25-15/1M Token</td></tr>
|
|
||||||
<tr><td>Google</td><td>Gemini 2.0 Flash/Pro</td><td>Multimodal, Speed</td><td>$0.075-5/1M Token</td></tr>
|
|
||||||
<tr><td>Ollama</td><td>Llama 3, Mistral, Phi</td><td>Lokal, Datenschutz</td><td>Nur Hardware-Kosten</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h3>3. Vergleichs-Architektur</h3>
|
|
||||||
<pre>
|
|
||||||
┌────────────────────────────────────────────────────────────────┐
|
|
||||||
│ Frontend UI │
|
|
||||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
|
|
||||||
│ │ Prompt Input │ │ Provider Sel │ │ Results Comparison │ │
|
|
||||||
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
|
|
||||||
└────────────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
v
|
|
||||||
┌────────────────────────────────────────────────────────────────┐
|
|
||||||
│ Backend Orchestrator │
|
|
||||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ Parallel Request Handler | Response Aggregator │ │
|
|
||||||
│ └──────────────────────────────────────────────────────────┘ │
|
|
||||||
└────────────────────────────────────────────────────────────────┘
|
|
||||||
│ │ │ │
|
|
||||||
v v v v
|
|
||||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
||||||
│ OpenAI │ │Anthropic│ │ Google │ │ Ollama │
|
|
||||||
└─────────┘ └─────────┘ └─────────┘ └─────────┘
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3>4. Metriken</h3>
|
|
||||||
<table>
|
|
||||||
<tr><th>Metrik</th><th>Beschreibung</th><th>Messung</th></tr>
|
|
||||||
<tr><td>TTFB</td><td>Time to First Byte</td><td>Millisekunden</td></tr>
|
|
||||||
<tr><td>Total Latency</td><td>Gesamtantwortzeit</td><td>Millisekunden</td></tr>
|
|
||||||
<tr><td>Tokens/Sekunde</td><td>Generierungsgeschwindigkeit</td><td>Output Tokens/s</td></tr>
|
|
||||||
<tr><td>Kosten</td><td>Gesamtkosten</td><td>USD</td></tr>
|
|
||||||
<tr><td>Qualitaet</td><td>Manuelle/Auto Bewertung</td><td>1-5 Sterne</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h3>5. API Endpoints</h3>
|
|
||||||
<table>
|
|
||||||
<tr><th>Endpoint</th><th>Methode</th><th>Beschreibung</th></tr>
|
|
||||||
<tr><td>/api/llm/compare</td><td>POST</td><td>Parallelen Vergleich starten</td></tr>
|
|
||||||
<tr><td>/api/llm/providers</td><td>GET</td><td>Verfuegbare Provider listen</td></tr>
|
|
||||||
<tr><td>/api/llm/stats</td><td>GET</td><td>Nutzungsstatistiken</td></tr>
|
|
||||||
<tr><td>/api/llm/benchmark</td><td>POST</td><td>Benchmark-Suite ausfuehren</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h3>6. Benchmark-Suite</h3>
|
|
||||||
<p>Vordefinierte Tests fuer verschiedene Use Cases:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Summarization:</strong> Textzusammenfassung verschiedener Laengen</li>
|
|
||||||
<li><strong>QA:</strong> Frage-Antwort auf Dokumenten</li>
|
|
||||||
<li><strong>Coding:</strong> Code-Generierung und -Erklaerung</li>
|
|
||||||
<li><strong>Classification:</strong> Textkategorisierung</li>
|
|
||||||
<li><strong>Translation:</strong> Mehrsprachige Uebersetzung</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>7. Kostenmanagement</h3>
|
|
||||||
<pre>
|
|
||||||
Budgetkontrolle
|
|
||||||
│
|
|
||||||
├── Monatliches Limit pro Provider
|
|
||||||
├── Echtzeit-Kostentracking
|
|
||||||
├── Alerts bei 80%/90%/100%
|
|
||||||
└── Auto-Fallback bei Limit
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3>8. Datenschutz-Konfiguration</h3>
|
|
||||||
<table>
|
|
||||||
<tr><th>Einstellung</th><th>Optionen</th><th>Default</th></tr>
|
|
||||||
<tr><td>Prompt-Logging</td><td>Ein/Aus/Anonymisiert</td><td>Anonymisiert</td></tr>
|
|
||||||
<tr><td>Response-Speicherung</td><td>Ein/Aus/24h</td><td>24h</td></tr>
|
|
||||||
<tr><td>Metriken-Retention</td><td>30/90/365 Tage</td><td>90 Tage</td></tr>
|
|
||||||
<tr><td>PII-Filter</td><td>Ein/Aus</td><td>Ein (geplant)</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h3>9. Fehlerbehandlung</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Timeout:</strong> 30 Sekunden default, konfigurierbar</li>
|
|
||||||
<li><strong>Rate Limit:</strong> Automatisches Retry mit Backoff</li>
|
|
||||||
<li><strong>Provider Down:</strong> Skip und Warnung</li>
|
|
||||||
<li><strong>API Error:</strong> Fehlerdetails in Response</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>10. Best Practices</h3>
|
|
||||||
<ol>
|
|
||||||
<li>Immer mindestens 3 Provider fuer aussagekraeftigen Vergleich</li>
|
|
||||||
<li>Benchmark-Suite fuer reproduzierbare Ergebnisse nutzen</li>
|
|
||||||
<li>Kosten und Qualitaet gemeinsam bewerten</li>
|
|
||||||
<li>Lokale Modelle fuer sensible Daten bevorzugen</li>
|
|
||||||
</ol>
|
|
||||||
`,
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user