'use client' import React, { useState, useCallback, useRef } from 'react' import { ComplianceResultTabs } from './ComplianceResultTabs' import { DocumentRow } from './DocumentRow' import { PreScanWizard, useScanContext, isContextComplete } from './PreScanWizard' import { DOCUMENT_TYPES, type DocTypeId } from './_document_types' import { STORAGE_KEY_STATE, STORAGE_KEY_RESULTS, STORAGE_KEY_HISTORY, STORAGE_KEY_CHECK_ID, countWords, initState, type DocState, type DocsState, type HistoryEntry, } from './_compliance_storage' import { useCompanyOrigin } from './_useCompanyOrigin' export function ComplianceCheckTab() { const [docs, setDocs] = useState(initState) const { companyName, setCompanyName, originDomain, setOriginDomain } = useCompanyOrigin() const [scanContext, setScanContext] = useScanContext() const [useAgent, setUseAgent] = useState(false) const [tdmOverride, setTdmOverride] = useState(false) const [tdmOverrideReason, setTdmOverrideReason] = useState('') const [loading, setLoading] = useState(false) const [progress, setProgress] = useState('') const [progressPct, setProgressPct] = useState(0) const [results, setResults] = useState(() => { if (typeof window === 'undefined') return null try { const s = localStorage.getItem(STORAGE_KEY_RESULTS); return s ? JSON.parse(s) : null } catch { return null } }) const [error, setError] = useState(null) const [activeCheckId, setActiveCheckId] = useState(() => typeof window !== 'undefined' ? localStorage.getItem(STORAGE_KEY_CHECK_ID) || '' : '' ) const [history, setHistory] = useState(() => { if (typeof window === 'undefined') return [] try { return JSON.parse(localStorage.getItem(STORAGE_KEY_HISTORY) || '[]') } catch { return [] } }) // SSE: progressive Themen-Tabs (additiv zum Polling). const esRef = useRef(null) React.useEffect(() => () => { try { esRef.current?.close() } catch { /* noop */ } }, []) // Persist URLs and texts (not loading/error state) React.useEffect(() => { const toSave: Record = {} for (const [key, val] of Object.entries(docs)) { toSave[key] = { url: val.url, text: val.text } } try { localStorage.setItem(STORAGE_KEY_STATE, JSON.stringify(toSave)) } catch { /* quota */ } }, [docs]) // Resume polling if check was in progress when navigating away React.useEffect(() => { if (!activeCheckId || results) return let cancelled = false setLoading(true) setProgress('Pruefung laeuft noch...') const poll = async () => { while (!cancelled) { await new Promise(r => setTimeout(r, 3000)) try { const res = await fetch(`/api/sdk/v1/agent/compliance-check?check_id=${activeCheckId}`) if (!res.ok) continue const data = await res.json() if (data.progress) setProgress(data.progress) if (typeof data.progress_pct === 'number') setProgressPct(data.progress_pct) if (data.status === 'completed' && data.result) { setResults(data.result); setProgress(''); setProgressPct(0); setLoading(false) localStorage.setItem(STORAGE_KEY_RESULTS, JSON.stringify(data.result)) localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') return } if (['failed', 'not_found', 'skipped_tdm'].includes(data.status)) { if (data.status !== 'not_found') setError(data.error || (data.status === 'skipped_tdm' ? 'TDM-Vorbehalt erkannt — Crawl uebersprungen' : 'Pruefung fehlgeschlagen')) setProgress(''); setProgressPct(0); setLoading(false); localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId(''); return } } catch { /* retry */ } } } poll() return () => { cancelled = true } }, []) // eslint-disable-line react-hooks/exhaustive-deps const updateDoc = useCallback((docType: DocTypeId, patch: Partial) => { setDocs(prev => ({ ...prev, [docType]: { ...prev[docType], ...patch } })) }, []) const handleFetchText = useCallback(async (docType: DocTypeId) => { const url = docs[docType].url.trim() if (!url) return updateDoc(docType, { loading: true, error: null }) try { const res = await fetch('/api/sdk/v1/agent/extract-text', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }), }) if (!res.ok) { const msg = res.status === 404 ? 'Seite nicht erreichbar' : `Fehler beim Laden (${res.status})` throw new Error(msg) } const data = await res.json() updateDoc(docType, { text: data.text || '', loading: false }) } catch (e) { updateDoc(docType, { loading: false, error: e instanceof Error ? e.message : 'Text konnte nicht geladen werden', }) } }, [docs, updateDoc]) const handleFileUpload = useCallback(async (docType: DocTypeId, file: File) => { // For now, read as text. PDF/DOCX parsing can be added server-side later. const reader = new FileReader() reader.onload = () => { updateDoc(docType, { text: reader.result as string }) } reader.readAsText(file) }, [updateDoc]) // SSE: füllt agent_outputs progressiv, sobald ein Thema fertig ist. // Das Polling unten liefert weiterhin das finale Gesamtergebnis. const openTopicStream = useCallback((checkId: string) => { try { esRef.current?.close() } catch { /* noop */ } const partial: any = { results: [], agent_outputs: {} } const es = new EventSource( `/api/sdk/v1/agent/compliance-check/${checkId}/stream`, ) esRef.current = es es.onmessage = (ev) => { try { const data = JSON.parse(ev.data) if (data.type === 'topic' && data.topic && data.output) { partial.agent_outputs = { ...partial.agent_outputs, [data.topic]: data.output, } setResults((prev: any) => (prev && Array.isArray(prev.results) && prev.results.length > 0) ? prev // finales Ergebnis schon da → behalten : { ...partial }, ) } else if (data.type === 'progress') { if (data.msg) setProgress(data.msg) if (typeof data.pct === 'number') setProgressPct(data.pct) } else if (data.type === 'complete' || data.type === 'stream_close') { try { es.close() } catch { /* noop */ } } } catch { /* noop */ } } es.onerror = () => { try { es.close() } catch { /* noop */ } } }, []) const filledCount = Object.values(docs).filter(d => d.url.trim() || d.text.trim()).length const handleSubmit = async () => { if (filledCount === 0) return setLoading(true) setError(null) setResults(null) setProgress('Compliance-Check wird gestartet...') setProgressPct(0) try { const entries = DOCUMENT_TYPES .filter(dt => docs[dt.id].url.trim() || docs[dt.id].text.trim()) .map(dt => ({ doc_type: dt.id, label: dt.label, url: docs[dt.id].url.trim(), text: docs[dt.id].text.trim() || undefined, })) const startRes = await fetch('/api/sdk/v1/agent/compliance-check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ documents: entries, use_agent: useAgent, tdm_override: tdmOverride && tdmOverrideReason.trim().length >= 10, tdm_override_reason: tdmOverrideReason.trim(), company_name: companyName.trim() || undefined, origin_domain: originDomain.trim() || undefined, // P79 — Pre-Scan-Wizard 8 Pflichtfelder; treibt MC-Scope-Filter (P72) scan_context: scanContext, }), }) if (!startRes.ok) throw new Error(`Pruefung konnte nicht gestartet werden: ${startRes.status}`) const { check_id } = await startRes.json() if (!check_id) throw new Error('Keine Check-ID erhalten') setActiveCheckId(check_id) localStorage.setItem(STORAGE_KEY_CHECK_ID, check_id) openTopicStream(check_id) // Poll for results (max 25 min = 500 polls x 3s) let attempts = 0 while (attempts < 500) { await new Promise(r => setTimeout(r, 3000)) const pollRes = await fetch(`/api/sdk/v1/agent/compliance-check?check_id=${check_id}`) if (!pollRes.ok) { attempts++; continue } const pollData = await pollRes.json() if (pollData.progress) setProgress(pollData.progress) if (typeof pollData.progress_pct === 'number') setProgressPct(pollData.progress_pct) if (pollData.status === 'completed' && pollData.result) { setResults(pollData.result) setProgress('') setProgressPct(0) localStorage.setItem(STORAGE_KEY_RESULTS, JSON.stringify(pollData.result)) localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') const resultKey = `compliance-check-result-${Date.now()}` try { localStorage.setItem(resultKey, JSON.stringify(pollData.result)) } catch { /* quota */ } const entry: HistoryEntry = { date: new Date().toISOString(), docCount: entries.length, findings: pollData.result.total_findings || 0, resultKey, } const updated = [entry, ...history].slice(0, 30) setHistory(updated) localStorage.setItem(STORAGE_KEY_HISTORY, JSON.stringify(updated)) break } if (['failed', 'skipped_tdm'].includes(pollData.status)) { localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') throw new Error(pollData.error || (pollData.status === 'skipped_tdm' ? 'TDM-Vorbehalt' : 'Pruefung fehlgeschlagen')) } attempts++ } if (attempts >= 500) { localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') throw new Error('Zeitlimit ueberschritten (15 Min)') } } catch (e) { setError(e instanceof Error ? e.message : 'Unbekannter Fehler') setProgress('') setProgressPct(0) try { esRef.current?.close() } catch { /* noop */ } } finally { setLoading(false) } } const loadFromHistory = (entry: HistoryEntry) => { if (entry.resultKey) { try { const saved = localStorage.getItem(entry.resultKey) if (saved) { setResults(JSON.parse(saved)); return } } catch { /* ignore */ } } try { const last = localStorage.getItem(STORAGE_KEY_RESULTS) if (last) setResults(JSON.parse(last)) } catch { /* ignore */ } } const contextReady = isContextComplete(scanContext) return (
{/* Info box */}

Compliance-Check (Alle Dokumente)

Geben Sie die URLs Ihrer Rechtstexte ein oder laden Sie die Dokumente hoch. Das System prueft alle Pflichtangaben nach DSGVO, TDDDG, TMG und UWG. Pflichtdokumente sind mit * markiert.

{/* Firma + Domain (priorisiert vor extracted_profile-LLM-Inferenz) */}
{/* P79 Pre-Scan-Wizard — 8 Pflichtfelder zum MC-Scope-Filter (P72) */} {/* Document rows */}
{DOCUMENT_TYPES.map(dt => ( updateDoc(dt.id, { url })} onFetchText={() => handleFetchText(dt.id)} onTextChange={text => updateDoc(dt.id, { text })} onFileUpload={file => handleFileUpload(dt.id, file)} /> ))}
{/* Agent toggle + submit */}
{filledCount} von {DOCUMENT_TYPES.length} Dokumenten ausgefuellt
{tdmOverride && setTdmOverrideReason(e.target.value)} placeholder="z.B. Auftragsbeziehung Safetykon GmbH, Email Hr. X vom 18.05.2026" className="w-full px-3 py-2 text-xs border border-amber-300 rounded bg-white" />} {tdmOverride && tdmOverrideReason.trim().length < 10 &&

Pflicht: Reason mit min. 10 Zeichen (Audit-Spur).

}
{/* Submit button — Wizard muss vollstaendig sein (P79) */} {/* Progress */} {progress && (
{progress} {progressPct}%
)} {/* Error */} {error && (
{error}
)} {/* Results — strukturierte Themen-Tabs (Impressum, …) + Roh-Checkliste */} {results && results.results && ( )} {/* History */} {history.length > 0 && (

Letzte Compliance-Checks

{history.map((h, i) => ( ))}
)}
) }