Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
762 lines
28 KiB
TypeScript
762 lines
28 KiB
TypeScript
'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>
|
|
)
|
|
}
|