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>
492 lines
19 KiB
TypeScript
492 lines
19 KiB
TypeScript
'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>
|
|
)
|
|
}
|