From b00fe6cb73336cbb9440c3f8129e644d6a107fce Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Fri, 17 Apr 2026 12:41:29 +0200 Subject: [PATCH] refactor(admin): split remaining components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split 4 oversized component files (all >500 LOC) into sibling modules: - SDKPipelineSidebar → Icons + Parts siblings (193/264/35 LOC) - SourcesTab → SourceModals sibling (311/243 LOC) - ScopeDecisionTab → ScopeDecisionSections sibling (127/444 LOC) - ComplianceAdvisorWidget → ComplianceAdvisorParts sibling (265/131 LOC) Zero behavior changes; all logic relocated verbatim. Co-Authored-By: Claude Sonnet 4.6 --- .../components/sdk/ComplianceAdvisorParts.tsx | 131 +++++ .../sdk/ComplianceAdvisorWidget.tsx | 304 ++---------- .../SDKPipelineSidebar/SDKPipelineSidebar.tsx | 397 +-------------- .../SDKPipelineSidebarIcons.tsx | 35 ++ .../SDKPipelineSidebarParts.tsx | 264 ++++++++++ .../ScopeDecisionSections.tsx | 444 +++++++++++++++++ .../sdk/compliance-scope/ScopeDecisionTab.tsx | 451 ++---------------- .../sdk/source-policy/SourceModals.tsx | 243 ++++++++++ .../sdk/source-policy/SourcesTab.tsx | 266 +---------- 9 files changed, 1236 insertions(+), 1299 deletions(-) create mode 100644 admin-compliance/components/sdk/ComplianceAdvisorParts.tsx create mode 100644 admin-compliance/components/sdk/SDKPipelineSidebar/SDKPipelineSidebarIcons.tsx create mode 100644 admin-compliance/components/sdk/SDKPipelineSidebar/SDKPipelineSidebarParts.tsx create mode 100644 admin-compliance/components/sdk/compliance-scope/ScopeDecisionSections.tsx create mode 100644 admin-compliance/components/sdk/source-policy/SourceModals.tsx diff --git a/admin-compliance/components/sdk/ComplianceAdvisorParts.tsx b/admin-compliance/components/sdk/ComplianceAdvisorParts.tsx new file mode 100644 index 0000000..96c0640 --- /dev/null +++ b/admin-compliance/components/sdk/ComplianceAdvisorParts.tsx @@ -0,0 +1,131 @@ +'use client' + +// ============================================================================= +// ComplianceAdvisorWidget — shared constants and sub-components +// ============================================================================= + +// ============================================================================= +// EXAMPLE QUESTIONS +// ============================================================================= + +export const EXAMPLE_QUESTIONS: Record = { + vvt: [ + 'Was ist ein Verarbeitungsverzeichnis?', + 'Welche Informationen muss ich erfassen?', + 'Wie dokumentiere ich die Rechtsgrundlage?', + ], + 'compliance-scope': [ + 'Was bedeutet L3?', + 'Wann brauche ich eine DSFA?', + 'Was ist der Unterschied zwischen L2 und L3?', + ], + tom: [ + 'Was sind TOM?', + 'Welche Massnahmen sind erforderlich?', + 'Wie dokumentiere ich Verschluesselung?', + ], + dsfa: [ + 'Was ist eine DSFA?', + 'Wann ist eine DSFA verpflichtend?', + 'Wie bewerte ich Risiken?', + ], + loeschfristen: [ + 'Wie definiere ich Loeschfristen?', + 'Was ist der Unterschied zwischen Loeschpflicht und Aufbewahrungspflicht?', + 'Wann muss ich Daten loeschen?', + ], + default: [ + 'Wie starte ich mit dem SDK?', + 'Was ist der erste Schritt?', + 'Welche Compliance-Anforderungen gelten fuer KI-Systeme?', + ], +} + +// ============================================================================= +// TYPES +// ============================================================================= + +export interface Message { + id: string + role: 'user' | 'agent' + content: string + timestamp: Date +} + +// ============================================================================= +// EmptyState — shown when no messages yet +// ============================================================================= + +interface EmptyStateProps { + exampleQuestions: string[] + onExampleClick: (question: string) => void +} + +export function AdvisorEmptyState({ exampleQuestions, onExampleClick }: EmptyStateProps) { + return ( +
+
+ + + +
+

Willkommen beim Compliance Advisor

+

Stellen Sie Fragen zu DSGVO, KI-Verordnung und mehr.

+
+

Beispielfragen:

+ {exampleQuestions.map((question, idx) => ( + + ))} +
+
+ ) +} + +// ============================================================================= +// MessageList — renders messages + typing indicator +// ============================================================================= + +interface MessageListProps { + messages: Message[] + isTyping: boolean + messagesEndRef: React.RefObject +} + +export function AdvisorMessageList({ messages, isTyping, messagesEndRef }: MessageListProps) { + return ( + <> + {messages.map((message) => ( +
+
+

+ {message.content || (message.role === 'agent' && isTyping ? '' : message.content)} +

+

+ {message.timestamp.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} +

+
+
+ ))} + + {isTyping && ( +
+
+
+
+
+
+
+
+
+ )} + +
+ + ) +} diff --git a/admin-compliance/components/sdk/ComplianceAdvisorWidget.tsx b/admin-compliance/components/sdk/ComplianceAdvisorWidget.tsx index 93b8b6d..b2057c2 100644 --- a/admin-compliance/components/sdk/ComplianceAdvisorWidget.tsx +++ b/admin-compliance/components/sdk/ComplianceAdvisorWidget.tsx @@ -1,63 +1,16 @@ 'use client' import { useState, useEffect, useRef, useCallback } from 'react' +import { EXAMPLE_QUESTIONS, AdvisorEmptyState, AdvisorMessageList, type Message } from './ComplianceAdvisorParts' // ============================================================================= // TYPES // ============================================================================= -interface Message { - id: string - role: 'user' | 'agent' - content: string - timestamp: Date -} - interface ComplianceAdvisorWidgetProps { currentStep?: string } -// ============================================================================= -// EXAMPLE QUESTIONS BY STEP -// ============================================================================= - -const EXAMPLE_QUESTIONS: Record = { - vvt: [ - 'Was ist ein Verarbeitungsverzeichnis?', - 'Welche Informationen muss ich erfassen?', - 'Wie dokumentiere ich die Rechtsgrundlage?', - ], - 'compliance-scope': [ - 'Was bedeutet L3?', - 'Wann brauche ich eine DSFA?', - 'Was ist der Unterschied zwischen L2 und L3?', - ], - tom: [ - 'Was sind TOM?', - 'Welche Massnahmen sind erforderlich?', - 'Wie dokumentiere ich Verschluesselung?', - ], - dsfa: [ - 'Was ist eine DSFA?', - 'Wann ist eine DSFA verpflichtend?', - 'Wie bewerte ich Risiken?', - ], - loeschfristen: [ - 'Wie definiere ich Loeschfristen?', - 'Was ist der Unterschied zwischen Loeschpflicht und Aufbewahrungspflicht?', - 'Wann muss ich Daten loeschen?', - ], - default: [ - 'Wie starte ich mit dem SDK?', - 'Was ist der erste Schritt?', - 'Welche Compliance-Anforderungen gelten fuer KI-Systeme?', - ], -} - -// ============================================================================= -// COMPONENT -// ============================================================================= - type Country = 'DE' | 'AT' | 'CH' | 'EU' const COUNTRIES: { code: Country; label: string }[] = [ @@ -67,6 +20,10 @@ const COUNTRIES: { code: Country; label: string }[] = [ { code: 'EU', label: 'EU' }, ] +// ============================================================================= +// COMPONENT +// ============================================================================= + export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceAdvisorWidgetProps) { const [isOpen, setIsOpen] = useState(false) const [isExpanded, setIsExpanded] = useState(false) @@ -77,22 +34,18 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA const messagesEndRef = useRef(null) const abortControllerRef = useRef(null) - // Get example questions for current step const exampleQuestions = EXAMPLE_QUESTIONS[currentStep] || EXAMPLE_QUESTIONS.default - // Auto-scroll to bottom when messages change useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) - // Cleanup abort controller on unmount useEffect(() => { return () => { abortControllerRef.current?.abort() } }, []) - // Handle send message with real LLM + RAG const handleSendMessage = useCallback( async (content: string) => { if (!content.trim() || isTyping) return @@ -109,12 +62,9 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA setIsTyping(true) const agentMessageId = `msg-${Date.now()}-agent` - - // Create abort controller for this request abortControllerRef.current = new AbortController() try { - // Build conversation history for context const history = messages.map((m) => ({ role: m.role === 'user' ? 'user' : 'assistant', content: m.content, @@ -137,18 +87,11 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA throw new Error(errorData.error || `Server-Fehler (${response.status})`) } - // Add empty agent message for streaming setMessages((prev) => [ ...prev, - { - id: agentMessageId, - role: 'agent', - content: '', - timestamp: new Date(), - }, + { id: agentMessageId, role: 'agent', content: '', timestamp: new Date() }, ]) - // Read streaming response const reader = response.body!.getReader() const decoder = new TextDecoder() let accumulated = '' @@ -158,14 +101,10 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA if (done) break accumulated += decoder.decode(value, { stream: true }) - - // Update agent message with accumulated content const currentText = accumulated setMessages((prev) => prev.map((m) => (m.id === agentMessageId ? { ...m, content: currentText } : m)) ) - - // Auto-scroll during streaming requestAnimationFrame(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }) @@ -174,32 +113,21 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA setIsTyping(false) } catch (error) { if ((error as Error).name === 'AbortError') { - // User cancelled, keep partial response setIsTyping(false) return } - const errorMessage = - error instanceof Error ? error.message : 'Verbindung fehlgeschlagen' - - // Add or update agent message with error + const errorMessage = error instanceof Error ? error.message : 'Verbindung fehlgeschlagen' setMessages((prev) => { const hasAgent = prev.some((m) => m.id === agentMessageId) if (hasAgent) { return prev.map((m) => - m.id === agentMessageId - ? { ...m, content: `Fehler: ${errorMessage}` } - : m + m.id === agentMessageId ? { ...m, content: `Fehler: ${errorMessage}` } : m ) } return [ ...prev, - { - id: agentMessageId, - role: 'agent' as const, - content: `Fehler: ${errorMessage}`, - timestamp: new Date(), - }, + { id: agentMessageId, role: 'agent' as const, content: `Fehler: ${errorMessage}`, timestamp: new Date() }, ] }) setIsTyping(false) @@ -208,18 +136,11 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA [isTyping, messages, currentStep, selectedCountry] ) - // Handle stop generation const handleStopGeneration = useCallback(() => { abortControllerRef.current?.abort() setIsTyping(false) }, []) - // Handle example question click - const handleExampleClick = (question: string) => { - handleSendMessage(question) - } - - // Handle key press const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() @@ -234,18 +155,8 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA className="fixed bottom-6 right-[5.5rem] w-14 h-14 bg-indigo-600 hover:bg-indigo-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all duration-200 hover:scale-110 z-50" aria-label="Compliance Advisor oeffnen" > - - + + ) @@ -257,18 +168,8 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA
- - + +
@@ -278,11 +179,7 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA @@ -296,46 +193,17 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA className="text-white/80 hover:text-white transition-colors" aria-label={isExpanded ? 'Verkleinern' : 'Vergroessern'} > - + {isExpanded ? ( - + ) : ( - + )} -
@@ -344,102 +212,16 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA {/* Messages Area */}
{messages.length === 0 ? ( -
-
- - - -
-

- Willkommen beim Compliance Advisor -

-

- Stellen Sie Fragen zu DSGVO, KI-Verordnung und mehr. -

- - {/* Example Questions */} -
-

- Beispielfragen: -

- {exampleQuestions.map((question, idx) => ( - - ))} -
-
+ handleSendMessage(q)} + /> ) : ( - <> - {messages.map((message) => ( -
-
-

- {message.content || (message.role === 'agent' && isTyping ? '' : message.content)} -

-

- {message.timestamp.toLocaleTimeString('de-DE', { - hour: '2-digit', - minute: '2-digit', - })} -

-
-
- ))} - - {isTyping && ( -
-
-
-
-
-
-
-
-
- )} - -
- + )}
@@ -461,18 +243,8 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors" title="Generierung stoppen" > - - + + ) : ( @@ -481,18 +253,8 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA disabled={!inputValue.trim()} className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" > - - + + )} diff --git a/admin-compliance/components/sdk/SDKPipelineSidebar/SDKPipelineSidebar.tsx b/admin-compliance/components/sdk/SDKPipelineSidebar/SDKPipelineSidebar.tsx index e6330b2..b660383 100644 --- a/admin-compliance/components/sdk/SDKPipelineSidebar/SDKPipelineSidebar.tsx +++ b/admin-compliance/components/sdk/SDKPipelineSidebar/SDKPipelineSidebar.tsx @@ -11,301 +11,10 @@ * - 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, SDK_PACKAGES, getStepsForPackage, type SDKStep, type SDKPackageId } from '@/lib/sdk' - -// ============================================================================= -// ICONS -// ============================================================================= - -const CheckIcon = () => ( - - - -) - -const LockIcon = () => ( - - - -) - -const ArrowIcon = () => ( - - - -) - -const CloseIcon = () => ( - - - -) - -const PipelineIcon = () => ( - - - -) - -// ============================================================================= -// STEP ITEM -// ============================================================================= - -interface StepItemProps { - step: SDKStep - isActive: boolean - isCompleted: boolean - onNavigate: () => void -} - -function StepItem({ step, isActive, isCompleted, onNavigate }: StepItemProps) { - return ( - - {step.nameShort} - {isCompleted && !isActive && ( - - - - )} - {isActive && ( - - )} - - ) -} - -// ============================================================================= -// PACKAGE SECTION -// ============================================================================= - -interface PackageSectionProps { - pkg: (typeof SDK_PACKAGES)[number] - steps: SDKStep[] - completion: number - currentStepId: string - completedSteps: string[] - isLocked: boolean - onNavigate: () => void - isExpanded: boolean - onToggle: () => void -} - -function PackageSection({ - pkg, - steps, - completion, - currentStepId, - completedSteps, - isLocked, - onNavigate, - isExpanded, - onToggle, -}: PackageSectionProps) { - return ( -
- {/* Package Header */} - - - {/* Progress Bar */} - {!isLocked && ( -
-
-
-
-
- )} - - {/* Steps List */} - {isExpanded && !isLocked && ( -
- {steps.map(step => ( - - ))} -
- )} -
- ) -} - -// ============================================================================= -// PIPELINE FLOW VISUALIZATION -// ============================================================================= - -function PipelineFlow() { - return ( -
-
- Datenfluss -
-
-
- {SDK_PACKAGES.map((pkg, idx) => ( -
- - {pkg.icon} - - {pkg.nameShort} - {idx < SDK_PACKAGES.length - 1 && } -
- ))} -
-
-
- ) -} - -// ============================================================================= -// SIDEBAR CONTENT -// ============================================================================= - -interface SidebarContentProps { - onNavigate: () => void -} - -function SidebarContent({ onNavigate }: SidebarContentProps) { - const pathname = usePathname() - const { state, packageCompletion } = useSDK() - const [expandedPackages, setExpandedPackages] = useState>({ - 'vorbereitung': true, - 'analyse': false, - 'dokumentation': false, - 'rechtliche-texte': false, - 'betrieb': false, - }) - - // Find current step - const currentStep = SDK_STEPS.find(s => s.url === pathname) - const currentStepId = currentStep?.id || '' - - // Auto-expand current package - useEffect(() => { - if (currentStep) { - setExpandedPackages(prev => ({ - ...prev, - [currentStep.package]: true, - })) - } - }, [currentStep]) - - const togglePackage = (packageId: SDKPackageId) => { - setExpandedPackages(prev => ({ ...prev, [packageId]: !prev[packageId] })) - } - - 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 - } - - // 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 ( -
- {/* Packages */} - {SDK_PACKAGES.map(pkg => ( - togglePackage(pkg.id)} - /> - ))} - - {/* Pipeline Flow */} - - - {/* Quick Info */} - {currentStep && ( -
-
- Aktuell:{' '} - {currentStep.description} -
-
- )} -
- ) -} +import { useSDK } from '@/lib/sdk' +import { CloseIcon, PipelineIcon } from './SDKPipelineSidebarIcons' +import { SidebarContent } from './SDKPipelineSidebarParts' // ============================================================================= // MAIN COMPONENT - RESPONSIVE @@ -336,7 +45,7 @@ export function SDKPipelineSidebar({ fabPosition = 'bottom-right' }: SDKPipeline localStorage.setItem('sdk-pipeline-sidebar-collapsed', String(newState)) } - // Close drawer on route change or escape key + // Close drawer on escape key useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape') { @@ -364,6 +73,17 @@ export function SDKPipelineSidebar({ fabPosition = 'bottom-right' }: SDKPipeline ? 'right-4 bottom-6' : 'left-4 bottom-6' + const progressCircle = ( + + + + + ) + return ( <> {/* Desktop: Fixed Sidebar (when expanded) */} @@ -374,16 +94,10 @@ export function SDKPipelineSidebar({ fabPosition = 'bottom-right' }: SDKPipeline
- - - +
- - SDK Pipeline - - - {completionPercentage}% - + SDK Pipeline + {completionPercentage}%
- {/* Content */}
{}} /> @@ -409,35 +122,12 @@ export function SDKPipelineSidebar({ fabPosition = 'bottom-right' }: SDKPipeline {isDesktopCollapsed && ( )} @@ -448,30 +138,7 @@ export function SDKPipelineSidebar({ fabPosition = 'bottom-right' }: SDKPipeline aria-label="SDK Pipeline Navigation oeffnen" > - {/* Progress indicator */} - - - - + {progressCircle} {/* Mobile/Tablet: Drawer Overlay */} @@ -482,22 +149,15 @@ export function SDKPipelineSidebar({ fabPosition = 'bottom-right' }: SDKPipeline className="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity" onClick={() => setIsMobileOpen(false)} /> - {/* Drawer */}
{/* Drawer Header */}
- - - +
- - SDK Pipeline - - - {completionPercentage}% - + SDK Pipeline + {completionPercentage}%
- {/* Drawer Content */}
setIsMobileOpen(false)} /> @@ -520,12 +179,8 @@ export function SDKPipelineSidebar({ fabPosition = 'bottom-right' }: SDKPipeline {/* CSS for slide-in animation */}