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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user