[split-required] Split website + studio-v2 monoliths (Phase 3 continued)

Website (14 monoliths split):
- compliance/page.tsx (1,519 → 9), docs/audit (1,262 → 20)
- quality (1,231 → 16), alerts (1,203 → 10), docs (1,202 → 11)
- i18n.ts (1,173 → 8 language files)
- unity-bridge (1,094 → 12), backlog (1,087 → 6)
- training (1,066 → 8), rag (1,063 → 8)
- Deleted index_original.ts (4,899 LOC dead backup)

Studio-v2 (5 monoliths split):
- meet/page.tsx (1,481 → 9), messages (1,166 → 9)
- AlertsB2BContext.tsx (1,165 → 5 modules)
- alerts-b2b/page.tsx (1,019 → 6), korrektur/archiv (1,001 → 6)

All existing imports preserved. Zero new TypeScript errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-24 17:52:36 +02:00
parent b681ddb131
commit 0b37c5e692
143 changed files with 15822 additions and 15889 deletions

View File

@@ -0,0 +1,38 @@
export function FailedTestsList({ testIds, onViewDetails }: { testIds: string[]; onViewDetails?: (id: string) => void }) {
if (testIds.length === 0) {
return (
<div className="text-center py-8 text-emerald-600">
<svg className="w-12 h-12 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Alle Tests bestanden!
</div>
)
}
return (
<div className="space-y-2 max-h-64 overflow-y-auto">
{testIds.map((testId) => (
<div
key={testId}
className="flex items-center justify-between p-3 bg-red-50 rounded-lg border border-red-100"
>
<div className="flex items-center gap-2">
<svg className="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
<span className="font-mono text-sm text-red-700">{testId}</span>
</div>
{onViewDetails && (
<button
onClick={() => onViewDetails(testId)}
className="text-xs text-red-600 hover:text-red-800"
>
Details
</button>
)}
</div>
))}
</div>
)
}

View File

@@ -0,0 +1,73 @@
import { BQASMetrics } from '../types'
import { IntentScoresChart } from './IntentScoresChart'
import { FailedTestsList } from './FailedTestsList'
export function GoldenTab({
goldenMetrics,
isRunningGolden,
runGoldenTests,
}: {
goldenMetrics: BQASMetrics | null
isRunningGolden: boolean
runGoldenTests: () => void
}) {
return (
<div className="space-y-6">
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-slate-900">Golden Test Suite</h3>
<p className="text-sm text-slate-500">Validierte Referenz-Tests gegen definierte Erwartungen</p>
</div>
<button
onClick={runGoldenTests}
disabled={isRunningGolden}
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:bg-slate-300"
>
{isRunningGolden ? 'Laeuft...' : 'Tests starten'}
</button>
</div>
{goldenMetrics && (
<>
{/* Metrics Overview */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<div className="text-center p-4 bg-slate-50 rounded-lg">
<p className="text-2xl font-bold text-slate-900">{goldenMetrics.total_tests}</p>
<p className="text-xs text-slate-500">Tests</p>
</div>
<div className="text-center p-4 bg-emerald-50 rounded-lg">
<p className="text-2xl font-bold text-emerald-600">{goldenMetrics.passed_tests}</p>
<p className="text-xs text-slate-500">Bestanden</p>
</div>
<div className="text-center p-4 bg-red-50 rounded-lg">
<p className="text-2xl font-bold text-red-600">{goldenMetrics.failed_tests}</p>
<p className="text-xs text-slate-500">Fehlgeschlagen</p>
</div>
<div className="text-center p-4 bg-blue-50 rounded-lg">
<p className="text-2xl font-bold text-blue-600">{goldenMetrics.avg_intent_accuracy.toFixed(0)}%</p>
<p className="text-xs text-slate-500">Intent Accuracy</p>
</div>
<div className="text-center p-4 bg-purple-50 rounded-lg">
<p className="text-2xl font-bold text-purple-600">{goldenMetrics.avg_composite_score.toFixed(2)}</p>
<p className="text-xs text-slate-500">Composite Score</p>
</div>
</div>
{/* Intent Scores & Failed Tests */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<h4 className="font-medium text-slate-900 mb-4">Scores nach Intent</h4>
<IntentScoresChart scores={goldenMetrics.scores_by_intent} />
</div>
<div>
<h4 className="font-medium text-slate-900 mb-4">Fehlgeschlagene Tests</h4>
<FailedTestsList testIds={goldenMetrics.failed_test_ids} />
</div>
</div>
</>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,38 @@
export function IntentScoresChart({ scores }: { scores: Record<string, number> }) {
const entries = Object.entries(scores).sort((a, b) => b[1] - a[1])
if (entries.length === 0) {
return (
<div className="text-center py-8 text-slate-400">
Keine Intent-Scores verfuegbar
</div>
)
}
return (
<div className="space-y-3">
{entries.map(([intent, score]) => (
<div key={intent}>
<div className="flex items-center justify-between text-sm mb-1">
<span className="text-slate-600 truncate max-w-[200px]">{intent.replace(/_/g, ' ')}</span>
<span
className={`font-medium ${
score >= 4 ? 'text-emerald-600' : score >= 3 ? 'text-amber-600' : 'text-red-600'
}`}
>
{score.toFixed(2)}
</span>
</div>
<div className="h-2 bg-slate-100 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all ${
score >= 4 ? 'bg-emerald-500' : score >= 3 ? 'bg-amber-500' : 'bg-red-500'
}`}
style={{ width: `${(score / 5) * 100}%` }}
/>
</div>
</div>
))}
</div>
)
}

View File

@@ -0,0 +1,52 @@
export function MetricCard({
title,
value,
subtitle,
trend,
color = 'blue',
}: {
title: string
value: string | number
subtitle?: string
trend?: 'up' | 'down' | 'stable'
color?: 'blue' | 'green' | 'red' | 'yellow' | 'purple'
}) {
const colorClasses = {
blue: 'bg-blue-50 border-blue-200',
green: 'bg-emerald-50 border-emerald-200',
red: 'bg-red-50 border-red-200',
yellow: 'bg-amber-50 border-amber-200',
purple: 'bg-purple-50 border-purple-200',
}
const trendIcons = {
up: (
<svg className="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
),
down: (
<svg className="w-4 h-4 text-red-500" 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>
),
stable: (
<svg className="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14" />
</svg>
),
}
return (
<div className={`rounded-xl border p-5 ${colorClasses[color]}`}>
<div className="flex items-start justify-between">
<div>
<p className="text-sm font-medium text-slate-600">{title}</p>
<p className="mt-1 text-2xl font-bold text-slate-900">{value}</p>
{subtitle && <p className="mt-1 text-xs text-slate-500">{subtitle}</p>}
</div>
{trend && <div className="mt-1">{trendIcons[trend]}</div>}
</div>
</div>
)
}

View File

@@ -0,0 +1,94 @@
import { BQASMetrics, TrendData, TestRun } from '../types'
import { MetricCard } from './MetricCard'
import { TrendChart } from './TrendChart'
import { TestSuiteCard } from './TestSuiteCard'
export function OverviewTab({
goldenMetrics,
ragMetrics,
syntheticMetrics,
trendData,
testRuns,
isRunningGolden,
isRunningRag,
isRunningSynthetic,
runGoldenTests,
runRagTests,
runSyntheticTests,
}: {
goldenMetrics: BQASMetrics | null
ragMetrics: BQASMetrics | null
syntheticMetrics: BQASMetrics | null
trendData: TrendData | null
testRuns: TestRun[]
isRunningGolden: boolean
isRunningRag: boolean
isRunningSynthetic: boolean
runGoldenTests: () => void
runRagTests: () => void
runSyntheticTests: () => void
}) {
return (
<div className="space-y-6">
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCard
title="Golden Score"
value={goldenMetrics?.avg_composite_score.toFixed(2) || '-'}
subtitle="Durchschnitt aller Golden Tests"
trend={trendData?.trend === 'improving' ? 'up' : trendData?.trend === 'declining' ? 'down' : 'stable'}
color="blue"
/>
<MetricCard
title="Pass Rate"
value={goldenMetrics ? `${((goldenMetrics.passed_tests / goldenMetrics.total_tests) * 100).toFixed(0)}%` : '-'}
subtitle={goldenMetrics ? `${goldenMetrics.passed_tests}/${goldenMetrics.total_tests} bestanden` : undefined}
color="green"
/>
<MetricCard
title="RAG Qualitaet"
value={ragMetrics?.avg_composite_score.toFixed(2) || '-'}
subtitle="RAG Retrieval Score"
color="purple"
/>
<MetricCard
title="Test Runs"
value={testRuns.length}
subtitle="Letzte 30 Tage"
color="yellow"
/>
</div>
{/* Trend Chart */}
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Score-Trend (30 Tage)</h3>
<TrendChart data={trendData || { dates: [], scores: [], trend: 'insufficient_data' }} />
</div>
{/* Test Suites Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<TestSuiteCard
title="Golden Suite"
description="97 validierte Referenz-Tests fuer Intent-Erkennung"
metrics={goldenMetrics || undefined}
onRun={runGoldenTests}
isRunning={isRunningGolden}
/>
<TestSuiteCard
title="RAG/Korrektur Tests"
description="EH-Retrieval, Operatoren-Alignment, Citation Tests"
metrics={ragMetrics || undefined}
onRun={runRagTests}
isRunning={isRunningRag}
/>
<TestSuiteCard
title="Synthetic Tests"
description="LLM-generierte Variationen fuer Robustheit"
metrics={syntheticMetrics || undefined}
onRun={runSyntheticTests}
isRunning={isRunningSynthetic}
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,97 @@
import { BQASMetrics } from '../types'
import { IntentScoresChart } from './IntentScoresChart'
import { FailedTestsList } from './FailedTestsList'
export function RagTab({
ragMetrics,
isRunningRag,
runRagTests,
}: {
ragMetrics: BQASMetrics | null
isRunningRag: boolean
runRagTests: () => void
}) {
return (
<div className="space-y-6">
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-slate-900">RAG/Korrektur Test Suite</h3>
<p className="text-sm text-slate-500">Erwartungshorizont-Retrieval, Operatoren-Alignment, Citations</p>
</div>
<button
onClick={runRagTests}
disabled={isRunningRag}
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:bg-slate-300"
>
{isRunningRag ? 'Laeuft...' : 'Tests starten'}
</button>
</div>
{ragMetrics ? (
<>
{/* RAG Metrics */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="text-center p-4 bg-slate-50 rounded-lg">
<p className="text-2xl font-bold text-slate-900">{ragMetrics.total_tests}</p>
<p className="text-xs text-slate-500">Tests</p>
</div>
<div className="text-center p-4 bg-purple-50 rounded-lg">
<p className="text-2xl font-bold text-purple-600">{ragMetrics.avg_faithfulness.toFixed(2)}</p>
<p className="text-xs text-slate-500">Faithfulness</p>
</div>
<div className="text-center p-4 bg-blue-50 rounded-lg">
<p className="text-2xl font-bold text-blue-600">{ragMetrics.avg_relevance.toFixed(2)}</p>
<p className="text-xs text-slate-500">Relevance</p>
</div>
<div className="text-center p-4 bg-emerald-50 rounded-lg">
<p className="text-2xl font-bold text-emerald-600">{(ragMetrics.safety_pass_rate * 100).toFixed(0)}%</p>
<p className="text-xs text-slate-500">Safety Pass</p>
</div>
</div>
{/* RAG Categories */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<h4 className="font-medium text-slate-900 mb-4">RAG Kategorien</h4>
<IntentScoresChart scores={ragMetrics.scores_by_intent} />
</div>
<div>
<h4 className="font-medium text-slate-900 mb-4">Fehlgeschlagene Tests</h4>
<FailedTestsList testIds={ragMetrics.failed_test_ids} />
</div>
</div>
</>
) : (
<div className="text-center py-12 text-slate-400">
<svg className="w-16 h-16 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p>Noch keine RAG-Test-Ergebnisse</p>
<p className="text-sm mt-2">Klicke &quot;Tests starten&quot; um die RAG-Suite auszufuehren</p>
</div>
)}
</div>
{/* RAG Test Categories Explanation */}
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Test-Kategorien</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[
{ name: 'EH Retrieval', desc: 'Korrektes Abrufen von Erwartungshorizont-Passagen', color: 'blue' },
{ name: 'Operator Alignment', desc: 'Passende Operatoren fuer Abitur-Aufgaben', color: 'purple' },
{ name: 'Hallucination Control', desc: 'Keine erfundenen Fakten oder Inhalte', color: 'red' },
{ name: 'Citation Enforcement', desc: 'Quellenangaben bei EH-Bezuegen', color: 'green' },
{ name: 'Privacy Compliance', desc: 'Keine PII-Leaks, DSGVO-Konformitaet', color: 'amber' },
{ name: 'Namespace Isolation', desc: 'Strikte Trennung zwischen Lehrern', color: 'slate' },
].map((cat) => (
<div key={cat.name} className={`p-4 rounded-lg border bg-${cat.color}-50 border-${cat.color}-200`}>
<h4 className="font-medium text-slate-900">{cat.name}</h4>
<p className="text-sm text-slate-600 mt-1">{cat.desc}</p>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,40 @@
export function SchedulerStatusCard({
title,
status,
description,
icon,
}: {
title: string
status: 'active' | 'inactive' | 'warning' | 'unknown'
description: string
icon: React.ReactNode
}) {
const statusColors = {
active: 'bg-emerald-100 border-emerald-200 text-emerald-700',
inactive: 'bg-slate-100 border-slate-200 text-slate-700',
warning: 'bg-amber-100 border-amber-200 text-amber-700',
unknown: 'bg-slate-100 border-slate-200 text-slate-500',
}
const statusBadges = {
active: 'bg-emerald-500',
inactive: 'bg-slate-400',
warning: 'bg-amber-500',
unknown: 'bg-slate-300',
}
return (
<div className={`rounded-xl border p-5 ${statusColors[status]}`}>
<div className="flex items-start gap-4">
<div className="flex-shrink-0">{icon}</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<h4 className="font-semibold">{title}</h4>
<span className={`w-2 h-2 rounded-full ${statusBadges[status]}`} />
</div>
<p className="text-sm mt-1 opacity-80">{description}</p>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,266 @@
import { SchedulerStatusCard } from './SchedulerStatusCard'
import { SpinnerIcon } from './SpinnerIcon'
export function SchedulerTab({
isRunningGolden,
isRunningRag,
isRunningSynthetic,
runGoldenTests,
runRagTests,
runSyntheticTests,
}: {
isRunningGolden: boolean
isRunningRag: boolean
isRunningSynthetic: boolean
runGoldenTests: () => void
runRagTests: () => void
runSyntheticTests: () => void
}) {
return (
<div className="space-y-6">
{/* Status Overview */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<SchedulerStatusCard
title="launchd Job"
status="active"
description="Taeglich um 07:00 Uhr automatisch"
icon={
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
}
/>
<SchedulerStatusCard
title="Git Hook"
status="active"
description="Quick Tests bei voice-service Aenderungen"
icon={
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
}
/>
<SchedulerStatusCard
title="Benachrichtigungen"
status="active"
description="Desktop-Alerts bei Fehlern aktiviert"
icon={
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
}
/>
</div>
{/* Quick Actions */}
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Quick Actions</h3>
<div className="flex flex-wrap gap-3">
<QuickActionButton
label="Golden Suite starten"
isRunning={isRunningGolden}
onClick={runGoldenTests}
colorClass="bg-blue-600 hover:bg-blue-700"
/>
<QuickActionButton
label="RAG Tests starten"
isRunning={isRunningRag}
onClick={runRagTests}
colorClass="bg-purple-600 hover:bg-purple-700"
/>
<QuickActionButton
label="Synthetic Tests"
isRunning={isRunningSynthetic}
onClick={runSyntheticTests}
colorClass="bg-emerald-600 hover:bg-emerald-700"
/>
</div>
</div>
{/* GitHub Actions vs Local - Comparison */}
<ComparisonTable />
{/* Configuration Details */}
<ConfigurationSection />
{/* Detailed Explanation */}
<ExplanationSection />
</div>
)
}
function QuickActionButton({
label,
isRunning,
onClick,
colorClass,
}: {
label: string
isRunning: boolean
onClick: () => void
colorClass: string
}) {
return (
<button
onClick={onClick}
disabled={isRunning}
className={`px-4 py-2 ${colorClass} text-white rounded-lg disabled:bg-slate-300 flex items-center gap-2`}
>
{isRunning ? (
<SpinnerIcon />
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)}
{label}
</button>
)
}
function ComparisonTable() {
const rows = [
{ feature: 'Taegliche Tests (07:00)', gh: 'schedule: cron', local: 'macOS launchd', localColor: 'emerald' },
{ feature: 'Push-basierte Tests', gh: 'on: push', local: 'Git post-commit Hook', localColor: 'emerald' },
{ feature: 'PR-basierte Tests', gh: 'on: pull_request', ghColor: 'emerald', local: 'Nicht moeglich', localColor: 'amber' },
{ feature: 'Regression-Check', gh: 'API-Call', local: 'Identischer API-Call', localColor: 'emerald' },
{ feature: 'Benachrichtigungen', gh: 'GitHub Issues', local: 'Desktop/Slack/Email', localColor: 'emerald' },
{ feature: 'DSGVO-Konformitaet', gh: 'Daten bei GitHub (US)', ghColor: 'amber', local: '100% lokal', localColor: 'emerald' },
{ feature: 'Offline-Faehig', gh: 'Nein', ghColor: 'red', local: 'Ja', localColor: 'emerald' },
]
return (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">GitHub Actions Alternative</h3>
<p className="text-slate-600 mb-4">
Der lokale BQAS Scheduler ersetzt GitHub Actions und bietet DSGVO-konforme, vollstaendig lokale Test-Ausfuehrung.
</p>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-200 bg-slate-50">
<th className="text-left py-3 px-4 font-medium text-slate-700">Feature</th>
<th className="text-center py-3 px-4 font-medium text-slate-700">GitHub Actions</th>
<th className="text-center py-3 px-4 font-medium text-slate-700">Lokaler Scheduler</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.feature} className="border-b border-slate-100">
<td className="py-3 px-4 text-slate-600">{row.feature}</td>
<td className="py-3 px-4 text-center">
{row.ghColor ? (
<span className={`px-2 py-1 bg-${row.ghColor}-100 text-${row.ghColor}-700 rounded text-xs font-medium`}>
{row.gh}
</span>
) : (
<span className="text-slate-600">{row.gh}</span>
)}
</td>
<td className="py-3 px-4 text-center">
<span className={`px-2 py-1 bg-${row.localColor}-100 text-${row.localColor}-700 rounded text-xs font-medium`}>
{row.local}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
function ConfigurationSection() {
return (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Konfiguration</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* launchd Configuration */}
<div>
<h4 className="font-medium text-slate-800 mb-3">launchd Job</h4>
<div className="bg-slate-900 rounded-lg p-4 font-mono text-sm text-slate-100 overflow-x-auto">
<pre>{`# ~/Library/LaunchAgents/com.breakpilot.bqas.plist
Label: com.breakpilot.bqas
Schedule: 07:00 taeglich
Script: /voice-service/scripts/run_bqas.sh
Logs: /var/log/bqas/`}</pre>
</div>
</div>
{/* Environment Variables */}
<div>
<h4 className="font-medium text-slate-800 mb-3">Umgebungsvariablen</h4>
<div className="space-y-2 text-sm">
{[
{ key: 'BQAS_SERVICE_URL', value: 'http://localhost:8091', color: 'text-slate-900' },
{ key: 'BQAS_REGRESSION_THRESHOLD', value: '0.1', color: 'text-slate-900' },
{ key: 'BQAS_NOTIFY_DESKTOP', value: 'true', color: 'text-emerald-600 font-medium' },
{ key: 'BQAS_NOTIFY_SLACK', value: 'false', color: 'text-slate-400' },
].map((env) => (
<div key={env.key} className="flex justify-between p-2 bg-slate-50 rounded">
<span className="font-mono text-slate-600">{env.key}</span>
<span className={env.color}>{env.value}</span>
</div>
))}
</div>
</div>
</div>
</div>
)
}
function ExplanationSection() {
return (
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4 flex items-center gap-2">
<svg className="w-5 h-5 text-blue-600" 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>
Detaillierte Erklaerung
</h3>
<div className="prose prose-sm max-w-none text-slate-700">
<h4 className="text-base font-semibold mt-4 mb-2">Warum ein lokaler Scheduler?</h4>
<p className="mb-4">
Der lokale BQAS Scheduler wurde entwickelt, um die gleiche Funktionalitaet wie GitHub Actions zu bieten,
aber mit dem entscheidenden Vorteil, dass <strong>alle Daten zu 100% auf dem lokalen Mac Mini verbleiben</strong>.
Dies ist besonders wichtig fuer DSGVO-Konformitaet, da keine Schuelerdaten oder Testergebnisse an externe Server uebertragen werden.
</p>
<h4 className="text-base font-semibold mt-4 mb-2">Komponenten</h4>
<ul className="list-disc list-inside space-y-2 mb-4">
<li>
<strong>run_bqas.sh</strong> - Hauptscript das pytest ausfuehrt, Regression-Checks macht und Benachrichtigungen versendet
</li>
<li>
<strong>launchd Job</strong> - macOS-nativer Scheduler der das Script taeglich um 07:00 Uhr startet
</li>
<li>
<strong>Git Hook</strong> - post-commit Hook der bei Aenderungen im voice-service automatisch Quick-Tests startet
</li>
<li>
<strong>Notifier</strong> - Python-Modul das Desktop-, Slack- und E-Mail-Benachrichtigungen versendet
</li>
</ul>
<h4 className="text-base font-semibold mt-4 mb-2">Installation</h4>
<div className="bg-slate-900 rounded-lg p-3 font-mono text-sm text-slate-100 mb-4">
<code>./voice-service/scripts/install_bqas_scheduler.sh install</code>
</div>
<h4 className="text-base font-semibold mt-4 mb-2">Vorteile gegenueber GitHub Actions</h4>
<ul className="list-disc list-inside space-y-1">
<li>100% DSGVO-konform - alle Daten bleiben lokal</li>
<li>Keine Internet-Abhaengigkeit - funktioniert auch offline</li>
<li>Keine GitHub-Kosten fuer private Repositories</li>
<li>Schnellere Ausfuehrung ohne Cloud-Overhead</li>
<li>Volle Kontrolle ueber Scheduling und Benachrichtigungen</li>
</ul>
</div>
</div>
)
}

View File

@@ -0,0 +1,12 @@
export function SpinnerIcon() {
return (
<svg className="animate-spin h-4 w-4" 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>
)
}

View File

@@ -0,0 +1,75 @@
import { BQASMetrics } from '../types'
import { IntentScoresChart } from './IntentScoresChart'
import { FailedTestsList } from './FailedTestsList'
export function SyntheticTab({
syntheticMetrics,
isRunningSynthetic,
runSyntheticTests,
}: {
syntheticMetrics: BQASMetrics | null
isRunningSynthetic: boolean
runSyntheticTests: () => void
}) {
return (
<div className="space-y-6">
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-slate-900">Synthetic Test Suite</h3>
<p className="text-sm text-slate-500">LLM-generierte Variationen fuer Robustheit-Tests</p>
</div>
<button
onClick={runSyntheticTests}
disabled={isRunningSynthetic}
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:bg-slate-300"
>
{isRunningSynthetic ? 'Laeuft...' : 'Tests starten'}
</button>
</div>
{syntheticMetrics ? (
<>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="text-center p-4 bg-slate-50 rounded-lg">
<p className="text-2xl font-bold text-slate-900">{syntheticMetrics.total_tests}</p>
<p className="text-xs text-slate-500">Generierte Tests</p>
</div>
<div className="text-center p-4 bg-emerald-50 rounded-lg">
<p className="text-2xl font-bold text-emerald-600">{syntheticMetrics.passed_tests}</p>
<p className="text-xs text-slate-500">Bestanden</p>
</div>
<div className="text-center p-4 bg-blue-50 rounded-lg">
<p className="text-2xl font-bold text-blue-600">{syntheticMetrics.avg_composite_score.toFixed(2)}</p>
<p className="text-xs text-slate-500">Avg Score</p>
</div>
<div className="text-center p-4 bg-purple-50 rounded-lg">
<p className="text-2xl font-bold text-purple-600">{syntheticMetrics.avg_coherence.toFixed(2)}</p>
<p className="text-xs text-slate-500">Coherence</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<h4 className="font-medium text-slate-900 mb-4">Intent-Variationen</h4>
<IntentScoresChart scores={syntheticMetrics.scores_by_intent} />
</div>
<div>
<h4 className="font-medium text-slate-900 mb-4">Fehlgeschlagene Tests</h4>
<FailedTestsList testIds={syntheticMetrics.failed_test_ids} />
</div>
</div>
</>
) : (
<div className="text-center py-12 text-slate-400">
<svg className="w-16 h-16 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>
<p>Noch keine synthetischen Tests ausgefuehrt</p>
<p className="text-sm mt-2">Klicke &quot;Tests starten&quot; um Variationen zu generieren</p>
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,60 @@
import { TestRun } from '../types'
export function TestRunsTable({ runs }: { runs: TestRun[] }) {
if (runs.length === 0) {
return (
<div className="text-center py-8 text-slate-400">
Keine Test-Laeufe vorhanden
</div>
)
}
return (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-200">
<th className="text-left py-3 px-4 font-medium text-slate-600">ID</th>
<th className="text-left py-3 px-4 font-medium text-slate-600">Zeitpunkt</th>
<th className="text-left py-3 px-4 font-medium text-slate-600">Commit</th>
<th className="text-right py-3 px-4 font-medium text-slate-600">Golden Score</th>
<th className="text-right py-3 px-4 font-medium text-slate-600">Tests</th>
<th className="text-right py-3 px-4 font-medium text-slate-600">Bestanden</th>
<th className="text-right py-3 px-4 font-medium text-slate-600">Dauer</th>
</tr>
</thead>
<tbody>
{runs.map((run) => (
<tr key={run.id} className="border-b border-slate-100 hover:bg-slate-50">
<td className="py-3 px-4 font-mono text-slate-900">#{run.id}</td>
<td className="py-3 px-4 text-slate-600">
{new Date(run.timestamp).toLocaleString('de-DE')}
</td>
<td className="py-3 px-4 font-mono text-xs text-slate-500">
{run.git_commit?.slice(0, 7) || '-'}
</td>
<td className="py-3 px-4 text-right">
<span
className={`font-medium ${
run.golden_score >= 4 ? 'text-emerald-600' : run.golden_score >= 3 ? 'text-amber-600' : 'text-red-600'
}`}
>
{run.golden_score.toFixed(2)}
</span>
</td>
<td className="py-3 px-4 text-right text-slate-600">{run.total_tests}</td>
<td className="py-3 px-4 text-right">
<span className="text-emerald-600">{run.passed_tests}</span>
<span className="text-slate-400"> / </span>
<span className="text-red-600">{run.failed_tests}</span>
</td>
<td className="py-3 px-4 text-right text-slate-500">
{run.duration_seconds.toFixed(1)}s
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}

View File

@@ -0,0 +1,97 @@
import { BQASMetrics } from '../types'
export function TestSuiteCard({
title,
description,
metrics,
onRun,
isRunning,
lastRun,
}: {
title: string
description: string
metrics?: BQASMetrics
onRun: () => void
isRunning: boolean
lastRun?: string
}) {
const passRate = metrics ? (metrics.passed_tests / metrics.total_tests) * 100 : 0
return (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex items-start justify-between">
<div>
<h3 className="text-lg font-semibold text-slate-900">{title}</h3>
<p className="mt-1 text-sm text-slate-500">{description}</p>
</div>
<button
onClick={onRun}
disabled={isRunning}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
isRunning
? 'bg-slate-100 text-slate-400 cursor-not-allowed'
: 'bg-primary-600 text-white hover:bg-primary-700'
}`}
>
{isRunning ? (
<span className="flex items-center gap-2">
<svg className="animate-spin h-4 w-4" 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 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Laeuft...
</span>
) : (
'Tests starten'
)}
</button>
</div>
{metrics && (
<div className="mt-6">
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600">Pass Rate</span>
<span className="font-medium text-slate-900">{passRate.toFixed(1)}%</span>
</div>
<div className="mt-2 h-2 bg-slate-100 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all ${
passRate >= 80 ? 'bg-emerald-500' : passRate >= 60 ? 'bg-amber-500' : 'bg-red-500'
}`}
style={{ width: `${passRate}%` }}
/>
</div>
<div className="mt-4 grid grid-cols-3 gap-4">
<div className="text-center">
<p className="text-2xl font-bold text-slate-900">{metrics.total_tests}</p>
<p className="text-xs text-slate-500">Tests</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-emerald-600">{metrics.passed_tests}</p>
<p className="text-xs text-slate-500">Bestanden</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-red-600">{metrics.failed_tests}</p>
<p className="text-xs text-slate-500">Fehlgeschlagen</p>
</div>
</div>
<div className="mt-4 pt-4 border-t border-slate-100">
<p className="text-xs text-slate-500">
Durchschnittlicher Score: <span className="font-medium">{metrics.avg_composite_score.toFixed(2)}</span>
</p>
</div>
</div>
)}
{lastRun && (
<p className="mt-4 text-xs text-slate-400">Letzter Lauf: {new Date(lastRun).toLocaleString('de-DE')}</p>
)}
</div>
)
}

View File

@@ -0,0 +1,79 @@
import { TrendData } from '../types'
export function TrendChart({ data }: { data: TrendData }) {
if (!data || data.dates.length === 0) {
return (
<div className="h-48 flex items-center justify-center text-slate-400">
Keine Trend-Daten verfuegbar
</div>
)
}
const maxScore = Math.max(...data.scores, 5)
const minScore = Math.min(...data.scores, 0)
const range = maxScore - minScore || 1
return (
<div className="h-48 relative">
{/* Y-Axis Labels */}
<div className="absolute left-0 top-0 bottom-4 w-8 flex flex-col justify-between text-xs text-slate-400">
<span>{maxScore.toFixed(1)}</span>
<span>{((maxScore + minScore) / 2).toFixed(1)}</span>
<span>{minScore.toFixed(1)}</span>
</div>
{/* Chart Area */}
<div className="ml-10 h-full pr-4">
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
{/* Grid Lines */}
<line x1="0" y1="0" x2="100" y2="0" stroke="#e2e8f0" strokeWidth="0.5" />
<line x1="0" y1="50" x2="100" y2="50" stroke="#e2e8f0" strokeWidth="0.5" />
<line x1="0" y1="100" x2="100" y2="100" stroke="#e2e8f0" strokeWidth="0.5" />
{/* Line Chart */}
<polyline
fill="none"
stroke="#0ea5e9"
strokeWidth="2"
points={data.scores
.map((score, i) => {
const x = (i / (data.scores.length - 1 || 1)) * 100
const y = 100 - ((score - minScore) / range) * 100
return `${x},${y}`
})
.join(' ')}
/>
{/* Data Points */}
{data.scores.map((score, i) => {
const x = (i / (data.scores.length - 1 || 1)) * 100
const y = 100 - ((score - minScore) / range) * 100
return <circle key={i} cx={x} cy={y} r="2" fill="#0ea5e9" />
})}
</svg>
</div>
{/* X-Axis Labels */}
<div className="ml-10 flex justify-between text-xs text-slate-400 mt-1">
{data.dates.slice(0, 5).map((date, i) => (
<span key={i}>{new Date(date).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' })}</span>
))}
</div>
{/* Trend Indicator */}
<div className="absolute top-2 right-2">
<span
className={`px-2 py-1 rounded text-xs font-medium ${
data.trend === 'improving'
? 'bg-emerald-100 text-emerald-700'
: data.trend === 'declining'
? 'bg-red-100 text-red-700'
: 'bg-slate-100 text-slate-700'
}`}
>
{data.trend === 'improving' ? 'Verbessernd' : data.trend === 'declining' ? 'Verschlechternd' : 'Stabil'}
</span>
</div>
</div>
)
}