A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
406 lines
16 KiB
TypeScript
406 lines
16 KiB
TypeScript
'use client'
|
||
|
||
/**
|
||
* AI Module Sidebar
|
||
*
|
||
* Kompakte Sidebar-Komponente für Cross-Navigation zwischen AI-Modulen.
|
||
* Zeigt den Datenfluss und ermöglicht schnelle Navigation.
|
||
*
|
||
* Features:
|
||
* - Desktop: Fixierte Sidebar rechts
|
||
* - Mobile: Floating Action Button mit Slide-In Drawer
|
||
*/
|
||
|
||
import Link from 'next/link'
|
||
import { useState, useEffect } from 'react'
|
||
import { AI_MODULES, DATA_FLOW_CONNECTIONS, type AIModuleLink } from '@/types/ai-modules'
|
||
|
||
export interface AIModuleSidebarProps {
|
||
/** ID des aktuell aktiven Moduls */
|
||
currentModule: 'magic-help' | 'ocr-labeling' | 'rag-pipeline' | 'rag'
|
||
/** Optional: Kompakter Modus (nur Icons) */
|
||
compact?: boolean
|
||
/** Optional: Zusätzliche CSS-Klassen */
|
||
className?: string
|
||
}
|
||
|
||
export interface AIModuleSidebarResponsiveProps extends AIModuleSidebarProps {
|
||
/** Position des FAB auf Mobile */
|
||
fabPosition?: 'bottom-right' | 'bottom-left'
|
||
}
|
||
|
||
// Icons für die Module
|
||
const ModuleIcon = ({ id }: { id: string }) => {
|
||
switch (id) {
|
||
case 'magic-help':
|
||
return (
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
||
</svg>
|
||
)
|
||
case 'ocr-labeling':
|
||
return (
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||
</svg>
|
||
)
|
||
case 'rag-pipeline':
|
||
return (
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||
</svg>
|
||
)
|
||
case 'rag':
|
||
return (
|
||
<svg className="w-5 h-5" 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>
|
||
)
|
||
default:
|
||
return null
|
||
}
|
||
}
|
||
|
||
// Pfeil-Icon für Datenfluss
|
||
const ArrowIcon = () => (
|
||
<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="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||
</svg>
|
||
)
|
||
|
||
export function AIModuleSidebar({
|
||
currentModule,
|
||
compact = false,
|
||
className = '',
|
||
}: AIModuleSidebarProps) {
|
||
const [isExpanded, setIsExpanded] = useState(!compact)
|
||
|
||
return (
|
||
<div className={`bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-slate-200 dark:border-gray-700 overflow-hidden ${className}`}>
|
||
{/* Header */}
|
||
<div
|
||
className="px-4 py-3 bg-gradient-to-r from-teal-50 to-cyan-50 dark:from-teal-900/20 dark:to-cyan-900/20 border-b border-slate-200 dark:border-gray-700 cursor-pointer"
|
||
onClick={() => setIsExpanded(!isExpanded)}
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-teal-600 dark:text-teal-400">
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||
</svg>
|
||
</span>
|
||
<span className="font-semibold text-slate-700 dark:text-slate-200 text-sm">
|
||
KI-Daten-Pipeline
|
||
</span>
|
||
</div>
|
||
<svg
|
||
className={`w-4 h-4 text-slate-400 transition-transform ${isExpanded ? '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>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
{isExpanded && (
|
||
<div className="p-3 space-y-3">
|
||
{/* Module Links */}
|
||
<div className="space-y-1">
|
||
{AI_MODULES.map((module) => (
|
||
<Link
|
||
key={module.id}
|
||
href={module.href}
|
||
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
|
||
currentModule === module.id
|
||
? 'bg-teal-100 dark:bg-teal-900/30 text-teal-700 dark:text-teal-300 font-medium'
|
||
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-gray-700'
|
||
}`}
|
||
>
|
||
<ModuleIcon id={module.id} />
|
||
<div className="flex-1 min-w-0">
|
||
<div className="text-sm font-medium truncate">{module.name}</div>
|
||
<div className="text-xs text-slate-500 dark:text-slate-500 truncate">
|
||
{module.description}
|
||
</div>
|
||
</div>
|
||
{currentModule === module.id && (
|
||
<span className="flex-shrink-0 w-2 h-2 bg-teal-500 rounded-full" />
|
||
)}
|
||
</Link>
|
||
))}
|
||
</div>
|
||
|
||
{/* Datenfluss-Visualisierung */}
|
||
<div className="pt-2 border-t border-slate-200 dark:border-gray-700">
|
||
<div className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-2 px-1">
|
||
Datenfluss
|
||
</div>
|
||
<div className="flex items-center justify-between px-2 py-2 bg-slate-50 dark:bg-gray-900 rounded-lg">
|
||
<div className="flex items-center gap-1.5 text-xs">
|
||
<span title="Magic Help">✨</span>
|
||
<span className="text-slate-400" title="Bidirektional">⟷</span>
|
||
<span title="OCR-Labeling">🏷️</span>
|
||
<ArrowIcon />
|
||
<span title="RAG Pipeline">🔄</span>
|
||
<ArrowIcon />
|
||
<span title="Daten & RAG">🔍</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Quick Info zum aktuellen Modul */}
|
||
<div className="pt-2 border-t border-slate-200 dark:border-gray-700">
|
||
<div className="text-xs text-slate-500 dark:text-slate-400 px-1">
|
||
{currentModule === 'magic-help' && (
|
||
<span>Testen und verbessern Sie die TrOCR-Handschrifterkennung</span>
|
||
)}
|
||
{currentModule === 'ocr-labeling' && (
|
||
<span>Erstellen Sie Ground Truth Daten für das OCR-Training</span>
|
||
)}
|
||
{currentModule === 'rag-pipeline' && (
|
||
<span>Indexieren Sie Dokumente für die semantische Suche</span>
|
||
)}
|
||
{currentModule === 'rag' && (
|
||
<span>Verwalten und durchsuchen Sie indexierte Dokumente</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Kompakte Inline-Version für mobile Ansichten
|
||
*/
|
||
export function AIModuleNav({ currentModule }: { currentModule: string }) {
|
||
return (
|
||
<div className="flex items-center gap-1 p-1 bg-slate-100 dark:bg-gray-800 rounded-lg">
|
||
{AI_MODULES.map((module) => (
|
||
<Link
|
||
key={module.id}
|
||
href={module.href}
|
||
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
|
||
currentModule === module.id
|
||
? 'bg-white dark:bg-gray-700 text-teal-600 dark:text-teal-400 font-medium shadow-sm'
|
||
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200'
|
||
}`}
|
||
title={module.description}
|
||
>
|
||
{module.name}
|
||
</Link>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Responsive Sidebar mit Mobile FAB + Drawer
|
||
*
|
||
* Desktop (xl+): Fixierte Sidebar rechts
|
||
* Mobile/Tablet: Floating Action Button unten rechts, öffnet Drawer
|
||
*/
|
||
export function AIModuleSidebarResponsive({
|
||
currentModule,
|
||
compact = false,
|
||
className = '',
|
||
fabPosition = 'bottom-right',
|
||
}: AIModuleSidebarResponsiveProps) {
|
||
const [isMobileOpen, setIsMobileOpen] = useState(false)
|
||
|
||
// Close drawer on route change or escape key
|
||
useEffect(() => {
|
||
const handleEscape = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape') setIsMobileOpen(false)
|
||
}
|
||
window.addEventListener('keydown', handleEscape)
|
||
return () => window.removeEventListener('keydown', handleEscape)
|
||
}, [])
|
||
|
||
// Prevent body scroll when drawer is open
|
||
useEffect(() => {
|
||
if (isMobileOpen) {
|
||
document.body.style.overflow = 'hidden'
|
||
} else {
|
||
document.body.style.overflow = ''
|
||
}
|
||
return () => {
|
||
document.body.style.overflow = ''
|
||
}
|
||
}, [isMobileOpen])
|
||
|
||
const fabPositionClasses = fabPosition === 'bottom-right'
|
||
? 'right-4 bottom-20'
|
||
: 'left-4 bottom-20'
|
||
|
||
return (
|
||
<>
|
||
{/* Desktop: Fixed Sidebar */}
|
||
<div className={`hidden xl:block fixed right-6 top-24 w-64 z-10 ${className}`}>
|
||
<AIModuleSidebar currentModule={currentModule} compact={compact} />
|
||
</div>
|
||
|
||
{/* Mobile/Tablet: FAB */}
|
||
<button
|
||
onClick={() => setIsMobileOpen(true)}
|
||
className={`xl:hidden fixed ${fabPositionClasses} z-40 w-14 h-14 bg-gradient-to-r from-teal-500 to-cyan-500 text-white rounded-full shadow-lg hover:shadow-xl transition-all flex items-center justify-center group`}
|
||
aria-label="KI-Daten-Pipeline Navigation öffnen"
|
||
>
|
||
<svg
|
||
className="w-6 h-6 group-hover:scale-110 transition-transform"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||
</svg>
|
||
{/* Pulse indicator */}
|
||
<span className="absolute -top-1 -right-1 w-3 h-3 bg-orange-400 rounded-full animate-pulse" />
|
||
</button>
|
||
|
||
{/* Mobile/Tablet: Drawer Overlay */}
|
||
{isMobileOpen && (
|
||
<div className="xl:hidden fixed inset-0 z-50">
|
||
{/* Backdrop */}
|
||
<div
|
||
className="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity"
|
||
onClick={() => setIsMobileOpen(false)}
|
||
/>
|
||
|
||
{/* Drawer */}
|
||
<div className="absolute right-0 top-0 bottom-0 w-80 max-w-[85vw] bg-white dark:bg-gray-900 shadow-2xl transform transition-transform animate-slide-in-right">
|
||
{/* Drawer Header */}
|
||
<div className="flex items-center justify-between px-4 py-4 border-b border-slate-200 dark:border-gray-700 bg-gradient-to-r from-teal-50 to-cyan-50 dark:from-teal-900/20 dark:to-cyan-900/20">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-teal-600 dark:text-teal-400">
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||
</svg>
|
||
</span>
|
||
<span className="font-semibold text-slate-700 dark:text-slate-200">
|
||
KI-Daten-Pipeline
|
||
</span>
|
||
</div>
|
||
<button
|
||
onClick={() => setIsMobileOpen(false)}
|
||
className="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 rounded-lg hover:bg-slate-100 dark:hover:bg-gray-800 transition-colors"
|
||
aria-label="Schließen"
|
||
>
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Drawer Content */}
|
||
<div className="p-4 space-y-4 overflow-y-auto max-h-[calc(100vh-80px)]">
|
||
{/* Module Links */}
|
||
<div className="space-y-2">
|
||
{AI_MODULES.map((module) => (
|
||
<Link
|
||
key={module.id}
|
||
href={module.href}
|
||
onClick={() => setIsMobileOpen(false)}
|
||
className={`flex items-center gap-3 px-4 py-3 rounded-xl transition-all ${
|
||
currentModule === module.id
|
||
? 'bg-teal-100 dark:bg-teal-900/30 text-teal-700 dark:text-teal-300 font-medium shadow-sm'
|
||
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-gray-800'
|
||
}`}
|
||
>
|
||
<ModuleIcon id={module.id} />
|
||
<div className="flex-1">
|
||
<div className="text-base font-medium">{module.name}</div>
|
||
<div className="text-sm text-slate-500 dark:text-slate-500">
|
||
{module.description}
|
||
</div>
|
||
</div>
|
||
{currentModule === module.id && (
|
||
<span className="flex-shrink-0 w-2.5 h-2.5 bg-teal-500 rounded-full" />
|
||
)}
|
||
</Link>
|
||
))}
|
||
</div>
|
||
|
||
{/* Datenfluss-Visualisierung */}
|
||
<div className="pt-4 border-t border-slate-200 dark:border-gray-700">
|
||
<div className="text-sm font-medium text-slate-500 dark:text-slate-400 mb-3">
|
||
Datenfluss
|
||
</div>
|
||
<div className="flex items-center justify-center gap-2 p-4 bg-slate-50 dark:bg-gray-800 rounded-xl">
|
||
<div className="flex flex-col items-center">
|
||
<span className="text-2xl">✨</span>
|
||
<span className="text-xs text-slate-500 mt-1">Magic</span>
|
||
</div>
|
||
<span className="text-slate-400 text-lg" title="Bidirektional">⟷</span>
|
||
<div className="flex flex-col items-center">
|
||
<span className="text-2xl">🏷️</span>
|
||
<span className="text-xs text-slate-500 mt-1">OCR</span>
|
||
</div>
|
||
<ArrowIcon />
|
||
<div className="flex flex-col items-center">
|
||
<span className="text-2xl">🔄</span>
|
||
<span className="text-xs text-slate-500 mt-1">Pipeline</span>
|
||
</div>
|
||
<ArrowIcon />
|
||
<div className="flex flex-col items-center">
|
||
<span className="text-2xl">🔍</span>
|
||
<span className="text-xs text-slate-500 mt-1">RAG</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Quick Info */}
|
||
<div className="pt-4 border-t border-slate-200 dark:border-gray-700">
|
||
<div className="text-sm text-slate-600 dark:text-slate-400 p-3 bg-slate-50 dark:bg-gray-800 rounded-xl">
|
||
{currentModule === 'magic-help' && (
|
||
<>
|
||
<strong className="text-slate-700 dark:text-slate-300">Aktuell:</strong> TrOCR-Handschrifterkennung testen und verbessern
|
||
</>
|
||
)}
|
||
{currentModule === 'ocr-labeling' && (
|
||
<>
|
||
<strong className="text-slate-700 dark:text-slate-300">Aktuell:</strong> Ground Truth Daten für OCR-Training erstellen
|
||
</>
|
||
)}
|
||
{currentModule === 'rag-pipeline' && (
|
||
<>
|
||
<strong className="text-slate-700 dark:text-slate-300">Aktuell:</strong> Dokumente für semantische Suche indexieren
|
||
</>
|
||
)}
|
||
{currentModule === 'rag' && (
|
||
<>
|
||
<strong className="text-slate-700 dark:text-slate-300">Aktuell:</strong> Indexierte Dokumente verwalten und durchsuchen
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* CSS for slide-in animation */}
|
||
<style jsx>{`
|
||
@keyframes slide-in-right {
|
||
from {
|
||
transform: translateX(100%);
|
||
}
|
||
to {
|
||
transform: translateX(0);
|
||
}
|
||
}
|
||
.animate-slide-in-right {
|
||
animation: slide-in-right 0.2s ease-out;
|
||
}
|
||
`}</style>
|
||
</>
|
||
)
|
||
}
|
||
|
||
export default AIModuleSidebar
|