'use client' import React from 'react' import Link from 'next/link' import { usePathname } from 'next/navigation' import { useSDK, SDK_STEPS, SDK_PACKAGES, getStepsForPackage, type SDKPackageId, type SDKStep, type RAGCorpusStatus, } from '@/lib/sdk' /** * Append ?project= to a URL if a projectId is set */ function withProject(url: string, projectId?: string): string { if (!projectId) return url const separator = url.includes('?') ? '&' : '?' return `${url}${separator}project=${projectId}` } // ============================================================================= // ICONS // ============================================================================= const CheckIcon = () => ( ) const LockIcon = () => ( ) const WarningIcon = () => ( ) const ChevronDownIcon = ({ className = '' }: { className?: string }) => ( ) const CollapseIcon = ({ collapsed }: { collapsed: boolean }) => ( ) // ============================================================================= // PROGRESS BAR // ============================================================================= interface ProgressBarProps { value: number className?: string } function ProgressBar({ value, className = '' }: ProgressBarProps) { return (
) } // ============================================================================= // PACKAGE INDICATOR // ============================================================================= interface PackageIndicatorProps { packageId: SDKPackageId order: number name: string icon: string completion: number isActive: boolean isExpanded: boolean isLocked: boolean onToggle: () => void collapsed: boolean } function PackageIndicator({ order, name, icon, completion, isActive, isExpanded, isLocked, onToggle, collapsed, }: PackageIndicatorProps) { if (collapsed) { return ( ) } return ( ) } // ============================================================================= // STEP ITEM // ============================================================================= interface StepItemProps { step: SDKStep isActive: boolean isCompleted: boolean isLocked: boolean checkpointStatus?: 'passed' | 'failed' | 'warning' | 'pending' collapsed: boolean projectId?: string } function StepItem({ step, isActive, isCompleted, isLocked, checkpointStatus, collapsed, projectId }: StepItemProps) { const content = (
{/* Step indicator */}
{isCompleted ? (
) : isLocked ? (
) : isActive ? (
) : (
)}
{/* Step name - hidden when collapsed */} {!collapsed && {step.nameShort}} {/* Checkpoint status - hidden when collapsed */} {!collapsed && checkpointStatus && checkpointStatus !== 'pending' && (
{checkpointStatus === 'passed' ? (
) : checkpointStatus === 'failed' ? (
!
) : (
)}
)}
) if (isLocked) { return content } return ( {content} ) } // ============================================================================= // ADDITIONAL MODULE ITEM // ============================================================================= interface AdditionalModuleItemProps { href: string icon: React.ReactNode label: string isActive: boolean collapsed: boolean projectId?: string } function AdditionalModuleItem({ href, icon, label, isActive, collapsed, projectId }: AdditionalModuleItemProps) { const isExternal = href.startsWith('http') const className = `flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${ collapsed ? 'justify-center' : '' } ${ isActive ? 'bg-purple-100 text-purple-900 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900' }` if (isExternal) { return ( {icon} {!collapsed && ( {label} )} ) } return ( {icon} {!collapsed && {label}} ) } // ============================================================================= // MAIN SIDEBAR // ============================================================================= interface SDKSidebarProps { collapsed?: boolean onCollapsedChange?: (collapsed: boolean) => void } // ============================================================================= // CORPUS STALENESS INFO // ============================================================================= function CorpusStalenessInfo({ ragCorpusStatus }: { ragCorpusStatus: RAGCorpusStatus }) { const collections = ragCorpusStatus.collections const collectionNames = Object.keys(collections) if (collectionNames.length === 0) return null // Check if corpus was updated after the last fetch (simplified: show last update time) const lastUpdated = collectionNames.reduce((latest, name) => { const updated = new Date(collections[name].last_updated) return updated > latest ? updated : latest }, new Date(0)) const daysSinceUpdate = Math.floor((Date.now() - lastUpdated.getTime()) / (1000 * 60 * 60 * 24)) const totalChunks = collectionNames.reduce((sum, name) => sum + collections[name].chunks_count, 0) return (
30 ? 'bg-amber-400' : 'bg-green-400'}`} /> RAG Corpus: {totalChunks} Chunks
{daysSinceUpdate > 30 && (
Corpus {daysSinceUpdate}d alt — Re-Evaluation empfohlen
)}
) } export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarProps) { const pathname = usePathname() const { state, packageCompletion, completionPercentage, getCheckpointStatus, projectId } = useSDK() const [pendingCRCount, setPendingCRCount] = React.useState(0) // Poll pending change-request count every 60s React.useEffect(() => { let active = true async function fetchCRCount() { try { const res = await fetch('/api/sdk/v1/compliance/change-requests/stats') if (res.ok) { const data = await res.json() if (active) setPendingCRCount(data.total_pending || 0) } } catch { /* ignore */ } } fetchCRCount() const interval = setInterval(fetchCRCount, 60000) return () => { active = false; clearInterval(interval) } }, []) const [expandedPackages, setExpandedPackages] = React.useState>({ 'vorbereitung': true, 'analyse': false, 'dokumentation': false, 'rechtliche-texte': false, 'betrieb': false, }) // Auto-expand current package React.useEffect(() => { const currentStep = SDK_STEPS.find(s => s.url === pathname) if (currentStep) { setExpandedPackages(prev => ({ ...prev, [currentStep.package]: true, })) } }, [pathname]) const togglePackage = (packageId: SDKPackageId) => { setExpandedPackages(prev => ({ ...prev, [packageId]: !prev[packageId] })) } const isStepLocked = (step: SDKStep): boolean => { if (state.preferences?.allowParallelWork) return false return step.prerequisiteSteps.some(prereq => !state.completedSteps.includes(prereq)) } const isPackageLocked = (packageId: SDKPackageId): boolean => { if (state.preferences?.allowParallelWork) return false const pkg = SDK_PACKAGES.find(p => p.id === packageId) if (!pkg || pkg.order === 1) return false // Check if previous package is complete const prevPkg = SDK_PACKAGES.find(p => p.order === pkg.order - 1) if (!prevPkg) return false return packageCompletion[prevPkg.id] < 100 } const getStepCheckpointStatus = (step: SDKStep): 'passed' | 'failed' | 'warning' | 'pending' => { const status = getCheckpointStatus(step.checkpointId) if (!status) return 'pending' if (status.passed) return 'passed' if (status.errors.length > 0) return 'failed' if (status.warnings.length > 0) return 'warning' return 'pending' } const isStepActive = (stepUrl: string) => pathname === stepUrl const isPackageActive = (packageId: SDKPackageId) => { const steps = getStepsForPackage(packageId) return steps.some(s => s.url === pathname) } // Filter steps based on visibleWhen conditions const getVisibleStepsForPackage = (packageId: SDKPackageId): SDKStep[] => { const steps = getStepsForPackage(packageId) return steps.filter(step => { if (step.visibleWhen) return step.visibleWhen(state) return true }) } return ( ) }