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>
562 lines
19 KiB
TypeScript
562 lines
19 KiB
TypeScript
'use client'
|
||
|
||
/**
|
||
* SDK Pipeline Sidebar
|
||
*
|
||
* Floating Action Button mit Drawer zur Visualisierung der SDK-Pipeline.
|
||
* Zeigt die zwei Phasen (Compliance Assessment & Dokumentengenerierung)
|
||
* mit Fortschritt und ermöglicht schnelle Navigation.
|
||
*
|
||
* Features:
|
||
* - Desktop (xl+): Fixierte Sidebar rechts
|
||
* - Mobile/Tablet: Floating Action Button mit Slide-In Drawer
|
||
*/
|
||
|
||
import Link from 'next/link'
|
||
import { useState, useEffect } from 'react'
|
||
import { usePathname } from 'next/navigation'
|
||
import { useSDK, SDK_STEPS, getStepsForPhase, type SDKStep } from '@/lib/sdk'
|
||
|
||
// =============================================================================
|
||
// ICONS
|
||
// =============================================================================
|
||
|
||
const CheckIcon = () => (
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||
</svg>
|
||
)
|
||
|
||
const ArrowIcon = () => (
|
||
<svg className="w-3 h-3 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||
</svg>
|
||
)
|
||
|
||
const CloseIcon = () => (
|
||
<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>
|
||
)
|
||
|
||
const PipelineIcon = () => (
|
||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
||
</svg>
|
||
)
|
||
|
||
// Step Icons als Emojis
|
||
const STEP_ICONS: Record<string, string> = {
|
||
// Phase 1
|
||
'use-case-workshop': '📋',
|
||
'screening': '🔍',
|
||
'modules': '📦',
|
||
'requirements': '📜',
|
||
'controls': '🛡️',
|
||
'evidence': '📎',
|
||
'audit-checklist': '✅',
|
||
'risks': '⚠️',
|
||
// Phase 2
|
||
'ai-act': '🤖',
|
||
'obligations': '📑',
|
||
'dsfa': '📄',
|
||
'tom': '🔒',
|
||
'einwilligungen': '✍️',
|
||
'loeschfristen': '🗑️',
|
||
'vvt': '📊',
|
||
'consent': '📝',
|
||
'cookie-banner': '🍪',
|
||
'dsr': '👤',
|
||
'escalations': '🚨',
|
||
}
|
||
|
||
// =============================================================================
|
||
// STEP ITEM
|
||
// =============================================================================
|
||
|
||
interface StepItemProps {
|
||
step: SDKStep
|
||
isActive: boolean
|
||
isCompleted: boolean
|
||
onNavigate: () => void
|
||
}
|
||
|
||
function StepItem({ step, isActive, isCompleted, onNavigate }: StepItemProps) {
|
||
const icon = STEP_ICONS[step.id] || '•'
|
||
|
||
return (
|
||
<Link
|
||
href={step.url}
|
||
onClick={onNavigate}
|
||
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-all ${
|
||
isActive
|
||
? 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 font-medium'
|
||
: isCompleted
|
||
? 'text-green-600 dark:text-green-400 hover:bg-slate-100 dark:hover:bg-gray-800'
|
||
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-gray-800'
|
||
}`}
|
||
>
|
||
<span className="text-lg flex-shrink-0">{icon}</span>
|
||
<span className="flex-1 text-sm truncate">{step.nameShort}</span>
|
||
{isCompleted && !isActive && (
|
||
<span className="flex-shrink-0 w-4 h-4 bg-green-500 text-white rounded-full flex items-center justify-center">
|
||
<CheckIcon />
|
||
</span>
|
||
)}
|
||
{isActive && (
|
||
<span className="flex-shrink-0 w-2 h-2 bg-purple-500 rounded-full animate-pulse" />
|
||
)}
|
||
</Link>
|
||
)
|
||
}
|
||
|
||
// =============================================================================
|
||
// PHASE SECTION
|
||
// =============================================================================
|
||
|
||
interface PhaseSectionProps {
|
||
phase: 1 | 2
|
||
title: string
|
||
steps: SDKStep[]
|
||
completion: number
|
||
currentStepId: string
|
||
completedSteps: string[]
|
||
onNavigate: () => void
|
||
isExpanded: boolean
|
||
onToggle: () => void
|
||
}
|
||
|
||
function PhaseSection({
|
||
phase,
|
||
title,
|
||
steps,
|
||
completion,
|
||
currentStepId,
|
||
completedSteps,
|
||
onNavigate,
|
||
isExpanded,
|
||
onToggle,
|
||
}: PhaseSectionProps) {
|
||
return (
|
||
<div className="space-y-2">
|
||
{/* Phase Header */}
|
||
<button
|
||
onClick={onToggle}
|
||
className="w-full flex items-center justify-between px-3 py-2 rounded-lg bg-slate-50 dark:bg-gray-800 hover:bg-slate-100 dark:hover:bg-gray-700 transition-colors"
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
<div
|
||
className={`w-7 h-7 rounded-full flex items-center justify-center text-sm font-bold ${
|
||
completion === 100
|
||
? 'bg-green-500 text-white'
|
||
: 'bg-purple-600 text-white'
|
||
}`}
|
||
>
|
||
{completion === 100 ? <CheckIcon /> : phase}
|
||
</div>
|
||
<div className="text-left">
|
||
<div className="text-sm font-medium text-slate-700 dark:text-slate-200">{title}</div>
|
||
<div className="text-xs text-slate-500 dark:text-slate-400">{completion}% abgeschlossen</div>
|
||
</div>
|
||
</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>
|
||
</button>
|
||
|
||
{/* Progress Bar */}
|
||
<div className="px-3">
|
||
<div className="h-1.5 bg-slate-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||
<div
|
||
className={`h-full rounded-full transition-all duration-500 ${
|
||
completion === 100 ? 'bg-green-500' : 'bg-purple-600'
|
||
}`}
|
||
style={{ width: `${completion}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Steps List */}
|
||
{isExpanded && (
|
||
<div className="space-y-1 pl-2">
|
||
{steps.map(step => (
|
||
<StepItem
|
||
key={step.id}
|
||
step={step}
|
||
isActive={currentStepId === step.id}
|
||
isCompleted={completedSteps.includes(step.id)}
|
||
onNavigate={onNavigate}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// =============================================================================
|
||
// PIPELINE FLOW VISUALIZATION
|
||
// =============================================================================
|
||
|
||
function PipelineFlow() {
|
||
return (
|
||
<div className="pt-3 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="p-3 bg-slate-50 dark:bg-gray-900 rounded-lg">
|
||
<div className="flex flex-col gap-2">
|
||
{/* Phase 1 Flow */}
|
||
<div className="flex items-center justify-between text-xs">
|
||
<span className="text-purple-600 dark:text-purple-400 font-medium">Phase 1</span>
|
||
<div className="flex items-center gap-1">
|
||
<span title="Use Case">📋</span>
|
||
<ArrowIcon />
|
||
<span title="Screening">🔍</span>
|
||
<ArrowIcon />
|
||
<span title="Risiken">⚠️</span>
|
||
<ArrowIcon />
|
||
<span title="Controls">🛡️</span>
|
||
</div>
|
||
</div>
|
||
{/* Arrow Down */}
|
||
<div className="flex justify-center">
|
||
<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="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||
</svg>
|
||
</div>
|
||
{/* Phase 2 Flow */}
|
||
<div className="flex items-center justify-between text-xs">
|
||
<span className="text-indigo-600 dark:text-indigo-400 font-medium">Phase 2</span>
|
||
<div className="flex items-center gap-1">
|
||
<span title="DSFA">📄</span>
|
||
<ArrowIcon />
|
||
<span title="TOM">🔒</span>
|
||
<ArrowIcon />
|
||
<span title="VVT">📊</span>
|
||
<ArrowIcon />
|
||
<span title="Export">✅</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// =============================================================================
|
||
// SIDEBAR CONTENT
|
||
// =============================================================================
|
||
|
||
interface SidebarContentProps {
|
||
onNavigate: () => void
|
||
}
|
||
|
||
function SidebarContent({ onNavigate }: SidebarContentProps) {
|
||
const pathname = usePathname()
|
||
const { state, phase1Completion, phase2Completion } = useSDK()
|
||
const [expandedPhases, setExpandedPhases] = useState<Record<number, boolean>>({
|
||
1: true,
|
||
2: false,
|
||
})
|
||
|
||
const phase1Steps = getStepsForPhase(1)
|
||
const phase2Steps = getStepsForPhase(2)
|
||
|
||
// Find current step
|
||
const currentStep = SDK_STEPS.find(s => s.url === pathname)
|
||
const currentStepId = currentStep?.id || ''
|
||
|
||
// Auto-expand current phase
|
||
useEffect(() => {
|
||
if (currentStep) {
|
||
setExpandedPhases(prev => ({
|
||
...prev,
|
||
[currentStep.phase]: true,
|
||
}))
|
||
}
|
||
}, [currentStep])
|
||
|
||
const togglePhase = (phase: number) => {
|
||
setExpandedPhases(prev => ({ ...prev, [phase]: !prev[phase] }))
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
{/* Phase 1 */}
|
||
<PhaseSection
|
||
phase={1}
|
||
title="Compliance Assessment"
|
||
steps={phase1Steps}
|
||
completion={phase1Completion}
|
||
currentStepId={currentStepId}
|
||
completedSteps={state.completedSteps}
|
||
onNavigate={onNavigate}
|
||
isExpanded={expandedPhases[1]}
|
||
onToggle={() => togglePhase(1)}
|
||
/>
|
||
|
||
{/* Phase 2 */}
|
||
<PhaseSection
|
||
phase={2}
|
||
title="Dokumentengenerierung"
|
||
steps={phase2Steps}
|
||
completion={phase2Completion}
|
||
currentStepId={currentStepId}
|
||
completedSteps={state.completedSteps}
|
||
onNavigate={onNavigate}
|
||
isExpanded={expandedPhases[2]}
|
||
onToggle={() => togglePhase(2)}
|
||
/>
|
||
|
||
{/* Pipeline Flow */}
|
||
<PipelineFlow />
|
||
|
||
{/* Quick Info */}
|
||
{currentStep && (
|
||
<div className="pt-3 border-t border-slate-200 dark:border-gray-700">
|
||
<div className="text-xs text-slate-600 dark:text-slate-400 p-3 bg-slate-50 dark:bg-gray-800 rounded-lg">
|
||
<strong className="text-slate-700 dark:text-slate-300">Aktuell:</strong>{' '}
|
||
{currentStep.description}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// =============================================================================
|
||
// MAIN COMPONENT - RESPONSIVE
|
||
// =============================================================================
|
||
|
||
export interface SDKPipelineSidebarProps {
|
||
/** Position des FAB auf Mobile */
|
||
fabPosition?: 'bottom-right' | 'bottom-left'
|
||
}
|
||
|
||
export function SDKPipelineSidebar({ fabPosition = 'bottom-right' }: SDKPipelineSidebarProps) {
|
||
const [isMobileOpen, setIsMobileOpen] = useState(false)
|
||
const [isDesktopCollapsed, setIsDesktopCollapsed] = useState(true) // Start collapsed
|
||
const { completionPercentage } = useSDK()
|
||
|
||
// Load collapsed state from localStorage
|
||
useEffect(() => {
|
||
const stored = localStorage.getItem('sdk-pipeline-sidebar-collapsed')
|
||
if (stored !== null) {
|
||
setIsDesktopCollapsed(stored === 'true')
|
||
}
|
||
}, [])
|
||
|
||
// Save collapsed state to localStorage
|
||
const toggleDesktopSidebar = () => {
|
||
const newState = !isDesktopCollapsed
|
||
setIsDesktopCollapsed(newState)
|
||
localStorage.setItem('sdk-pipeline-sidebar-collapsed', String(newState))
|
||
}
|
||
|
||
// Close drawer on route change or escape key
|
||
useEffect(() => {
|
||
const handleEscape = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape') {
|
||
setIsMobileOpen(false)
|
||
setIsDesktopCollapsed(true)
|
||
}
|
||
}
|
||
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 (when expanded) */}
|
||
{!isDesktopCollapsed && (
|
||
<div className="hidden xl:block fixed right-6 top-24 w-72 z-10">
|
||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-slate-200 dark:border-gray-700 overflow-hidden">
|
||
{/* Header with close button */}
|
||
<div className="px-4 py-3 bg-gradient-to-r from-purple-50 to-indigo-50 dark:from-purple-900/20 dark:to-indigo-900/20 border-b border-slate-200 dark:border-gray-700">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-purple-600 dark:text-purple-400">
|
||
<PipelineIcon />
|
||
</span>
|
||
<div>
|
||
<span className="font-semibold text-slate-700 dark:text-slate-200 text-sm">
|
||
SDK Pipeline
|
||
</span>
|
||
<span className="ml-2 text-xs text-purple-600 dark:text-purple-400">
|
||
{completionPercentage}%
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={toggleDesktopSidebar}
|
||
className="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 rounded-lg hover:bg-slate-100 dark:hover:bg-gray-700 transition-colors"
|
||
aria-label="Sidebar einklappen"
|
||
title="Einklappen"
|
||
>
|
||
<CloseIcon />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div className="p-3 max-h-[calc(100vh-200px)] overflow-y-auto">
|
||
<SidebarContent onNavigate={() => {}} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Desktop: FAB (when collapsed) */}
|
||
{isDesktopCollapsed && (
|
||
<button
|
||
onClick={toggleDesktopSidebar}
|
||
className={`hidden xl:flex fixed right-6 bottom-20 z-40 w-14 h-14 bg-gradient-to-r from-purple-500 to-indigo-500 text-white rounded-full shadow-lg hover:shadow-xl transition-all items-center justify-center group`}
|
||
aria-label="SDK Pipeline Navigation oeffnen"
|
||
title="Pipeline anzeigen"
|
||
>
|
||
<PipelineIcon />
|
||
{/* Progress indicator */}
|
||
<svg
|
||
className="absolute inset-0 w-full h-full -rotate-90"
|
||
viewBox="0 0 56 56"
|
||
>
|
||
<circle
|
||
cx="28"
|
||
cy="28"
|
||
r="26"
|
||
fill="none"
|
||
stroke="rgba(255,255,255,0.3)"
|
||
strokeWidth="2"
|
||
/>
|
||
<circle
|
||
cx="28"
|
||
cy="28"
|
||
r="26"
|
||
fill="none"
|
||
stroke="white"
|
||
strokeWidth="2"
|
||
strokeDasharray={`${(completionPercentage / 100) * 163.36} 163.36`}
|
||
strokeLinecap="round"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
)}
|
||
|
||
{/* Mobile/Tablet: FAB */}
|
||
<button
|
||
onClick={() => setIsMobileOpen(true)}
|
||
className={`xl:hidden fixed ${fabPositionClasses} z-40 w-14 h-14 bg-gradient-to-r from-purple-500 to-indigo-500 text-white rounded-full shadow-lg hover:shadow-xl transition-all flex items-center justify-center group`}
|
||
aria-label="SDK Pipeline Navigation oeffnen"
|
||
>
|
||
<PipelineIcon />
|
||
{/* Progress indicator */}
|
||
<svg
|
||
className="absolute inset-0 w-full h-full -rotate-90"
|
||
viewBox="0 0 56 56"
|
||
>
|
||
<circle
|
||
cx="28"
|
||
cy="28"
|
||
r="26"
|
||
fill="none"
|
||
stroke="rgba(255,255,255,0.3)"
|
||
strokeWidth="2"
|
||
/>
|
||
<circle
|
||
cx="28"
|
||
cy="28"
|
||
r="26"
|
||
fill="none"
|
||
stroke="white"
|
||
strokeWidth="2"
|
||
strokeDasharray={`${(completionPercentage / 100) * 163.36} 163.36`}
|
||
strokeLinecap="round"
|
||
/>
|
||
</svg>
|
||
</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-purple-50 to-indigo-50 dark:from-purple-900/20 dark:to-indigo-900/20">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-purple-600 dark:text-purple-400">
|
||
<PipelineIcon />
|
||
</span>
|
||
<div>
|
||
<span className="font-semibold text-slate-700 dark:text-slate-200">
|
||
SDK Pipeline
|
||
</span>
|
||
<span className="ml-2 text-sm text-purple-600 dark:text-purple-400">
|
||
{completionPercentage}%
|
||
</span>
|
||
</div>
|
||
</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="Schliessen"
|
||
>
|
||
<CloseIcon />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Drawer Content */}
|
||
<div className="p-4 overflow-y-auto max-h-[calc(100vh-80px)]">
|
||
<SidebarContent onNavigate={() => setIsMobileOpen(false)} />
|
||
</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 SDKPipelineSidebar
|