'use client' import { useState, useEffect, useRef, useCallback } from 'react' import { EXAMPLE_QUESTIONS, AdvisorEmptyState, AdvisorMessageList, type Message } from './ComplianceAdvisorParts' // ============================================================================= // TYPES // ============================================================================= interface ComplianceAdvisorWidgetProps { currentStep?: string } 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' }, ] // ============================================================================= // COMPONENT // ============================================================================= export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceAdvisorWidgetProps) { 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) const exampleQuestions = EXAMPLE_QUESTIONS[currentStep] || EXAMPLE_QUESTIONS.default useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) useEffect(() => { return () => { abortControllerRef.current?.abort() } }, []) 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` abortControllerRef.current = new AbortController() try { 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})`) } setMessages((prev) => [ ...prev, { id: agentMessageId, role: 'agent', content: '', timestamp: new Date() }, ]) 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 }) const currentText = accumulated setMessages((prev) => prev.map((m) => (m.id === agentMessageId ? { ...m, content: currentText } : m)) ) requestAnimationFrame(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }) } setIsTyping(false) } catch (error) { if ((error as Error).name === 'AbortError') { setIsTyping(false) return } 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 ) } return [ ...prev, { id: agentMessageId, role: 'agent' as const, content: `Fehler: ${errorMessage}`, timestamp: new Date() }, ] }) setIsTyping(false) } }, [isTyping, messages, currentStep, selectedCountry] ) const handleStopGeneration = useCallback(() => { abortControllerRef.current?.abort() setIsTyping(false) }, []) const [emailSending, setEmailSending] = useState(false) const [emailSent, setEmailSent] = useState(false) const handleSendAsEmail = useCallback(async () => { if (messages.length === 0 || emailSending) return setEmailSending(true) try { // Build HTML from chat messages const qaPairs = messages.reduce<{ q: string; a: string }[]>((acc, m, i) => { if (m.role === 'user') { const next = messages[i + 1] acc.push({ q: m.content, a: next?.role === 'agent' ? next.content : '(keine Antwort)' }) } return acc }, []) const qaHtml = qaPairs.map(({ q, a }) => `

Frage: ${q}

${a}

` ).join('') const bodyHtml = `

Compliance Advisor — Beratungsprotokoll

Datum: ${new Date().toLocaleString('de-DE')} | Land: ${selectedCountry} | Kontext: ${currentStep}


${qaHtml}

Automatisch erstellt vom BreakPilot Compliance Advisor (Qwen)

` await fetch('/api/sdk/v1/agent/notify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ recipient: 'dsb@breakpilot.local', subject: `Compliance Advisor — ${qaPairs.length} Fragen (${currentStep})`, body_html: bodyHtml, role: 'Datenschutzbeauftragter', }), }) setEmailSent(true) setTimeout(() => setEmailSent(false), 3000) } catch (e) { console.error('Email send failed:', e) } finally { setEmailSending(false) } }, [messages, emailSending, selectedCountry, currentStep]) 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 }) => ( ))}
{/* Send as Email */} {messages.length > 0 && ( )}
{/* Messages Area */}
{messages.length === 0 ? ( handleSendMessage(q)} /> ) : ( )}
{/* 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 ? ( ) : ( )}
) }