Files
breakpilot-compliance/admin-compliance/app/sdk/agent/_components/DocumentRow.tsx
T
Benjamin Admin 0d0e705117 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>
2026-05-11 20:56:10 +02:00

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>
)
}