feat: hybrid website compliance checks (§312k BGB, §5 TMG, Art. 13 DSGVO)

- Scan public website for cancellation button, imprint, privacy link, cookie consent
- Generate follow-up questions when checks can't be verified without login
- User answers "no" → finding with legal basis is added to results
- Frontend: FollowUpQuestions component with Ja/Nein buttons
- Sidebar: "Compliance Agent" entry added under KI-Compliance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-28 13:25:44 +02:00
parent 41fd7e36d1
commit cb5aa2949b
5 changed files with 265 additions and 20 deletions

View File

@@ -0,0 +1,91 @@
'use client'
import React from 'react'
import type { FollowUpQuestion } from '../_hooks/useAgentAnalysis'
const SEVERITY_STYLE: Record<string, { border: string; bg: string; icon: string }> = {
high: { border: 'border-red-300', bg: 'bg-red-50', icon: '!!' },
medium: { border: 'border-yellow-300', bg: 'bg-yellow-50', icon: '!' },
low: { border: 'border-blue-300', bg: 'bg-blue-50', icon: 'i' },
}
interface Props {
questions: FollowUpQuestion[]
answers: Record<string, boolean>
onAnswer: (questionId: string, answer: boolean) => void
}
export function FollowUpQuestions({ questions, answers, onAnswer }: Props) {
const unanswered = questions.filter(q => answers[q.id] === undefined)
const answered = questions.filter(q => answers[q.id] !== undefined)
if (questions.length === 0) return null
return (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-700 flex items-center gap-2">
<svg className="w-4 h-4 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Rueckfragen zur manuellen Pruefung ({unanswered.length} offen)
</h4>
{/* Unanswered questions */}
{unanswered.map(q => {
const style = SEVERITY_STYLE[q.severity] || SEVERITY_STYLE.medium
return (
<div key={q.id} className={`border ${style.border} ${style.bg} rounded-lg p-4`}>
<div className="flex items-start gap-3">
<span className={`mt-0.5 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold ${
q.severity === 'high' ? 'bg-red-200 text-red-800' :
q.severity === 'medium' ? 'bg-yellow-200 text-yellow-800' :
'bg-blue-200 text-blue-800'
}`}>
{SEVERITY_STYLE[q.severity]?.icon || '?'}
</span>
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">{q.question}</p>
<p className="text-xs text-gray-500 mt-1">Rechtsgrundlage: {q.legal_basis}</p>
<div className="flex gap-2 mt-3">
<button
onClick={() => onAnswer(q.id, true)}
className="px-4 py-1.5 text-sm bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors"
>
Ja
</button>
<button
onClick={() => onAnswer(q.id, false)}
className="px-4 py-1.5 text-sm bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
>
Nein
</button>
</div>
</div>
</div>
</div>
)
})}
{/* Answered questions */}
{answered.map(q => {
const isYes = answers[q.id]
return (
<div key={q.id} className={`border rounded-lg p-3 ${isYes ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50'}`}>
<div className="flex items-center gap-2">
<span className={`text-sm ${isYes ? 'text-green-700' : 'text-red-700'}`}>
{isYes ? '✓' : '✗'}
</span>
<span className="text-sm text-gray-700">{q.question}</span>
<span className={`ml-auto text-xs font-medium ${isYes ? 'text-green-600' : 'text-red-600'}`}>
{isYes ? 'Ja — OK' : 'Nein — Finding erstellt'}
</span>
</div>
{!isYes && (
<p className="text-xs text-red-600 mt-1 ml-6">{q.finding_if_no}</p>
)}
</div>
)
})}
</div>
)
}

View File

@@ -2,6 +2,14 @@
import { useState } from 'react'
export interface FollowUpQuestion {
id: string
question: string
legal_basis: string
severity: 'high' | 'medium' | 'low'
finding_if_no: string
}
export interface AnalysisResult {
url: string
classification: string
@@ -14,6 +22,8 @@ export interface AnalysisResult {
summary: string
email_status: string
analyzed_at: string
follow_up_questions: FollowUpQuestion[]
follow_up_answers: Record<string, boolean>
}
const ESCALATION_ROLES: Record<string, string> = {
@@ -23,12 +33,6 @@ const ESCALATION_ROLES: Record<string, string> = {
E3: 'DSB + Rechtsabteilung',
}
const SDK_HEADERS = {
'Content-Type': 'application/json',
'X-Tenant-ID': '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e',
'X-User-ID': '00000000-0000-0000-0000-000000000001',
}
export function useAgentAnalysis() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
@@ -41,7 +45,6 @@ export function useAgentAnalysis() {
setResult(null)
try {
// Step 1: Fetch and classify
const fetchRes = await fetch('/api/sdk/v1/agent/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -65,6 +68,8 @@ export function useAgentAnalysis() {
summary: data.summary || '',
email_status: data.email_status || 'pending',
analyzed_at: new Date().toISOString(),
follow_up_questions: data.follow_up_questions || [],
follow_up_answers: {},
}
setResult(analysisResult)
@@ -76,5 +81,26 @@ export function useAgentAnalysis() {
}
}
return { analyze, loading, error, result, history }
function answerFollowUp(questionId: string, answer: boolean) {
if (!result) return
const question = result.follow_up_questions.find(q => q.id === questionId)
const newAnswers = { ...result.follow_up_answers, [questionId]: answer }
const newFindings = [...result.findings]
// If user answered "no" → add the finding
if (!answer && question) {
newFindings.push(question.finding_if_no)
}
const updated = {
...result,
findings: newFindings,
follow_up_answers: newAnswers,
}
setResult(updated)
// Update history too
setHistory(prev => prev.map(h => h.analyzed_at === result.analyzed_at ? updated : h))
}
return { analyze, answerFollowUp, loading, error, result, history }
}

View File

@@ -4,10 +4,11 @@ import React, { useState } from 'react'
import { useAgentAnalysis } from './_hooks/useAgentAnalysis'
import { AnalysisResult } from './_components/AnalysisResult'
import { AnalysisHistory } from './_components/AnalysisHistory'
import { FollowUpQuestions } from './_components/FollowUpQuestions'
export default function AgentPage() {
const [url, setUrl] = useState('')
const { analyze, loading, error, result, history } = useAgentAnalysis()
const { analyze, answerFollowUp, loading, error, result, history } = useAgentAnalysis()
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
@@ -65,8 +66,19 @@ export default function AgentPage() {
{/* Result */}
{result && (
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm space-y-6">
<AnalysisResult result={result} />
{/* Follow-Up Questions */}
{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>
)}