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,20 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
const CONSENT_URL = process.env.CONSENT_TESTER_URL || 'http://bp-compliance-consent-tester:8094'
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.text()
|
||||||
|
const response = await fetch(`${CONSENT_URL}/authenticated-scan`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body,
|
||||||
|
signal: AbortSignal.timeout(120000),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
return NextResponse.json({ error: `Auth-Test: ${response.status}` }, { status: response.status })
|
||||||
|
}
|
||||||
|
return NextResponse.json(await response.json())
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ error: 'Auth-Test fehlgeschlagen' }, { status: 503 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
const BACKEND_URL = process.env.BACKEND_API_URL || 'http://backend-compliance:8002'
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.text()
|
||||||
|
const response = await fetch(`${BACKEND_URL}/api/compliance/agent/compare`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body,
|
||||||
|
signal: AbortSignal.timeout(300000),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
return NextResponse.json({ error: `Backend: ${response.status}` }, { status: response.status })
|
||||||
|
}
|
||||||
|
return NextResponse.json(await response.json())
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ error: 'Vergleich fehlgeschlagen' }, { status: 503 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* PDF Export Proxy
|
||||||
|
* POST /api/sdk/v1/agent/scans/pdf → backend /api/compliance/agent/scans/pdf
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
const BACKEND_URL = process.env.BACKEND_API_URL || 'http://backend-compliance:8002'
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.text()
|
||||||
|
|
||||||
|
const response = await fetch(`${BACKEND_URL}/api/compliance/agent/scans/pdf`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body,
|
||||||
|
signal: AbortSignal.timeout(30000),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return NextResponse.json({ error: 'PDF generation failed' }, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdfBytes = await response.arrayBuffer()
|
||||||
|
return new NextResponse(pdfBytes, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/pdf',
|
||||||
|
'Content-Disposition': 'attachment; filename="compliance-report.pdf"',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('PDF proxy error:', error)
|
||||||
|
return NextResponse.json({ error: 'PDF generation failed' }, { status: 503 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,82 +7,93 @@ import { AnalysisHistory } from './_components/AnalysisHistory'
|
|||||||
import { FollowUpQuestions } from './_components/FollowUpQuestions'
|
import { FollowUpQuestions } from './_components/FollowUpQuestions'
|
||||||
import { ScanResult } from './_components/ScanResult'
|
import { ScanResult } from './_components/ScanResult'
|
||||||
import { ConsentTestResult } from './_components/ConsentTestResult'
|
import { ConsentTestResult } from './_components/ConsentTestResult'
|
||||||
|
import { CompareResult } from './_components/CompareResult'
|
||||||
|
import { AuthTestResult } from './_components/AuthTestResult'
|
||||||
|
|
||||||
type AnalysisMode = 'pre_launch' | 'post_launch'
|
type Mode = 'pre_launch' | 'post_launch'
|
||||||
type AnalysisTab = 'quick' | 'scan' | 'consent'
|
type Tab = 'quick' | 'scan' | 'consent' | 'compare' | 'auth'
|
||||||
|
|
||||||
const MODES: { id: AnalysisMode; label: string; desc: string; icon: string }[] = [
|
const MODES = [
|
||||||
{ id: 'pre_launch', label: 'Internes Dokument', desc: 'Vor Veroeffentlichung pruefen', icon: '📋' },
|
{ id: 'pre_launch' as Mode, label: 'Internes Dokument', desc: 'Vor Veroeffentlichung', icon: '📋' },
|
||||||
{ id: 'post_launch', label: 'Live-Website', desc: 'Bereits online analysieren', icon: '🌐' },
|
{ id: 'post_launch' as Mode, label: 'Live-Website', desc: 'Bereits online', icon: '🌐' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const TABS: { id: AnalysisTab; label: string; info: string }[] = [
|
const TABS = [
|
||||||
{ id: 'quick', label: 'Schnellanalyse', info: 'Analysiert nur die eingegebene URL. Fuer einen umfassenden Check nutzen Sie den Website-Scan.' },
|
{ id: 'quick' as Tab, label: 'Schnellanalyse', info: 'Einzelne URL klassifizieren und bewerten.' },
|
||||||
{ id: 'scan', label: 'Website-Scan', info: 'Scannt automatisch 5-10 Unterseiten und gleicht erkannte Dienste mit der Datenschutzerklaerung ab.' },
|
{ id: 'scan' as Tab, label: 'Website-Scan', info: '5-10 Seiten scannen, Dienstleister abgleichen, Pflichtinhalte pruefen.' },
|
||||||
{ id: 'consent', label: 'Cookie-Test', info: 'Testet mit echtem Browser was VOR und NACH Cookie-Einwilligung geladen wird. Erkennt Verstoesse gegen §25 TDDDG.' },
|
{ 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() {
|
export default function AgentPage() {
|
||||||
const [url, setUrl] = useState('')
|
const [url, setUrl] = useState('')
|
||||||
const [mode, setMode] = useState<AnalysisMode>('post_launch')
|
const [urls, setUrls] = useState('')
|
||||||
const [tab, setTab] = useState<AnalysisTab>('quick')
|
const [mode, setMode] = useState<Mode>('post_launch')
|
||||||
|
const [tab, setTab] = useState<Tab>('quick')
|
||||||
const [scanLoading, setScanLoading] = useState(false)
|
const [scanLoading, setScanLoading] = useState(false)
|
||||||
const [scanError, setScanError] = useState<string | null>(null)
|
const [scanError, setScanError] = useState<string | null>(null)
|
||||||
const [scanData, setScanData] = useState<any>(null)
|
const [scanData, setScanData] = useState<any>(null)
|
||||||
const [scanHistory, setScanHistory] = useState<any[]>([])
|
const [scanHistory, setScanHistory] = useState<any[]>([])
|
||||||
const [consentLoading, setConsentLoading] = useState(false)
|
|
||||||
const [consentError, setConsentError] = useState<string | null>(null)
|
|
||||||
const [consentData, setConsentData] = useState<any>(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 { analyze, answerFollowUp, loading, error, result, history } = useAgentAnalysis()
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!url.trim()) return
|
setScanLoading(true)
|
||||||
|
setScanError(null)
|
||||||
|
|
||||||
if (tab === 'quick') {
|
try {
|
||||||
analyze(url.trim(), mode)
|
if (tab === 'quick') {
|
||||||
} else if (tab === 'scan') {
|
setScanLoading(false)
|
||||||
setScanLoading(true)
|
analyze(url.trim(), mode)
|
||||||
setScanError(null)
|
return
|
||||||
setScanData(null)
|
}
|
||||||
try {
|
|
||||||
const res = await fetch('/api/sdk/v1/agent/scan', {
|
let endpoint = ''
|
||||||
method: 'POST',
|
let body: any = {}
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ url: url.trim(), mode }),
|
if (tab === 'scan') {
|
||||||
})
|
endpoint = '/api/sdk/v1/agent/scan'
|
||||||
if (!res.ok) throw new Error(`Scan fehlgeschlagen: ${res.status}`)
|
body = { url: url.trim(), mode }
|
||||||
const data = await res.json()
|
} 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)
|
setScanData(data)
|
||||||
setScanHistory(prev => [{ url: url.trim(), ...data, scanned_at: new Date().toISOString() }, ...prev].slice(0, 20))
|
setScanHistory(prev => [{ url: url.trim(), ...data, scanned_at: new Date().toISOString() }, ...prev].slice(0, 20))
|
||||||
} catch (e) {
|
} else if (tab === 'consent') setConsentData(data)
|
||||||
setScanError(e instanceof Error ? e.message : 'Unbekannter Fehler')
|
else if (tab === 'compare') setCompareData(data)
|
||||||
} finally {
|
else if (tab === 'auth') setAuthData(data)
|
||||||
setScanLoading(false)
|
} catch (e) {
|
||||||
}
|
setScanError(e instanceof Error ? e.message : 'Fehler')
|
||||||
} else {
|
} finally {
|
||||||
setConsentLoading(true)
|
setScanLoading(false)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoading = tab === 'quick' ? loading : tab === 'scan' ? scanLoading : consentLoading
|
const isLoading = tab === 'quick' ? loading : scanLoading
|
||||||
const currentError = tab === 'quick' ? error : tab === 'scan' ? scanError : consentError
|
const currentError = tab === 'quick' ? error : scanError
|
||||||
const currentTab = TABS.find(t => t.id === tab)!
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 max-w-4xl">
|
<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>
|
<p className="text-gray-500 mt-1">Analysiere Dokumente und Webseiten auf DSGVO-Konformitaet.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mode Selection */}
|
{/* Mode */}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{MODES.map(m => (
|
{MODES.map(m => (
|
||||||
<button key={m.id} onClick={() => setMode(m.id)}
|
<button key={m.id} onClick={() => setMode(m.id)}
|
||||||
className={`p-3 rounded-xl border-2 text-left transition-all ${
|
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'}`}>
|
||||||
mode === m.id ? 'border-purple-500 bg-purple-50' : 'border-gray-200 hover:border-gray-300'}`}>
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-xl">{m.icon}</span>
|
<span className="text-xl">{m.icon}</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -108,90 +118,84 @@ export default function AgentPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab Selection + Info */}
|
{/* Tabs */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex border-b border-gray-200">
|
<div className="flex border-b border-gray-200 overflow-x-auto">
|
||||||
{TABS.map(t => (
|
{TABS.map(t => (
|
||||||
<button key={t.id} onClick={() => setTab(t.id)}
|
<button key={t.id} onClick={() => setTab(t.id)}
|
||||||
className={`px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
|
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'}`}>
|
||||||
tab === t.id
|
|
||||||
? 'border-purple-500 text-purple-700'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700'}`}>
|
|
||||||
{t.label}
|
{t.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* URL Input */}
|
{/* Input */}
|
||||||
<form onSubmit={handleSubmit} className="flex gap-3">
|
<form onSubmit={handleSubmit} className="space-y-3">
|
||||||
<input type="url" value={url} onChange={e => setUrl(e.target.value)}
|
{tab === 'compare' ? (
|
||||||
placeholder={tab === 'consent' ? 'https://www.example.com/' : tab === 'scan' ? 'https://www.example.com/' : 'https://example.com/datenschutz'}
|
<textarea value={urls} onChange={e => setUrls(e.target.value)}
|
||||||
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"
|
placeholder="https://www.opodo.de https://www.booking.com https://www.expedia.de"
|
||||||
disabled={isLoading} required />
|
rows={3}
|
||||||
<button type="submit" disabled={isLoading || !url.trim()}
|
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">
|
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 ? (
|
{isLoading ? (
|
||||||
<><svg className="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
<><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" />
|
<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" />
|
<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...'}</>
|
</svg>Analysiere...</>
|
||||||
) : tab === 'consent' ? 'Cookie-Test starten' : tab === 'scan' ? 'Website scannen' : 'Analysieren'}
|
) : TABS.find(t => t.id === tab)?.label || 'Starten'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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 && (
|
{tab === 'quick' && result && (
|
||||||
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm space-y-6">
|
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm space-y-6">
|
||||||
<AnalysisResult result={result} />
|
<AnalysisResult result={result} />
|
||||||
{result.follow_up_questions.length > 0 && (
|
{result.follow_up_questions.length > 0 && (
|
||||||
<div className="border-t pt-4">
|
<div className="border-t pt-4"><FollowUpQuestions questions={result.follow_up_questions} answers={result.follow_up_answers} onAnswer={answerFollowUp} /></div>
|
||||||
<FollowUpQuestions questions={result.follow_up_questions} answers={result.follow_up_answers} onAnswer={answerFollowUp} />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{tab === 'scan' && scanData && <div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm"><ScanResult data={scanData} /></div>}
|
||||||
{/* Scan 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 && (
|
{tab === 'compare' && compareData?.sites && <div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm"><CompareResult sites={compareData.sites} /></div>}
|
||||||
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
|
{tab === 'auth' && authData && <div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm"><AuthTestResult data={authData} /></div>}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* History */}
|
{/* History */}
|
||||||
{tab === 'quick' && (
|
{tab === 'quick' && <AnalysisHistory history={history} onSelect={r => { setUrl(r.url); analyze(r.url, mode) }} />}
|
||||||
<AnalysisHistory history={history} onSelect={r => { setUrl(r.url); analyze(r.url, mode) }} />
|
|
||||||
)}
|
|
||||||
{tab === 'scan' && scanHistory.length > 0 && (
|
{tab === 'scan' && scanHistory.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-gray-700 mb-3">Letzte Scans</h3>
|
<h3 className="text-sm font-medium text-gray-700 mb-3">Letzte Scans</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{scanHistory.map((item, i) => (
|
{scanHistory.map((item, i) => (
|
||||||
<button key={i} onClick={() => setUrl(item.url)}
|
<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">
|
<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-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'}`}>
|
<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>
|
||||||
{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>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user