'use client' import React, { useState, useCallback } from 'react' import { ChecklistView } from './ChecklistView' import { DocumentRow } from './DocumentRow' const DOCUMENT_TYPES = [ { id: 'dse', label: 'DSI (Datenschutzinformation)', required: true }, { id: 'impressum', label: 'Impressum', required: true }, { id: 'social_media', label: 'Social Media DSE', required: false }, { id: 'cookie', label: 'Cookie-Richtlinie', required: false }, { id: 'agb', label: 'AGB', required: false }, { id: 'nutzungsbedingungen', label: 'Nutzungsbedingungen', required: false }, { id: 'widerruf', label: 'Widerrufsbelehrung', required: false }, { id: 'dsb', label: 'DSB-Kontakt', required: false }, ] as const type DocTypeId = typeof DOCUMENT_TYPES[number]['id'] interface DocState { url: string text: string loading: boolean error: string | null } type DocsState = Record const STORAGE_KEY_STATE = 'compliance-check-state' const STORAGE_KEY_RESULTS = 'compliance-check-results' const STORAGE_KEY_HISTORY = 'compliance-check-history' const STORAGE_KEY_CHECK_ID = 'compliance-check-active-id' function emptyDocState(): DocState { return { url: '', text: '', loading: false, error: null } } function initState(): DocsState { if (typeof window === 'undefined') { return Object.fromEntries(DOCUMENT_TYPES.map(d => [d.id, emptyDocState()])) as DocsState } try { const saved = localStorage.getItem(STORAGE_KEY_STATE) if (saved) { const parsed = JSON.parse(saved) as Record return Object.fromEntries( DOCUMENT_TYPES.map(d => [d.id, { url: parsed[d.id]?.url || '', text: parsed[d.id]?.text || '', loading: false, error: null, }]) ) as DocsState } } catch { /* ignore */ } return Object.fromEntries(DOCUMENT_TYPES.map(d => [d.id, emptyDocState()])) as DocsState } function countWords(text: string): number { if (!text.trim()) return 0 return text.trim().split(/\s+/).length } interface HistoryEntry { date: string docCount: number findings: number resultKey: string } export function ComplianceCheckTab() { const [docs, setDocs] = useState(initState) const [useAgent, setUseAgent] = useState(false) const [loading, setLoading] = useState(false) const [progress, setProgress] = useState('') 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 [] } }) // 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 (data.status === 'completed' && data.result) { setResults(data.result); setProgress(''); setLoading(false) localStorage.setItem(STORAGE_KEY_RESULTS, JSON.stringify(data.result)) localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') return } if (data.status === 'failed' || data.status === 'not_found') { if (data.status === 'failed') setError(data.error || 'Pruefung fehlgeschlagen') setProgress(''); 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]) 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...') 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, }), }) 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) // Poll for results (max 15 min = 300 polls x 3s) let attempts = 0 while (attempts < 300) { 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 (pollData.status === 'completed' && pollData.result) { setResults(pollData.result) setProgress('') 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 (pollData.status === 'failed') { localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') throw new Error(pollData.error || 'Pruefung fehlgeschlagen') } attempts++ } if (attempts >= 300) { 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('') } 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 */ } } 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.

{/* 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
{/* Submit button */} {/* Progress */} {progress && (
{progress}
)} {/* Error */} {error && (
{error}
)} {/* Results */} {results && results.results && (
{/* Business Profile */} {results.business_profile && (
Erkanntes Geschaeftsmodell
Typ: {results.business_profile.business_type?.toUpperCase()} Branche: {results.business_profile.industry} {results.business_profile.has_online_shop && Online-Shop} {results.business_profile.is_regulated_profession && Regulierter Beruf ({results.business_profile.regulated_profession_type})}
)} {/* Extracted Profile — pre-fill suggestion */} {results.extracted_profile?.company_profile && Object.keys(results.extracted_profile.company_profile).length > 0 && (
Aus Dokumenten extrahiert
{results.extracted_profile.company_profile.companyName && ( Firma: {results.extracted_profile.company_profile.companyName} )} {results.extracted_profile.company_profile.legalForm && ( Rechtsform: {results.extracted_profile.company_profile.legalForm.toUpperCase()} )} {results.extracted_profile.company_profile.headquartersCity && ( Sitz: {results.extracted_profile.company_profile.headquartersZip} {results.extracted_profile.company_profile.headquartersCity} )} {results.extracted_profile.company_profile.dpoEmail && ( DSB: {results.extracted_profile.company_profile.dpoEmail} )} {results.extracted_profile.company_profile.ustIdNr && ( USt-IdNr: {results.extracted_profile.company_profile.ustIdNr} )}
{results.extracted_profile.compliance_scope_hints?.length > 0 && (
Scope-Hinweise: {results.extracted_profile.compliance_scope_hints.map((h: any, i: number) => ( {h.source} ))}
)}
)} {/* Banner Check Result */} {results.banner_result && (
0 ? 'bg-amber-50 border-amber-200' : results.banner_result.detected ? 'bg-green-50 border-green-200' : 'bg-gray-50 border-gray-200' }`}>
0 ? 'bg-amber-500' : results.banner_result.detected ? 'bg-green-500' : 'bg-gray-400' }`} /> Cookie-Banner-Check (automatisch)
{results.banner_result.detected ? ( <> Banner erkannt{results.banner_result.provider ? ` (${results.banner_result.provider})` : ''}. {results.banner_result.violations > 0 ? ` ${results.banner_result.violations} Auffaelligkeit${results.banner_result.violations !== 1 ? 'en' : ''} gefunden.` : ' Keine Auffaelligkeiten.'} ) : ( 'Kein Cookie-Banner erkannt oder Banner-Check nicht moeglich.' )}
)} {/* Email status */} {results.email_status && (
E-Mail: {results.email_status === 'sent' ? 'Gesendet' : results.email_status}
)}
)} {/* History */} {history.length > 0 && (

Letzte Compliance-Checks

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