Files
breakpilot-compliance/admin-compliance/components/sdk/DraftingEngineWidget.tsx
Benjamin Admin 274dc68e24
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 23s
feat: Drafting Agent Kompetenzbereich erweitert — alle 18 Dokumenttypen, Gap-Banner, Redirect-Logic
- DOCUMENT_SDK_STEP_MAP: 12 kaputte URLs korrigiert (z.B. /sdk/loeschkonzept → /sdk/loeschfristen)
- Go Backend: iace_ce_assessment zur validTypes-Whitelist hinzugefuegt
- SOUL-Datei: von 17 auf ~80 Zeilen erweitert (18 draftbare Typen, Redirects, operative Module)
- Intent Classifier: 10 fehlende Dokumenttyp-Patterns + 5 Redirect-Patterns (Impressum/AGB/Widerruf → Document Generator)
- State Projector: getExistingDocumentTypes von 6 auf 11 Checks erweitert (risks, escalations, iace, obligations, dsr)
- DraftingEngineWidget: Gap-Banner fuer kritische Luecken mit Analysieren-Button
- Cross-Validation: 4 neue deterministische Regeln (DSFA-NO-VVT, DSFA-NO-TOM, DSI-NO-LF, AV-NO-VVT)
- Prose Blocks: 5 neue Dokumenttypen (av_vertrag, betroffenenrechte, risikoanalyse, notfallplan, iace_ce_assessment)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 09:07:07 +01:00

444 lines
20 KiB
TypeScript

'use client'
/**
* DraftingEngineWidget - Erweitert den ComplianceAdvisor um 4 Modi
*
* Mode-Indicator Pills: Explain / Ask / Draft / Validate
* Document-Type Selector aus requiredDocuments der ScopeDecision
* Feature-Flag enableDraftingEngine fuer schrittweises Rollout
*/
import { useState, useEffect, useRef, useCallback } from 'react'
import { useSDK } from '@/lib/sdk/context'
import { useDraftingEngine } from '@/lib/sdk/drafting-engine/use-drafting-engine'
import { DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types'
import type { AgentMode } from '@/lib/sdk/drafting-engine/types'
import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types'
import { DraftEditor } from './DraftEditor'
import { ValidationReport } from './ValidationReport'
interface DraftingEngineWidgetProps {
currentStep?: string
enableDraftingEngine?: boolean
}
const MODE_CONFIG: Record<AgentMode, { label: string; color: string; activeColor: string; icon: string }> = {
explain: { label: 'Explain', color: 'bg-gray-100 text-gray-600', activeColor: 'bg-purple-100 text-purple-700 ring-1 ring-purple-300', icon: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' },
ask: { label: 'Ask', color: 'bg-gray-100 text-gray-600', activeColor: 'bg-amber-100 text-amber-700 ring-1 ring-amber-300', icon: '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' },
draft: { label: 'Draft', color: 'bg-gray-100 text-gray-600', activeColor: 'bg-blue-100 text-blue-700 ring-1 ring-blue-300', icon: 'M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z' },
validate: { label: 'Validate', color: 'bg-gray-100 text-gray-600', activeColor: 'bg-green-100 text-green-700 ring-1 ring-green-300', icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z' },
}
const EXAMPLE_QUESTIONS: Record<AgentMode, string[]> = {
explain: [
'Was ist ein Verarbeitungsverzeichnis?',
'Wann brauche ich eine DSFA?',
'Was sind TOM nach Art. 32 DSGVO?',
],
ask: [
'Welche Luecken hat mein Compliance-Profil?',
'Was fehlt noch fuer die Zertifizierung?',
'Welche Dokumente muss ich noch erstellen?',
],
draft: [
'Erstelle einen VVT-Eintrag fuer unseren Hauptprozess',
'Erstelle TOM fuer unsere Cloud-Infrastruktur',
'Erstelle eine Datenschutzerklaerung',
],
validate: [
'Pruefe die Konsistenz meiner Dokumente',
'Stimmen VVT und TOM ueberein?',
'Gibt es Luecken bei den Loeschfristen?',
],
}
export function DraftingEngineWidget({
currentStep = 'default',
enableDraftingEngine = true,
}: DraftingEngineWidgetProps) {
const { state } = useSDK()
const engine = useDraftingEngine()
const [isOpen, setIsOpen] = useState(false)
const [isExpanded, setIsExpanded] = useState(false)
const [inputValue, setInputValue] = useState('')
const [showDraftEditor, setShowDraftEditor] = useState(false)
const [showValidationReport, setShowValidationReport] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
// Available document types from scope decision
const availableDocumentTypes: ScopeDocumentType[] =
state.complianceScope?.decision?.requiredDocuments
?.filter(d => d.required)
.map(d => d.documentType as ScopeDocumentType) ?? ['vvt', 'tom', 'lf']
// Auto-scroll
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [engine.messages])
// Open draft editor when a new draft arrives
useEffect(() => {
if (engine.currentDraft) {
setShowDraftEditor(true)
}
}, [engine.currentDraft])
// Open validation report when new results arrive
useEffect(() => {
if (engine.validationResult) {
setShowValidationReport(true)
}
}, [engine.validationResult])
const handleSendMessage = useCallback(
(content: string) => {
if (!content.trim()) return
setInputValue('')
engine.sendMessage(content)
},
[engine]
)
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSendMessage(inputValue)
}
}
const exampleQuestions = EXAMPLE_QUESTIONS[engine.currentMode]
if (!isOpen) {
return (
<button
onClick={() => setIsOpen(true)}
className="fixed bottom-6 right-[5.5rem] w-14 h-14 bg-indigo-600 hover:bg-indigo-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all duration-200 hover:scale-110 z-50"
aria-label="Drafting Engine oeffnen"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
)
}
// Draft Editor full-screen overlay
if (showDraftEditor && engine.currentDraft) {
return (
<DraftEditor
draft={engine.currentDraft}
documentType={engine.activeDocumentType}
constraintCheck={engine.constraintCheck}
onAccept={() => {
engine.acceptDraft()
setShowDraftEditor(false)
}}
onValidate={() => {
engine.validateDraft()
}}
onClose={() => setShowDraftEditor(false)}
onRefine={(instruction: string) => {
engine.requestDraft(instruction)
}}
validationResult={engine.validationResult}
isTyping={engine.isTyping}
/>
)
}
return (
<div className={`fixed bottom-6 right-6 ${isExpanded ? 'w-[700px] h-[80vh]' : 'w-[420px] h-[560px]'} max-h-screen bg-white rounded-2xl shadow-2xl flex flex-col z-50 border border-gray-200 transition-all duration-200`}>
{/* Header */}
<div className="bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-4 py-3 rounded-t-2xl flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</div>
<div>
<div className="font-semibold text-sm">Drafting Engine</div>
<div className="text-xs text-white/80">Compliance-Dokumententwurf</div>
</div>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-white/80 hover:text-white transition-colors p-1"
aria-label={isExpanded ? 'Verkleinern' : 'Vergroessern'}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{isExpanded ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 9L4 4m0 0v4m0-4h4m6 6l5 5m0 0v-4m0 4h-4" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5" />
)}
</svg>
</button>
<button
onClick={() => {
engine.clearMessages()
setIsOpen(false)
}}
className="text-white/80 hover:text-white transition-colors p-1"
aria-label="Schliessen"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Mode Pills */}
<div className="flex items-center gap-1 px-3 py-2 border-b border-gray-100 bg-white">
{(Object.keys(MODE_CONFIG) as AgentMode[]).map((mode) => {
const config = MODE_CONFIG[mode]
const isActive = engine.currentMode === mode
return (
<button
key={mode}
onClick={() => engine.setMode(mode)}
className={`flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium transition-all ${isActive ? config.activeColor : config.color} hover:opacity-80`}
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={config.icon} />
</svg>
{config.label}
</button>
)
})}
</div>
{/* Document Type Selector (visible in draft/validate mode) */}
{(engine.currentMode === 'draft' || engine.currentMode === 'validate') && (
<div className="px-3 py-2 border-b border-gray-100 bg-gray-50/50">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500 shrink-0">Dokument:</span>
<select
value={engine.activeDocumentType || ''}
onChange={(e) => engine.setDocumentType(e.target.value as ScopeDocumentType)}
className="flex-1 text-xs border border-gray-200 rounded-md px-2 py-1 bg-white focus:outline-none focus:ring-1 focus:ring-purple-400"
>
<option value="">Dokumenttyp waehlen...</option>
{availableDocumentTypes.map((dt) => (
<option key={dt} value={dt}>
{DOCUMENT_TYPE_LABELS[dt] || dt}
</option>
))}
</select>
</div>
</div>
)}
{/* Gap Banner */}
{(() => {
const gaps = state.complianceScope?.decision?.gaps?.filter(
(g: { severity: string }) => g.severity === 'HIGH' || g.severity === 'CRITICAL'
) ?? []
if (gaps.length > 0) {
return (
<div className="mx-3 mt-2 px-3 py-2 bg-amber-50 border border-amber-200 rounded-lg flex items-center justify-between">
<div className="flex items-center gap-2 text-xs text-amber-800">
<svg className="w-4 h-4 text-amber-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<span className="font-medium">{gaps.length} kritische Luecke{gaps.length !== 1 ? 'n' : ''} erkannt</span>
</div>
<button
onClick={() => handleSendMessage('Was fehlt noch in meinem Compliance-Profil?')}
className="text-xs font-medium text-amber-700 hover:text-amber-900 px-2 py-0.5 rounded hover:bg-amber-100 transition-colors"
>
Analysieren
</button>
</div>
)
}
return null
})()}
{/* Error Banner */}
{engine.error && (
<div className="mx-3 mt-2 px-3 py-2 bg-red-50 border border-red-200 rounded-lg text-xs text-red-700 flex items-center justify-between">
<span>{engine.error}</span>
<button onClick={() => engine.clearMessages()} className="text-red-500 hover:text-red-700 ml-2">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)}
{/* Validation Report Inline */}
{showValidationReport && engine.validationResult && (
<div className="mx-3 mt-2 max-h-48 overflow-y-auto">
<ValidationReport
result={engine.validationResult}
onClose={() => setShowValidationReport(false)}
compact
/>
</div>
)}
{/* Messages Area */}
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-gray-50">
{engine.messages.length === 0 ? (
<div className="text-center py-6">
<div className="w-14 h-14 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-3">
<svg className="w-7 h-7 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={MODE_CONFIG[engine.currentMode].icon} />
</svg>
</div>
<h3 className="text-sm font-medium text-gray-900 mb-1">
{engine.currentMode === 'explain' && 'Fragen beantworten'}
{engine.currentMode === 'ask' && 'Luecken erkennen'}
{engine.currentMode === 'draft' && 'Dokumente entwerfen'}
{engine.currentMode === 'validate' && 'Konsistenz pruefen'}
</h3>
<p className="text-xs text-gray-500 mb-4">
{engine.currentMode === 'explain' && 'Stellen Sie Fragen zu DSGVO, AI Act und Compliance.'}
{engine.currentMode === 'ask' && 'Identifiziert Luecken in Ihrem Compliance-Profil.'}
{engine.currentMode === 'draft' && 'Erstellt strukturierte Compliance-Dokumente.'}
{engine.currentMode === 'validate' && 'Prueft Cross-Dokument-Konsistenz.'}
</p>
<div className="text-left space-y-2">
<p className="text-xs font-medium text-gray-700 mb-2">Beispiele:</p>
{exampleQuestions.map((q, idx) => (
<button
key={idx}
onClick={() => handleSendMessage(q)}
className="w-full text-left px-3 py-2 text-xs bg-white hover:bg-purple-50 border border-gray-200 rounded-lg transition-colors text-gray-700"
>
{q}
</button>
))}
</div>
{/* Quick Actions for Draft/Validate */}
{engine.currentMode === 'draft' && engine.activeDocumentType && (
<button
onClick={() => engine.requestDraft()}
className="mt-4 px-4 py-2 bg-blue-600 text-white text-xs font-medium rounded-lg hover:bg-blue-700 transition-colors"
>
Draft fuer {DOCUMENT_TYPE_LABELS[engine.activeDocumentType]?.split(' (')[0] || engine.activeDocumentType} erstellen
</button>
)}
{engine.currentMode === 'validate' && (
<button
onClick={() => engine.validateDraft()}
className="mt-4 px-4 py-2 bg-green-600 text-white text-xs font-medium rounded-lg hover:bg-green-700 transition-colors"
>
Validierung starten
</button>
)}
</div>
) : (
<>
{engine.messages.map((message, idx) => (
<div
key={idx}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[85%] rounded-lg px-3 py-2 ${
message.role === 'user'
? 'bg-indigo-600 text-white'
: 'bg-white border border-gray-200 text-gray-800'
}`}
>
<p className={`text-sm ${message.role === 'assistant' ? 'whitespace-pre-wrap' : ''}`}>
{message.content}
</p>
{/* Draft ready indicator */}
{message.metadata?.hasDraft && engine.currentDraft && (
<button
onClick={() => setShowDraftEditor(true)}
className="mt-2 flex items-center gap-1.5 px-3 py-1.5 bg-blue-50 border border-blue-200 rounded-md text-xs text-blue-700 hover:bg-blue-100 transition-colors"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
Im Editor oeffnen
</button>
)}
{/* Validation ready indicator */}
{message.metadata?.hasValidation && engine.validationResult && (
<button
onClick={() => setShowValidationReport(true)}
className="mt-2 flex items-center gap-1.5 px-3 py-1.5 bg-green-50 border border-green-200 rounded-md text-xs text-green-700 hover:bg-green-100 transition-colors"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Validierungsbericht anzeigen
</button>
)}
</div>
</div>
))}
{engine.isTyping && (
<div className="flex justify-start">
<div className="bg-white border border-gray-200 rounded-lg px-3 py-2">
<div className="flex space-x-1">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }} />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }} />
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</>
)}
</div>
{/* Input Area */}
<div className="border-t border-gray-200 p-3 bg-white rounded-b-2xl">
<div className="flex gap-2">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={
engine.currentMode === 'draft'
? 'Anweisung fuer den Entwurf...'
: engine.currentMode === 'validate'
? 'Validierungsfrage...'
: 'Frage eingeben...'
}
disabled={engine.isTyping}
className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50"
/>
{engine.isTyping ? (
<button
onClick={engine.stopGeneration}
className="px-3 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
title="Generierung stoppen"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 6h12v12H6z" />
</svg>
</button>
) : (
<button
onClick={() => handleSendMessage(inputValue)}
disabled={!inputValue.trim()}
className="px-3 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
</button>
)}
</div>
</div>
</div>
)
}