cc919eb608
- Impressum-Check: Toggle activates 75 Impressum MCs via agent - Banner-Check: Toggle runs additional cookie doc-check (381 MCs) after the Playwright banner test completes - Both use the same use_agent flag through doc-check endpoint Green pill button consistent across all tabs: 'KI-Agent aus' / 'KI-Agent aktiv (X MCs)' Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
182 lines
8.5 KiB
TypeScript
182 lines
8.5 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import { ChecklistView } from './ChecklistView'
|
|
|
|
interface CheckItem {
|
|
id: string; label: string; passed: boolean; severity: string
|
|
matched_text: string; level?: number; parent?: string | null
|
|
skipped?: boolean; hint?: string
|
|
}
|
|
|
|
export function ImpressumCheckTab() {
|
|
const [url, setUrl] = useState(() =>
|
|
typeof window !== 'undefined' ? localStorage.getItem('impressum-check-url') || '' : ''
|
|
)
|
|
const [loading, setLoading] = useState(false)
|
|
const [progress, setProgress] = useState('')
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [results, setResults] = useState<any>(() => {
|
|
if (typeof window === 'undefined') return null
|
|
try { const s = localStorage.getItem('impressum-check-results'); return s ? JSON.parse(s) : null } catch { return null }
|
|
})
|
|
const [history, setHistory] = useState<{ url: string; date: string; findings: number; pct: number; resultKey: string }[]>(() => {
|
|
if (typeof window === 'undefined') return []
|
|
try { return JSON.parse(localStorage.getItem('impressum-check-history') || '[]') } catch { return [] }
|
|
})
|
|
|
|
const [useAgent, setUseAgent] = useState(false)
|
|
|
|
React.useEffect(() => { localStorage.setItem('impressum-check-url', url) }, [url])
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!url.trim()) return
|
|
|
|
setLoading(true)
|
|
setError(null)
|
|
setResults(null)
|
|
setProgress('Impressum wird geprueft...')
|
|
|
|
try {
|
|
const startRes = await fetch('/api/sdk/v1/agent/doc-check', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
entries: [{ doc_type: 'impressum', label: 'Impressum', url: url.trim() }],
|
|
recipient: 'dsb@breakpilot.local',
|
|
use_agent: useAgent,
|
|
}),
|
|
})
|
|
if (!startRes.ok) throw new Error(`Fehler: ${startRes.status}`)
|
|
const { check_id } = await startRes.json()
|
|
if (!check_id) throw new Error('Keine Check-ID erhalten')
|
|
|
|
let attempts = 0
|
|
while (attempts < 120) {
|
|
await new Promise(r => setTimeout(r, 3000))
|
|
const pollRes = await fetch(`/api/sdk/v1/agent/doc-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('impressum-check-results', JSON.stringify(pollData.result))
|
|
const resultKey = `impressum-result-${Date.now()}`
|
|
try { localStorage.setItem(resultKey, JSON.stringify(pollData.result)) } catch {}
|
|
const total = pollData.result.total_findings || 0
|
|
const pct = pollData.result.results?.[0]?.completeness_pct || 0
|
|
const entry = { url: url.trim(), date: new Date().toISOString(), findings: total, pct, resultKey }
|
|
const updated = [entry, ...history].slice(0, 30)
|
|
setHistory(updated)
|
|
localStorage.setItem('impressum-check-history', JSON.stringify(updated))
|
|
break
|
|
}
|
|
if (pollData.status === 'failed') throw new Error(pollData.error || 'Pruefung fehlgeschlagen')
|
|
attempts++
|
|
}
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
|
|
setProgress('')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
|
|
<h3 className="text-sm font-semibold text-amber-900">Impressum-Check (§5 TMG / §18 MStV)</h3>
|
|
<p className="text-xs text-amber-700 mt-1">
|
|
Prueft 16 Pflichtangaben: Anbietername, Anschrift, Kontaktdaten, Handelsregister,
|
|
USt-IdNr., Vertretungsberechtigte, V.i.S.d.P., Streitbeilegung.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<button type="button" onClick={() => setUseAgent(!useAgent)}
|
|
className={`flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-medium border transition-colors ${
|
|
useAgent ? 'bg-emerald-100 border-emerald-300 text-emerald-800' : 'bg-gray-50 border-gray-200 text-gray-500 hover:bg-gray-100'
|
|
}`}>
|
|
<span className={`w-2 h-2 rounded-full ${useAgent ? 'bg-emerald-500' : 'bg-gray-300'}`} />
|
|
{useAgent ? 'KI-Agent aktiv (75 MCs)' : 'KI-Agent aus'}
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="flex gap-3">
|
|
<input type="url" value={url} onChange={e => setUrl(e.target.value)}
|
|
placeholder="https://www.example.com/impressum"
|
|
className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent text-sm"
|
|
disabled={loading} required />
|
|
<button type="submit" disabled={loading || !url.trim()}
|
|
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors flex items-center gap-2 text-sm font-medium whitespace-nowrap">
|
|
{loading ? (
|
|
<><svg className="animate-spin w-4 h-4" 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>Pruefe...</>
|
|
) : 'Impressum pruefen'}
|
|
</button>
|
|
</form>
|
|
|
|
{progress && (
|
|
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4 text-sm text-purple-700 flex items-center gap-3">
|
|
<svg className="animate-spin w-5 h-5 text-purple-500 shrink-0" 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>
|
|
{progress}
|
|
</div>
|
|
)}
|
|
|
|
{error && <div className="bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-700">{error}</div>}
|
|
|
|
{results?.results && (
|
|
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
|
|
<ChecklistView results={results.results} />
|
|
{results.email_status && (
|
|
<div className="mt-3 text-xs text-gray-500 flex items-center gap-2">
|
|
<span className={`w-2 h-2 rounded-full ${results.email_status === 'sent' ? 'bg-green-400' : 'bg-gray-300'}`} />
|
|
E-Mail: {results.email_status === 'sent' ? 'Gesendet' : results.email_status}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{history.length > 0 && (
|
|
<div className="border border-gray-200 rounded-xl p-4">
|
|
<h4 className="text-sm font-medium text-gray-700 mb-2">Letzte Impressum-Checks</h4>
|
|
<div className="space-y-1">
|
|
{history.map((h, i) => (
|
|
<button key={i} onClick={() => {
|
|
setUrl(h.url)
|
|
if (h.resultKey) {
|
|
try { const s = localStorage.getItem(h.resultKey); if (s) { setResults(JSON.parse(s)); return } } catch {}
|
|
}
|
|
try { const l = localStorage.getItem('impressum-check-results'); if (l) setResults(JSON.parse(l)) } catch {}
|
|
}}
|
|
className="w-full flex items-center justify-between p-2.5 rounded-lg border border-gray-100 hover:border-purple-200 hover:bg-purple-50/30 transition-all text-left">
|
|
<div className="min-w-0 flex-1">
|
|
<div className="text-sm font-medium text-gray-900 truncate">{h.url}</div>
|
|
<div className="text-xs text-gray-500">
|
|
{new Date(h.date).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3 shrink-0 ml-3">
|
|
<span className={`text-xs font-medium ${h.findings > 0 ? 'text-red-600' : 'text-green-600'}`}>
|
|
{h.findings} Findings
|
|
</span>
|
|
<span className={`text-xs font-medium ${h.pct === 100 ? 'text-green-700' : h.pct >= 50 ? 'text-yellow-700' : 'text-red-700'}`}>
|
|
{h.pct}%
|
|
</span>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|