Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
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>
|
|
)
|
|
}
|