fix(quality): Ruff/CVE/TS-Fixes, 104 neue Tests, Complexity-Refactoring
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 30s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 17s
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 30s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 17s
- Ruff: 144 auto-fixes (unused imports, == None → is None), F821/F811/F841 manuell - CVEs: python-multipart>=0.0.22, weasyprint>=68.0, pillow>=12.1.1, npm audit fix (0 vulns) - TS: 5 tote Drafting-Engine-Dateien entfernt, allowed-facts/sanitizer/StepHeader/context fixes - Tests: +104 (ISMS 58, Evidence 18, VVT 14, Generation 14) → 1449 passed - Refactoring: collect_ci_evidence (F→A), row_to_response (E→A), extract_requirements (E→A) - Dead Code: pca-platform, 7 Go-Handler, dsr_api.py, duplicate Schemas entfernt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,6 @@ interface Message {
|
||||
|
||||
interface ComplianceAdvisorWidgetProps {
|
||||
currentStep?: string
|
||||
enableDraftingEngine?: boolean
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -68,13 +67,7 @@ const COUNTRIES: { code: Country; label: string }[] = [
|
||||
{ code: 'EU', label: 'EU' },
|
||||
]
|
||||
|
||||
export function ComplianceAdvisorWidget({ currentStep = 'default', enableDraftingEngine = false }: ComplianceAdvisorWidgetProps) {
|
||||
// Feature-flag: If Drafting Engine enabled, render DraftingEngineWidget instead
|
||||
if (enableDraftingEngine) {
|
||||
const { DraftingEngineWidget } = require('./DraftingEngineWidget')
|
||||
return <DraftingEngineWidget currentStep={currentStep} enableDraftingEngine />
|
||||
}
|
||||
|
||||
export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceAdvisorWidgetProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@@ -1,443 +0,0 @@
|
||||
'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>
|
||||
)}
|
||||
|
||||
{/* Gap Banner */}
|
||||
{(() => {
|
||||
const gaps = state.complianceScope?.decision?.gaps?.filter(
|
||||
(g: { severity: string }) => g.severity === 'HIGH' || g.severity === 'CRITICAL'
|
||||
) ?? []
|
||||
if (gaps.length > 0) {
|
||||
return (
|
||||
<div className="mx-3 mt-2 px-3 py-2 bg-amber-50 border border-amber-200 rounded-lg flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-xs text-amber-800">
|
||||
<svg className="w-4 h-4 text-amber-500 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 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
<span className="font-medium">{gaps.length} kritische Luecke{gaps.length !== 1 ? 'n' : ''} erkannt</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleSendMessage('Was fehlt noch in meinem Compliance-Profil?')}
|
||||
className="text-xs font-medium text-amber-700 hover:text-amber-900 px-2 py-0.5 rounded hover:bg-amber-100 transition-colors"
|
||||
>
|
||||
Analysieren
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})()}
|
||||
|
||||
{/* 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>
|
||||
)
|
||||
}
|
||||
@@ -17,9 +17,9 @@ export interface StepTip {
|
||||
|
||||
interface StepHeaderProps {
|
||||
stepId: string
|
||||
title: string
|
||||
description: string
|
||||
explanation: string
|
||||
title?: string
|
||||
description?: string
|
||||
explanation?: string
|
||||
tips?: StepTip[]
|
||||
showNavigation?: boolean
|
||||
showProgress?: boolean
|
||||
@@ -95,10 +95,10 @@ const tipIconColors = {
|
||||
|
||||
export function StepHeader({
|
||||
stepId,
|
||||
title,
|
||||
description,
|
||||
explanation,
|
||||
tips = [],
|
||||
title: titleProp,
|
||||
description: descriptionProp,
|
||||
explanation: explanationProp,
|
||||
tips: tipsProp,
|
||||
showNavigation = true,
|
||||
showProgress = true,
|
||||
onComplete,
|
||||
@@ -109,6 +109,13 @@ export function StepHeader({
|
||||
const { state, dispatch } = useSDK()
|
||||
const [showHelp, setShowHelp] = useState(false)
|
||||
|
||||
// Look up defaults from STEP_EXPLANATIONS when props are not provided
|
||||
const preset = STEP_EXPLANATIONS[stepId as keyof typeof STEP_EXPLANATIONS]
|
||||
const title = titleProp ?? preset?.title ?? stepId
|
||||
const description = descriptionProp ?? preset?.description ?? ''
|
||||
const explanation = explanationProp ?? preset?.explanation ?? ''
|
||||
const tips = tipsProp ?? preset?.tips ?? []
|
||||
|
||||
const currentStep = getStepById(stepId)
|
||||
const prevStep = getPreviousStep(stepId)
|
||||
const nextStep = getNextStep(stepId)
|
||||
@@ -996,6 +1003,50 @@ export const STEP_EXPLANATIONS = {
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
'email-templates': {
|
||||
title: 'E-Mail-Templates',
|
||||
description: 'Verwalten Sie Vorlagen fuer alle DSGVO-relevanten Benachrichtigungen',
|
||||
explanation: 'E-Mail-Templates definieren die Texte und das Layout fuer automatisierte DSGVO-Benachrichtigungen: Einwilligungsbestaetigung, Widerrufsbestaetigung, Auskunftsantwort, Loeschbestaetigung und weitere Lifecycle-E-Mails. Alle 16 Template-Typen koennen individuell angepasst und mit Variablen personalisiert werden.',
|
||||
tips: [
|
||||
{
|
||||
icon: 'info' as const,
|
||||
title: '16 Lifecycle-E-Mails',
|
||||
description: 'Von der Registrierungsbestaetigung bis zur Kontoloeschung — alle relevanten Touchpoints sind mit Vorlagen abgedeckt.',
|
||||
},
|
||||
{
|
||||
icon: 'warning' as const,
|
||||
title: 'Pflichtangaben',
|
||||
description: 'Stellen Sie sicher, dass jede E-Mail die gesetzlich vorgeschriebenen Angaben enthaelt: Impressum, Datenschutzhinweis und Widerrufsmoeglichkeit.',
|
||||
},
|
||||
{
|
||||
icon: 'lightbulb' as const,
|
||||
title: 'Variablen',
|
||||
description: 'Nutzen Sie Platzhalter wie {{name}}, {{email}} und {{company}} fuer automatische Personalisierung.',
|
||||
},
|
||||
],
|
||||
},
|
||||
'use-case-workshop': {
|
||||
title: 'Use Case Workshop',
|
||||
description: 'Erfassen und bewerten Sie Ihre KI-Anwendungsfaelle im Workshop-Format',
|
||||
explanation: 'Im Use Case Workshop erfassen Sie Ihre KI-Anwendungsfaelle strukturiert in einem gefuehrten Prozess. Der Workshop leitet Sie durch Identifikation, Beschreibung, Datenkategorien, Risikobewertung und Stakeholder-Analyse. Die Ergebnisse fliessen direkt in die Compliance-Bewertung ein.',
|
||||
tips: [
|
||||
{
|
||||
icon: 'lightbulb' as const,
|
||||
title: 'Vollstaendigkeit',
|
||||
description: 'Erfassen Sie alle KI-Anwendungsfaelle — auch solche, die nur intern genutzt werden oder sich noch in der Planungsphase befinden.',
|
||||
},
|
||||
{
|
||||
icon: 'info' as const,
|
||||
title: 'Stakeholder einbeziehen',
|
||||
description: 'Beziehen Sie Fachbereiche und IT in den Workshop ein, um alle Anwendungsfaelle zu identifizieren.',
|
||||
},
|
||||
{
|
||||
icon: 'warning' as const,
|
||||
title: 'Risikobewertung',
|
||||
description: 'Jeder Anwendungsfall wird nach EU AI Act Risikostufen klassifiziert. Hochrisiko-Systeme erfordern zusaetzliche Dokumentation.',
|
||||
},
|
||||
],
|
||||
},
|
||||
} satisfies Record<string, { title: string; description: string; explanation: string; tips: StepTip[] }>
|
||||
|
||||
export default StepHeader
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* ValidationReport - Strukturierte Anzeige von Validierungsergebnissen
|
||||
*
|
||||
* Errors (Scope-Violations) in Rot
|
||||
* Warnings (Inkonsistenzen) in Amber
|
||||
* Suggestions in Blau
|
||||
*/
|
||||
|
||||
import { DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types'
|
||||
import type { ValidationResult, ValidationFinding } from '@/lib/sdk/drafting-engine/types'
|
||||
|
||||
interface ValidationReportProps {
|
||||
result: ValidationResult
|
||||
onClose: () => void
|
||||
/** Compact mode for inline display in widget */
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
const SEVERITY_CONFIG = {
|
||||
error: {
|
||||
bg: 'bg-red-50',
|
||||
border: 'border-red-200',
|
||||
text: 'text-red-700',
|
||||
icon: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
label: 'Fehler',
|
||||
dotColor: 'bg-red-500',
|
||||
},
|
||||
warning: {
|
||||
bg: 'bg-amber-50',
|
||||
border: 'border-amber-200',
|
||||
text: 'text-amber-700',
|
||||
icon: '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',
|
||||
label: 'Warnungen',
|
||||
dotColor: 'bg-amber-500',
|
||||
},
|
||||
suggestion: {
|
||||
bg: 'bg-blue-50',
|
||||
border: 'border-blue-200',
|
||||
text: 'text-blue-700',
|
||||
icon: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
label: 'Vorschlaege',
|
||||
dotColor: 'bg-blue-500',
|
||||
},
|
||||
}
|
||||
|
||||
function FindingCard({ finding, compact }: { finding: ValidationFinding; compact?: boolean }) {
|
||||
const config = SEVERITY_CONFIG[finding.severity]
|
||||
const docLabel = DOCUMENT_TYPE_LABELS[finding.documentType]?.split(' (')[0] || finding.documentType
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className={`flex items-start gap-2 px-2.5 py-1.5 ${config.bg} rounded-md border ${config.border}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full mt-1.5 shrink-0 ${config.dotColor}`} />
|
||||
<div className="min-w-0">
|
||||
<p className={`text-xs font-medium ${config.text}`}>{finding.title}</p>
|
||||
<p className="text-xs text-gray-500 truncate">{finding.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${config.bg} rounded-lg border ${config.border} p-3`}>
|
||||
<div className="flex items-start gap-2">
|
||||
<svg className={`w-4 h-4 mt-0.5 shrink-0 ${config.text}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={config.icon} />
|
||||
</svg>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className={`text-sm font-medium ${config.text}`}>{finding.title}</h4>
|
||||
<span className="text-xs text-gray-400 bg-gray-100 px-1.5 py-0.5 rounded">{docLabel}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">{finding.description}</p>
|
||||
|
||||
{finding.crossReferenceType && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Cross-Referenz: {DOCUMENT_TYPE_LABELS[finding.crossReferenceType]?.split(' (')[0] || finding.crossReferenceType}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{finding.legalReference && (
|
||||
<p className="text-xs text-gray-500 mt-1 font-mono">{finding.legalReference}</p>
|
||||
)}
|
||||
|
||||
{finding.suggestion && (
|
||||
<div className="mt-2 flex items-start gap-1.5 px-2.5 py-1.5 bg-white/60 rounded border border-gray-100">
|
||||
<svg className="w-3.5 h-3.5 mt-0.5 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
|
||||
</svg>
|
||||
<p className="text-xs text-gray-600">{finding.suggestion}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ValidationReport({ result, onClose, compact }: ValidationReportProps) {
|
||||
const totalFindings = result.errors.length + result.warnings.length + result.suggestions.length
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-gray-50 border-b border-gray-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${result.passed ? 'bg-green-500' : 'bg-red-500'}`} />
|
||||
<span className="text-xs font-medium text-gray-700">
|
||||
{result.passed ? 'Validierung bestanden' : 'Validierung fehlgeschlagen'}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
({totalFindings} {totalFindings === 1 ? 'Fund' : 'Funde'})
|
||||
</span>
|
||||
</div>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
<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>
|
||||
<div className="p-2 space-y-1.5 max-h-36 overflow-y-auto">
|
||||
{result.errors.map((f) => <FindingCard key={f.id} finding={f} compact />)}
|
||||
{result.warnings.map((f) => <FindingCard key={f.id} finding={f} compact />)}
|
||||
{result.suggestions.map((f) => <FindingCard key={f.id} finding={f} compact />)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Summary Header */}
|
||||
<div className={`rounded-lg border p-4 ${result.passed ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${result.passed ? 'bg-green-100' : 'bg-red-100'}`}>
|
||||
<svg className={`w-5 h-5 ${result.passed ? 'text-green-600' : 'text-red-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={result.passed ? 'M5 13l4 4L19 7' : 'M6 18L18 6M6 6l12 12'} />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`text-sm font-semibold ${result.passed ? 'text-green-800' : 'text-red-800'}`}>
|
||||
{result.passed ? 'Validierung bestanden' : 'Validierung fehlgeschlagen'}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
Level {result.scopeLevel} | {new Date(result.timestamp).toLocaleString('de-DE')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-3">
|
||||
{result.errors.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500" />
|
||||
<span className="text-xs font-medium text-red-700">{result.errors.length}</span>
|
||||
</div>
|
||||
)}
|
||||
{result.warnings.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-amber-500" />
|
||||
<span className="text-xs font-medium text-amber-700">{result.warnings.length}</span>
|
||||
</div>
|
||||
)}
|
||||
{result.suggestions.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500" />
|
||||
<span className="text-xs font-medium text-blue-700">{result.suggestions.length}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 ml-2">
|
||||
<svg className="w-4 h-4" 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>
|
||||
</div>
|
||||
|
||||
{/* Errors */}
|
||||
{result.errors.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-red-700 uppercase tracking-wide mb-2">
|
||||
Fehler ({result.errors.length})
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{result.errors.map((f) => <FindingCard key={f.id} finding={f} />)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Warnings */}
|
||||
{result.warnings.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-amber-700 uppercase tracking-wide mb-2">
|
||||
Warnungen ({result.warnings.length})
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{result.warnings.map((f) => <FindingCard key={f.id} finding={f} />)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Suggestions */}
|
||||
{result.suggestions.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-blue-700 uppercase tracking-wide mb-2">
|
||||
Vorschlaege ({result.suggestions.length})
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{result.suggestions.map((f) => <FindingCard key={f.id} finding={f} />)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { STEP_EXPLANATIONS } from '../StepHeader'
|
||||
|
||||
type StepExplanationKey = keyof typeof STEP_EXPLANATIONS
|
||||
|
||||
// Focus on testing the STEP_EXPLANATIONS data structure
|
||||
// Component tests require more complex SDK context mocking
|
||||
|
||||
describe('STEP_EXPLANATIONS', () => {
|
||||
it('should have explanations for all Phase 1 steps', () => {
|
||||
const phase1Steps = [
|
||||
const phase1Steps: StepExplanationKey[] = [
|
||||
'use-case-workshop',
|
||||
'screening',
|
||||
'modules',
|
||||
@@ -29,7 +31,7 @@ describe('STEP_EXPLANATIONS', () => {
|
||||
})
|
||||
|
||||
it('should have explanations for all Phase 2 steps', () => {
|
||||
const phase2Steps = [
|
||||
const phase2Steps: StepExplanationKey[] = [
|
||||
'ai-act',
|
||||
'obligations',
|
||||
'dsfa',
|
||||
@@ -93,8 +95,8 @@ describe('STEP_EXPLANATIONS', () => {
|
||||
expect(dsfa.explanation.length).toBeGreaterThan(50)
|
||||
})
|
||||
|
||||
it('should cover all 19 SDK steps', () => {
|
||||
const allStepIds = [
|
||||
it('should cover all core SDK steps', () => {
|
||||
const coreStepIds: StepExplanationKey[] = [
|
||||
// Phase 1
|
||||
'use-case-workshop',
|
||||
'screening',
|
||||
@@ -118,10 +120,11 @@ describe('STEP_EXPLANATIONS', () => {
|
||||
'escalations',
|
||||
]
|
||||
|
||||
expect(Object.keys(STEP_EXPLANATIONS).length).toBe(allStepIds.length)
|
||||
|
||||
allStepIds.forEach(stepId => {
|
||||
coreStepIds.forEach(stepId => {
|
||||
expect(STEP_EXPLANATIONS[stepId]).toBeDefined()
|
||||
})
|
||||
|
||||
// Ensure we have at least the core steps plus additional module explanations
|
||||
expect(Object.keys(STEP_EXPLANATIONS).length).toBeGreaterThanOrEqual(coreStepIds.length)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user