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>
301 lines
12 KiB
TypeScript
301 lines
12 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* DraftEditor - Split-Pane Editor fuer Compliance-Dokument-Entwuerfe
|
|
*
|
|
* Links (2/3): Gerenderter Draft mit Section-Headern
|
|
* Rechts (1/3): Chat-Panel fuer iterative Verfeinerung
|
|
* Oben: Document-Type Label, Depth-Level Badge, Constraint-Compliance
|
|
*/
|
|
|
|
import { useState, useRef, useCallback } from 'react'
|
|
import { DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types'
|
|
import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types'
|
|
import type {
|
|
DraftRevision,
|
|
ConstraintCheckResult,
|
|
ValidationResult,
|
|
} from '@/lib/sdk/drafting-engine/types'
|
|
|
|
interface DraftEditorProps {
|
|
draft: DraftRevision
|
|
documentType: ScopeDocumentType | null
|
|
constraintCheck: ConstraintCheckResult | null
|
|
validationResult: ValidationResult | null
|
|
isTyping: boolean
|
|
onAccept: () => void
|
|
onValidate: () => void
|
|
onClose: () => void
|
|
onRefine: (instruction: string) => void
|
|
}
|
|
|
|
export function DraftEditor({
|
|
draft,
|
|
documentType,
|
|
constraintCheck,
|
|
validationResult,
|
|
isTyping,
|
|
onAccept,
|
|
onValidate,
|
|
onClose,
|
|
onRefine,
|
|
}: DraftEditorProps) {
|
|
const [refineInput, setRefineInput] = useState('')
|
|
const [activeSection, setActiveSection] = useState<string | null>(null)
|
|
const contentRef = useRef<HTMLDivElement>(null)
|
|
|
|
const handleRefine = useCallback(() => {
|
|
if (!refineInput.trim() || isTyping) return
|
|
onRefine(refineInput.trim())
|
|
setRefineInput('')
|
|
}, [refineInput, isTyping, onRefine])
|
|
|
|
const handleRefineKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault()
|
|
handleRefine()
|
|
}
|
|
}
|
|
|
|
const docLabel = documentType
|
|
? DOCUMENT_TYPE_LABELS[documentType]?.split(' (')[0] || documentType
|
|
: 'Dokument'
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-gray-900/50 z-50 flex items-center justify-center p-4">
|
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-6xl h-[85vh] flex flex-col overflow-hidden">
|
|
{/* Header */}
|
|
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 text-white px-6 py-3 flex items-center justify-between shrink-0">
|
|
<div className="flex items-center gap-3">
|
|
<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 className="font-semibold text-sm">{docLabel} - Entwurf</div>
|
|
<div className="text-xs text-white/70">
|
|
{draft.sections.length} Sections | Erstellt {new Date(draft.createdAt).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{/* Constraint Badge */}
|
|
{constraintCheck && (
|
|
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
constraintCheck.allowed
|
|
? 'bg-green-500/20 text-green-100'
|
|
: 'bg-red-500/20 text-red-100'
|
|
}`}>
|
|
{constraintCheck.allowed ? 'Constraints OK' : 'Constraint-Verletzung'}
|
|
</span>
|
|
)}
|
|
|
|
{/* Validation Badge */}
|
|
{validationResult && (
|
|
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
validationResult.passed
|
|
? 'bg-green-500/20 text-green-100'
|
|
: 'bg-amber-500/20 text-amber-100'
|
|
}`}>
|
|
{validationResult.passed ? 'Validiert' : `${validationResult.errors.length} Fehler`}
|
|
</span>
|
|
)}
|
|
|
|
<button
|
|
onClick={onClose}
|
|
className="text-white/80 hover:text-white transition-colors p-1"
|
|
aria-label="Editor 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>
|
|
|
|
{/* Adjustment Warnings */}
|
|
{constraintCheck && constraintCheck.adjustments.length > 0 && (
|
|
<div className="px-6 py-2 bg-amber-50 border-b border-amber-200 shrink-0">
|
|
{constraintCheck.adjustments.map((adj, i) => (
|
|
<p key={i} className="text-xs text-amber-700 flex items-start gap-1">
|
|
<svg className="w-3.5 h-3.5 mt-0.5 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 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
|
</svg>
|
|
{adj}
|
|
</p>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Main Content: 2/3 Editor + 1/3 Chat */}
|
|
<div className="flex-1 flex overflow-hidden">
|
|
{/* Left: Draft Content (2/3) */}
|
|
<div className="w-2/3 border-r border-gray-200 overflow-y-auto" ref={contentRef}>
|
|
{/* Section Navigation */}
|
|
<div className="sticky top-0 bg-white border-b border-gray-100 px-4 py-2 flex items-center gap-1 overflow-x-auto z-10">
|
|
{draft.sections.map((section) => (
|
|
<button
|
|
key={section.id}
|
|
onClick={() => {
|
|
setActiveSection(section.id)
|
|
document.getElementById(`section-${section.id}`)?.scrollIntoView({ behavior: 'smooth' })
|
|
}}
|
|
className={`px-2.5 py-1 rounded-md text-xs font-medium whitespace-nowrap transition-colors ${
|
|
activeSection === section.id
|
|
? 'bg-blue-100 text-blue-700'
|
|
: 'text-gray-500 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
{section.title}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Sections */}
|
|
<div className="p-6 space-y-6">
|
|
{draft.sections.map((section) => (
|
|
<div
|
|
key={section.id}
|
|
id={`section-${section.id}`}
|
|
className={`rounded-lg border transition-colors ${
|
|
activeSection === section.id
|
|
? 'border-blue-300 bg-blue-50/30'
|
|
: 'border-gray-200 bg-white'
|
|
}`}
|
|
>
|
|
<div className="px-4 py-2.5 border-b border-gray-100 flex items-center justify-between">
|
|
<h3 className="text-sm font-semibold text-gray-900">{section.title}</h3>
|
|
{section.schemaField && (
|
|
<span className="text-xs text-gray-400 font-mono">{section.schemaField}</span>
|
|
)}
|
|
</div>
|
|
<div className="px-4 py-3">
|
|
<div className="text-sm text-gray-700 whitespace-pre-wrap leading-relaxed">
|
|
{section.content}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right: Refinement Chat (1/3) */}
|
|
<div className="w-1/3 flex flex-col bg-gray-50">
|
|
<div className="px-4 py-3 border-b border-gray-200 bg-white">
|
|
<h3 className="text-sm font-semibold text-gray-900">Verfeinerung</h3>
|
|
<p className="text-xs text-gray-500">Geben Sie Anweisungen zur Verbesserung</p>
|
|
</div>
|
|
|
|
{/* Validation Summary (if present) */}
|
|
{validationResult && (
|
|
<div className="px-4 py-3 border-b border-gray-100">
|
|
<div className="space-y-1.5">
|
|
{validationResult.errors.length > 0 && (
|
|
<div className="flex items-center gap-1.5 text-xs text-red-600">
|
|
<span className="w-2 h-2 rounded-full bg-red-500" />
|
|
{validationResult.errors.length} Fehler
|
|
</div>
|
|
)}
|
|
{validationResult.warnings.length > 0 && (
|
|
<div className="flex items-center gap-1.5 text-xs text-amber-600">
|
|
<span className="w-2 h-2 rounded-full bg-amber-500" />
|
|
{validationResult.warnings.length} Warnungen
|
|
</div>
|
|
)}
|
|
{validationResult.suggestions.length > 0 && (
|
|
<div className="flex items-center gap-1.5 text-xs text-blue-600">
|
|
<span className="w-2 h-2 rounded-full bg-blue-500" />
|
|
{validationResult.suggestions.length} Vorschlaege
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Refinement Area */}
|
|
<div className="flex-1 p-4 overflow-y-auto">
|
|
<div className="space-y-3">
|
|
<p className="text-xs text-gray-500">
|
|
Beschreiben Sie, was geaendert werden soll. Der Agent erstellt eine ueberarbeitete Version unter Beachtung der Scope-Constraints.
|
|
</p>
|
|
|
|
{/* Quick Refinement Buttons */}
|
|
<div className="space-y-1.5">
|
|
{[
|
|
'Mehr Details hinzufuegen',
|
|
'Platzhalter ausfuellen',
|
|
'Rechtliche Referenzen ergaenzen',
|
|
'Sprache vereinfachen',
|
|
].map((suggestion) => (
|
|
<button
|
|
key={suggestion}
|
|
onClick={() => onRefine(suggestion)}
|
|
disabled={isTyping}
|
|
className="w-full text-left px-3 py-1.5 text-xs bg-white hover:bg-blue-50 border border-gray-200 rounded-md transition-colors text-gray-600 disabled:opacity-50"
|
|
>
|
|
{suggestion}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Refinement Input */}
|
|
<div className="border-t border-gray-200 p-3 bg-white">
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={refineInput}
|
|
onChange={(e) => setRefineInput(e.target.value)}
|
|
onKeyDown={handleRefineKeyDown}
|
|
placeholder="Anweisung eingeben..."
|
|
disabled={isTyping}
|
|
className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50"
|
|
/>
|
|
<button
|
|
onClick={handleRefine}
|
|
disabled={!refineInput.trim() || isTyping}
|
|
className="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
<svg className="w-4 h-4" 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>
|
|
</div>
|
|
|
|
{/* Footer Actions */}
|
|
<div className="border-t border-gray-200 px-6 py-3 bg-white flex items-center justify-between shrink-0">
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={onValidate}
|
|
disabled={isTyping}
|
|
className="px-4 py-2 text-sm font-medium text-green-700 bg-green-50 border border-green-200 rounded-lg hover:bg-green-100 disabled:opacity-50 transition-colors"
|
|
>
|
|
Validieren
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={onClose}
|
|
className="px-4 py-2 text-sm font-medium text-gray-600 bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 transition-colors"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
onClick={onAccept}
|
|
disabled={isTyping}
|
|
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
|
>
|
|
Draft akzeptieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|