feat: Unified Compliance-Check — 8 document types in one form
New 3-tab structure: Website-Scan, Compliance-Check, Banner-Check. Compliance-Check Tab (replaces Dokumenten-Pruefung + Impressum-Check): - 8 document rows: DSI, Impressum, Social Media, Cookie, AGB, Nutzungsbedingungen, Widerruf, DSB-Kontakt - Each row: URL input + "Text laden" + file upload + manual text - "Text laden" extracts via consent-tester, shows in editable textarea - User verifies/corrects text before checking - Empty fields = "not present" → own finding Business Profiler (business_profiler.py): - Detects B2B/B2C/B2G from all documents together - Recognizes regulated professions, online shops, editorial content - Context-aware: INFO checks become PASS/FAIL based on profile Backend: /compliance-check + /extract-text endpoints Frontend: ComplianceCheckTab.tsx + DocumentRow.tsx API proxies: compliance-check/route.ts + extract-text/route.ts Also: Impressum regex fixes (Telefon, AG, Geschaeftsfuehrung) and INFO severity for context-dependent checks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useRef } from 'react'
|
||||
|
||||
interface DocumentRowProps {
|
||||
label: string
|
||||
docType: string
|
||||
required?: boolean
|
||||
url: string
|
||||
text: string
|
||||
loading: boolean
|
||||
error: string | null
|
||||
wordCount: number
|
||||
onUrlChange: (url: string) => void
|
||||
onFetchText: () => void
|
||||
onTextChange: (text: string) => void
|
||||
onFileUpload: (file: File) => void
|
||||
}
|
||||
|
||||
export function DocumentRow({
|
||||
label,
|
||||
docType,
|
||||
required,
|
||||
url,
|
||||
text,
|
||||
loading,
|
||||
error,
|
||||
wordCount,
|
||||
onUrlChange,
|
||||
onFetchText,
|
||||
onTextChange,
|
||||
onFileUpload,
|
||||
}: DocumentRowProps) {
|
||||
const [showText, setShowText] = useState(false)
|
||||
const fileRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const textVisible = showText || text.length > 0
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
// Read text-based files directly
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const content = reader.result as string
|
||||
onTextChange(content)
|
||||
}
|
||||
reader.onerror = () => {
|
||||
// Let parent handle via onFileUpload for binary formats
|
||||
onFileUpload(file)
|
||||
}
|
||||
|
||||
if (file.name.endsWith('.txt') || file.type === 'text/plain') {
|
||||
reader.readAsText(file)
|
||||
} else {
|
||||
// PDF, DOCX — pass to parent for server-side parsing
|
||||
onFileUpload(file)
|
||||
}
|
||||
|
||||
// Reset input so the same file can be re-selected
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 rounded-lg p-3 space-y-2">
|
||||
{/* Header row: label + inputs */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-52 shrink-0">
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-0.5">*</span>}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={e => onUrlChange(e.target.value)}
|
||||
placeholder="https://example.com/datenschutz"
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
|
||||
{/* Fetch text button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onFetchText}
|
||||
disabled={loading || !url.trim()}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-40 disabled:cursor-not-allowed whitespace-nowrap transition-colors"
|
||||
>
|
||||
{loading ? (
|
||||
<svg className="animate-spin w-4 h-4 text-purple-500" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
) : (
|
||||
'Text laden'
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* File upload button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileRef.current?.click()}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
title="PDF, DOCX oder TXT hochladen"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||
</svg>
|
||||
</button>
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept=".pdf,.docx,.doc,.txt"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
{/* Toggle text area */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowText(!showText)}
|
||||
className={`px-3 py-2 border rounded-lg text-sm transition-colors ${
|
||||
textVisible
|
||||
? 'border-purple-300 bg-purple-50 text-purple-700'
|
||||
: 'border-gray-300 text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
title={textVisible ? 'Text ausblenden' : 'Text anzeigen'}
|
||||
>
|
||||
<svg className={`w-4 h-4 transition-transform ${textVisible ? 'rotate-180' : ''}`}
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Word count badge */}
|
||||
{wordCount > 0 && (
|
||||
<span className="text-xs px-2 py-1 rounded-full bg-green-100 text-green-700 font-medium shrink-0">
|
||||
{wordCount.toLocaleString('de-DE')} W.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="text-xs text-red-600 px-1">{error}</div>
|
||||
)}
|
||||
|
||||
{/* Collapsible textarea */}
|
||||
{textVisible && (
|
||||
<textarea
|
||||
value={text}
|
||||
onChange={e => onTextChange(e.target.value)}
|
||||
placeholder="Dokumenttext hier einfuegen oder per URL / Upload laden..."
|
||||
rows={6}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm font-mono resize-y focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user