Remove standalone services (ai-compliance-sdk root, developer-portal, dsms-gateway, dsms-node, night-scheduler) and legacy compliance/dsgvo pages. Add new SDK pipeline modules (academy, document-crawler, dsb-portal, incidents, whistleblower, reporting, sso, multi-tenant, industry-templates). Add drafting engine, legal corpus files (AT/CH/DE), pitch-deck, blog and Förderantrag pages. Co-Authored-By: Claude Sonnet 4.5 <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>
|
|
)
|
|
}
|