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>
344 lines
10 KiB
TypeScript
344 lines
10 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* useDraftingEngine - React Hook fuer die Drafting Engine
|
|
*
|
|
* Managed: currentMode, activeDocumentType, draftSessions, validationState
|
|
* Handled: State-Projection, API-Calls, Streaming
|
|
* Provides: sendMessage(), requestDraft(), validateDraft(), acceptDraft()
|
|
*/
|
|
|
|
import { useState, useCallback, useRef } from 'react'
|
|
import { useSDK } from '../context'
|
|
import { stateProjector } from './state-projector'
|
|
import { intentClassifier } from './intent-classifier'
|
|
import { constraintEnforcer } from './constraint-enforcer'
|
|
import type {
|
|
AgentMode,
|
|
DraftSession,
|
|
DraftRevision,
|
|
DraftingChatMessage,
|
|
ValidationResult,
|
|
ConstraintCheckResult,
|
|
DraftContext,
|
|
GapContext,
|
|
ValidationContext,
|
|
} from './types'
|
|
import type { ScopeDocumentType } from '../compliance-scope-types'
|
|
|
|
export interface DraftingEngineState {
|
|
currentMode: AgentMode
|
|
activeDocumentType: ScopeDocumentType | null
|
|
messages: DraftingChatMessage[]
|
|
isTyping: boolean
|
|
currentDraft: DraftRevision | null
|
|
validationResult: ValidationResult | null
|
|
constraintCheck: ConstraintCheckResult | null
|
|
error: string | null
|
|
}
|
|
|
|
export interface DraftingEngineActions {
|
|
setMode: (mode: AgentMode) => void
|
|
setDocumentType: (type: ScopeDocumentType) => void
|
|
sendMessage: (content: string) => Promise<void>
|
|
requestDraft: (instructions?: string) => Promise<void>
|
|
validateDraft: () => Promise<void>
|
|
acceptDraft: () => void
|
|
stopGeneration: () => void
|
|
clearMessages: () => void
|
|
}
|
|
|
|
export function useDraftingEngine(): DraftingEngineState & DraftingEngineActions {
|
|
const { state, dispatch } = useSDK()
|
|
const abortControllerRef = useRef<AbortController | null>(null)
|
|
|
|
const [currentMode, setCurrentMode] = useState<AgentMode>('explain')
|
|
const [activeDocumentType, setActiveDocumentType] = useState<ScopeDocumentType | null>(null)
|
|
const [messages, setMessages] = useState<DraftingChatMessage[]>([])
|
|
const [isTyping, setIsTyping] = useState(false)
|
|
const [currentDraft, setCurrentDraft] = useState<DraftRevision | null>(null)
|
|
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null)
|
|
const [constraintCheck, setConstraintCheck] = useState<ConstraintCheckResult | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
// Get state projection based on mode
|
|
const getProjection = useCallback(() => {
|
|
switch (currentMode) {
|
|
case 'draft':
|
|
return activeDocumentType
|
|
? stateProjector.projectForDraft(state, activeDocumentType)
|
|
: null
|
|
case 'ask':
|
|
return stateProjector.projectForAsk(state)
|
|
case 'validate':
|
|
return activeDocumentType
|
|
? stateProjector.projectForValidate(state, [activeDocumentType])
|
|
: stateProjector.projectForValidate(state, ['vvt', 'tom', 'lf'])
|
|
default:
|
|
return activeDocumentType
|
|
? stateProjector.projectForDraft(state, activeDocumentType)
|
|
: null
|
|
}
|
|
}, [state, currentMode, activeDocumentType])
|
|
|
|
const setMode = useCallback((mode: AgentMode) => {
|
|
setCurrentMode(mode)
|
|
}, [])
|
|
|
|
const setDocumentType = useCallback((type: ScopeDocumentType) => {
|
|
setActiveDocumentType(type)
|
|
}, [])
|
|
|
|
const sendMessage = useCallback(async (content: string) => {
|
|
if (!content.trim() || isTyping) return
|
|
setError(null)
|
|
|
|
// Auto-detect mode if needed
|
|
const classification = intentClassifier.classify(content)
|
|
if (classification.confidence > 0.7 && classification.mode !== currentMode) {
|
|
setCurrentMode(classification.mode)
|
|
}
|
|
if (classification.detectedDocumentType && !activeDocumentType) {
|
|
setActiveDocumentType(classification.detectedDocumentType)
|
|
}
|
|
|
|
const userMessage: DraftingChatMessage = {
|
|
role: 'user',
|
|
content: content.trim(),
|
|
}
|
|
setMessages(prev => [...prev, userMessage])
|
|
setIsTyping(true)
|
|
|
|
abortControllerRef.current = new AbortController()
|
|
|
|
try {
|
|
const projection = getProjection()
|
|
const response = await fetch('/api/sdk/drafting-engine/chat', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
message: content.trim(),
|
|
history: messages.map(m => ({ role: m.role, content: m.content })),
|
|
sdkStateProjection: projection,
|
|
mode: currentMode,
|
|
documentType: activeDocumentType,
|
|
}),
|
|
signal: abortControllerRef.current.signal,
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' }))
|
|
throw new Error(errorData.error || `Server-Fehler (${response.status})`)
|
|
}
|
|
|
|
const agentMessageId = `msg-${Date.now()}-agent`
|
|
setMessages(prev => [...prev, {
|
|
role: 'assistant',
|
|
content: '',
|
|
metadata: { mode: currentMode, documentType: activeDocumentType ?? undefined },
|
|
}])
|
|
|
|
// Stream response
|
|
const reader = response.body!.getReader()
|
|
const decoder = new TextDecoder()
|
|
let accumulated = ''
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
accumulated += decoder.decode(value, { stream: true })
|
|
const text = accumulated
|
|
setMessages(prev =>
|
|
prev.map((m, i) => i === prev.length - 1 ? { ...m, content: text } : m)
|
|
)
|
|
}
|
|
|
|
setIsTyping(false)
|
|
} catch (err) {
|
|
if ((err as Error).name === 'AbortError') {
|
|
setIsTyping(false)
|
|
return
|
|
}
|
|
setError((err as Error).message)
|
|
setMessages(prev => [...prev, {
|
|
role: 'assistant',
|
|
content: `Fehler: ${(err as Error).message}`,
|
|
}])
|
|
setIsTyping(false)
|
|
}
|
|
}, [isTyping, messages, currentMode, activeDocumentType, getProjection])
|
|
|
|
const requestDraft = useCallback(async (instructions?: string) => {
|
|
if (!activeDocumentType) {
|
|
setError('Bitte waehlen Sie zuerst einen Dokumenttyp.')
|
|
return
|
|
}
|
|
setError(null)
|
|
setIsTyping(true)
|
|
|
|
try {
|
|
const draftContext = stateProjector.projectForDraft(state, activeDocumentType)
|
|
|
|
const response = await fetch('/api/sdk/drafting-engine/draft', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
documentType: activeDocumentType,
|
|
draftContext,
|
|
instructions,
|
|
existingDraft: currentDraft,
|
|
}),
|
|
})
|
|
|
|
const result = await response.json()
|
|
|
|
if (!response.ok) {
|
|
throw new Error(result.error || 'Draft-Generierung fehlgeschlagen')
|
|
}
|
|
|
|
setCurrentDraft(result.draft)
|
|
setConstraintCheck(result.constraintCheck)
|
|
|
|
setMessages(prev => [...prev, {
|
|
role: 'assistant',
|
|
content: `Draft fuer ${activeDocumentType} erstellt (${result.draft.sections.length} Sections). Oeffnen Sie den Editor zur Bearbeitung.`,
|
|
metadata: { mode: 'draft', documentType: activeDocumentType, hasDraft: true },
|
|
}])
|
|
|
|
setIsTyping(false)
|
|
} catch (err) {
|
|
setError((err as Error).message)
|
|
setIsTyping(false)
|
|
}
|
|
}, [activeDocumentType, state, currentDraft])
|
|
|
|
const validateDraft = useCallback(async () => {
|
|
setError(null)
|
|
setIsTyping(true)
|
|
|
|
try {
|
|
const docTypes: ScopeDocumentType[] = activeDocumentType
|
|
? [activeDocumentType]
|
|
: ['vvt', 'tom', 'lf']
|
|
const validationContext = stateProjector.projectForValidate(state, docTypes)
|
|
|
|
const response = await fetch('/api/sdk/drafting-engine/validate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
documentType: activeDocumentType || 'vvt',
|
|
draftContent: currentDraft?.content || '',
|
|
validationContext,
|
|
}),
|
|
})
|
|
|
|
const result = await response.json()
|
|
|
|
if (!response.ok) {
|
|
throw new Error(result.error || 'Validierung fehlgeschlagen')
|
|
}
|
|
|
|
setValidationResult(result)
|
|
|
|
const summary = result.passed
|
|
? `Validierung bestanden. ${result.warnings.length} Warnungen, ${result.suggestions.length} Vorschlaege.`
|
|
: `Validierung fehlgeschlagen. ${result.errors.length} Fehler, ${result.warnings.length} Warnungen.`
|
|
|
|
setMessages(prev => [...prev, {
|
|
role: 'assistant',
|
|
content: summary,
|
|
metadata: { mode: 'validate', hasValidation: true },
|
|
}])
|
|
|
|
setIsTyping(false)
|
|
} catch (err) {
|
|
setError((err as Error).message)
|
|
setIsTyping(false)
|
|
}
|
|
}, [activeDocumentType, state, currentDraft])
|
|
|
|
const acceptDraft = useCallback(() => {
|
|
if (!currentDraft || !activeDocumentType) return
|
|
|
|
// Dispatch the draft data into SDK state
|
|
switch (activeDocumentType) {
|
|
case 'vvt':
|
|
dispatch({
|
|
type: 'ADD_PROCESSING_ACTIVITY',
|
|
payload: {
|
|
id: `draft-vvt-${Date.now()}`,
|
|
name: currentDraft.sections.find(s => s.schemaField === 'name')?.content || 'Neuer VVT-Eintrag',
|
|
...Object.fromEntries(
|
|
currentDraft.sections
|
|
.filter(s => s.schemaField)
|
|
.map(s => [s.schemaField!, s.content])
|
|
),
|
|
},
|
|
})
|
|
break
|
|
case 'tom':
|
|
dispatch({
|
|
type: 'ADD_TOM',
|
|
payload: {
|
|
id: `draft-tom-${Date.now()}`,
|
|
name: 'TOM-Entwurf',
|
|
...Object.fromEntries(
|
|
currentDraft.sections
|
|
.filter(s => s.schemaField)
|
|
.map(s => [s.schemaField!, s.content])
|
|
),
|
|
},
|
|
})
|
|
break
|
|
default:
|
|
dispatch({
|
|
type: 'ADD_DOCUMENT',
|
|
payload: {
|
|
id: `draft-${activeDocumentType}-${Date.now()}`,
|
|
type: activeDocumentType,
|
|
content: currentDraft.content,
|
|
sections: currentDraft.sections,
|
|
},
|
|
})
|
|
}
|
|
|
|
setMessages(prev => [...prev, {
|
|
role: 'assistant',
|
|
content: `Draft wurde in den SDK-State uebernommen.`,
|
|
}])
|
|
setCurrentDraft(null)
|
|
}, [currentDraft, activeDocumentType, dispatch])
|
|
|
|
const stopGeneration = useCallback(() => {
|
|
abortControllerRef.current?.abort()
|
|
setIsTyping(false)
|
|
}, [])
|
|
|
|
const clearMessages = useCallback(() => {
|
|
setMessages([])
|
|
setCurrentDraft(null)
|
|
setValidationResult(null)
|
|
setConstraintCheck(null)
|
|
setError(null)
|
|
}, [])
|
|
|
|
return {
|
|
currentMode,
|
|
activeDocumentType,
|
|
messages,
|
|
isTyping,
|
|
currentDraft,
|
|
validationResult,
|
|
constraintCheck,
|
|
error,
|
|
setMode,
|
|
setDocumentType,
|
|
sendMessage,
|
|
requestDraft,
|
|
validateDraft,
|
|
acceptDraft,
|
|
stopGeneration,
|
|
clearMessages,
|
|
}
|
|
}
|