Files
breakpilot-lehrer/admin-lehrer/components/ai/AIModuleSidebar.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
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>
2026-02-11 23:47:26 +01:00

406 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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