website (17 pages + 3 components): - multiplayer/wizard, middleware/wizard+test-wizard, communication - builds/wizard, staff-search, voice, sbom/wizard - foerderantrag, mail/tasks, tools/communication, sbom - compliance/evidence, uni-crawler, brandbook (already done) - CollectionsTab, IngestionTab, RiskHeatmap backend-lehrer (5 files): - letters_api (641 → 2), certificates_api (636 → 2) - alerts_agent/db/models (636 → 3) - llm_gateway/communication_service (614 → 2) - game/database already done in prior batch klausur-service (2 files): - hybrid_vocab_extractor (664 → 2) - klausur-service/frontend: api.ts (620 → 3), EHUploadWizard (591 → 2) voice-service (3 files): - bqas/rag_judge (618 → 3), runner (529 → 2) - enhanced_task_orchestrator (519 → 2) studio-v2 (6 files): - korrektur/[klausurId] (578 → 4), fairness (569 → 2) - AlertsWizard (552 → 2), OnboardingWizard (513 → 2) - korrektur/api.ts (506 → 3), geo-lernwelt (501 → 2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
221 lines
11 KiB
TypeScript
221 lines
11 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { TASK_STATES, INTENT_GROUPS, DSGVO_CATEGORIES, API_ENDPOINTS } from './constants'
|
|
|
|
export function TabDemo() {
|
|
const [demoLoaded, setDemoLoaded] = useState(false)
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-semibold text-slate-900">Live Voice Demo</h3>
|
|
<a href="http://localhost:3001/voice-test" target="_blank" rel="noopener noreferrer" className="text-sm text-teal-600 hover:text-teal-700 flex items-center gap-1">
|
|
In neuem Tab oeffnen
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
<div className="bg-slate-100 rounded-lg p-4 text-sm text-slate-600 mb-4">
|
|
<p><strong>Hinweis:</strong> Die Demo erfordert, dass der Voice Service (Port 8091) und das Studio-v2 Frontend (Port 3001) laufen.</p>
|
|
<code className="block mt-2 bg-slate-200 p-2 rounded">docker compose up -d voice-service && cd studio-v2 && npm run dev</code>
|
|
</div>
|
|
<div className="relative bg-slate-900 rounded-lg overflow-hidden" style={{ height: '600px' }}>
|
|
{!demoLoaded && (
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
<button onClick={() => setDemoLoaded(true)} className="px-6 py-3 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors flex items-center gap-2">
|
|
<svg className="w-6 h-6" 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>
|
|
Voice Demo laden
|
|
</button>
|
|
</div>
|
|
)}
|
|
{demoLoaded && (
|
|
<iframe src="http://localhost:3001/voice-test?embed=true" className="w-full h-full border-0" title="Voice Demo" allow="microphone" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function TabTasks() {
|
|
return (
|
|
<div className="space-y-6">
|
|
<h3 className="text-lg font-semibold text-slate-900">Task State Machine (TaskOrchestrator)</h3>
|
|
<div className="bg-slate-50 rounded-lg p-6 font-mono text-sm overflow-x-auto">
|
|
<pre className="text-slate-700">{`
|
|
DRAFT → QUEUED → RUNNING → READY
|
|
│
|
|
┌───────────┴───────────┐
|
|
│ │
|
|
APPROVED REJECTED
|
|
│ │
|
|
COMPLETED DRAFT (revision)
|
|
|
|
Any State → EXPIRED (TTL)
|
|
Any State → PAUSED (User Interrupt)
|
|
`}</pre>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{TASK_STATES.map((state) => (
|
|
<div key={state.state} className={`${state.color} rounded-lg p-4`}>
|
|
<div className="font-semibold text-lg">{state.state}</div>
|
|
<p className="text-sm mt-1">{state.description}</p>
|
|
{state.next.length > 0 && (
|
|
<div className="mt-2 text-xs"><span className="opacity-75">Naechste:</span> {state.next.join(', ')}</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function TabIntents() {
|
|
return (
|
|
<div className="space-y-6">
|
|
<h3 className="text-lg font-semibold text-slate-900">Intent Types (22 unterstuetzte Typen)</h3>
|
|
{INTENT_GROUPS.map((group) => (
|
|
<div key={group.group} className={`${group.color} border rounded-lg p-4`}>
|
|
<h4 className="font-semibold text-slate-800 mb-3">{group.group}</h4>
|
|
<div className="space-y-2">
|
|
{group.intents.map((intent) => (
|
|
<div key={intent.type} className="bg-white rounded-lg p-3 shadow-sm">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<code className="text-sm font-mono text-teal-700 bg-teal-50 px-2 py-0.5 rounded">{intent.type}</code>
|
|
<p className="text-sm text-slate-600 mt-1">{intent.description}</p>
|
|
</div>
|
|
</div>
|
|
<div className="mt-2 text-xs text-slate-500 italic">Beispiel: "{intent.example}"</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function TabDsgvo() {
|
|
return (
|
|
<div className="space-y-6">
|
|
<h3 className="text-lg font-semibold text-slate-900">DSGVO-Compliance</h3>
|
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
|
<h4 className="font-semibold text-green-800 mb-2">Kernprinzipien</h4>
|
|
<ul className="list-disc list-inside text-sm text-green-700 space-y-1">
|
|
<li><strong>Audio NIEMALS persistiert</strong> - Nur transient im RAM</li>
|
|
<li><strong>Namespace-Verschluesselung</strong> - Key nur auf Lehrergeraet</li>
|
|
<li><strong>Keine Klartext-PII serverseitig</strong> - Nur verschluesselt oder pseudonymisiert</li>
|
|
<li><strong>TTL-basierte Auto-Loeschung</strong> - 7/30/90 Tage je nach Kategorie</li>
|
|
</ul>
|
|
</div>
|
|
<div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
|
|
<table className="min-w-full divide-y divide-slate-200">
|
|
<thead className="bg-slate-50">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Kategorie</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Verarbeitung</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Speicherort</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">TTL</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Risiko</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-200">
|
|
{DSGVO_CATEGORIES.map((cat) => (
|
|
<tr key={cat.category}>
|
|
<td className="px-4 py-3"><span className="mr-2">{cat.icon}</span><span className="font-medium">{cat.category}</span></td>
|
|
<td className="px-4 py-3 text-sm text-slate-600">{cat.processing}</td>
|
|
<td className="px-4 py-3 text-sm text-slate-600">{cat.storage}</td>
|
|
<td className="px-4 py-3 text-sm text-slate-600">{cat.ttl}</td>
|
|
<td className="px-4 py-3">
|
|
<span className={`px-2 py-1 rounded text-xs font-medium ${cat.risk === 'low' ? 'bg-green-100 text-green-700' : cat.risk === 'medium' ? 'bg-yellow-100 text-yellow-700' : 'bg-red-100 text-red-700'}`}>
|
|
{cat.risk.toUpperCase()}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4">
|
|
<h4 className="font-semibold text-slate-800 mb-2">Audit Logs (ohne PII)</h4>
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-green-600 font-medium">Erlaubt:</span>
|
|
<ul className="list-disc list-inside text-slate-600 mt-1"><li>ref_id (truncated)</li><li>content_type</li><li>size_bytes</li><li>ttl_hours</li></ul>
|
|
</div>
|
|
<div>
|
|
<span className="text-red-600 font-medium">Verboten:</span>
|
|
<ul className="list-disc list-inside text-slate-600 mt-1"><li>user_name</li><li>content / transcript</li><li>email</li><li>student_name</li></ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function TabApi() {
|
|
return (
|
|
<div className="space-y-6">
|
|
<h3 className="text-lg font-semibold text-slate-900">Voice Service API (Port 8091)</h3>
|
|
<div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
|
|
<table className="min-w-full divide-y divide-slate-200">
|
|
<thead className="bg-slate-50">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Methode</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Endpoint</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Beschreibung</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-200">
|
|
{API_ENDPOINTS.map((ep, idx) => (
|
|
<tr key={idx}>
|
|
<td className="px-4 py-3">
|
|
<span className={`px-2 py-1 rounded text-xs font-medium ${ep.method === 'GET' ? 'bg-green-100 text-green-700' : ep.method === 'POST' ? 'bg-blue-100 text-blue-700' : ep.method === 'PUT' ? 'bg-yellow-100 text-yellow-700' : ep.method === 'DELETE' ? 'bg-red-100 text-red-700' : 'bg-purple-100 text-purple-700'}`}>
|
|
{ep.method}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 font-mono text-sm">{ep.path}</td>
|
|
<td className="px-4 py-3 text-sm text-slate-600">{ep.description}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div className="bg-slate-50 rounded-lg p-4">
|
|
<h4 className="font-semibold text-slate-800 mb-3">WebSocket Protocol</h4>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
<div className="bg-white rounded-lg p-3 border border-slate-200">
|
|
<div className="font-medium text-slate-700 mb-2">Client → Server</div>
|
|
<ul className="list-disc list-inside text-slate-600 space-y-1">
|
|
<li><code className="bg-slate-100 px-1 rounded">Binary</code>: Int16 PCM Audio (24kHz, 80ms)</li>
|
|
<li><code className="bg-slate-100 px-1 rounded">JSON</code>: {`{type: "config|end_turn|interrupt"}`}</li>
|
|
</ul>
|
|
</div>
|
|
<div className="bg-white rounded-lg p-3 border border-slate-200">
|
|
<div className="font-medium text-slate-700 mb-2">Server → Client</div>
|
|
<ul className="list-disc list-inside text-slate-600 space-y-1">
|
|
<li><code className="bg-slate-100 px-1 rounded">Binary</code>: Audio Response (base64)</li>
|
|
<li><code className="bg-slate-100 px-1 rounded">JSON</code>: {`{type: "transcript|intent|status|error"}`}</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="bg-slate-900 rounded-lg p-4 text-sm">
|
|
<h4 className="font-semibold text-slate-300 mb-3">Beispiel: Session erstellen</h4>
|
|
<pre className="text-green-400 overflow-x-auto">{`curl -X POST http://localhost:8091/api/v1/sessions \\
|
|
-H "Content-Type: application/json" \\
|
|
-d '{
|
|
"namespace_id": "ns-12345678abcdef12345678abcdef12",
|
|
"key_hash": "sha256:dGVzdGtleWhhc2h0ZXN0a2V5aGFzaHRlc3Q=",
|
|
"device_type": "pwa"
|
|
}'`}</pre>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|