Extends the Compliance Advisor from a Q&A chatbot into a full drafting engine that can generate, validate, and refine compliance documents within Scope Engine constraints. Includes intent classifier, state projector, constraint enforcer, SOUL templates, Go backend endpoints, and React UI components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
418 lines
18 KiB
TypeScript
418 lines
18 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>
|
|
)}
|
|
|
|
{/* 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>
|
|
)
|
|
}
|