This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/components/sdk/SDKPipelineSidebar/SDKPipelineSidebar.tsx
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
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>
2026-02-09 09:51:32 +01:00

562 lines
19 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'
/**
* 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