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