This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/components/sdk/DraftEditor.tsx
Benjamin Admin 70f2b0ae64 refactor: Consolidate standalone services into admin-v2, add new SDK modules
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>
2026-02-15 09:05:18 +01:00

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>
)
}