0d0e705117
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>
164 lines
5.2 KiB
TypeScript
164 lines
5.2 KiB
TypeScript
'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>
|
|
)
|
|
}
|