refactor(admin): split remaining components

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 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-17 12:41:29 +02:00
parent d32ad81094
commit b00fe6cb73
9 changed files with 1236 additions and 1299 deletions

View File

@@ -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<string, string[]> = {
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<HTMLDivElement>(null)
const abortControllerRef = useRef<AbortController | null>(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"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
/>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
</svg>
</button>
)
@@ -257,18 +168,8 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA
<div className="bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-4 py-3 rounded-t-2xl flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center">
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
</div>
<div>
@@ -278,11 +179,7 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA
<button
key={code}
onClick={() => setSelectedCountry(code)}
className={`px-1.5 py-0.5 text-[10px] font-medium rounded transition-colors ${
selectedCountry === code
? 'bg-white text-indigo-700'
: 'bg-white/15 text-white/80 hover:bg-white/25'
}`}
className={`px-1.5 py-0.5 text-[10px] font-medium rounded transition-colors ${selectedCountry === code ? 'bg-white text-indigo-700' : 'bg-white/15 text-white/80 hover:bg-white/25'}`}
>
{label}
</button>
@@ -296,46 +193,17 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA
className="text-white/80 hover:text-white transition-colors"
aria-label={isExpanded ? 'Verkleinern' : 'Vergroessern'}
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{isExpanded ? (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 9L4 4m0 0v4m0-4h4m6 6l5 5m0 0v-4m0 4h-4"
/>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 9L4 4m0 0v4m0-4h4m6 6l5 5m0 0v-4m0 4h-4" />
) : (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5"
/>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5" />
)}
</svg>
</button>
<button
onClick={() => setIsOpen(false)}
className="text-white/80 hover:text-white transition-colors"
aria-label="Schliessen"
>
<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"
/>
<button onClick={() => setIsOpen(false)} className="text-white/80 hover:text-white transition-colors" aria-label="Schliessen">
<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>
</button>
</div>
@@ -344,102 +212,16 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA
{/* Messages Area */}
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50">
{messages.length === 0 ? (
<div className="text-center py-8">
<div className="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg
className="w-8 h-8 text-purple-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
/>
</svg>
</div>
<h3 className="text-sm font-medium text-gray-900 mb-2">
Willkommen beim Compliance Advisor
</h3>
<p className="text-xs text-gray-500 mb-4">
Stellen Sie Fragen zu DSGVO, KI-Verordnung und mehr.
</p>
{/* Example Questions */}
<div className="text-left space-y-2">
<p className="text-xs font-medium text-gray-700 mb-2">
Beispielfragen:
</p>
{exampleQuestions.map((question, idx) => (
<button
key={idx}
onClick={() => handleExampleClick(question)}
className="w-full text-left px-3 py-2 text-xs bg-white hover:bg-purple-50 border border-gray-200 rounded-lg transition-colors text-gray-700"
>
{question}
</button>
))}
</div>
</div>
<AdvisorEmptyState
exampleQuestions={exampleQuestions}
onExampleClick={(q) => handleSendMessage(q)}
/>
) : (
<>
{messages.map((message) => (
<div
key={message.id}
className={`flex ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[80%] rounded-lg px-3 py-2 ${
message.role === 'user'
? 'bg-indigo-600 text-white'
: 'bg-white border border-gray-200 text-gray-800'
}`}
>
<p
className={`text-sm ${message.role === 'agent' ? 'whitespace-pre-wrap' : ''}`}
>
{message.content || (message.role === 'agent' && isTyping ? '' : message.content)}
</p>
<p
className={`text-xs mt-1 ${
message.role === 'user'
? 'text-indigo-200'
: 'text-gray-400'
}`}
>
{message.timestamp.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
})}
</p>
</div>
</div>
))}
{isTyping && (
<div className="flex justify-start">
<div className="bg-white border border-gray-200 rounded-lg px-3 py-2">
<div className="flex space-x-1">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
<div
className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"
style={{ animationDelay: '0.1s' }}
/>
<div
className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"
style={{ animationDelay: '0.2s' }}
/>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</>
<AdvisorMessageList
messages={messages}
isTyping={isTyping}
messagesEndRef={messagesEndRef}
/>
)}
</div>
@@ -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"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 6h12v12H6z"
/>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 6h12v12H6z" />
</svg>
</button>
) : (
@@ -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"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
/>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
</button>
)}