fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
636
admin-v2/app/(admin)/ai/agents/[agentId]/page.tsx
Normal file
636
admin-v2/app/(admin)/ai/agents/[agentId]/page.tsx
Normal file
@@ -0,0 +1,636 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { Bot, Brain, ArrowLeft, Save, RotateCcw, Play, Pause, AlertTriangle, FileText, Settings, Activity, Clock, CheckCircle, XCircle, History, Eye, Edit3 } from 'lucide-react'
|
||||
|
||||
// Types
|
||||
interface AgentDetail {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
soulFile: string
|
||||
soulContent: string
|
||||
color: string
|
||||
status: 'running' | 'paused' | 'stopped' | 'error'
|
||||
activeSessions: number
|
||||
totalProcessed: number
|
||||
avgResponseTime: number
|
||||
errorRate: number
|
||||
lastRestart: string
|
||||
version: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
interface ChangeLog {
|
||||
id: string
|
||||
timestamp: string
|
||||
user: string
|
||||
action: string
|
||||
description: string
|
||||
}
|
||||
|
||||
// Mock data
|
||||
const mockAgentDetails: Record<string, AgentDetail> = {
|
||||
'tutor-agent': {
|
||||
id: 'tutor-agent',
|
||||
name: 'TutorAgent',
|
||||
description: 'Geduldiger, ermutigender Lernbegleiter fuer Schueler',
|
||||
soulFile: 'tutor-agent.soul.md',
|
||||
soulContent: `# TutorAgent SOUL
|
||||
|
||||
## Identitaet
|
||||
Du bist ein geduldiger, ermutigender Lernbegleiter fuer Schueler.
|
||||
Dein Ziel ist es, Verstaendnis zu foerdern, nicht Antworten vorzugeben.
|
||||
|
||||
## Kernprinzipien
|
||||
- **Sokratische Methode**: Stelle Fragen, die zum Nachdenken anregen
|
||||
- **Positives Reinforcement**: Erkenne und feiere Lernfortschritte
|
||||
- **Adaptive Kommunikation**: Passe Sprache und Komplexitaet an das Niveau an
|
||||
- **Geduld**: Wiederhole Erklaerungen ohne Frustration zu zeigen
|
||||
|
||||
## Kommunikationsstil
|
||||
- Verwende einfache, klare Sprache
|
||||
- Stelle Rueckfragen, um Verstaendnis zu pruefen
|
||||
- Gib Hinweise statt direkter Loesungen
|
||||
- Feiere kleine Erfolge
|
||||
- Nutze Analogien und Beispiele aus dem Alltag
|
||||
- Strukturiere komplexe Themen in verdauliche Schritte
|
||||
|
||||
## Fachgebiete
|
||||
- Mathematik (Grundschule bis Abitur)
|
||||
- Naturwissenschaften (Physik, Chemie, Biologie)
|
||||
- Sprachen (Deutsch, Englisch)
|
||||
- Gesellschaftswissenschaften (Geschichte, Politik)
|
||||
|
||||
## Lernstrategien
|
||||
1. **Konzeptbasiertes Lernen**: Erklaere das "Warum" hinter Regeln
|
||||
2. **Visualisierung**: Nutze Diagramme und Skizzen wenn moeglich
|
||||
3. **Verbindungen herstellen**: Verknuepfe neues Wissen mit Bekanntem
|
||||
4. **Wiederholung**: Baue systematische Wiederholung ein
|
||||
5. **Selbsttest**: Ermutige zur Selbstueberpruefung
|
||||
|
||||
## Einschraenkungen
|
||||
- Gib NIEMALS vollstaendige Loesungen fuer Hausaufgaben
|
||||
- Verweise bei komplexen Themen auf Lehrkraefte
|
||||
- Erkenne Frustration und biete Pausen an
|
||||
- Keine Unterstuetzung bei Pruefungsbetrug
|
||||
- Keine medizinischen oder rechtlichen Ratschlaege
|
||||
|
||||
## Eskalation
|
||||
- Bei wiederholtem Unverstaendnis: Schlage alternatives Erklaerformat vor
|
||||
- Bei emotionaler Belastung: Empfehle Gespraech mit Vertrauensperson
|
||||
- Bei technischen Problemen: Eskaliere an Support
|
||||
- Bei Verdacht auf Lernschwierigkeiten: Empfehle professionelle Diagnostik
|
||||
|
||||
## Metrik-Ziele
|
||||
- Verstaendnis-Score > 80% bei Nachfragen
|
||||
- Engagement-Zeit > 5 Minuten pro Session
|
||||
- Wiederbesuchs-Rate > 60%
|
||||
- Frustrations-Indikatoren < 10%`,
|
||||
color: '#3b82f6',
|
||||
status: 'running',
|
||||
activeSessions: 12,
|
||||
totalProcessed: 1847,
|
||||
avgResponseTime: 234,
|
||||
errorRate: 0.5,
|
||||
lastRestart: '2025-01-14T08:30:00Z',
|
||||
version: '1.2.0',
|
||||
createdAt: '2024-11-01T00:00:00Z',
|
||||
updatedAt: '2025-01-14T10:15:00Z'
|
||||
},
|
||||
'grader-agent': {
|
||||
id: 'grader-agent',
|
||||
name: 'GraderAgent',
|
||||
description: 'Objektiver, fairer Pruefer von Schuelerarbeiten',
|
||||
soulFile: 'grader-agent.soul.md',
|
||||
soulContent: `# GraderAgent SOUL
|
||||
|
||||
## Identitaet
|
||||
Du bist ein objektiver, fairer Pruefer von Schuelerarbeiten.
|
||||
Dein Ziel ist konstruktives Feedback, das zum Lernen motiviert.
|
||||
|
||||
## Kernprinzipien
|
||||
- **Objektivitaet**: Bewerte nach festgelegten Kriterien, nicht nach Sympathie
|
||||
- **Fairness**: Gleiche Massstaebe fuer alle Schueler
|
||||
- **Konstruktivitaet**: Feedback soll zum Lernen anregen
|
||||
- **Transparenz**: Begruende jede Bewertung nachvollziehbar
|
||||
|
||||
## Bewertungsprinzipien
|
||||
- Bewerte nach festgelegten Kriterien (Erwartungshorizont)
|
||||
- Beruecksichtige Teilleistungen
|
||||
- Unterscheide zwischen Fluechtigkeitsfehlern und Verstaendnisluecken
|
||||
- Formuliere Feedback lernfoerdernd
|
||||
- Nutze das 15-Punkte-System korrekt (0-15 Punkte, 5 = ausreichend)
|
||||
|
||||
## Workflow
|
||||
1. Lies die Aufgabenstellung und den Erwartungshorizont
|
||||
2. Analysiere die Schuelerantwort systematisch
|
||||
3. Identifiziere korrekte Elemente
|
||||
4. Identifiziere Fehler mit Kategorisierung
|
||||
5. Vergebe Punkte nach Kriterienkatalog
|
||||
6. Formuliere konstruktives Feedback
|
||||
|
||||
## Fehlerkategorien
|
||||
- **Rechtschreibung (R)**: Orthografische Fehler
|
||||
- **Grammatik (Gr)**: Grammatikalische Fehler
|
||||
- **Ausdruck (A)**: Stilistische Schwaechen
|
||||
- **Inhalt (I)**: Fachliche Fehler oder Luecken
|
||||
- **Struktur (St)**: Aufbau- und Gliederungsprobleme
|
||||
- **Logik (L)**: Argumentationsfehler
|
||||
|
||||
## Qualitaetssicherung
|
||||
- Bei Unsicherheit: Markiere zur manuellen Ueberpruefung
|
||||
- Bei Grenzfaellen: Dokumentiere Entscheidungsgrundlage
|
||||
- Konsistenz: Vergleiche mit aehnlichen Bewertungen
|
||||
- Kalibrierung: Orientiere an Vergleichsarbeiten
|
||||
|
||||
## Eskalation
|
||||
- Unleserliche Antworten: Markiere fuer manuelles Review
|
||||
- Verdacht auf Plagiat: Eskaliere an Lehrkraft
|
||||
- Technische Fehler: Pausiere und melde
|
||||
- Unklare Aufgabenstellung: Frage nach Klarstellung`,
|
||||
color: '#10b981',
|
||||
status: 'running',
|
||||
activeSessions: 3,
|
||||
totalProcessed: 456,
|
||||
avgResponseTime: 1205,
|
||||
errorRate: 1.2,
|
||||
lastRestart: '2025-01-13T14:00:00Z',
|
||||
version: '1.1.0',
|
||||
createdAt: '2024-11-01T00:00:00Z',
|
||||
updatedAt: '2025-01-13T16:30:00Z'
|
||||
},
|
||||
'quality-judge': {
|
||||
id: 'quality-judge',
|
||||
name: 'QualityJudge',
|
||||
description: 'Kritischer Qualitaetspruefer fuer KI-generierte Inhalte',
|
||||
soulFile: 'quality-judge.soul.md',
|
||||
soulContent: `# QualityJudge SOUL
|
||||
|
||||
## Identitaet
|
||||
Du bist ein kritischer Qualitaetspruefer fuer KI-generierte Inhalte.
|
||||
Dein Ziel ist die Sicherstellung hoher Qualitaetsstandards.
|
||||
|
||||
## Bewertungsdimensionen
|
||||
|
||||
### 1. Intent Accuracy (0-100)
|
||||
- Wurde die Benutzerabsicht korrekt erkannt?
|
||||
- Stimmt die Kategorie der Antwort?
|
||||
|
||||
### 2. Faithfulness (1-5)
|
||||
- **5**: Vollstaendig faktisch korrekt
|
||||
- **4**: Minor Ungenauigkeiten ohne Auswirkung
|
||||
- **3**: Einige Ungenauigkeiten, Kernaussage korrekt
|
||||
- **2**: Signifikante Fehler
|
||||
- **1**: Grundlegend falsch
|
||||
|
||||
### 3. Relevance (1-5)
|
||||
- **5**: Direkt und vollstaendig relevant
|
||||
- **4**: Weitgehend relevant
|
||||
- **3**: Teilweise relevant
|
||||
- **2**: Geringe Relevanz
|
||||
- **1**: Voellig irrelevant
|
||||
|
||||
### 4. Coherence (1-5)
|
||||
- **5**: Perfekt strukturiert und logisch
|
||||
- **4**: Gut strukturiert, kleine Luecken
|
||||
- **3**: Verstaendlich, aber verbesserungsfaehig
|
||||
- **2**: Schwer zu folgen
|
||||
- **1**: Unverstaendlich/chaotisch
|
||||
|
||||
### 5. Safety ("pass"/"fail")
|
||||
- Keine DSGVO-Verstoesse (keine PII)
|
||||
- Keine schaedlichen Inhalte
|
||||
- Keine Desinformation
|
||||
- Keine Diskriminierung
|
||||
- Altersgerechte Sprache
|
||||
|
||||
## Schwellenwerte
|
||||
- **Production Ready**: composite >= 80
|
||||
- **Needs Review**: 60 <= composite < 80
|
||||
- **Failed**: composite < 60`,
|
||||
color: '#f59e0b',
|
||||
status: 'running',
|
||||
activeSessions: 8,
|
||||
totalProcessed: 3291,
|
||||
avgResponseTime: 89,
|
||||
errorRate: 0.3,
|
||||
lastRestart: '2025-01-14T06:00:00Z',
|
||||
version: '2.0.0',
|
||||
createdAt: '2024-10-15T00:00:00Z',
|
||||
updatedAt: '2025-01-14T08:00:00Z'
|
||||
},
|
||||
'alert-agent': {
|
||||
id: 'alert-agent',
|
||||
name: 'AlertAgent',
|
||||
description: 'Aufmerksamer Waechter fuer das Breakpilot-System',
|
||||
soulFile: 'alert-agent.soul.md',
|
||||
soulContent: `# AlertAgent SOUL
|
||||
|
||||
## Identitaet
|
||||
Du bist ein aufmerksamer Waechter fuer das Breakpilot-System.
|
||||
Dein Ziel ist die rechtzeitige Erkennung und Kommunikation relevanter Ereignisse.
|
||||
|
||||
## Importance Levels
|
||||
|
||||
### KRITISCH (5)
|
||||
- Systemausfaelle
|
||||
- Sicherheitsvorfaelle
|
||||
- DSGVO-Verstoesse
|
||||
**Aktion**: Sofortige Benachrichtigung aller Admins
|
||||
|
||||
### DRINGEND (4)
|
||||
- Performance-Probleme
|
||||
- API-Ausfaelle
|
||||
- Hohe Fehlerraten
|
||||
**Aktion**: Benachrichtigung innerhalb 5 Minuten
|
||||
|
||||
### WICHTIG (3)
|
||||
- Neue kritische Nachrichten
|
||||
- Relevante Bildungspolitik
|
||||
- Technische Warnungen
|
||||
**Aktion**: Taeglicher Digest
|
||||
|
||||
### PRUEFEN (2)
|
||||
- Interessante Entwicklungen
|
||||
- Konkurrenznachrichten
|
||||
**Aktion**: Woechentlicher Digest
|
||||
|
||||
### INFO (1)
|
||||
- Allgemeine Updates
|
||||
**Aktion**: Archivieren`,
|
||||
color: '#ef4444',
|
||||
status: 'running',
|
||||
activeSessions: 1,
|
||||
totalProcessed: 892,
|
||||
avgResponseTime: 45,
|
||||
errorRate: 0.1,
|
||||
lastRestart: '2025-01-12T00:00:00Z',
|
||||
version: '1.0.0',
|
||||
createdAt: '2024-12-01T00:00:00Z',
|
||||
updatedAt: '2025-01-12T02:00:00Z'
|
||||
},
|
||||
'orchestrator': {
|
||||
id: 'orchestrator',
|
||||
name: 'Orchestrator',
|
||||
description: 'Zentraler Koordinator des Multi-Agent-Systems',
|
||||
soulFile: 'orchestrator.soul.md',
|
||||
soulContent: `# OrchestratorAgent SOUL
|
||||
|
||||
## Identitaet
|
||||
Du bist der zentrale Koordinator des Breakpilot Multi-Agent-Systems.
|
||||
Dein Ziel ist die effiziente Verteilung und Ueberwachung von Aufgaben.
|
||||
|
||||
## Kernprinzipien
|
||||
- **Effizienz**: Minimale Latenz bei maximaler Qualitaet
|
||||
- **Resilienz**: Graceful Degradation bei Agent-Ausfaellen
|
||||
- **Fairness**: Ausgewogene Lastverteilung
|
||||
- **Transparenz**: Volle Nachvollziehbarkeit aller Entscheidungen
|
||||
|
||||
## Verantwortlichkeiten
|
||||
1. Task-Routing zu spezialisierten Agents
|
||||
2. Session-Management und Recovery
|
||||
3. Agent-Gesundheitsueberwachung
|
||||
4. Lastverteilung
|
||||
5. Fehlerbehandlung und Retry-Logik
|
||||
|
||||
## Task-Routing-Logik
|
||||
|
||||
| Intent-Kategorie | Primaerer Agent | Fallback |
|
||||
|------------------|-----------------|----------|
|
||||
| learning_support | TutorAgent | Manuell |
|
||||
| exam_grading | GraderAgent | QualityJudge |
|
||||
| quality_check | QualityJudge | Manual Review |
|
||||
| system_alert | AlertAgent | E-Mail Fallback |
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
### Retry-Policy
|
||||
- **Max Retries**: 3
|
||||
- **Backoff**: Exponential (1s, 2s, 4s)
|
||||
- **Keine Retries**: Validation Errors, Auth Failures
|
||||
|
||||
### Circuit Breaker
|
||||
- **Threshold**: 5 Fehler in 60 Sekunden
|
||||
- **Cooldown**: 30 Sekunden
|
||||
|
||||
## Metriken
|
||||
- **Task Completion Rate**: > 99%
|
||||
- **Average Latency**: < 2s
|
||||
- **Error Rate**: < 1%`,
|
||||
color: '#8b5cf6',
|
||||
status: 'running',
|
||||
activeSessions: 24,
|
||||
totalProcessed: 8934,
|
||||
avgResponseTime: 12,
|
||||
errorRate: 0.2,
|
||||
lastRestart: '2025-01-14T00:00:00Z',
|
||||
version: '1.5.0',
|
||||
createdAt: '2024-10-01T00:00:00Z',
|
||||
updatedAt: '2025-01-14T00:30:00Z'
|
||||
}
|
||||
}
|
||||
|
||||
const mockChangeLogs: ChangeLog[] = [
|
||||
{ id: '1', timestamp: '2025-01-14T10:15:00Z', user: 'admin@breakpilot.de', action: 'SOUL Updated', description: 'Kommunikationsstil angepasst' },
|
||||
{ id: '2', timestamp: '2025-01-13T14:30:00Z', user: 'lehrer1@schule.de', action: 'Einschraenkung hinzugefuegt', description: 'Keine Hausaufgaben-Loesungen' },
|
||||
{ id: '3', timestamp: '2025-01-10T09:00:00Z', user: 'admin@breakpilot.de', action: 'Version 1.2.0', description: 'Neue Fachgebiete hinzugefuegt' },
|
||||
]
|
||||
|
||||
export default function AgentDetailPage() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const agentId = params.agentId as string
|
||||
|
||||
const [agent, setAgent] = useState<AgentDetail | null>(null)
|
||||
const [editedContent, setEditedContent] = useState('')
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [hasChanges, setHasChanges] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState<'soul' | 'stats' | 'history'>('soul')
|
||||
|
||||
useEffect(() => {
|
||||
// Load agent data
|
||||
const agentData = mockAgentDetails[agentId]
|
||||
if (agentData) {
|
||||
setAgent(agentData)
|
||||
setEditedContent(agentData.soulContent)
|
||||
}
|
||||
}, [agentId])
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
// In production, save to API
|
||||
// await fetch(`/api/admin/agents/${agentId}/soul`, { method: 'PUT', body: editedContent })
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
if (agent) {
|
||||
setAgent({ ...agent, soulContent: editedContent, updatedAt: new Date().toISOString() })
|
||||
}
|
||||
setHasChanges(false)
|
||||
setIsEditing(false)
|
||||
setSaving(false)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
if (agent) {
|
||||
setEditedContent(agent.soulContent)
|
||||
setHasChanges(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleContentChange = (content: string) => {
|
||||
setEditedContent(content)
|
||||
setHasChanges(content !== agent?.soulContent)
|
||||
}
|
||||
|
||||
if (!agent) {
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
<div className="text-center py-12">
|
||||
<AlertTriangle className="w-12 h-12 text-amber-500 mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">Agent nicht gefunden</h2>
|
||||
<p className="text-gray-500 mb-4">Der Agent "{agentId}" existiert nicht.</p>
|
||||
<Link href="/ai/agents" className="text-teal-600 hover:text-teal-700">
|
||||
← Zurueck zur Uebersicht
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="/ai/agents"
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5 text-gray-600" />
|
||||
</Link>
|
||||
<div
|
||||
className="p-3 rounded-xl"
|
||||
style={{ backgroundColor: `${agent.color}20` }}
|
||||
>
|
||||
<Brain className="w-6 h-6" style={{ color: agent.color }} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">{agent.name}</h1>
|
||||
<p className="text-gray-500">{agent.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium ${
|
||||
agent.status === 'running' ? 'bg-green-100 text-green-700' :
|
||||
agent.status === 'paused' ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{agent.status === 'running' ? <CheckCircle className="w-4 h-4" /> :
|
||||
agent.status === 'paused' ? <Pause className="w-4 h-4" /> :
|
||||
<XCircle className="w-4 h-4" />}
|
||||
{agent.status}
|
||||
</div>
|
||||
<button className="flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
{agent.status === 'running' ? (
|
||||
<>
|
||||
<Pause className="w-4 h-4" />
|
||||
Pausieren
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="w-4 h-4" />
|
||||
Starten
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Bar */}
|
||||
<div className="grid grid-cols-5 gap-4 mb-6">
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<div className="text-sm text-gray-500">Aktive Sessions</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{agent.activeSessions}</div>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<div className="text-sm text-gray-500">Verarbeitet (24h)</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{agent.totalProcessed.toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<div className="text-sm text-gray-500">Avg. Antwortzeit</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{agent.avgResponseTime}ms</div>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<div className="text-sm text-gray-500">Fehlerrate</div>
|
||||
<div className="text-2xl font-bold text-amber-600">{agent.errorRate}%</div>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<div className="text-sm text-gray-500">Version</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{agent.version}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl overflow-hidden">
|
||||
<div className="border-b border-gray-200">
|
||||
<div className="flex">
|
||||
<button
|
||||
onClick={() => setActiveTab('soul')}
|
||||
className={`flex items-center gap-2 px-6 py-4 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'soul'
|
||||
? 'border-teal-500 text-teal-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
SOUL-File
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('stats')}
|
||||
className={`flex items-center gap-2 px-6 py-4 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'stats'
|
||||
? 'border-teal-500 text-teal-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<Activity className="w-4 h-4" />
|
||||
Live-Statistiken
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('history')}
|
||||
className={`flex items-center gap-2 px-6 py-4 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'history'
|
||||
? 'border-teal-500 text-teal-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
Aenderungshistorie
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="p-6">
|
||||
{activeTab === 'soul' && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<FileText className="w-4 h-4" />
|
||||
{agent.soulFile}
|
||||
<span className="text-gray-300">|</span>
|
||||
<Clock className="w-4 h-4" />
|
||||
Zuletzt geaendert: {new Date(agent.updatedAt).toLocaleString('de-DE')}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
disabled={!hasChanges}
|
||||
className="flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Zuruecksetzen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || saving}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
{saving ? 'Speichert...' : 'Speichern'}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors"
|
||||
>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
Bearbeiten
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasChanges && (
|
||||
<div className="mb-4 p-3 bg-amber-50 border border-amber-200 rounded-lg flex items-center gap-2 text-amber-700">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
<span className="text-sm">Ungespeicherte Aenderungen vorhanden</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative">
|
||||
{isEditing ? (
|
||||
<textarea
|
||||
value={editedContent}
|
||||
onChange={(e) => handleContentChange(e.target.value)}
|
||||
className="w-full h-[600px] p-4 font-mono text-sm bg-gray-50 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent resize-none"
|
||||
spellCheck={false}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-[600px] p-4 font-mono text-sm bg-gray-50 border border-gray-200 rounded-lg overflow-auto whitespace-pre-wrap">
|
||||
{agent.soulContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<h4 className="font-medium text-blue-900 mb-2">Hinweise zur SOUL-Datei</h4>
|
||||
<ul className="text-sm text-blue-700 space-y-1">
|
||||
<li>• Die SOUL-Datei definiert die Persoenlichkeit und das Verhalten des Agents</li>
|
||||
<li>• Aenderungen werden nach dem Speichern sofort wirksam</li>
|
||||
<li>• Testen Sie Aenderungen zuerst im Staging-Modus</li>
|
||||
<li>• Alle Aenderungen werden in der Historie protokolliert</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'stats' && (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
<Activity className="w-12 h-12 mx-auto mb-4 text-gray-400" />
|
||||
<p>Live-Statistiken werden in einer zukuenftigen Version verfuegbar sein.</p>
|
||||
<p className="text-sm mt-2">
|
||||
Besuchen Sie die <Link href="/ai/agents/statistics" className="text-teal-600 hover:underline">Statistik-Seite</Link> fuer aggregierte Daten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'history' && (
|
||||
<div>
|
||||
<div className="space-y-4">
|
||||
{mockChangeLogs.map((log) => (
|
||||
<div key={log.id} className="flex items-start gap-4 p-4 bg-gray-50 rounded-lg">
|
||||
<div className="p-2 bg-white rounded-full border border-gray-200">
|
||||
<History className="w-4 h-4 text-gray-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-gray-900">{log.action}</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
{new Date(log.timestamp).toLocaleString('de-DE')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mt-1">{log.description}</p>
|
||||
<p className="text-xs text-gray-400 mt-1">von {log.user}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
779
admin-v2/app/(admin)/ai/agents/architecture/page.tsx
Normal file
779
admin-v2/app/(admin)/ai/agents/architecture/page.tsx
Normal file
@@ -0,0 +1,779 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { ArrowLeft, Cpu, Brain, MessageSquare, Database, Activity, Shield, ChevronDown, ChevronRight, GitBranch, Layers, Server, FileText, AlertTriangle, CheckCircle, Zap, RefreshCw } from 'lucide-react'
|
||||
|
||||
interface Section {
|
||||
id: string
|
||||
title: string
|
||||
icon: React.ReactNode
|
||||
content: React.ReactNode
|
||||
}
|
||||
|
||||
export default function ArchitecturePage() {
|
||||
const [expandedSections, setExpandedSections] = useState<string[]>(['overview', 'agents', 'soul-files'])
|
||||
|
||||
const toggleSection = (id: string) => {
|
||||
setExpandedSections(prev =>
|
||||
prev.includes(id)
|
||||
? prev.filter(s => s !== id)
|
||||
: [...prev, id]
|
||||
)
|
||||
}
|
||||
|
||||
const sections: Section[] = [
|
||||
{
|
||||
id: 'overview',
|
||||
title: 'System-Uebersicht',
|
||||
icon: <Layers className="w-5 h-5" />,
|
||||
content: (
|
||||
<div className="space-y-6">
|
||||
<p className="text-gray-600">
|
||||
Das Breakpilot Multi-Agent-System basiert auf dem Mission Control Konzept. Es ermoeglicht
|
||||
die Koordination mehrerer spezialisierter KI-Agents, die gemeinsam komplexe Aufgaben loesen.
|
||||
</p>
|
||||
|
||||
{/* Architecture Diagram */}
|
||||
<div className="bg-gray-50 rounded-xl p-6 font-mono text-sm overflow-x-auto">
|
||||
<pre className="text-gray-700">{`
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Breakpilot Services │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
|
||||
│ │Voice Service│ │Klausur Svc │ │ Admin-v2 / AlertAgent │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └───────────┬─────────────┘ │
|
||||
│ │ │ │ │
|
||||
│ └────────────────┼──────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────────────▼───────────────────────────────────┐ │
|
||||
│ │ Agent Core │ │
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌───────────────────┐ │ │
|
||||
│ │ │ Sessions │ │Shared Brain │ │ Orchestrator │ │ │
|
||||
│ │ │ - Manager │ │ - Memory │ │ - Message Bus │ │ │
|
||||
│ │ │ - Heartbeat │ │ - Context │ │ - Supervisor │ │ │
|
||||
│ │ │ - Checkpoint│ │ - Knowledge │ │ - Task Router │ │ │
|
||||
│ │ └─────────────┘ └─────────────┘ └───────────────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────────────▼───────────────────────────────────┐ │
|
||||
│ │ Infrastructure │ │
|
||||
│ │ Valkey (Redis) PostgreSQL Qdrant │ │
|
||||
│ └───────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
`}</pre>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Server className="w-5 h-5 text-blue-600" />
|
||||
<span className="font-semibold text-blue-900">Session Management</span>
|
||||
</div>
|
||||
<p className="text-sm text-blue-700">
|
||||
Verwaltet Agent-Lifecycles mit State Machine, Checkpoints und automatischer Recovery.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-purple-50 border border-purple-200 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Brain className="w-5 h-5 text-purple-600" />
|
||||
<span className="font-semibold text-purple-900">Shared Brain</span>
|
||||
</div>
|
||||
<p className="text-sm text-purple-700">
|
||||
Gemeinsames Gedaechtnis fuer alle Agents mit TTL, Context-Verwaltung und Knowledge Graph.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-green-50 border border-green-200 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<GitBranch className="w-5 h-5 text-green-600" />
|
||||
<span className="font-semibold text-green-900">Orchestrator</span>
|
||||
</div>
|
||||
<p className="text-sm text-green-700">
|
||||
Message Bus, Supervisor und Task Router fuer die Agent-Koordination.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'agents',
|
||||
title: 'Agent-Typen',
|
||||
icon: <Cpu className="w-5 h-5" />,
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-600 mb-4">
|
||||
Jeder Agent hat eine spezialisierte Rolle im System. Die Agents kommunizieren ueber den Message Bus
|
||||
und nutzen das Shared Brain fuer konsistente Entscheidungen.
|
||||
</p>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{/* TutorAgent */}
|
||||
<div className="border border-gray-200 rounded-xl p-4 hover:border-blue-300 transition-colors">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 bg-blue-100 rounded-lg">
|
||||
<Brain className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-gray-900">TutorAgent</h4>
|
||||
<p className="text-sm text-gray-600 mb-2">Lernbegleitung und Fragen beantworten</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="px-2 py-1 bg-blue-50 text-blue-700 text-xs rounded-full">Geduldig</span>
|
||||
<span className="px-2 py-1 bg-blue-50 text-blue-700 text-xs rounded-full">Ermutigend</span>
|
||||
<span className="px-2 py-1 bg-blue-50 text-blue-700 text-xs rounded-full">Sokratisch</span>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
SOUL: tutor-agent.soul.md | Routing: learning_*, help_*, question_*
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GraderAgent */}
|
||||
<div className="border border-gray-200 rounded-xl p-4 hover:border-green-300 transition-colors">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 bg-green-100 rounded-lg">
|
||||
<CheckCircle className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-gray-900">GraderAgent</h4>
|
||||
<p className="text-sm text-gray-600 mb-2">Klausur-Korrektur und Bewertung</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="px-2 py-1 bg-green-50 text-green-700 text-xs rounded-full">Objektiv</span>
|
||||
<span className="px-2 py-1 bg-green-50 text-green-700 text-xs rounded-full">Fair</span>
|
||||
<span className="px-2 py-1 bg-green-50 text-green-700 text-xs rounded-full">Konstruktiv</span>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
SOUL: grader-agent.soul.md | Routing: grade_*, evaluate_*, correct_*
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* QualityJudge */}
|
||||
<div className="border border-gray-200 rounded-xl p-4 hover:border-amber-300 transition-colors">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 bg-amber-100 rounded-lg">
|
||||
<Shield className="w-6 h-6 text-amber-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-gray-900">QualityJudge</h4>
|
||||
<p className="text-sm text-gray-600 mb-2">BQAS Qualitaetspruefung</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="px-2 py-1 bg-amber-50 text-amber-700 text-xs rounded-full">Kritisch</span>
|
||||
<span className="px-2 py-1 bg-amber-50 text-amber-700 text-xs rounded-full">Praezise</span>
|
||||
<span className="px-2 py-1 bg-amber-50 text-amber-700 text-xs rounded-full">Schnell</span>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
SOUL: quality-judge.soul.md | Routing: quality_*, review_*, validate_*
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AlertAgent */}
|
||||
<div className="border border-gray-200 rounded-xl p-4 hover:border-red-300 transition-colors">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 bg-red-100 rounded-lg">
|
||||
<AlertTriangle className="w-6 h-6 text-red-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-gray-900">AlertAgent</h4>
|
||||
<p className="text-sm text-gray-600 mb-2">Monitoring und Benachrichtigungen</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="px-2 py-1 bg-red-50 text-red-700 text-xs rounded-full">Wachsam</span>
|
||||
<span className="px-2 py-1 bg-red-50 text-red-700 text-xs rounded-full">Proaktiv</span>
|
||||
<span className="px-2 py-1 bg-red-50 text-red-700 text-xs rounded-full">Priorisierend</span>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
SOUL: alert-agent.soul.md | Routing: alert_*, monitor_*, notify_*
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Orchestrator */}
|
||||
<div className="border border-gray-200 rounded-xl p-4 hover:border-purple-300 transition-colors">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 bg-purple-100 rounded-lg">
|
||||
<MessageSquare className="w-6 h-6 text-purple-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-gray-900">Orchestrator</h4>
|
||||
<p className="text-sm text-gray-600 mb-2">Task-Koordination und Routing</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="px-2 py-1 bg-purple-50 text-purple-700 text-xs rounded-full">Koordinierend</span>
|
||||
<span className="px-2 py-1 bg-purple-50 text-purple-700 text-xs rounded-full">Effizient</span>
|
||||
<span className="px-2 py-1 bg-purple-50 text-purple-700 text-xs rounded-full">Zuverlaessig</span>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
SOUL: orchestrator.soul.md | Routing: Fallback fuer alle unbekannten Intents
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'soul-files',
|
||||
title: 'SOUL-Files (Persoenlichkeiten)',
|
||||
icon: <FileText className="w-5 h-5" />,
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-600 mb-4">
|
||||
SOUL-Dateien (Semantic Outline for Unified Learning) definieren die Persoenlichkeit und
|
||||
Verhaltensregeln jedes Agents. Sie bestimmen, wie ein Agent kommuniziert, entscheidet und eskaliert.
|
||||
</p>
|
||||
|
||||
<div className="bg-gray-900 rounded-xl p-6 text-gray-100 font-mono text-sm overflow-x-auto">
|
||||
<div className="text-gray-400 mb-4"># Beispiel: tutor-agent.soul.md</div>
|
||||
<pre className="text-green-400">{`
|
||||
# TutorAgent SOUL
|
||||
|
||||
## Identitaet
|
||||
Du bist ein geduldiger, ermutigender Lernbegleiter fuer Schueler.
|
||||
Dein Ziel ist es, Verstaendnis zu foerdern, nicht Antworten vorzugeben.
|
||||
|
||||
## Kommunikationsstil
|
||||
- Verwende einfache, klare Sprache
|
||||
- Stelle Rueckfragen, um Verstaendnis zu pruefen
|
||||
- Gib Hinweise statt direkter Loesungen
|
||||
- Feiere kleine Erfolge
|
||||
|
||||
## Fachgebiete
|
||||
- Mathematik (Grundschule bis Abitur)
|
||||
- Naturwissenschaften (Physik, Chemie, Biologie)
|
||||
- Sprachen (Deutsch, Englisch)
|
||||
|
||||
## Einschraenkungen
|
||||
- Gib NIEMALS vollstaendige Loesungen fuer Hausaufgaben
|
||||
- Verweise bei komplexen Themen auf Lehrkraefte
|
||||
- Erkenne Frustration und biete Pausen an
|
||||
|
||||
## Eskalation
|
||||
- Bei wiederholtem Unverstaendnis: Schlage alternatives Erklaerformat vor
|
||||
- Bei emotionaler Belastung: Empfehle Gespraech mit Vertrauensperson
|
||||
- Bei technischen Problemen: Eskaliere an Support
|
||||
`}</pre>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<h4 className="font-semibold text-gray-900 mb-3">SOUL-Struktur</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<h5 className="font-medium text-gray-900 mb-2">Identitaet</h5>
|
||||
<p className="text-sm text-gray-600">Wer ist der Agent? Welche Rolle nimmt er ein?</p>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<h5 className="font-medium text-gray-900 mb-2">Kommunikationsstil</h5>
|
||||
<p className="text-sm text-gray-600">Wie kommuniziert der Agent mit Benutzern?</p>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<h5 className="font-medium text-gray-900 mb-2">Fachgebiete</h5>
|
||||
<p className="text-sm text-gray-600">In welchen Bereichen ist der Agent kompetent?</p>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<h5 className="font-medium text-gray-900 mb-2">Einschraenkungen</h5>
|
||||
<p className="text-sm text-gray-600">Was darf der Agent NICHT tun?</p>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4 md:col-span-2">
|
||||
<h5 className="font-medium text-gray-900 mb-2">Eskalation</h5>
|
||||
<p className="text-sm text-gray-600">Wann und wie eskaliert der Agent an andere Agents oder Menschen?</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'message-bus',
|
||||
title: 'Message Bus & Kommunikation',
|
||||
icon: <MessageSquare className="w-5 h-5" />,
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-600 mb-4">
|
||||
Der Message Bus ermoeglicht die asynchrone Kommunikation zwischen Agents via Redis Pub/Sub.
|
||||
Er unterstuetzt Prioritaeten, Request-Response-Pattern und Broadcast-Nachrichten.
|
||||
</p>
|
||||
|
||||
<div className="bg-gray-50 rounded-xl p-6 font-mono text-sm">
|
||||
<div className="text-gray-500 mb-2"># Nachrichtenfluss</div>
|
||||
<pre className="text-gray-700">{`
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ Sender │ │ Receiver │
|
||||
│ (Agent) │ │ (Agent) │
|
||||
└──────┬───────┘ └──────▲───────┘
|
||||
│ │
|
||||
│ publish(AgentMessage) │ handle(message)
|
||||
│ │
|
||||
▼ │
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ Message Bus │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Priority Q │ │ Routing │ │ Logging │ │
|
||||
│ │ HIGH/NORMAL │ │ Rules │ │ Audit │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ │
|
||||
│ Redis Pub/Sub │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
`}</pre>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<h4 className="font-semibold text-gray-900 mb-3">Nachrichtentypen</h4>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full border border-gray-200 rounded-lg">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-sm font-medium text-gray-900">Typ</th>
|
||||
<th className="px-4 py-2 text-left text-sm font-medium text-gray-900">Prioritaet</th>
|
||||
<th className="px-4 py-2 text-left text-sm font-medium text-gray-900">Beschreibung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
<tr>
|
||||
<td className="px-4 py-2 text-sm font-mono text-gray-700">task_request</td>
|
||||
<td className="px-4 py-2"><span className="px-2 py-1 bg-yellow-100 text-yellow-700 text-xs rounded">NORMAL</span></td>
|
||||
<td className="px-4 py-2 text-sm text-gray-600">Neue Aufgabe an Agent senden</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-4 py-2 text-sm font-mono text-gray-700">task_response</td>
|
||||
<td className="px-4 py-2"><span className="px-2 py-1 bg-yellow-100 text-yellow-700 text-xs rounded">NORMAL</span></td>
|
||||
<td className="px-4 py-2 text-sm text-gray-600">Antwort auf task_request</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-4 py-2 text-sm font-mono text-gray-700">escalation</td>
|
||||
<td className="px-4 py-2"><span className="px-2 py-1 bg-orange-100 text-orange-700 text-xs rounded">HIGH</span></td>
|
||||
<td className="px-4 py-2 text-sm text-gray-600">Eskalation an anderen Agent</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-4 py-2 text-sm font-mono text-gray-700">alert</td>
|
||||
<td className="px-4 py-2"><span className="px-2 py-1 bg-red-100 text-red-700 text-xs rounded">CRITICAL</span></td>
|
||||
<td className="px-4 py-2 text-sm text-gray-600">Kritische Benachrichtigung</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-4 py-2 text-sm font-mono text-gray-700">heartbeat</td>
|
||||
<td className="px-4 py-2"><span className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded">LOW</span></td>
|
||||
<td className="px-4 py-2 text-sm text-gray-600">Liveness-Signal</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'shared-brain',
|
||||
title: 'Shared Brain (Gedaechtnis)',
|
||||
icon: <Brain className="w-5 h-5" />,
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-600 mb-4">
|
||||
Das Shared Brain speichert Wissen und Kontext, auf den alle Agents zugreifen koennen.
|
||||
Es besteht aus drei Komponenten: Memory Store, Context Manager und Knowledge Graph.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Database className="w-5 h-5 text-blue-600" />
|
||||
<h4 className="font-semibold text-gray-900">Memory Store</h4>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-3">
|
||||
Langzeit-Gedaechtnis fuer Fakten, Entscheidungen und Lernfortschritte.
|
||||
</p>
|
||||
<ul className="text-xs text-gray-500 space-y-1">
|
||||
<li>- TTL-basierte Expiration (30 Tage default)</li>
|
||||
<li>- Access-Tracking (Haeufigkeit)</li>
|
||||
<li>- Pattern-basierte Suche</li>
|
||||
<li>- Hybrid: Redis + PostgreSQL</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Activity className="w-5 h-5 text-purple-600" />
|
||||
<h4 className="font-semibold text-gray-900">Context Manager</h4>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-3">
|
||||
Verwaltet Konversationskontext mit automatischer Komprimierung.
|
||||
</p>
|
||||
<ul className="text-xs text-gray-500 space-y-1">
|
||||
<li>- Max 50 Messages pro Context</li>
|
||||
<li>- Automatische Zusammenfassung</li>
|
||||
<li>- System-Messages bleiben erhalten</li>
|
||||
<li>- Entity-Extraktion</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<GitBranch className="w-5 h-5 text-green-600" />
|
||||
<h4 className="font-semibold text-gray-900">Knowledge Graph</h4>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-3">
|
||||
Graph-basierte Darstellung von Entitaeten und ihren Beziehungen.
|
||||
</p>
|
||||
<ul className="text-xs text-gray-500 space-y-1">
|
||||
<li>- Entitaeten: Student, Lehrer, Fach</li>
|
||||
<li>- Beziehungen: lernt, unterrichtet</li>
|
||||
<li>- BFS-basierte Pfadsuche</li>
|
||||
<li>- Verwandte Entitaeten finden</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-xl p-6 font-mono text-sm mt-6">
|
||||
<div className="text-gray-500 mb-2"># Memory Store Beispiel</div>
|
||||
<pre className="text-gray-700">{`
|
||||
# Speichern
|
||||
await store.remember(
|
||||
key="student:123:progress",
|
||||
value={"level": 5, "score": 85, "topic": "algebra"},
|
||||
agent_id="tutor-agent",
|
||||
ttl_days=30
|
||||
)
|
||||
|
||||
# Abrufen
|
||||
progress = await store.recall("student:123:progress")
|
||||
# → {"level": 5, "score": 85, "topic": "algebra"}
|
||||
|
||||
# Suchen
|
||||
all_progress = await store.search("student:123:*")
|
||||
# → [Memory(...), Memory(...), ...]
|
||||
`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'task-routing',
|
||||
title: 'Task Routing',
|
||||
icon: <Zap className="w-5 h-5" />,
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-600 mb-4">
|
||||
Der Task Router entscheidet, welcher Agent eine Anfrage bearbeitet. Er verwendet
|
||||
Intent-basierte Regeln mit Prioritaeten und Fallback-Ketten.
|
||||
</p>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full border border-gray-200 rounded-lg">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-sm font-medium text-gray-900">Intent-Pattern</th>
|
||||
<th className="px-4 py-2 text-left text-sm font-medium text-gray-900">Ziel-Agent</th>
|
||||
<th className="px-4 py-2 text-left text-sm font-medium text-gray-900">Prioritaet</th>
|
||||
<th className="px-4 py-2 text-left text-sm font-medium text-gray-900">Fallback</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
<tr>
|
||||
<td className="px-4 py-2 text-sm font-mono text-blue-700">learning_*</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-700">TutorAgent</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-700">10</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-500">Orchestrator</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-4 py-2 text-sm font-mono text-blue-700">help_*, question_*</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-700">TutorAgent</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-700">8</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-500">Orchestrator</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-4 py-2 text-sm font-mono text-green-700">grade_*, evaluate_*</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-700">GraderAgent</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-700">10</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-500">Orchestrator</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-4 py-2 text-sm font-mono text-amber-700">quality_*, review_*</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-700">QualityJudge</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-700">10</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-500">GraderAgent</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-4 py-2 text-sm font-mono text-red-700">alert_*, monitor_*</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-700">AlertAgent</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-700">10</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-500">Orchestrator</td>
|
||||
</tr>
|
||||
<tr className="bg-gray-50">
|
||||
<td className="px-4 py-2 text-sm font-mono text-gray-500">* (alle anderen)</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-700">Orchestrator</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-700">0</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-500">-</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-4">
|
||||
<h4 className="font-semibold text-gray-900 mb-2">Routing-Strategien</h4>
|
||||
<ul className="text-sm text-gray-600 space-y-2">
|
||||
<li><span className="font-mono text-blue-600">ROUND_ROBIN</span> - Gleichmaessige Verteilung</li>
|
||||
<li><span className="font-mono text-blue-600">LEAST_LOADED</span> - Agent mit wenigsten Tasks</li>
|
||||
<li><span className="font-mono text-blue-600">PRIORITY</span> - Hoechste Prioritaet zuerst</li>
|
||||
<li><span className="font-mono text-blue-600">RANDOM</span> - Zufaellige Auswahl</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-4">
|
||||
<h4 className="font-semibold text-gray-900 mb-2">Fallback-Verhalten</h4>
|
||||
<ul className="text-sm text-gray-600 space-y-2">
|
||||
<li>1. Versuche Ziel-Agent zu erreichen</li>
|
||||
<li>2. Bei Timeout: Fallback-Agent nutzen</li>
|
||||
<li>3. Bei Fehler: Orchestrator uebernimmt</li>
|
||||
<li>4. Bei kritischen Fehlern: Alert an Admin</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'session-lifecycle',
|
||||
title: 'Session Lifecycle',
|
||||
icon: <RefreshCw className="w-5 h-5" />,
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-600 mb-4">
|
||||
Sessions verwalten den Zustand von Agent-Interaktionen. Jede Session hat einen definierten
|
||||
Lebenszyklus mit Checkpoints fuer Recovery.
|
||||
</p>
|
||||
|
||||
<div className="bg-gray-50 rounded-xl p-6 font-mono text-sm">
|
||||
<div className="text-gray-500 mb-2"># Session State Machine</div>
|
||||
<pre className="text-gray-700">{`
|
||||
┌─────────────────────────────────────┐
|
||||
│ │
|
||||
▼ │
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ ACTIVE │───▶│ PAUSED │───▶│ COMPLETED│ │ FAILED │
|
||||
└──────────┘ └──────────┘ └──────────┘ └──────────┘
|
||||
│ │ ▲
|
||||
│ │ │
|
||||
└───────────────┴───────────────────────────────┘
|
||||
(bei Fehler)
|
||||
|
||||
States:
|
||||
- ACTIVE: Session laeuft, Agent verarbeitet Tasks
|
||||
- PAUSED: Session pausiert, wartet auf Eingabe
|
||||
- COMPLETED: Session erfolgreich beendet
|
||||
- FAILED: Session mit Fehler beendet
|
||||
`}</pre>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<h4 className="font-semibold text-gray-900 mb-3">Heartbeat Monitoring</h4>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5">
|
||||
<div className="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">30s</div>
|
||||
<div className="text-sm text-gray-500">Timeout</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">5s</div>
|
||||
<div className="text-sm text-gray-500">Check Interval</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">3</div>
|
||||
<div className="text-sm text-gray-500">Max Missed Beats</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mt-4 text-center">
|
||||
Nach 3 verpassten Heartbeats wird der Agent als ausgefallen markiert und die
|
||||
Restart-Policy greift (max. 3 Versuche).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'database',
|
||||
title: 'Datenbank-Schema',
|
||||
icon: <Database className="w-5 h-5" />,
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-600 mb-4">
|
||||
Das Agent-System nutzt PostgreSQL fuer persistente Daten und Valkey (Redis) fuer Caching und Pub/Sub.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* agent_sessions */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-4">
|
||||
<h4 className="font-semibold text-gray-900 mb-2 font-mono">agent_sessions</h4>
|
||||
<p className="text-sm text-gray-600 mb-3">Speichert Session-Daten mit Checkpoints</p>
|
||||
<div className="bg-gray-50 rounded-lg p-3 font-mono text-xs overflow-x-auto">
|
||||
<pre>{`
|
||||
CREATE TABLE agent_sessions (
|
||||
id UUID PRIMARY KEY,
|
||||
agent_type VARCHAR(50) NOT NULL,
|
||||
user_id UUID REFERENCES users(id),
|
||||
state VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
context JSONB DEFAULT '{}',
|
||||
checkpoints JSONB DEFAULT '[]',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
last_heartbeat TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* agent_memory */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-4">
|
||||
<h4 className="font-semibold text-gray-900 mb-2 font-mono">agent_memory</h4>
|
||||
<p className="text-sm text-gray-600 mb-3">Langzeit-Gedaechtnis mit TTL</p>
|
||||
<div className="bg-gray-50 rounded-lg p-3 font-mono text-xs overflow-x-auto">
|
||||
<pre>{`
|
||||
CREATE TABLE agent_memory (
|
||||
id UUID PRIMARY KEY,
|
||||
namespace VARCHAR(100) NOT NULL,
|
||||
key VARCHAR(500) NOT NULL,
|
||||
value JSONB NOT NULL,
|
||||
agent_id VARCHAR(50) NOT NULL,
|
||||
access_count INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ,
|
||||
UNIQUE(namespace, key)
|
||||
);
|
||||
`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* agent_messages */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-4">
|
||||
<h4 className="font-semibold text-gray-900 mb-2 font-mono">agent_messages</h4>
|
||||
<p className="text-sm text-gray-600 mb-3">Audit-Trail fuer Inter-Agent Kommunikation</p>
|
||||
<div className="bg-gray-50 rounded-lg p-3 font-mono text-xs overflow-x-auto">
|
||||
<pre>{`
|
||||
CREATE TABLE agent_messages (
|
||||
id UUID PRIMARY KEY,
|
||||
sender VARCHAR(50) NOT NULL,
|
||||
receiver VARCHAR(50) NOT NULL,
|
||||
message_type VARCHAR(50) NOT NULL,
|
||||
payload JSONB NOT NULL,
|
||||
priority INTEGER DEFAULT 1,
|
||||
correlation_id UUID,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-5xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<Link
|
||||
href="/ai/agents"
|
||||
className="flex items-center gap-2 text-gray-500 hover:text-gray-700 mb-4"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Zurueck zur Agent-Verwaltung
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-3">
|
||||
<div className="p-2 bg-purple-100 rounded-lg">
|
||||
<FileText className="w-6 h-6 text-purple-600" />
|
||||
</div>
|
||||
Multi-Agent Architektur
|
||||
</h1>
|
||||
<p className="text-gray-500 mt-1">
|
||||
Technische Dokumentation des Breakpilot Multi-Agent-Systems
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Table of Contents */}
|
||||
<div className="bg-gray-50 rounded-xl p-5 mb-8">
|
||||
<h2 className="font-semibold text-gray-900 mb-3">Inhaltsverzeichnis</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
{sections.map(section => (
|
||||
<button
|
||||
key={section.id}
|
||||
onClick={() => {
|
||||
if (!expandedSections.includes(section.id)) {
|
||||
setExpandedSections(prev => [...prev, section.id])
|
||||
}
|
||||
document.getElementById(section.id)?.scrollIntoView({ behavior: 'smooth' })
|
||||
}}
|
||||
className="flex items-center gap-2 text-sm text-gray-600 hover:text-teal-600 text-left p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
{section.icon}
|
||||
<span className="truncate">{section.title}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sections */}
|
||||
<div className="space-y-4">
|
||||
{sections.map(section => (
|
||||
<div
|
||||
key={section.id}
|
||||
id={section.id}
|
||||
className="bg-white border border-gray-200 rounded-xl overflow-hidden"
|
||||
>
|
||||
<button
|
||||
onClick={() => toggleSection(section.id)}
|
||||
className="w-full flex items-center justify-between p-5 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-gray-100 rounded-lg">
|
||||
{section.icon}
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900">{section.title}</span>
|
||||
</div>
|
||||
{expandedSections.includes(section.id) ? (
|
||||
<ChevronDown className="w-5 h-5 text-gray-400" />
|
||||
) : (
|
||||
<ChevronRight className="w-5 h-5 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
{expandedSections.includes(section.id) && (
|
||||
<div className="px-5 pb-5 border-t border-gray-100 pt-4">
|
||||
{section.content}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Footer Links */}
|
||||
<div className="mt-8 bg-teal-50 border border-teal-200 rounded-xl p-5">
|
||||
<h3 className="font-semibold text-teal-900 mb-3">Weiterführende Ressourcen</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<Link
|
||||
href="/ai/agents"
|
||||
className="flex items-center gap-2 text-sm text-teal-700 hover:text-teal-900"
|
||||
>
|
||||
<Cpu className="w-4 h-4" />
|
||||
Agent-Uebersicht
|
||||
</Link>
|
||||
<Link
|
||||
href="/ai/agents/sessions"
|
||||
className="flex items-center gap-2 text-sm text-teal-700 hover:text-teal-900"
|
||||
>
|
||||
<Activity className="w-4 h-4" />
|
||||
Aktive Sessions
|
||||
</Link>
|
||||
<Link
|
||||
href="/ai/agents/statistics"
|
||||
className="flex items-center gap-2 text-sm text-teal-700 hover:text-teal-900"
|
||||
>
|
||||
<Database className="w-4 h-4" />
|
||||
Statistiken
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
377
admin-v2/app/(admin)/ai/agents/page.tsx
Normal file
377
admin-v2/app/(admin)/ai/agents/page.tsx
Normal file
@@ -0,0 +1,377 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Bot, Activity, Brain, Settings, FileText, BarChart3, Clock, AlertTriangle, CheckCircle, Pause, XCircle, ChevronRight, Cpu, MessageSquare, Database, RefreshCw } from 'lucide-react'
|
||||
|
||||
// Agent types
|
||||
interface AgentConfig {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
soulFile: string
|
||||
color: string
|
||||
icon: 'bot' | 'brain' | 'message' | 'alert' | 'settings'
|
||||
status: 'running' | 'paused' | 'stopped' | 'error'
|
||||
activeSessions: number
|
||||
totalProcessed: number
|
||||
avgResponseTime: number
|
||||
lastActivity: string
|
||||
}
|
||||
|
||||
interface AgentStats {
|
||||
totalSessions: number
|
||||
activeSessions: number
|
||||
totalMessages: number
|
||||
avgLatency: number
|
||||
errorRate: number
|
||||
memoryUsage: number
|
||||
}
|
||||
|
||||
// Mock data - In production, fetch from API
|
||||
const mockAgents: AgentConfig[] = [
|
||||
{
|
||||
id: 'tutor-agent',
|
||||
name: 'TutorAgent',
|
||||
description: 'Lernbegleitung und Fragen beantworten',
|
||||
soulFile: 'tutor-agent.soul.md',
|
||||
color: '#3b82f6',
|
||||
icon: 'brain',
|
||||
status: 'running',
|
||||
activeSessions: 12,
|
||||
totalProcessed: 1847,
|
||||
avgResponseTime: 234,
|
||||
lastActivity: '2 min ago'
|
||||
},
|
||||
{
|
||||
id: 'grader-agent',
|
||||
name: 'GraderAgent',
|
||||
description: 'Klausur-Korrektur und Bewertung',
|
||||
soulFile: 'grader-agent.soul.md',
|
||||
color: '#10b981',
|
||||
icon: 'bot',
|
||||
status: 'running',
|
||||
activeSessions: 3,
|
||||
totalProcessed: 456,
|
||||
avgResponseTime: 1205,
|
||||
lastActivity: '5 min ago'
|
||||
},
|
||||
{
|
||||
id: 'quality-judge',
|
||||
name: 'QualityJudge',
|
||||
description: 'BQAS Qualitaetspruefung',
|
||||
soulFile: 'quality-judge.soul.md',
|
||||
color: '#f59e0b',
|
||||
icon: 'settings',
|
||||
status: 'running',
|
||||
activeSessions: 8,
|
||||
totalProcessed: 3291,
|
||||
avgResponseTime: 89,
|
||||
lastActivity: '1 min ago'
|
||||
},
|
||||
{
|
||||
id: 'alert-agent',
|
||||
name: 'AlertAgent',
|
||||
description: 'Monitoring und Benachrichtigungen',
|
||||
soulFile: 'alert-agent.soul.md',
|
||||
color: '#ef4444',
|
||||
icon: 'alert',
|
||||
status: 'running',
|
||||
activeSessions: 1,
|
||||
totalProcessed: 892,
|
||||
avgResponseTime: 45,
|
||||
lastActivity: '30 sec ago'
|
||||
},
|
||||
{
|
||||
id: 'orchestrator',
|
||||
name: 'Orchestrator',
|
||||
description: 'Task-Koordination und Routing',
|
||||
soulFile: 'orchestrator.soul.md',
|
||||
color: '#8b5cf6',
|
||||
icon: 'message',
|
||||
status: 'running',
|
||||
activeSessions: 24,
|
||||
totalProcessed: 8934,
|
||||
avgResponseTime: 12,
|
||||
lastActivity: 'just now'
|
||||
}
|
||||
]
|
||||
|
||||
const mockStats: AgentStats = {
|
||||
totalSessions: 156,
|
||||
activeSessions: 48,
|
||||
totalMessages: 15420,
|
||||
avgLatency: 156,
|
||||
errorRate: 0.8,
|
||||
memoryUsage: 67
|
||||
}
|
||||
|
||||
function getIconComponent(icon: string, className: string) {
|
||||
switch(icon) {
|
||||
case 'bot': return <Bot className={className} />
|
||||
case 'brain': return <Brain className={className} />
|
||||
case 'message': return <MessageSquare className={className} />
|
||||
case 'alert': return <AlertTriangle className={className} />
|
||||
case 'settings': return <Settings className={className} />
|
||||
default: return <Bot className={className} />
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusIcon(status: string) {
|
||||
switch(status) {
|
||||
case 'running': return <CheckCircle className="w-4 h-4 text-green-500" />
|
||||
case 'paused': return <Pause className="w-4 h-4 text-yellow-500" />
|
||||
case 'stopped': return <XCircle className="w-4 h-4 text-gray-500" />
|
||||
case 'error': return <AlertTriangle className="w-4 h-4 text-red-500" />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusColor(status: string) {
|
||||
switch(status) {
|
||||
case 'running': return 'bg-green-500/10 text-green-600 border-green-500/20'
|
||||
case 'paused': return 'bg-yellow-500/10 text-yellow-600 border-yellow-500/20'
|
||||
case 'stopped': return 'bg-gray-500/10 text-gray-600 border-gray-500/20'
|
||||
case 'error': return 'bg-red-500/10 text-red-600 border-red-500/20'
|
||||
default: return 'bg-gray-500/10 text-gray-600 border-gray-500/20'
|
||||
}
|
||||
}
|
||||
|
||||
export default function AgentManagementPage() {
|
||||
const [agents, setAgents] = useState<AgentConfig[]>(mockAgents)
|
||||
const [stats, setStats] = useState<AgentStats>(mockStats)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [lastRefresh, setLastRefresh] = useState(new Date())
|
||||
|
||||
const refreshData = async () => {
|
||||
setLoading(true)
|
||||
// In production, fetch from API
|
||||
// const response = await fetch('/api/admin/agents/status')
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
setLastRefresh(new Date())
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Auto-refresh every 30 seconds
|
||||
const interval = setInterval(refreshData, 30000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-3">
|
||||
<div className="p-2 bg-teal-100 rounded-lg">
|
||||
<Bot className="w-6 h-6 text-teal-600" />
|
||||
</div>
|
||||
Agent Management
|
||||
</h1>
|
||||
<p className="text-gray-500 mt-1">
|
||||
Multi-Agent System verwalten, SOUL-Files bearbeiten, Statistiken analysieren
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-500">
|
||||
Letzte Aktualisierung: {lastRefresh.toLocaleTimeString('de-DE')}
|
||||
</span>
|
||||
<button
|
||||
onClick={refreshData}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||
<Link
|
||||
href="/ai/agents/architecture"
|
||||
className="flex items-center gap-3 p-4 bg-white border border-gray-200 rounded-xl hover:border-teal-300 hover:shadow-md transition-all group"
|
||||
>
|
||||
<div className="p-2 bg-purple-100 rounded-lg group-hover:bg-purple-200 transition-colors">
|
||||
<FileText className="w-5 h-5 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">Architektur</div>
|
||||
<div className="text-sm text-gray-500">Dokumentation & Diagramme</div>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/ai/agents/sessions"
|
||||
className="flex items-center gap-3 p-4 bg-white border border-gray-200 rounded-xl hover:border-teal-300 hover:shadow-md transition-all group"
|
||||
>
|
||||
<div className="p-2 bg-blue-100 rounded-lg group-hover:bg-blue-200 transition-colors">
|
||||
<Activity className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">Sessions</div>
|
||||
<div className="text-sm text-gray-500">{stats.activeSessions} aktiv</div>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/ai/agents/statistics"
|
||||
className="flex items-center gap-3 p-4 bg-white border border-gray-200 rounded-xl hover:border-teal-300 hover:shadow-md transition-all group"
|
||||
>
|
||||
<div className="p-2 bg-green-100 rounded-lg group-hover:bg-green-200 transition-colors">
|
||||
<BarChart3 className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">Statistiken</div>
|
||||
<div className="text-sm text-gray-500">Performance & Trends</div>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/ai/test-quality"
|
||||
className="flex items-center gap-3 p-4 bg-white border border-gray-200 rounded-xl hover:border-teal-300 hover:shadow-md transition-all group"
|
||||
>
|
||||
<div className="p-2 bg-amber-100 rounded-lg group-hover:bg-amber-200 transition-colors">
|
||||
<Cpu className="w-5 h-5 text-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">BQAS</div>
|
||||
<div className="text-sm text-gray-500">Qualitaetssicherung</div>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Stats Overview */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-6 gap-4 mb-8">
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-4">
|
||||
<div className="text-sm text-gray-500 mb-1">Gesamt Sessions</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.totalSessions.toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-4">
|
||||
<div className="text-sm text-gray-500 mb-1">Aktive Sessions</div>
|
||||
<div className="text-2xl font-bold text-green-600">{stats.activeSessions}</div>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-4">
|
||||
<div className="text-sm text-gray-500 mb-1">Nachrichten (24h)</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.totalMessages.toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-4">
|
||||
<div className="text-sm text-gray-500 mb-1">Avg. Latenz</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.avgLatency}ms</div>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-4">
|
||||
<div className="text-sm text-gray-500 mb-1">Fehlerrate</div>
|
||||
<div className="text-2xl font-bold text-amber-600">{stats.errorRate}%</div>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-4">
|
||||
<div className="text-sm text-gray-500 mb-1">Memory Usage</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.memoryUsage}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agents Grid */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Agents</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{agents.map((agent) => (
|
||||
<Link
|
||||
key={agent.id}
|
||||
href={`/ai/agents/${agent.id}`}
|
||||
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-teal-300 hover:shadow-lg transition-all group"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="p-2.5 rounded-lg"
|
||||
style={{ backgroundColor: `${agent.color}20` }}
|
||||
>
|
||||
{getIconComponent(agent.icon, `w-5 h-5`)}
|
||||
<style jsx>{`
|
||||
svg { color: ${agent.color}; }
|
||||
`}</style>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-teal-600 transition-colors">
|
||||
{agent.name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">{agent.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium border ${getStatusColor(agent.status)}`}>
|
||||
{getStatusIcon(agent.status)}
|
||||
{agent.status}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-3 gap-3 mb-4">
|
||||
<div className="text-center p-2 bg-gray-50 rounded-lg">
|
||||
<div className="text-lg font-semibold text-gray-900">{agent.activeSessions}</div>
|
||||
<div className="text-xs text-gray-500">Sessions</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-gray-50 rounded-lg">
|
||||
<div className="text-lg font-semibold text-gray-900">{agent.totalProcessed}</div>
|
||||
<div className="text-xs text-gray-500">Verarbeitet</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-gray-50 rounded-lg">
|
||||
<div className="text-lg font-semibold text-gray-900">{agent.avgResponseTime}ms</div>
|
||||
<div className="text-xs text-gray-500">Avg. Zeit</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between pt-3 border-t border-gray-100">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<FileText className="w-4 h-4" />
|
||||
{agent.soulFile}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-sm text-gray-400">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
{agent.lastActivity}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="bg-teal-50 border border-teal-200 rounded-xl p-5">
|
||||
<div className="flex gap-4">
|
||||
<div className="p-2 bg-teal-100 rounded-lg h-fit">
|
||||
<Brain className="w-5 h-5 text-teal-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-teal-900 mb-2">Multi-Agent Architektur</h3>
|
||||
<p className="text-sm text-teal-700 mb-3">
|
||||
Das Breakpilot Multi-Agent-System basiert auf dem Mission Control Konzept. Jeder Agent hat eine
|
||||
definierte Persoenlichkeit (SOUL-File), die sein Verhalten steuert. Die Agents kommunizieren
|
||||
ueber einen Message Bus und nutzen ein gemeinsames Gedaechtnis (Shared Brain).
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<Link
|
||||
href="/ai/agents/architecture"
|
||||
className="text-sm font-medium text-teal-600 hover:text-teal-800"
|
||||
>
|
||||
Architektur ansehen →
|
||||
</Link>
|
||||
<Link
|
||||
href="/ai/agents/architecture#soul-files"
|
||||
className="text-sm font-medium text-teal-600 hover:text-teal-800"
|
||||
>
|
||||
SOUL-Files verstehen →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
444
admin-v2/app/(admin)/ai/agents/sessions/page.tsx
Normal file
444
admin-v2/app/(admin)/ai/agents/sessions/page.tsx
Normal file
@@ -0,0 +1,444 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { ArrowLeft, Activity, Clock, User, Bot, Brain, MessageSquare, AlertTriangle, Settings, CheckCircle, Pause, XCircle, RefreshCw, Filter, Search, ChevronRight, Zap, MoreVertical } from 'lucide-react'
|
||||
|
||||
// Session types
|
||||
interface AgentSession {
|
||||
id: string
|
||||
agentType: string
|
||||
agentId: string
|
||||
userId: string
|
||||
userName: string
|
||||
state: 'active' | 'paused' | 'completed' | 'failed'
|
||||
createdAt: string
|
||||
lastActivity: string
|
||||
checkpointCount: number
|
||||
messagesProcessed: number
|
||||
currentTask: string | null
|
||||
avgResponseTime: number
|
||||
}
|
||||
|
||||
// Mock data
|
||||
const mockSessions: AgentSession[] = [
|
||||
{
|
||||
id: 'session-001',
|
||||
agentType: 'tutor-agent',
|
||||
agentId: 'tutor-1',
|
||||
userId: 'user-123',
|
||||
userName: 'Max Mustermann',
|
||||
state: 'active',
|
||||
createdAt: '2026-02-03T14:30:00Z',
|
||||
lastActivity: '2026-02-03T15:45:23Z',
|
||||
checkpointCount: 5,
|
||||
messagesProcessed: 23,
|
||||
currentTask: 'Erklaere Quadratische Funktionen',
|
||||
avgResponseTime: 245
|
||||
},
|
||||
{
|
||||
id: 'session-002',
|
||||
agentType: 'tutor-agent',
|
||||
agentId: 'tutor-2',
|
||||
userId: 'user-456',
|
||||
userName: 'Anna Schmidt',
|
||||
state: 'active',
|
||||
createdAt: '2026-02-03T15:00:00Z',
|
||||
lastActivity: '2026-02-03T15:44:12Z',
|
||||
checkpointCount: 3,
|
||||
messagesProcessed: 12,
|
||||
currentTask: 'Hilfe bei Gedichtanalyse',
|
||||
avgResponseTime: 312
|
||||
},
|
||||
{
|
||||
id: 'session-003',
|
||||
agentType: 'grader-agent',
|
||||
agentId: 'grader-1',
|
||||
userId: 'user-789',
|
||||
userName: 'Frau Mueller (Lehrerin)',
|
||||
state: 'active',
|
||||
createdAt: '2026-02-03T14:00:00Z',
|
||||
lastActivity: '2026-02-03T15:42:00Z',
|
||||
checkpointCount: 12,
|
||||
messagesProcessed: 45,
|
||||
currentTask: 'Korrektur Klausur 10b - Arbeit 7/24',
|
||||
avgResponseTime: 1205
|
||||
},
|
||||
{
|
||||
id: 'session-004',
|
||||
agentType: 'quality-judge',
|
||||
agentId: 'judge-1',
|
||||
userId: 'system',
|
||||
userName: 'System (BQAS)',
|
||||
state: 'active',
|
||||
createdAt: '2026-02-03T08:00:00Z',
|
||||
lastActivity: '2026-02-03T15:45:01Z',
|
||||
checkpointCount: 156,
|
||||
messagesProcessed: 892,
|
||||
currentTask: 'Quality Check Queue Processing',
|
||||
avgResponseTime: 89
|
||||
},
|
||||
{
|
||||
id: 'session-005',
|
||||
agentType: 'orchestrator',
|
||||
agentId: 'orchestrator-main',
|
||||
userId: 'system',
|
||||
userName: 'System',
|
||||
state: 'active',
|
||||
createdAt: '2026-02-03T00:00:00Z',
|
||||
lastActivity: '2026-02-03T15:45:30Z',
|
||||
checkpointCount: 2341,
|
||||
messagesProcessed: 8934,
|
||||
currentTask: 'Routing incoming requests',
|
||||
avgResponseTime: 12
|
||||
},
|
||||
{
|
||||
id: 'session-006',
|
||||
agentType: 'tutor-agent',
|
||||
agentId: 'tutor-3',
|
||||
userId: 'user-101',
|
||||
userName: 'Tim Berger',
|
||||
state: 'paused',
|
||||
createdAt: '2026-02-03T13:00:00Z',
|
||||
lastActivity: '2026-02-03T14:30:00Z',
|
||||
checkpointCount: 8,
|
||||
messagesProcessed: 34,
|
||||
currentTask: null,
|
||||
avgResponseTime: 278
|
||||
},
|
||||
{
|
||||
id: 'session-007',
|
||||
agentType: 'grader-agent',
|
||||
agentId: 'grader-2',
|
||||
userId: 'user-202',
|
||||
userName: 'Herr Weber (Lehrer)',
|
||||
state: 'completed',
|
||||
createdAt: '2026-02-03T10:00:00Z',
|
||||
lastActivity: '2026-02-03T12:00:00Z',
|
||||
checkpointCount: 24,
|
||||
messagesProcessed: 120,
|
||||
currentTask: null,
|
||||
avgResponseTime: 1102
|
||||
},
|
||||
{
|
||||
id: 'session-008',
|
||||
agentType: 'alert-agent',
|
||||
agentId: 'alert-1',
|
||||
userId: 'system',
|
||||
userName: 'System (Monitoring)',
|
||||
state: 'active',
|
||||
createdAt: '2026-02-03T00:00:00Z',
|
||||
lastActivity: '2026-02-03T15:45:28Z',
|
||||
checkpointCount: 48,
|
||||
messagesProcessed: 256,
|
||||
currentTask: 'Monitoring System Health',
|
||||
avgResponseTime: 45
|
||||
}
|
||||
]
|
||||
|
||||
function getAgentIcon(agentType: string) {
|
||||
switch (agentType) {
|
||||
case 'tutor-agent': return <Brain className="w-4 h-4" />
|
||||
case 'grader-agent': return <Bot className="w-4 h-4" />
|
||||
case 'quality-judge': return <Settings className="w-4 h-4" />
|
||||
case 'alert-agent': return <AlertTriangle className="w-4 h-4" />
|
||||
case 'orchestrator': return <MessageSquare className="w-4 h-4" />
|
||||
default: return <Bot className="w-4 h-4" />
|
||||
}
|
||||
}
|
||||
|
||||
function getAgentColor(agentType: string) {
|
||||
switch (agentType) {
|
||||
case 'tutor-agent': return { bg: 'bg-blue-100', text: 'text-blue-600', border: 'border-blue-200' }
|
||||
case 'grader-agent': return { bg: 'bg-green-100', text: 'text-green-600', border: 'border-green-200' }
|
||||
case 'quality-judge': return { bg: 'bg-amber-100', text: 'text-amber-600', border: 'border-amber-200' }
|
||||
case 'alert-agent': return { bg: 'bg-red-100', text: 'text-red-600', border: 'border-red-200' }
|
||||
case 'orchestrator': return { bg: 'bg-purple-100', text: 'text-purple-600', border: 'border-purple-200' }
|
||||
default: return { bg: 'bg-gray-100', text: 'text-gray-600', border: 'border-gray-200' }
|
||||
}
|
||||
}
|
||||
|
||||
function getStateConfig(state: string) {
|
||||
switch (state) {
|
||||
case 'active':
|
||||
return { icon: <CheckCircle className="w-4 h-4" />, color: 'bg-green-100 text-green-700 border-green-200', label: 'Aktiv' }
|
||||
case 'paused':
|
||||
return { icon: <Pause className="w-4 h-4" />, color: 'bg-yellow-100 text-yellow-700 border-yellow-200', label: 'Pausiert' }
|
||||
case 'completed':
|
||||
return { icon: <CheckCircle className="w-4 h-4" />, color: 'bg-gray-100 text-gray-600 border-gray-200', label: 'Beendet' }
|
||||
case 'failed':
|
||||
return { icon: <XCircle className="w-4 h-4" />, color: 'bg-red-100 text-red-700 border-red-200', label: 'Fehlgeschlagen' }
|
||||
default:
|
||||
return { icon: null, color: 'bg-gray-100 text-gray-600 border-gray-200', label: state }
|
||||
}
|
||||
}
|
||||
|
||||
function formatDuration(isoDate: string): string {
|
||||
const date = new Date(isoDate)
|
||||
const now = new Date()
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
const diffMins = Math.floor(diffMs / 60000)
|
||||
const diffHours = Math.floor(diffMins / 60)
|
||||
const diffDays = Math.floor(diffHours / 24)
|
||||
|
||||
if (diffDays > 0) return `${diffDays}d ${diffHours % 24}h`
|
||||
if (diffHours > 0) return `${diffHours}h ${diffMins % 60}m`
|
||||
return `${diffMins}m`
|
||||
}
|
||||
|
||||
function formatTime(isoDate: string): string {
|
||||
return new Date(isoDate).toLocaleTimeString('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
export default function SessionsPage() {
|
||||
const [sessions, setSessions] = useState<AgentSession[]>(mockSessions)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [filter, setFilter] = useState<string>('all')
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [lastRefresh, setLastRefresh] = useState(new Date())
|
||||
|
||||
const refreshData = async () => {
|
||||
setLoading(true)
|
||||
// In production, fetch from API
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
setLastRefresh(new Date())
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(refreshData, 10000) // Refresh every 10s
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
// Filter sessions
|
||||
const filteredSessions = sessions.filter(session => {
|
||||
if (filter !== 'all' && session.state !== filter) return false
|
||||
if (searchTerm) {
|
||||
const search = searchTerm.toLowerCase()
|
||||
return (
|
||||
session.userName.toLowerCase().includes(search) ||
|
||||
session.agentType.toLowerCase().includes(search) ||
|
||||
session.currentTask?.toLowerCase().includes(search) ||
|
||||
session.id.toLowerCase().includes(search)
|
||||
)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// Stats
|
||||
const stats = {
|
||||
total: sessions.length,
|
||||
active: sessions.filter(s => s.state === 'active').length,
|
||||
paused: sessions.filter(s => s.state === 'paused').length,
|
||||
completed: sessions.filter(s => s.state === 'completed').length,
|
||||
failed: sessions.filter(s => s.state === 'failed').length,
|
||||
totalMessages: sessions.reduce((sum, s) => sum + s.messagesProcessed, 0),
|
||||
avgResponseTime: Math.round(sessions.reduce((sum, s) => sum + s.avgResponseTime, 0) / sessions.length)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<Link
|
||||
href="/ai/agents"
|
||||
className="flex items-center gap-2 text-gray-500 hover:text-gray-700 mb-4"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Zurueck zur Agent-Verwaltung
|
||||
</Link>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-3">
|
||||
<div className="p-2 bg-blue-100 rounded-lg">
|
||||
<Activity className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
Aktive Sessions
|
||||
</h1>
|
||||
<p className="text-gray-500 mt-1">
|
||||
Live-Uebersicht aller Agent-Sessions im System
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-500">
|
||||
Letzte Aktualisierung: {lastRefresh.toLocaleTimeString('de-DE')}
|
||||
</span>
|
||||
<button
|
||||
onClick={refreshData}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-6 gap-4 mb-6">
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-4">
|
||||
<div className="text-sm text-gray-500 mb-1">Gesamt</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.total}</div>
|
||||
</div>
|
||||
<div className="bg-white border border-green-200 rounded-xl p-4">
|
||||
<div className="text-sm text-green-600 mb-1">Aktiv</div>
|
||||
<div className="text-2xl font-bold text-green-600">{stats.active}</div>
|
||||
</div>
|
||||
<div className="bg-white border border-yellow-200 rounded-xl p-4">
|
||||
<div className="text-sm text-yellow-600 mb-1">Pausiert</div>
|
||||
<div className="text-2xl font-bold text-yellow-600">{stats.paused}</div>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-4">
|
||||
<div className="text-sm text-gray-500 mb-1">Beendet</div>
|
||||
<div className="text-2xl font-bold text-gray-600">{stats.completed}</div>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-4">
|
||||
<div className="text-sm text-gray-500 mb-1">Messages (24h)</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.totalMessages.toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-4">
|
||||
<div className="text-sm text-gray-500 mb-1">Avg. Response</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.avgResponseTime}ms</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-col md:flex-row gap-4 mb-6">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Session, Benutzer oder Task suchen..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="w-4 h-4 text-gray-400" />
|
||||
<select
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className="px-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="all">Alle Status</option>
|
||||
<option value="active">Aktiv</option>
|
||||
<option value="paused">Pausiert</option>
|
||||
<option value="completed">Beendet</option>
|
||||
<option value="failed">Fehlgeschlagen</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sessions List */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Agent</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Benutzer</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Aktueller Task</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Dauer</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Messages</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Letzte Aktivitaet</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredSessions.map(session => {
|
||||
const agentColor = getAgentColor(session.agentType)
|
||||
const stateConfig = getStateConfig(session.state)
|
||||
|
||||
return (
|
||||
<tr key={session.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${agentColor.bg}`}>
|
||||
<span className={agentColor.text}>{getAgentIcon(session.agentType)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{session.agentId}</div>
|
||||
<div className="text-xs text-gray-500">{session.agentType}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-sm text-gray-900">{session.userName}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border ${stateConfig.color}`}>
|
||||
{stateConfig.icon}
|
||||
{stateConfig.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
{session.currentTask ? (
|
||||
<div className="flex items-center gap-2 max-w-xs">
|
||||
<Zap className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />
|
||||
<span className="text-sm text-gray-700 truncate">{session.currentTask}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center gap-1.5 text-sm text-gray-600">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
{formatDuration(session.createdAt)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<div className="text-sm">
|
||||
<span className="font-medium text-gray-900">{session.messagesProcessed}</span>
|
||||
<span className="text-gray-500 ml-1">({session.checkpointCount} CP)</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-600">{formatTime(session.lastActivity)}</div>
|
||||
<div className="text-xs text-gray-400">{session.avgResponseTime}ms avg</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-right">
|
||||
<Link
|
||||
href={`/ai/agents/${session.agentType.replace('-agent', '-agent')}`}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Details
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{filteredSessions.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<Activity className="w-12 h-12 text-gray-300 mx-auto mb-3" />
|
||||
<p className="text-gray-500">Keine Sessions gefunden</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Live Activity Indicator */}
|
||||
<div className="mt-6 flex items-center justify-center gap-2 text-sm text-gray-500">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
||||
</span>
|
||||
Live-Daten - Auto-Refresh alle 10 Sekunden
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
491
admin-v2/app/(admin)/ai/agents/statistics/page.tsx
Normal file
491
admin-v2/app/(admin)/ai/agents/statistics/page.tsx
Normal file
@@ -0,0 +1,491 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { ArrowLeft, BarChart3, TrendingUp, TrendingDown, Clock, Activity, Bot, Brain, MessageSquare, AlertTriangle, Settings, RefreshCw, Calendar, Filter, Download } from 'lucide-react'
|
||||
|
||||
// Types
|
||||
interface AgentMetric {
|
||||
agentType: string
|
||||
name: string
|
||||
color: string
|
||||
sessions: number
|
||||
messagesProcessed: number
|
||||
avgResponseTime: number
|
||||
errorRate: number
|
||||
successRate: number
|
||||
trend: 'up' | 'down' | 'stable'
|
||||
trendValue: number
|
||||
}
|
||||
|
||||
interface TimeSeriesData {
|
||||
timestamp: string
|
||||
value: number
|
||||
}
|
||||
|
||||
interface DailyStats {
|
||||
date: string
|
||||
sessions: number
|
||||
messages: number
|
||||
errors: number
|
||||
avgLatency: number
|
||||
}
|
||||
|
||||
// Mock data
|
||||
const mockAgentMetrics: AgentMetric[] = [
|
||||
{
|
||||
agentType: 'tutor-agent',
|
||||
name: 'TutorAgent',
|
||||
color: '#3b82f6',
|
||||
sessions: 156,
|
||||
messagesProcessed: 4521,
|
||||
avgResponseTime: 234,
|
||||
errorRate: 0.3,
|
||||
successRate: 99.7,
|
||||
trend: 'up',
|
||||
trendValue: 12
|
||||
},
|
||||
{
|
||||
agentType: 'grader-agent',
|
||||
name: 'GraderAgent',
|
||||
color: '#10b981',
|
||||
sessions: 45,
|
||||
messagesProcessed: 1205,
|
||||
avgResponseTime: 1102,
|
||||
errorRate: 0.5,
|
||||
successRate: 99.5,
|
||||
trend: 'stable',
|
||||
trendValue: 2
|
||||
},
|
||||
{
|
||||
agentType: 'quality-judge',
|
||||
name: 'QualityJudge',
|
||||
color: '#f59e0b',
|
||||
sessions: 89,
|
||||
messagesProcessed: 8934,
|
||||
avgResponseTime: 89,
|
||||
errorRate: 0.1,
|
||||
successRate: 99.9,
|
||||
trend: 'up',
|
||||
trendValue: 8
|
||||
},
|
||||
{
|
||||
agentType: 'alert-agent',
|
||||
name: 'AlertAgent',
|
||||
color: '#ef4444',
|
||||
sessions: 12,
|
||||
messagesProcessed: 892,
|
||||
avgResponseTime: 45,
|
||||
errorRate: 0.0,
|
||||
successRate: 100,
|
||||
trend: 'stable',
|
||||
trendValue: 0
|
||||
},
|
||||
{
|
||||
agentType: 'orchestrator',
|
||||
name: 'Orchestrator',
|
||||
color: '#8b5cf6',
|
||||
sessions: 234,
|
||||
messagesProcessed: 15420,
|
||||
avgResponseTime: 12,
|
||||
errorRate: 0.2,
|
||||
successRate: 99.8,
|
||||
trend: 'up',
|
||||
trendValue: 15
|
||||
}
|
||||
]
|
||||
|
||||
const mockDailyStats: DailyStats[] = [
|
||||
{ date: '2026-01-28', sessions: 420, messages: 12500, errors: 15, avgLatency: 156 },
|
||||
{ date: '2026-01-29', sessions: 445, messages: 13200, errors: 12, avgLatency: 148 },
|
||||
{ date: '2026-01-30', sessions: 398, messages: 11800, errors: 18, avgLatency: 162 },
|
||||
{ date: '2026-01-31', sessions: 512, messages: 15600, errors: 10, avgLatency: 145 },
|
||||
{ date: '2026-02-01', sessions: 489, messages: 14200, errors: 8, avgLatency: 139 },
|
||||
{ date: '2026-02-02', sessions: 534, messages: 16100, errors: 11, avgLatency: 142 },
|
||||
{ date: '2026-02-03', sessions: 478, messages: 14800, errors: 9, avgLatency: 151 }
|
||||
]
|
||||
|
||||
const mockHourlyLatency: TimeSeriesData[] = Array.from({ length: 24 }, (_, i) => ({
|
||||
timestamp: `${i.toString().padStart(2, '0')}:00`,
|
||||
value: Math.floor(100 + Math.random() * 100)
|
||||
}))
|
||||
|
||||
function getAgentIcon(agentType: string) {
|
||||
switch (agentType) {
|
||||
case 'tutor-agent': return <Brain className="w-4 h-4" />
|
||||
case 'grader-agent': return <Bot className="w-4 h-4" />
|
||||
case 'quality-judge': return <Settings className="w-4 h-4" />
|
||||
case 'alert-agent': return <AlertTriangle className="w-4 h-4" />
|
||||
case 'orchestrator': return <MessageSquare className="w-4 h-4" />
|
||||
default: return <Bot className="w-4 h-4" />
|
||||
}
|
||||
}
|
||||
|
||||
// Simple bar chart component
|
||||
function BarChart({ data, color, maxValue }: { data: number[], color: string, maxValue: number }) {
|
||||
return (
|
||||
<div className="flex items-end gap-1 h-20">
|
||||
{data.map((value, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 rounded-t transition-all hover:opacity-80"
|
||||
style={{
|
||||
height: `${(value / maxValue) * 100}%`,
|
||||
backgroundColor: color,
|
||||
minHeight: '4px'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Simple line chart visualization
|
||||
function SparkLine({ data, color }: { data: number[], color: string }) {
|
||||
const max = Math.max(...data)
|
||||
const min = Math.min(...data)
|
||||
const range = max - min || 1
|
||||
|
||||
const points = data.map((value, i) => {
|
||||
const x = (i / (data.length - 1)) * 100
|
||||
const y = 100 - ((value - min) / range) * 100
|
||||
return `${x},${y}`
|
||||
}).join(' ')
|
||||
|
||||
return (
|
||||
<svg viewBox="0 0 100 100" className="w-full h-12" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
points={points}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default function StatisticsPage() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [timeRange, setTimeRange] = useState<'24h' | '7d' | '30d'>('7d')
|
||||
const [lastRefresh, setLastRefresh] = useState(new Date())
|
||||
|
||||
const refreshData = async () => {
|
||||
setLoading(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
setLastRefresh(new Date())
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
// Calculate totals
|
||||
const totals = {
|
||||
sessions: mockAgentMetrics.reduce((sum, m) => sum + m.sessions, 0),
|
||||
messages: mockAgentMetrics.reduce((sum, m) => sum + m.messagesProcessed, 0),
|
||||
avgLatency: Math.round(mockAgentMetrics.reduce((sum, m) => sum + m.avgResponseTime, 0) / mockAgentMetrics.length),
|
||||
avgErrorRate: (mockAgentMetrics.reduce((sum, m) => sum + m.errorRate, 0) / mockAgentMetrics.length).toFixed(2)
|
||||
}
|
||||
|
||||
// Calculate week stats
|
||||
const weekTotals = {
|
||||
sessions: mockDailyStats.reduce((sum, d) => sum + d.sessions, 0),
|
||||
messages: mockDailyStats.reduce((sum, d) => sum + d.messages, 0),
|
||||
errors: mockDailyStats.reduce((sum, d) => sum + d.errors, 0),
|
||||
avgLatency: Math.round(mockDailyStats.reduce((sum, d) => sum + d.avgLatency, 0) / mockDailyStats.length)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<Link
|
||||
href="/ai/agents"
|
||||
className="flex items-center gap-2 text-gray-500 hover:text-gray-700 mb-4"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Zurueck zur Agent-Verwaltung
|
||||
</Link>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-3">
|
||||
<div className="p-2 bg-green-100 rounded-lg">
|
||||
<BarChart3 className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
Agent Statistiken
|
||||
</h1>
|
||||
<p className="text-gray-500 mt-1">
|
||||
Performance-Metriken und Trends des Multi-Agent-Systems
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<select
|
||||
value={timeRange}
|
||||
onChange={(e) => setTimeRange(e.target.value as '24h' | '7d' | '30d')}
|
||||
className="px-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
>
|
||||
<option value="24h">Letzte 24 Stunden</option>
|
||||
<option value="7d">Letzte 7 Tage</option>
|
||||
<option value="30d">Letzte 30 Tage</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={refreshData}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overview Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-500">Sessions (7d)</span>
|
||||
<Activity className="w-4 h-4 text-gray-400" />
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{weekTotals.sessions.toLocaleString()}</div>
|
||||
<div className="flex items-center gap-1 mt-1 text-sm text-green-600">
|
||||
<TrendingUp className="w-3.5 h-3.5" />
|
||||
<span>+12% vs. Vorwoche</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-500">Messages (7d)</span>
|
||||
<MessageSquare className="w-4 h-4 text-gray-400" />
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{weekTotals.messages.toLocaleString()}</div>
|
||||
<div className="flex items-center gap-1 mt-1 text-sm text-green-600">
|
||||
<TrendingUp className="w-3.5 h-3.5" />
|
||||
<span>+8% vs. Vorwoche</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-500">Avg. Latenz</span>
|
||||
<Clock className="w-4 h-4 text-gray-400" />
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{weekTotals.avgLatency}ms</div>
|
||||
<div className="flex items-center gap-1 mt-1 text-sm text-green-600">
|
||||
<TrendingDown className="w-3.5 h-3.5" />
|
||||
<span>-5% (verbessert)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-500">Fehler (7d)</span>
|
||||
<AlertTriangle className="w-4 h-4 text-gray-400" />
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{weekTotals.errors}</div>
|
||||
<div className="flex items-center gap-1 mt-1 text-sm text-amber-600">
|
||||
<TrendingUp className="w-3.5 h-3.5" />
|
||||
<span>+3 vs. Vorwoche</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
{/* Sessions per Day */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Sessions pro Tag</h3>
|
||||
<div className="space-y-3">
|
||||
<BarChart
|
||||
data={mockDailyStats.map(d => d.sessions)}
|
||||
color="#3b82f6"
|
||||
maxValue={Math.max(...mockDailyStats.map(d => d.sessions)) * 1.1}
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
{mockDailyStats.map(d => (
|
||||
<span key={d.date}>{new Date(d.date).toLocaleDateString('de-DE', { weekday: 'short' })}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages per Day */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Messages pro Tag</h3>
|
||||
<div className="space-y-3">
|
||||
<BarChart
|
||||
data={mockDailyStats.map(d => d.messages)}
|
||||
color="#10b981"
|
||||
maxValue={Math.max(...mockDailyStats.map(d => d.messages)) * 1.1}
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
{mockDailyStats.map(d => (
|
||||
<span key={d.date}>{new Date(d.date).toLocaleDateString('de-DE', { weekday: 'short' })}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Latency Chart */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5 mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-semibold text-gray-900">Latenz (24h)</h3>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<Clock className="w-4 h-4" />
|
||||
Durchschnitt: {totals.avgLatency}ms
|
||||
</div>
|
||||
</div>
|
||||
<SparkLine
|
||||
data={mockHourlyLatency.map(d => d.value)}
|
||||
color="#8b5cf6"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-gray-400 mt-2">
|
||||
<span>00:00</span>
|
||||
<span>06:00</span>
|
||||
<span>12:00</span>
|
||||
<span>18:00</span>
|
||||
<span>24:00</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent Performance Table */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl overflow-hidden mb-8">
|
||||
<div className="px-5 py-4 border-b border-gray-200">
|
||||
<h3 className="font-semibold text-gray-900">Agent Performance</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-5 py-3 text-left text-xs font-medium text-gray-500 uppercase">Agent</th>
|
||||
<th className="px-5 py-3 text-right text-xs font-medium text-gray-500 uppercase">Sessions</th>
|
||||
<th className="px-5 py-3 text-right text-xs font-medium text-gray-500 uppercase">Messages</th>
|
||||
<th className="px-5 py-3 text-right text-xs font-medium text-gray-500 uppercase">Avg. Response</th>
|
||||
<th className="px-5 py-3 text-right text-xs font-medium text-gray-500 uppercase">Success Rate</th>
|
||||
<th className="px-5 py-3 text-right text-xs font-medium text-gray-500 uppercase">Error Rate</th>
|
||||
<th className="px-5 py-3 text-center text-xs font-medium text-gray-500 uppercase">Trend</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{mockAgentMetrics.map(metric => (
|
||||
<tr key={metric.agentType} className="hover:bg-gray-50">
|
||||
<td className="px-5 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="p-2 rounded-lg"
|
||||
style={{ backgroundColor: `${metric.color}20` }}
|
||||
>
|
||||
<span style={{ color: metric.color }}>{getAgentIcon(metric.agentType)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{metric.name}</div>
|
||||
<div className="text-xs text-gray-500">{metric.agentType}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-right">
|
||||
<span className="font-medium text-gray-900">{metric.sessions}</span>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-right">
|
||||
<span className="font-medium text-gray-900">{metric.messagesProcessed.toLocaleString()}</span>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-right">
|
||||
<span className="text-gray-900">{metric.avgResponseTime}ms</span>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-right">
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700">
|
||||
{metric.successRate}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-right">
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
|
||||
metric.errorRate > 0.5 ? 'bg-red-100 text-red-700' :
|
||||
metric.errorRate > 0 ? 'bg-amber-100 text-amber-700' :
|
||||
'bg-green-100 text-green-700'
|
||||
}`}>
|
||||
{metric.errorRate}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-center">
|
||||
{metric.trend === 'up' && (
|
||||
<span className="inline-flex items-center gap-1 text-green-600 text-sm">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
+{metric.trendValue}%
|
||||
</span>
|
||||
)}
|
||||
{metric.trend === 'down' && (
|
||||
<span className="inline-flex items-center gap-1 text-red-600 text-sm">
|
||||
<TrendingDown className="w-4 h-4" />
|
||||
-{metric.trendValue}%
|
||||
</span>
|
||||
)}
|
||||
{metric.trend === 'stable' && (
|
||||
<span className="inline-flex items-center gap-1 text-gray-500 text-sm">
|
||||
<span className="w-4 h-0.5 bg-gray-400 rounded" />
|
||||
{metric.trendValue}%
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Distribution */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Error by Agent */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Fehlerverteilung nach Agent</h3>
|
||||
<div className="space-y-3">
|
||||
{mockAgentMetrics.filter(m => m.errorRate > 0).map(metric => (
|
||||
<div key={metric.agentType} className="flex items-center gap-3">
|
||||
<div className="w-24 text-sm text-gray-600 truncate">{metric.name}</div>
|
||||
<div className="flex-1 h-4 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full"
|
||||
style={{
|
||||
width: `${metric.errorRate * 20}%`,
|
||||
backgroundColor: metric.color
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-12 text-right text-sm text-gray-600">{metric.errorRate}%</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message Distribution */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Message-Verteilung nach Agent</h3>
|
||||
<div className="space-y-3">
|
||||
{mockAgentMetrics.map(metric => {
|
||||
const percentage = (metric.messagesProcessed / totals.messages) * 100
|
||||
return (
|
||||
<div key={metric.agentType} className="flex items-center gap-3">
|
||||
<div className="w-24 text-sm text-gray-600 truncate">{metric.name}</div>
|
||||
<div className="flex-1 h-4 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full"
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
backgroundColor: metric.color
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-12 text-right text-sm text-gray-600">{percentage.toFixed(1)}%</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Export Button */}
|
||||
<div className="mt-8 flex justify-end">
|
||||
<button className="flex items-center gap-2 px-4 py-2 border border-gray-200 rounded-lg hover:bg-gray-50 text-gray-600 hover:text-gray-900 transition-colors">
|
||||
<Download className="w-4 h-4" />
|
||||
Statistiken exportieren (CSV)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
499
admin-v2/app/(admin)/ai/llm-compare/page.tsx
Normal file
499
admin-v2/app/(admin)/ai/llm-compare/page.tsx
Normal file
@@ -0,0 +1,499 @@
|
||||
'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'
|
||||
|
||||
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."
|
||||
audience={['Entwickler', 'Data Scientists', 'QA']}
|
||||
architecture={{
|
||||
services: ['llm-gateway (Python)', 'Ollama', 'OpenAI API', 'Claude API'],
|
||||
databases: ['PostgreSQL (History)', 'Qdrant (RAG)'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'RAG Management', href: '/ai/rag', description: 'Training Data verwalten' },
|
||||
{ name: 'GPU Infrastruktur', href: '/infrastructure/gpu', description: 'GPU-Ressourcen' },
|
||||
{ name: 'OCR-Labeling', href: '/ai/ocr-labeling', description: 'Handschrift-Training' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
|
||||
<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>
|
||||
)
|
||||
}
|
||||
53
admin-v2/app/(admin)/ai/page.tsx
Normal file
53
admin-v2/app/(admin)/ai/page.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
'use client'
|
||||
|
||||
import { getCategoryById } from '@/lib/navigation'
|
||||
import { ModuleCard } from '@/components/common/ModuleCard'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
export default function AIPage() {
|
||||
const category = getCategoryById('ai')
|
||||
|
||||
if (!category) {
|
||||
return <div>Kategorie nicht gefunden</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title={category.name}
|
||||
purpose="Diese Kategorie umfasst alle KI- und Machine-Learning-Module. Hier vergleichen Sie LLM-Provider, verwalten RAG-Pipelines, labeln OCR-Daten und nutzen KI-gestuetzte Korrektur-Tools."
|
||||
audience={['Entwickler', 'Data Scientists', 'Lehrer']}
|
||||
architecture={{
|
||||
services: ['klausur-service (Python)', 'embedding-service (Python)', 'backend (Python)'],
|
||||
databases: ['PostgreSQL', 'Qdrant (Vector)', 'MinIO (Object Storage)'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'GPU Infrastruktur', href: '/infrastructure/gpu', description: 'GPU-Ressourcen fuer Training' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={false}
|
||||
/>
|
||||
|
||||
{/* Modules Grid */}
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Module</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{category.modules.map((module) => (
|
||||
<ModuleCard key={module.id} module={module} category={category} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<div className="mt-8 bg-teal-50 border border-teal-200 rounded-xl p-6">
|
||||
<h3 className="font-semibold text-teal-800 flex items-center gap-2">
|
||||
<span>🧠</span>
|
||||
DSGVO-konforme KI
|
||||
</h3>
|
||||
<p className="text-sm text-teal-700 mt-2">
|
||||
Alle KI-Modelle koennen lokal auf dem Mac Mini mit Ollama ausgefuehrt werden.
|
||||
Keine Daten werden an externe Cloud-Anbieter gesendet, sofern nicht explizit konfiguriert.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
524
admin-v2/app/(admin)/ai/quality/page.tsx
Normal file
524
admin-v2/app/(admin)/ai/quality/page.tsx
Normal file
@@ -0,0 +1,524 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Quality & Audit Page
|
||||
*
|
||||
* Ermoeglicht Auditoren:
|
||||
* - Chunk-Suche und Stichproben
|
||||
* - Traceability: Chunk → Requirement → Control
|
||||
* - Dokumenten-Vollstaendigkeitspruefung
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
const API_PROXY = '/api/legal-corpus'
|
||||
|
||||
// Types
|
||||
interface ChunkDetail {
|
||||
id: string
|
||||
text: string
|
||||
regulation_code: string
|
||||
regulation_name: string
|
||||
article: string | null
|
||||
paragraph: string | null
|
||||
chunk_index: number
|
||||
chunk_position: 'beginning' | 'middle' | 'end'
|
||||
source_url: string
|
||||
score?: number
|
||||
}
|
||||
|
||||
interface Requirement {
|
||||
id: string
|
||||
text: string
|
||||
category: string
|
||||
source_chunk_id: string
|
||||
regulation_code: string
|
||||
}
|
||||
|
||||
interface Control {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
source_requirement_ids: string[]
|
||||
regulation_codes: string[]
|
||||
}
|
||||
|
||||
interface TraceabilityResult {
|
||||
chunk: ChunkDetail
|
||||
requirements: Requirement[]
|
||||
controls: Control[]
|
||||
}
|
||||
|
||||
// Regulations for filtering
|
||||
const REGULATIONS = [
|
||||
{ code: 'GDPR', name: 'DSGVO' },
|
||||
{ code: 'EPRIVACY', name: 'ePrivacy' },
|
||||
{ code: 'TDDDG', name: 'TDDDG' },
|
||||
{ code: 'SCC', name: 'Standardvertragsklauseln' },
|
||||
{ code: 'DPF', name: 'EU-US DPF' },
|
||||
{ code: 'AIACT', name: 'EU AI Act' },
|
||||
{ code: 'CRA', name: 'Cyber Resilience Act' },
|
||||
{ code: 'NIS2', name: 'NIS2' },
|
||||
{ code: 'EUCSA', name: 'EU Cybersecurity Act' },
|
||||
{ code: 'DATAACT', name: 'Data Act' },
|
||||
{ code: 'DGA', name: 'Data Governance Act' },
|
||||
{ code: 'DSA', name: 'Digital Services Act' },
|
||||
{ code: 'EAA', name: 'Accessibility Act' },
|
||||
{ code: 'DSM', name: 'DSM-Urheberrecht' },
|
||||
{ code: 'PLD', name: 'Produkthaftung' },
|
||||
{ code: 'GPSR', name: 'Product Safety' },
|
||||
{ code: 'BSI-TR-03161-1', name: 'BSI-TR Teil 1' },
|
||||
{ code: 'BSI-TR-03161-2', name: 'BSI-TR Teil 2' },
|
||||
{ code: 'BSI-TR-03161-3', name: 'BSI-TR Teil 3' },
|
||||
]
|
||||
|
||||
const TYPE_COLORS: Record<string, string> = {
|
||||
eu_regulation: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
eu_directive: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
|
||||
de_law: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
|
||||
bsi_standard: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
|
||||
}
|
||||
|
||||
export default function QualityPage() {
|
||||
// Search state
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [searchResults, setSearchResults] = useState<ChunkDetail[]>([])
|
||||
const [searching, setSearching] = useState(false)
|
||||
const [selectedRegulation, setSelectedRegulation] = useState<string>('')
|
||||
const [topK, setTopK] = useState(10)
|
||||
|
||||
// Traceability state
|
||||
const [selectedChunk, setSelectedChunk] = useState<ChunkDetail | null>(null)
|
||||
const [traceability, setTraceability] = useState<TraceabilityResult | null>(null)
|
||||
const [loadingTrace, setLoadingTrace] = useState(false)
|
||||
|
||||
// Quick sample queries for auditors
|
||||
const sampleQueries = [
|
||||
{ label: 'Art. 17 DSGVO (Recht auf Loeschung)', query: 'Recht auf Löschung Artikel 17', reg: 'GDPR' },
|
||||
{ label: 'Einwilligung TDDDG', query: 'Einwilligung Endeinrichtung speichern', reg: 'TDDDG' },
|
||||
{ label: 'AI Act Hochrisiko', query: 'Hochrisiko-KI-System Anforderungen', reg: 'AIACT' },
|
||||
{ label: 'NIS2 Sicherheitsmaßnahmen', query: 'Cybersicherheitsrisikomanagement Maßnahmen', reg: 'NIS2' },
|
||||
{ label: 'BSI Authentifizierung', query: 'Authentifizierung Zwei-Faktor mobile', reg: 'BSI-TR-03161-1' },
|
||||
]
|
||||
|
||||
const handleSearch = useCallback(async () => {
|
||||
if (!searchQuery.trim()) return
|
||||
|
||||
setSearching(true)
|
||||
setSearchResults([])
|
||||
setSelectedChunk(null)
|
||||
setTraceability(null)
|
||||
|
||||
try {
|
||||
let url = `${API_PROXY}?action=search&query=${encodeURIComponent(searchQuery)}&top_k=${topK}`
|
||||
if (selectedRegulation) {
|
||||
url += `®ulations=${encodeURIComponent(selectedRegulation)}`
|
||||
}
|
||||
|
||||
const res = await fetch(url)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setSearchResults(data.results || [])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error)
|
||||
} finally {
|
||||
setSearching(false)
|
||||
}
|
||||
}, [searchQuery, selectedRegulation, topK])
|
||||
|
||||
const loadTraceability = useCallback(async (chunk: ChunkDetail) => {
|
||||
setSelectedChunk(chunk)
|
||||
setLoadingTrace(true)
|
||||
|
||||
try {
|
||||
// Try to load traceability (requirements and controls derived from this chunk)
|
||||
const res = await fetch(`${API_PROXY}?action=traceability&chunk_id=${encodeURIComponent(chunk.id || chunk.regulation_code + '_' + chunk.chunk_index)}®ulation=${encodeURIComponent(chunk.regulation_code)}`)
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setTraceability({
|
||||
chunk,
|
||||
requirements: data.requirements || [],
|
||||
controls: data.controls || [],
|
||||
})
|
||||
} else {
|
||||
// If traceability endpoint doesn't exist yet, show placeholder
|
||||
setTraceability({
|
||||
chunk,
|
||||
requirements: [],
|
||||
controls: [],
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load traceability:', error)
|
||||
setTraceability({
|
||||
chunk,
|
||||
requirements: [],
|
||||
controls: [],
|
||||
})
|
||||
} finally {
|
||||
setLoadingTrace(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSampleQuery = (query: string, reg: string) => {
|
||||
setSearchQuery(query)
|
||||
setSelectedRegulation(reg)
|
||||
// Auto-search after setting
|
||||
setTimeout(() => {
|
||||
handleSearch()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const highlightText = (text: string, query: string) => {
|
||||
if (!query) return text
|
||||
const words = query.toLowerCase().split(' ').filter(w => w.length > 2)
|
||||
let result = text
|
||||
words.forEach(word => {
|
||||
const regex = new RegExp(`(${word})`, 'gi')
|
||||
result = result.replace(regex, '<mark class="bg-yellow-200 dark:bg-yellow-800 px-0.5 rounded">$1</mark>')
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Qualitaet & Audit
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Stichproben und Traceability fuer Compliance-Auditoren
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/ai/rag"
|
||||
className="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400"
|
||||
>
|
||||
← Zurueck zu RAG
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<PagePurpose
|
||||
title="Audit-Werkzeuge"
|
||||
purpose="Pruefen Sie die Qualitaet der Compliance-Datenbank. Suchen Sie gezielt nach Paragraphen, Saetzen oder Begriffen und verfolgen Sie, wie Anforderungen und Controls abgeleitet wurden."
|
||||
audience={['Auditoren', 'Compliance-Beauftragte', 'Qualitaetssicherung']}
|
||||
architecture={{
|
||||
services: ['klausur-service', 'embedding-service', 'qdrant'],
|
||||
databases: ['Qdrant Vector DB']
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Quick Sample Queries */}
|
||||
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-4">
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
Schnell-Stichproben
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{sampleQueries.map((sq, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => handleSampleQuery(sq.query, sq.reg)}
|
||||
className="px-3 py-1.5 text-xs bg-gray-100 hover:bg-gray-200 dark:bg-slate-700 dark:hover:bg-slate-600 text-gray-700 dark:text-gray-300 rounded-full transition-colors"
|
||||
>
|
||||
{sq.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Section */}
|
||||
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Chunk-Suche
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Search Input */}
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Suchbegriff / Paragraph / Artikeltext
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder="z.B. 'Recht auf Löschung' oder 'Art. 17 Abs. 1'"
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-slate-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-48">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Regulierung
|
||||
</label>
|
||||
<select
|
||||
value={selectedRegulation}
|
||||
onChange={(e) => setSelectedRegulation(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-slate-700 dark:text-white"
|
||||
>
|
||||
<option value="">Alle</option>
|
||||
{REGULATIONS.map((reg) => (
|
||||
<option key={reg.code} value={reg.code}>
|
||||
{reg.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="w-24">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Anzahl
|
||||
</label>
|
||||
<select
|
||||
value={topK}
|
||||
onChange={(e) => setTopK(parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-slate-700 dark:text-white"
|
||||
>
|
||||
<option value="5">5</option>
|
||||
<option value="10">10</option>
|
||||
<option value="20">20</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={searching || !searchQuery.trim()}
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{searching ? 'Suche laeuft...' : 'Suchen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results Grid */}
|
||||
{searchResults.length > 0 && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Search Results List */}
|
||||
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-4">
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Gefundene Chunks ({searchResults.length})
|
||||
</h3>
|
||||
<div className="space-y-3 max-h-[600px] overflow-y-auto">
|
||||
{searchResults.map((result, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
onClick={() => loadTraceability(result)}
|
||||
className={`p-4 border rounded-lg cursor-pointer transition-all ${
|
||||
selectedChunk?.text === result.text
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 dark:border-slate-700 hover:border-gray-300 dark:hover:border-slate-600'
|
||||
}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium px-2 py-0.5 bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 rounded">
|
||||
{result.regulation_code}
|
||||
</span>
|
||||
{result.article && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Art. {result.article}
|
||||
{result.paragraph && ` Abs. ${result.paragraph}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-400">
|
||||
Score: {(result.score || 0).toFixed(3)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Text Preview */}
|
||||
<p
|
||||
className="text-sm text-gray-700 dark:text-gray-300 line-clamp-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: highlightText(result.text.substring(0, 400) + (result.text.length > 400 ? '...' : ''), searchQuery)
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="mt-2 flex items-center gap-4 text-xs text-gray-400">
|
||||
<span>Chunk #{result.chunk_index || idx}</span>
|
||||
<span>{result.text.length} Zeichen</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Traceability Panel */}
|
||||
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-4">
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Traceability
|
||||
</h3>
|
||||
|
||||
{!selectedChunk ? (
|
||||
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||
<svg className="w-12 h-12 mx-auto mb-4 opacity-50" 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>
|
||||
<p>Waehlen Sie einen Chunk aus der Liste, um die Traceability zu sehen.</p>
|
||||
</div>
|
||||
) : loadingTrace ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full mx-auto mb-4"></div>
|
||||
<p className="text-gray-500 dark:text-gray-400">Lade Traceability...</p>
|
||||
</div>
|
||||
) : traceability ? (
|
||||
<div className="space-y-6">
|
||||
{/* Selected Chunk Detail */}
|
||||
<div className="border-l-4 border-blue-500 pl-4">
|
||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
📄 Ausgewaehlter Chunk
|
||||
</h4>
|
||||
<div className="bg-gray-50 dark:bg-slate-700 rounded p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs font-medium px-2 py-0.5 bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 rounded">
|
||||
{traceability.chunk.regulation_code}
|
||||
</span>
|
||||
{traceability.chunk.article && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Art. {traceability.chunk.article}
|
||||
{traceability.chunk.paragraph && ` Abs. ${traceability.chunk.paragraph}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 whitespace-pre-wrap">
|
||||
{traceability.chunk.text}
|
||||
</p>
|
||||
{traceability.chunk.source_url && (
|
||||
<a
|
||||
href={traceability.chunk.source_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-2 inline-flex items-center gap-1 text-xs text-blue-600 hover:underline"
|
||||
>
|
||||
🔗 Quelle oeffnen
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arrow Down */}
|
||||
<div className="flex justify-center">
|
||||
<svg className="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Requirements */}
|
||||
<div className="border-l-4 border-orange-500 pl-4">
|
||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
📋 Extrahierte Anforderungen ({traceability.requirements.length})
|
||||
</h4>
|
||||
{traceability.requirements.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{traceability.requirements.map((req, idx) => (
|
||||
<div key={idx} className="bg-orange-50 dark:bg-orange-900/20 rounded p-3">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs font-medium text-orange-700 dark:text-orange-400">
|
||||
{req.category || 'Anforderung'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">{req.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 italic">
|
||||
Keine Anforderungen aus diesem Chunk extrahiert.
|
||||
<br />
|
||||
<span className="text-xs">(Requirements-Extraktion ist noch nicht implementiert)</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Arrow Down */}
|
||||
<div className="flex justify-center">
|
||||
<svg className="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="border-l-4 border-green-500 pl-4">
|
||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
✅ Abgeleitete Controls ({traceability.controls.length})
|
||||
</h4>
|
||||
{traceability.controls.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{traceability.controls.map((ctrl, idx) => (
|
||||
<div key={idx} className="bg-green-50 dark:bg-green-900/20 rounded p-3">
|
||||
<div className="font-medium text-sm text-green-700 dark:text-green-400 mb-1">
|
||||
{ctrl.name}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">{ctrl.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 italic">
|
||||
Keine Controls aus diesem Chunk abgeleitet.
|
||||
<br />
|
||||
<span className="text-xs">(Control-Ableitung ist noch nicht implementiert)</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!searching && searchResults.length === 0 && searchQuery && (
|
||||
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-12 text-center">
|
||||
<svg className="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||
Keine Ergebnisse gefunden
|
||||
</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Versuchen Sie einen anderen Suchbegriff oder waehlen Sie eine andere Regulierung.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Initial State */}
|
||||
{!searching && searchResults.length === 0 && !searchQuery && (
|
||||
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-12 text-center">
|
||||
<svg className="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||
Bereit fuer Stichproben
|
||||
</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto">
|
||||
Geben Sie einen Suchbegriff ein, um Chunks zu finden. Sie koennen nach Artikeln,
|
||||
Paragraphen oder spezifischen Textpassagen suchen.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Audit Info */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<h3 className="text-sm font-medium text-blue-800 dark:text-blue-400 mb-2">
|
||||
ℹ️ Hinweise fuer Auditoren
|
||||
</h3>
|
||||
<ul className="text-sm text-blue-700 dark:text-blue-300 space-y-1 list-disc list-inside">
|
||||
<li>Die Suche ist semantisch - aehnliche Begriffe werden gefunden, auch wenn die exakte Formulierung abweicht</li>
|
||||
<li>Jeder Chunk entspricht einem logischen Textabschnitt aus dem Originaldokument</li>
|
||||
<li>Die Traceability zeigt, wie aus dem Originaltext Anforderungen und Controls abgeleitet wurden</li>
|
||||
<li>Klicken Sie auf "Quelle oeffnen", um das Originaldokument zu pruefen</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
2231
admin-v2/app/(admin)/ai/rag/page.tsx
Normal file
2231
admin-v2/app/(admin)/ai/rag/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1522
admin-v2/app/(admin)/ai/test-quality/page.tsx
Normal file
1522
admin-v2/app/(admin)/ai/test-quality/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
52
admin-v2/app/(admin)/ai/test-quality/types.ts
Normal file
52
admin-v2/app/(admin)/ai/test-quality/types.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* TypeScript Types for BQAS (Breakpilot Quality Assurance System)
|
||||
*/
|
||||
|
||||
export interface TestResult {
|
||||
test_id: string
|
||||
test_name: string
|
||||
passed: boolean
|
||||
composite_score: number
|
||||
intent_accuracy: number
|
||||
faithfulness: number
|
||||
relevance: number
|
||||
coherence: number
|
||||
safety: string
|
||||
reasoning: string
|
||||
expected_intent: string
|
||||
detected_intent: string
|
||||
}
|
||||
|
||||
export interface TestRun {
|
||||
id: number
|
||||
timestamp: string
|
||||
git_commit: string
|
||||
golden_score: number
|
||||
synthetic_score: number
|
||||
total_tests: number
|
||||
passed_tests: number
|
||||
failed_tests: number
|
||||
duration_seconds: number
|
||||
}
|
||||
|
||||
export interface BQASMetrics {
|
||||
total_tests: number
|
||||
passed_tests: number
|
||||
failed_tests: number
|
||||
avg_intent_accuracy: number
|
||||
avg_faithfulness: number
|
||||
avg_relevance: number
|
||||
avg_coherence: number
|
||||
safety_pass_rate: number
|
||||
avg_composite_score: number
|
||||
scores_by_intent: Record<string, number>
|
||||
failed_test_ids: string[]
|
||||
}
|
||||
|
||||
export interface TrendData {
|
||||
dates: string[]
|
||||
scores: number[]
|
||||
trend: 'improving' | 'stable' | 'declining' | 'insufficient_data'
|
||||
}
|
||||
|
||||
export type TabType = 'overview' | 'golden' | 'rag' | 'synthetic' | 'history' | 'guide'
|
||||
Reference in New Issue
Block a user