feat: 5-tab agent UI — PDF export, compare, auth test, all proxies
- 5 tabs: Schnellanalyse, Website-Scan, Cookie-Test, Vergleich, Login-Test - PDF download button in ScanResult - CompareResult: side-by-side compliance comparison table - AuthTestResult: 5 post-login checks with legal refs - API proxies: /scans/pdf, /compare, /authenticated-scan - Compare: textarea for 2-5 URLs, parallel scanning Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
interface AuthCheck {
|
||||
found: boolean
|
||||
text: string
|
||||
legal_ref: string
|
||||
}
|
||||
|
||||
interface AuthData {
|
||||
url: string
|
||||
authenticated: boolean
|
||||
login_error: string
|
||||
checks: Record<string, AuthCheck>
|
||||
findings_count: number
|
||||
}
|
||||
|
||||
const CHECK_LABELS: Record<string, { label: string; icon: string }> = {
|
||||
cancel_subscription: { label: 'Kuendigungsbutton (2 Klicks)', icon: '🚫' },
|
||||
delete_account: { label: 'Konto loeschen', icon: '🗑️' },
|
||||
export_data: { label: 'Daten exportieren', icon: '📥' },
|
||||
consent_settings: { label: 'Einwilligungen widerrufen', icon: '⚙️' },
|
||||
profile_visible: { label: 'Profildaten einsehen', icon: '👤' },
|
||||
}
|
||||
|
||||
export function AuthTestResult({ data }: { data: AuthData }) {
|
||||
if (!data.authenticated) {
|
||||
return (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<p className="text-sm font-medium text-red-800">Login fehlgeschlagen</p>
|
||||
<p className="text-xs text-red-600 mt-1">{data.login_error || 'Credentials oder Formular nicht erkannt'}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 rounded-full bg-green-500" />
|
||||
<span className="text-sm font-medium text-gray-900">Erfolgreich eingeloggt</span>
|
||||
<span className={`ml-auto text-xs px-2 py-1 rounded font-medium ${data.findings_count > 0 ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}`}>
|
||||
{data.findings_count} fehlende Funktionen
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{Object.entries(data.checks).map(([key, check]) => {
|
||||
const info = CHECK_LABELS[key] || { label: key, icon: '❓' }
|
||||
return (
|
||||
<div key={key} className={`flex items-center gap-3 p-3 rounded-lg border ${check.found ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}>
|
||||
<span className="text-lg">{info.icon}</span>
|
||||
<div className="flex-1">
|
||||
<p className={`text-sm font-medium ${check.found ? 'text-green-800' : 'text-red-800'}`}>
|
||||
{check.found ? '✓' : '✗'} {info.label}
|
||||
</p>
|
||||
{check.text && <p className="text-xs text-gray-500 mt-0.5">{check.text}</p>}
|
||||
</div>
|
||||
<span className="text-[10px] text-gray-400">{check.legal_ref}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{data.findings_count > 0 && (
|
||||
<div className="bg-red-50 border-l-4 border-red-500 p-3 text-xs text-red-700">
|
||||
<strong>{data.findings_count} Pflichtfunktion(en) fehlen.</strong> Der Nutzer kann seine Rechte
|
||||
nach DSGVO nicht vollstaendig ausueben.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
interface SiteResult {
|
||||
url: string
|
||||
domain: string
|
||||
risk_level: string
|
||||
risk_score: number
|
||||
findings_count: number
|
||||
services_count: number
|
||||
has_impressum: boolean
|
||||
has_datenschutz: boolean
|
||||
has_cookie_banner: boolean
|
||||
has_google_fonts: boolean
|
||||
scan_status: string
|
||||
}
|
||||
|
||||
const RISK_COLOR: Record<string, string> = {
|
||||
MINIMAL: 'text-green-700 bg-green-50',
|
||||
LOW: 'text-yellow-700 bg-yellow-50',
|
||||
LIMITED: 'text-orange-700 bg-orange-50',
|
||||
HIGH: 'text-red-700 bg-red-50',
|
||||
UNACCEPTABLE: 'text-red-900 bg-red-100',
|
||||
}
|
||||
|
||||
export function CompareResult({ sites }: { sites: SiteResult[] }) {
|
||||
if (!sites.length) return null
|
||||
|
||||
const checks = [
|
||||
{ key: 'has_datenschutz', label: 'Datenschutzerklaerung' },
|
||||
{ key: 'has_impressum', label: 'Impressum' },
|
||||
{ key: 'has_cookie_banner', label: 'Cookie-Banner' },
|
||||
{ key: 'has_google_fonts', label: 'Google Fonts (lokal?)' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th className="text-left px-3 py-2 text-xs font-medium text-gray-500 w-44">Pruefung</th>
|
||||
{sites.map((s, i) => (
|
||||
<th key={i} className="text-center px-3 py-2 text-xs font-medium text-gray-700">
|
||||
{s.domain}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
<tr>
|
||||
<td className="px-3 py-2 text-gray-600">Risiko-Score</td>
|
||||
{sites.map((s, i) => (
|
||||
<td key={i} className="px-3 py-2 text-center">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${RISK_COLOR[s.risk_level] || 'text-gray-600 bg-gray-50'}`}>
|
||||
{s.risk_level || '?'} ({s.risk_score}/100)
|
||||
</span>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 text-gray-600">Findings</td>
|
||||
{sites.map((s, i) => (
|
||||
<td key={i} className={`px-3 py-2 text-center font-medium ${s.findings_count > 0 ? 'text-red-700' : 'text-green-700'}`}>
|
||||
{s.findings_count}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 text-gray-600">Dienste erkannt</td>
|
||||
{sites.map((s, i) => (
|
||||
<td key={i} className="px-3 py-2 text-center text-gray-700">{s.services_count}</td>
|
||||
))}
|
||||
</tr>
|
||||
{checks.map(check => (
|
||||
<tr key={check.key}>
|
||||
<td className="px-3 py-2 text-gray-600">{check.label}</td>
|
||||
{sites.map((s, i) => {
|
||||
const val = (s as any)[check.key]
|
||||
const isInverted = check.key === 'has_google_fonts'
|
||||
const good = isInverted ? !val : val
|
||||
return (
|
||||
<td key={i} className={`px-3 py-2 text-center font-medium ${good ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{good ? '✓' : '✗'}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -207,6 +207,35 @@ export function ScanResult({ data }: { data: ScanData }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* PDF Export Button */}
|
||||
<div className="pt-4 border-t flex gap-3">
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const res = await fetch('/api/sdk/v1/agent/scans/pdf', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: '', scan_type: 'scan', analysis_mode: 'post_launch', result: data }),
|
||||
})
|
||||
if (res.ok) {
|
||||
const blob = await res.blob()
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'compliance-report.pdf'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
} catch (e) { console.error('PDF export failed:', e) }
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-purple-700 bg-purple-50 border border-purple-200 rounded-lg hover:bg-purple-100 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
PDF herunterladen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,82 +7,93 @@ import { AnalysisHistory } from './_components/AnalysisHistory'
|
||||
import { FollowUpQuestions } from './_components/FollowUpQuestions'
|
||||
import { ScanResult } from './_components/ScanResult'
|
||||
import { ConsentTestResult } from './_components/ConsentTestResult'
|
||||
import { CompareResult } from './_components/CompareResult'
|
||||
import { AuthTestResult } from './_components/AuthTestResult'
|
||||
|
||||
type AnalysisMode = 'pre_launch' | 'post_launch'
|
||||
type AnalysisTab = 'quick' | 'scan' | 'consent'
|
||||
type Mode = 'pre_launch' | 'post_launch'
|
||||
type Tab = 'quick' | 'scan' | 'consent' | 'compare' | 'auth'
|
||||
|
||||
const MODES: { id: AnalysisMode; label: string; desc: string; icon: string }[] = [
|
||||
{ id: 'pre_launch', label: 'Internes Dokument', desc: 'Vor Veroeffentlichung pruefen', icon: '📋' },
|
||||
{ id: 'post_launch', label: 'Live-Website', desc: 'Bereits online analysieren', icon: '🌐' },
|
||||
const MODES = [
|
||||
{ id: 'pre_launch' as Mode, label: 'Internes Dokument', desc: 'Vor Veroeffentlichung', icon: '📋' },
|
||||
{ id: 'post_launch' as Mode, label: 'Live-Website', desc: 'Bereits online', icon: '🌐' },
|
||||
]
|
||||
|
||||
const TABS: { id: AnalysisTab; label: string; info: string }[] = [
|
||||
{ id: 'quick', label: 'Schnellanalyse', info: 'Analysiert nur die eingegebene URL. Fuer einen umfassenden Check nutzen Sie den Website-Scan.' },
|
||||
{ id: 'scan', label: 'Website-Scan', info: 'Scannt automatisch 5-10 Unterseiten und gleicht erkannte Dienste mit der Datenschutzerklaerung ab.' },
|
||||
{ id: 'consent', label: 'Cookie-Test', info: 'Testet mit echtem Browser was VOR und NACH Cookie-Einwilligung geladen wird. Erkennt Verstoesse gegen §25 TDDDG.' },
|
||||
const TABS = [
|
||||
{ id: 'quick' as Tab, label: 'Schnellanalyse', info: 'Einzelne URL klassifizieren und bewerten.' },
|
||||
{ id: 'scan' as Tab, label: 'Website-Scan', info: '5-10 Seiten scannen, Dienstleister abgleichen, Pflichtinhalte pruefen.' },
|
||||
{ id: 'consent' as Tab, label: 'Cookie-Test', info: 'Testet mit Browser was VOR und NACH Cookie-Einwilligung geladen wird.' },
|
||||
{ id: 'compare' as Tab, label: 'Vergleich', info: '2-5 Websites parallel scannen und Compliance vergleichen.' },
|
||||
{ id: 'auth' as Tab, label: 'Login-Test', info: 'Nach Login pruefen: Kuendigung, Daten loeschen, Export, Einwilligungen.' },
|
||||
]
|
||||
|
||||
export default function AgentPage() {
|
||||
const [url, setUrl] = useState('')
|
||||
const [mode, setMode] = useState<AnalysisMode>('post_launch')
|
||||
const [tab, setTab] = useState<AnalysisTab>('quick')
|
||||
const [urls, setUrls] = useState('')
|
||||
const [mode, setMode] = useState<Mode>('post_launch')
|
||||
const [tab, setTab] = useState<Tab>('quick')
|
||||
const [scanLoading, setScanLoading] = useState(false)
|
||||
const [scanError, setScanError] = useState<string | null>(null)
|
||||
const [scanData, setScanData] = useState<any>(null)
|
||||
const [scanHistory, setScanHistory] = useState<any[]>([])
|
||||
const [consentLoading, setConsentLoading] = useState(false)
|
||||
const [consentError, setConsentError] = useState<string | null>(null)
|
||||
const [consentData, setConsentData] = useState<any>(null)
|
||||
const [compareData, setCompareData] = useState<any>(null)
|
||||
const [authData, setAuthData] = useState<any>(null)
|
||||
const [authUser, setAuthUser] = useState('')
|
||||
const [authPass, setAuthPass] = useState('')
|
||||
const { analyze, answerFollowUp, loading, error, result, history } = useAgentAnalysis()
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!url.trim()) return
|
||||
setScanLoading(true)
|
||||
setScanError(null)
|
||||
|
||||
if (tab === 'quick') {
|
||||
analyze(url.trim(), mode)
|
||||
} else if (tab === 'scan') {
|
||||
setScanLoading(true)
|
||||
setScanError(null)
|
||||
setScanData(null)
|
||||
try {
|
||||
const res = await fetch('/api/sdk/v1/agent/scan', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: url.trim(), mode }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`Scan fehlgeschlagen: ${res.status}`)
|
||||
const data = await res.json()
|
||||
try {
|
||||
if (tab === 'quick') {
|
||||
setScanLoading(false)
|
||||
analyze(url.trim(), mode)
|
||||
return
|
||||
}
|
||||
|
||||
let endpoint = ''
|
||||
let body: any = {}
|
||||
|
||||
if (tab === 'scan') {
|
||||
endpoint = '/api/sdk/v1/agent/scan'
|
||||
body = { url: url.trim(), mode }
|
||||
} else if (tab === 'consent') {
|
||||
endpoint = '/api/sdk/v1/agent/consent-test'
|
||||
body = { url: url.trim() }
|
||||
} else if (tab === 'compare') {
|
||||
endpoint = '/api/sdk/v1/agent/compare'
|
||||
body = { urls: urls.split('\n').map(u => u.trim()).filter(Boolean), mode }
|
||||
} else if (tab === 'auth') {
|
||||
endpoint = '/api/sdk/v1/agent/authenticated-scan'
|
||||
body = { url: url.trim(), username: authUser, password: authPass }
|
||||
}
|
||||
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
if (!res.ok) throw new Error(`Fehlgeschlagen: ${res.status}`)
|
||||
const data = await res.json()
|
||||
|
||||
if (tab === 'scan') {
|
||||
setScanData(data)
|
||||
setScanHistory(prev => [{ url: url.trim(), ...data, scanned_at: new Date().toISOString() }, ...prev].slice(0, 20))
|
||||
} catch (e) {
|
||||
setScanError(e instanceof Error ? e.message : 'Unbekannter Fehler')
|
||||
} finally {
|
||||
setScanLoading(false)
|
||||
}
|
||||
} else {
|
||||
setConsentLoading(true)
|
||||
setConsentError(null)
|
||||
setConsentData(null)
|
||||
try {
|
||||
const res = await fetch('/api/sdk/v1/agent/consent-test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: url.trim() }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`Cookie-Test fehlgeschlagen: ${res.status}`)
|
||||
setConsentData(await res.json())
|
||||
} catch (e) {
|
||||
setConsentError(e instanceof Error ? e.message : 'Unbekannter Fehler')
|
||||
} finally {
|
||||
setConsentLoading(false)
|
||||
}
|
||||
} else if (tab === 'consent') setConsentData(data)
|
||||
else if (tab === 'compare') setCompareData(data)
|
||||
else if (tab === 'auth') setAuthData(data)
|
||||
} catch (e) {
|
||||
setScanError(e instanceof Error ? e.message : 'Fehler')
|
||||
} finally {
|
||||
setScanLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const isLoading = tab === 'quick' ? loading : tab === 'scan' ? scanLoading : consentLoading
|
||||
const currentError = tab === 'quick' ? error : tab === 'scan' ? scanError : consentError
|
||||
const currentTab = TABS.find(t => t.id === tab)!
|
||||
const isLoading = tab === 'quick' ? loading : scanLoading
|
||||
const currentError = tab === 'quick' ? error : scanError
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-4xl">
|
||||
@@ -91,12 +102,11 @@ export default function AgentPage() {
|
||||
<p className="text-gray-500 mt-1">Analysiere Dokumente und Webseiten auf DSGVO-Konformitaet.</p>
|
||||
</div>
|
||||
|
||||
{/* Mode Selection */}
|
||||
{/* Mode */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{MODES.map(m => (
|
||||
<button key={m.id} onClick={() => setMode(m.id)}
|
||||
className={`p-3 rounded-xl border-2 text-left transition-all ${
|
||||
mode === m.id ? 'border-purple-500 bg-purple-50' : 'border-gray-200 hover:border-gray-300'}`}>
|
||||
className={`p-3 rounded-xl border-2 text-left transition-all ${mode === m.id ? 'border-purple-500 bg-purple-50' : 'border-gray-200 hover:border-gray-300'}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl">{m.icon}</span>
|
||||
<div>
|
||||
@@ -108,90 +118,84 @@ export default function AgentPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab Selection + Info */}
|
||||
{/* Tabs */}
|
||||
<div>
|
||||
<div className="flex border-b border-gray-200">
|
||||
<div className="flex border-b border-gray-200 overflow-x-auto">
|
||||
{TABS.map(t => (
|
||||
<button key={t.id} onClick={() => setTab(t.id)}
|
||||
className={`px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
|
||||
tab === t.id
|
||||
? 'border-purple-500 text-purple-700'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'}`}>
|
||||
className={`px-3 py-2.5 text-sm font-medium border-b-2 whitespace-nowrap transition-colors ${tab === t.id ? 'border-purple-500 text-purple-700' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-2 px-1">{currentTab.info}</p>
|
||||
<p className="text-xs text-gray-400 mt-2 px-1">{TABS.find(t => t.id === tab)?.info}</p>
|
||||
</div>
|
||||
|
||||
{/* URL Input */}
|
||||
<form onSubmit={handleSubmit} className="flex gap-3">
|
||||
<input type="url" value={url} onChange={e => setUrl(e.target.value)}
|
||||
placeholder={tab === 'consent' ? 'https://www.example.com/' : tab === 'scan' ? 'https://www.example.com/' : 'https://example.com/datenschutz'}
|
||||
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={isLoading} required />
|
||||
<button type="submit" disabled={isLoading || !url.trim()}
|
||||
{/* Input */}
|
||||
<form onSubmit={handleSubmit} className="space-y-3">
|
||||
{tab === 'compare' ? (
|
||||
<textarea value={urls} onChange={e => setUrls(e.target.value)}
|
||||
placeholder="https://www.opodo.de https://www.booking.com https://www.expedia.de"
|
||||
rows={3}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 text-sm"
|
||||
disabled={isLoading} />
|
||||
) : (
|
||||
<input type="url" value={url} onChange={e => setUrl(e.target.value)}
|
||||
placeholder={tab === 'auth' ? 'https://www.example.com/login' : 'https://www.example.com/'}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 text-sm"
|
||||
disabled={isLoading} required />
|
||||
)}
|
||||
{tab === 'auth' && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<input type="text" value={authUser} onChange={e => setAuthUser(e.target.value)}
|
||||
placeholder="Email / Benutzername" autoComplete="off"
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg text-sm" />
|
||||
<input type="password" value={authPass} onChange={e => setAuthPass(e.target.value)}
|
||||
placeholder="Passwort" autoComplete="off"
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg text-sm" />
|
||||
<p className="col-span-2 text-[10px] text-gray-400">Credentials werden NICHT gespeichert — nur fuer diesen Test im Browser-Kontext.</p>
|
||||
</div>
|
||||
)}
|
||||
<button type="submit" disabled={isLoading || (!url.trim() && tab !== 'compare') || (tab === 'compare' && !urls.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">
|
||||
{isLoading ? (
|
||||
<><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>{tab === 'consent' ? 'Teste Cookies...' : tab === 'scan' ? 'Scanne...' : 'Analysiere...'}</>
|
||||
) : tab === 'consent' ? 'Cookie-Test starten' : tab === 'scan' ? 'Website scannen' : 'Analysieren'}
|
||||
</svg>Analysiere...</>
|
||||
) : TABS.find(t => t.id === tab)?.label || 'Starten'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Error */}
|
||||
{currentError && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-700">{currentError}</div>
|
||||
)}
|
||||
{currentError && <div className="bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-700">{currentError}</div>}
|
||||
|
||||
{/* Quick Analysis Result */}
|
||||
{/* Results */}
|
||||
{tab === 'quick' && result && (
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm space-y-6">
|
||||
<AnalysisResult result={result} />
|
||||
{result.follow_up_questions.length > 0 && (
|
||||
<div className="border-t pt-4">
|
||||
<FollowUpQuestions questions={result.follow_up_questions} answers={result.follow_up_answers} onAnswer={answerFollowUp} />
|
||||
</div>
|
||||
<div className="border-t pt-4"><FollowUpQuestions questions={result.follow_up_questions} answers={result.follow_up_answers} onAnswer={answerFollowUp} /></div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Scan Result */}
|
||||
{tab === 'scan' && scanData && (
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
|
||||
<ScanResult data={scanData} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Consent Test Result */}
|
||||
{tab === 'consent' && consentData && (
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
|
||||
<ConsentTestResult data={consentData} />
|
||||
</div>
|
||||
)}
|
||||
{tab === 'scan' && scanData && <div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm"><ScanResult data={scanData} /></div>}
|
||||
{tab === 'consent' && consentData && <div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm"><ConsentTestResult data={consentData} /></div>}
|
||||
{tab === 'compare' && compareData?.sites && <div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm"><CompareResult sites={compareData.sites} /></div>}
|
||||
{tab === 'auth' && authData && <div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm"><AuthTestResult data={authData} /></div>}
|
||||
|
||||
{/* History */}
|
||||
{tab === 'quick' && (
|
||||
<AnalysisHistory history={history} onSelect={r => { setUrl(r.url); analyze(r.url, mode) }} />
|
||||
)}
|
||||
{tab === 'quick' && <AnalysisHistory history={history} onSelect={r => { setUrl(r.url); analyze(r.url, mode) }} />}
|
||||
{tab === 'scan' && scanHistory.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-700 mb-3">Letzte Scans</h3>
|
||||
<div className="space-y-2">
|
||||
{scanHistory.map((item, i) => (
|
||||
<button key={i} onClick={() => setUrl(item.url)}
|
||||
className="w-full text-left p-3 bg-white border border-gray-200 rounded-lg hover:border-purple-300 hover:bg-purple-50 transition-colors">
|
||||
className="w-full text-left p-3 bg-white border border-gray-200 rounded-lg hover:border-purple-300 transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs font-medium text-gray-500 w-8">{item.pages_scanned}p</span>
|
||||
<span className="text-xs text-gray-500 w-8">{item.pages_scanned}p</span>
|
||||
<span className="text-sm text-gray-700 truncate flex-1">{item.url}</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${item.findings?.length > 0 ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}`}>
|
||||
{item.findings?.length || 0} Findings
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{new Date(item.scanned_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${item.findings?.length > 0 ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}`}>{item.findings?.length || 0}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user