Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 49s
CI/CD / test-python-backend-compliance (push) Successful in 46s
CI/CD / test-python-document-crawler (push) Successful in 30s
CI/CD / test-python-dsms-gateway (push) Successful in 31s
CI/CD / validate-canonical-controls (push) Successful in 23s
CI/CD / Deploy (push) Failing after 7s
- SDKSidebar (918→236 LOC): extracted icons to SidebarIcons, sub-components (ProgressBar, PackageIndicator, StepItem, CorpusStalenessInfo, AdditionalModuleItem) to SidebarSubComponents, and the full module nav list to SidebarModuleNav - ScopeWizardTab (794→339 LOC): extracted DatenkategorienBlock9 and its dept mapping constants to DatenkategorienBlock, and question rendering (all switch-case types + help text) to ScopeQuestionRenderer - All files now under 500 LOC hard cap; zero behavior changes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
237 lines
8.4 KiB
TypeScript
237 lines
8.4 KiB
TypeScript
'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,
|
|
} from '@/lib/sdk'
|
|
import { CollapseIcon } from './SidebarIcons'
|
|
import {
|
|
ProgressBar,
|
|
PackageIndicator,
|
|
StepItem,
|
|
CorpusStalenessInfo,
|
|
} from './SidebarSubComponents'
|
|
import { SidebarModuleNav } from './SidebarModuleNav'
|
|
|
|
interface SDKSidebarProps {
|
|
collapsed?: boolean
|
|
onCollapsedChange?: (collapsed: boolean) => void
|
|
}
|
|
|
|
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<Record<SDKPackageId, boolean>>({
|
|
'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
|
|
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)
|
|
}
|
|
|
|
const getVisibleStepsForPackage = (packageId: SDKPackageId): SDKStep[] => {
|
|
const steps = getStepsForPackage(packageId)
|
|
return steps.filter(step => {
|
|
if (step.visibleWhen) return step.visibleWhen(state)
|
|
return true
|
|
})
|
|
}
|
|
|
|
return (
|
|
<aside className={`fixed left-0 top-0 h-screen ${collapsed ? 'w-16' : 'w-64'} bg-white border-r border-gray-200 flex flex-col z-40 transition-all duration-300`}>
|
|
{/* Header */}
|
|
<div className={`p-4 border-b border-gray-200 ${collapsed ? 'flex justify-center' : ''}`}>
|
|
<Link
|
|
href="/sdk"
|
|
className={`flex items-center gap-3 ${collapsed ? 'justify-center' : ''} hover:opacity-80 transition-opacity`}
|
|
title="Zurueck zur Projektliste"
|
|
>
|
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-purple-600 to-indigo-600 flex items-center justify-center flex-shrink-0">
|
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
{!collapsed && (
|
|
<div className="text-left">
|
|
<div className="font-bold text-gray-900">AI Compliance</div>
|
|
<div className="text-xs text-gray-500 truncate max-w-[140px]">
|
|
{state.projectInfo?.name || 'SDK'}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Overall Progress - hidden when collapsed */}
|
|
{!collapsed && (
|
|
<div className="px-4 py-3 border-b border-gray-100">
|
|
<div className="flex items-center justify-between text-sm mb-2">
|
|
<span className="text-gray-600">Gesamtfortschritt</span>
|
|
<span className="font-medium text-purple-600">{completionPercentage}%</span>
|
|
</div>
|
|
<ProgressBar value={completionPercentage} />
|
|
</div>
|
|
)}
|
|
|
|
{/* RAG Corpus Staleness Badge */}
|
|
{!collapsed && state.ragCorpusStatus && (
|
|
<CorpusStalenessInfo ragCorpusStatus={state.ragCorpusStatus} />
|
|
)}
|
|
|
|
{/* Navigation - 5 Packages */}
|
|
<nav className="flex-1 overflow-y-auto">
|
|
{SDK_PACKAGES.map(pkg => {
|
|
const steps = getVisibleStepsForPackage(pkg.id)
|
|
const isLocked = isPackageLocked(pkg.id)
|
|
const isActive = isPackageActive(pkg.id)
|
|
|
|
return (
|
|
<div key={pkg.id} className={pkg.order > 1 ? 'border-t border-gray-100' : ''}>
|
|
<PackageIndicator
|
|
packageId={pkg.id}
|
|
order={pkg.order}
|
|
name={pkg.name}
|
|
icon={pkg.icon}
|
|
completion={packageCompletion[pkg.id]}
|
|
isActive={isActive}
|
|
isExpanded={expandedPackages[pkg.id]}
|
|
isLocked={isLocked}
|
|
onToggle={() => togglePackage(pkg.id)}
|
|
collapsed={collapsed}
|
|
/>
|
|
{expandedPackages[pkg.id] && !isLocked && (
|
|
<div className="py-1">
|
|
{steps.map(step => (
|
|
<StepItem
|
|
key={step.id}
|
|
step={step}
|
|
isActive={isStepActive(step.url)}
|
|
isCompleted={state.completedSteps.includes(step.id)}
|
|
isLocked={isStepLocked(step)}
|
|
checkpointStatus={getStepCheckpointStatus(step)}
|
|
collapsed={collapsed}
|
|
projectId={projectId}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
|
|
<SidebarModuleNav
|
|
pathname={pathname}
|
|
collapsed={collapsed}
|
|
projectId={projectId}
|
|
pendingCRCount={pendingCRCount}
|
|
/>
|
|
</nav>
|
|
|
|
{/* Footer */}
|
|
<div className={`${collapsed ? 'p-2' : 'p-4'} border-t border-gray-200 bg-gray-50`}>
|
|
<button
|
|
onClick={() => onCollapsedChange?.(!collapsed)}
|
|
className={`w-full flex items-center justify-center gap-2 ${collapsed ? 'p-2' : 'px-4 py-2'} text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors ${collapsed ? '' : 'mb-2'}`}
|
|
title={collapsed ? 'Sidebar erweitern' : 'Sidebar einklappen'}
|
|
>
|
|
<CollapseIcon collapsed={collapsed} />
|
|
{!collapsed && <span>Einklappen</span>}
|
|
</button>
|
|
|
|
{!collapsed && (
|
|
<button
|
|
onClick={() => {}}
|
|
className="w-full flex items-center justify-center gap-2 px-4 py-2 text-sm text-purple-600 hover:text-purple-700 hover:bg-purple-50 rounded-lg transition-colors"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"
|
|
/>
|
|
</svg>
|
|
<span>Exportieren</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</aside>
|
|
)
|
|
}
|