'use client' import { useState, useEffect, useRef, useCallback } from 'react' // ============================================================================= // TYPES // ============================================================================= interface Message { id: string role: 'user' | 'agent' content: string timestamp: Date } interface ComplianceAdvisorWidgetProps { currentStep?: string enableDraftingEngine?: boolean } // ============================================================================= // 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 }[] = [ { code: 'DE', label: 'DE' }, { code: 'AT', label: 'AT' }, { code: 'CH', label: 'CH' }, { code: 'EU', label: 'EU' }, ] export function ComplianceAdvisorWidget({ currentStep = 'default', enableDraftingEngine = false }: ComplianceAdvisorWidgetProps) { // Feature-flag: If Drafting Engine enabled, render DraftingEngineWidget instead if (enableDraftingEngine) { const { DraftingEngineWidget } = require('./DraftingEngineWidget') return } const [isOpen, setIsOpen] = useState(false) const [isExpanded, setIsExpanded] = useState(false) const [messages, setMessages] = useState([]) const [inputValue, setInputValue] = useState('') const [isTyping, setIsTyping] = useState(false) const [selectedCountry, setSelectedCountry] = useState('DE') 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 const userMessage: Message = { id: `msg-${Date.now()}`, role: 'user', content: content.trim(), timestamp: new Date(), } setMessages((prev) => [...prev, userMessage]) setInputValue('') 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, })) const response = await fetch('/api/sdk/compliance-advisor/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: content.trim(), history, currentStep, country: selectedCountry, }), signal: abortControllerRef.current.signal, }) if (!response.ok) { const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' })) 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(), }, ]) // Read streaming response const reader = response.body!.getReader() const decoder = new TextDecoder() let accumulated = '' while (true) { const { done, value } = await reader.read() 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' }) }) } 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 setMessages((prev) => { const hasAgent = prev.some((m) => m.id === agentMessageId) if (hasAgent) { return prev.map((m) => m.id === agentMessageId ? { ...m, content: `Fehler: ${errorMessage}` } : m ) } return [ ...prev, { id: agentMessageId, role: 'agent' as const, content: `Fehler: ${errorMessage}`, timestamp: new Date(), }, ] }) setIsTyping(false) } }, [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() handleSendMessage(inputValue) } } if (!isOpen) { return ( ) } return (
{/* Header */}
Compliance Advisor
{COUNTRIES.map(({ code, label }) => ( ))}
{/* 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) => ( ))}
) : ( <> {messages.map((message) => (

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

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

))} {isTyping && (
)}
)}
{/* Input Area */}
setInputValue(e.target.value)} onKeyDown={handleKeyDown} placeholder="Frage eingeben..." disabled={isTyping} className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50" /> {isTyping ? ( ) : ( )}
) }