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:
Benjamin Admin
2026-03-05 17:34:54 +01:00
parent 9912997187
commit b9c3c47a37
19 changed files with 1 additions and 1885 deletions

View File

@@ -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 &quot;Vergleich starten&quot;, 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>
)
}

View File

@@ -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">
&larr; 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>
)
}

View File

@@ -126,7 +126,6 @@ const ADMIN_SCREENS: ScreenDefinition[] = [
{ 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-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-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' },
@@ -155,12 +154,10 @@ const ADMIN_CONNECTIONS: ConnectionDef[] = [
{ source: 'admin-dashboard', target: 'admin-compliance' },
{ source: 'admin-onboarding', target: 'admin-gpu' },
{ source: 'admin-onboarding', target: 'admin-consent' },
{ source: 'admin-onboarding', target: 'admin-llm-compare' },
{ source: 'admin-consent', target: 'admin-dsr' },
{ source: 'admin-dsr', target: 'admin-dsms' },
{ source: 'admin-dsms', target: 'admin-compliance' },
{ source: 'admin-compliance', target: 'admin-docs-audit' },
{ source: 'admin-llm-compare', target: 'admin-rag' },
{ source: 'admin-rag', target: 'admin-ocr-labeling' },
{ source: 'admin-ocr-labeling', target: 'admin-magic-help' },
{ source: 'admin-magic-help', target: 'admin-companion' },