'use client' import React, { useState, useEffect, useCallback, useMemo } from 'react' import { useSDK } from '@/lib/sdk' import { useEinwilligungen } from '@/lib/sdk/einwilligungen/context' import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' import { LegalTemplateResult, TemplateType, Jurisdiction, LicenseType, GeneratedDocument, TEMPLATE_TYPE_LABELS, LICENSE_TYPE_LABELS, JURISDICTION_LABELS, DEFAULT_PLACEHOLDERS, } from '@/lib/sdk/types' import { DataPointsPreview } from './components/DataPointsPreview' import { DocumentValidation } from './components/DocumentValidation' import { generateAllPlaceholders } from '@/lib/sdk/document-generator/datapoint-helpers' // ============================================================================= // API CLIENT // ============================================================================= const KLAUSUR_SERVICE_URL = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086' async function searchTemplates(params: { query: string templateType?: TemplateType licenseTypes?: LicenseType[] language?: 'de' | 'en' jurisdiction?: Jurisdiction limit?: number }): Promise { const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/search`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: params.query, template_type: params.templateType, license_types: params.licenseTypes, language: params.language, jurisdiction: params.jurisdiction, limit: params.limit || 10, }), }) if (!response.ok) { throw new Error('Search failed') } const data = await response.json() return data.map((r: any) => ({ id: r.id, score: r.score, text: r.text, documentTitle: r.document_title, templateType: r.template_type, clauseCategory: r.clause_category, language: r.language, jurisdiction: r.jurisdiction, licenseId: r.license_id, licenseName: r.license_name, licenseUrl: r.license_url, attributionRequired: r.attribution_required, attributionText: r.attribution_text, sourceName: r.source_name, sourceUrl: r.source_url, sourceRepo: r.source_repo, placeholders: r.placeholders || [], isCompleteDocument: r.is_complete_document, isModular: r.is_modular, requiresCustomization: r.requires_customization, outputAllowed: r.output_allowed ?? true, modificationAllowed: r.modification_allowed ?? true, distortionProhibited: r.distortion_prohibited ?? false, })) } async function getTemplatesStatus(): Promise { const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/status`) if (!response.ok) return null return response.json() } async function getSources(): Promise { const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/sources`) if (!response.ok) return [] const data = await response.json() return data.sources || [] } // ============================================================================= // COMPONENTS // ============================================================================= function StatusBadge({ status }: { status: string }) { const colors: Record = { ready: 'bg-green-100 text-green-700', empty: 'bg-yellow-100 text-yellow-700', error: 'bg-red-100 text-red-700', running: 'bg-blue-100 text-blue-700', } return ( {status} ) } function LicenseBadge({ licenseId, small = false }: { licenseId: LicenseType | null; small?: boolean }) { if (!licenseId) return null const colors: Record = { public_domain: 'bg-green-100 text-green-700 border-green-200', cc0: 'bg-green-100 text-green-700 border-green-200', unlicense: 'bg-green-100 text-green-700 border-green-200', mit: 'bg-blue-100 text-blue-700 border-blue-200', cc_by_4: 'bg-purple-100 text-purple-700 border-purple-200', reuse_notice: 'bg-orange-100 text-orange-700 border-orange-200', } return ( {LICENSE_TYPE_LABELS[licenseId] || licenseId} ) } function TemplateCard({ template, selected, onSelect, }: { template: LegalTemplateResult selected: boolean onSelect: () => void }) { return (
{template.documentTitle || 'Untitled'} {template.score.toFixed(2)}
{template.templateType && ( {TEMPLATE_TYPE_LABELS[template.templateType as TemplateType] || template.templateType} )} {template.language}

{template.text}

{template.attributionRequired && template.attributionText && (
Attribution: {template.attributionText}
)} {template.placeholders && template.placeholders.length > 0 && (
{template.placeholders.slice(0, 5).map((p, i) => ( {p} ))} {template.placeholders.length > 5 && ( +{template.placeholders.length - 5} more )}
)}
Source: {template.sourceName}
) } function PlaceholderEditor({ placeholders, values, onChange, }: { placeholders: string[] values: Record onChange: (key: string, value: string) => void }) { if (placeholders.length === 0) return null return (

Platzhalter ausfuellen

{placeholders.map((placeholder) => (
onChange(placeholder, e.target.value)} placeholder={`Wert fuer ${placeholder}`} className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500" />
))}
) } function AttributionFooter({ templates }: { templates: LegalTemplateResult[] }) { const attributionTemplates = templates.filter((t) => t.attributionRequired) if (attributionTemplates.length === 0) return null return (

Quellenangaben (werden automatisch hinzugefuegt)

Dieses Dokument wurde unter Verwendung folgender Quellen erstellt:

    {attributionTemplates.map((t, i) => (
  • {t.attributionText || `${t.sourceName} (${t.licenseName})`}
  • ))}
) } function DocumentPreview({ content, placeholders, }: { content: string placeholders: Record }) { // Replace placeholders in content let processedContent = content for (const [key, value] of Object.entries(placeholders)) { if (value) { processedContent = processedContent.replace(new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), value) } } return (
{processedContent}
) } // ============================================================================= // MAIN PAGE // ============================================================================= export default function DocumentGeneratorPage() { const { state } = useSDK() const { selectedDataPointsData } = useEinwilligungen() // Status state const [status, setStatus] = useState(null) const [sources, setSources] = useState([]) const [isLoading, setIsLoading] = useState(true) // Search state const [searchQuery, setSearchQuery] = useState('') const [selectedType, setSelectedType] = useState('') const [selectedLanguage, setSelectedLanguage] = useState<'de' | 'en' | ''>('') const [selectedJurisdiction, setSelectedJurisdiction] = useState('') const [searchResults, setSearchResults] = useState([]) const [isSearching, setIsSearching] = useState(false) // Selection state const [selectedTemplates, setSelectedTemplates] = useState([]) // Editor state const [placeholderValues, setPlaceholderValues] = useState>({}) const [activeTab, setActiveTab] = useState<'search' | 'compose' | 'preview'>('search') // Load initial status useEffect(() => { async function loadStatus() { try { const [statusData, sourcesData] = await Promise.all([ getTemplatesStatus(), getSources(), ]) setStatus(statusData) setSources(sourcesData) } catch (error) { console.error('Failed to load status:', error) } finally { setIsLoading(false) } } loadStatus() }, []) // Pre-fill placeholders from company profile useEffect(() => { if (state?.companyProfile) { const profile = state.companyProfile setPlaceholderValues((prev) => ({ ...prev, '[COMPANY_NAME]': profile.companyName || '', '[FIRMENNAME]': profile.companyName || '', '[EMAIL]': profile.dpoEmail || '', '[DSB_EMAIL]': profile.dpoEmail || '', '[DPO_NAME]': profile.dpoName || '', '[DSB_NAME]': profile.dpoName || '', })) } }, [state?.companyProfile]) // Pre-fill placeholders from Einwilligungen data points useEffect(() => { if (selectedDataPointsData && selectedDataPointsData.length > 0) { const einwilligungenPlaceholders = generateAllPlaceholders(selectedDataPointsData, 'de') setPlaceholderValues((prev) => ({ ...prev, ...einwilligungenPlaceholders, })) } }, [selectedDataPointsData]) // Handler for inserting placeholders from DataPointsPreview const handleInsertPlaceholder = useCallback((placeholder: string) => { // This is a simplified version - in a real editor you would insert at cursor position // For now, we just ensure the placeholder is in the values so it can be replaced if (!placeholderValues[placeholder]) { // The placeholder value will be generated from einwilligungen data const einwilligungenPlaceholders = generateAllPlaceholders(selectedDataPointsData || [], 'de') if (einwilligungenPlaceholders[placeholder as keyof typeof einwilligungenPlaceholders]) { setPlaceholderValues((prev) => ({ ...prev, [placeholder]: einwilligungenPlaceholders[placeholder as keyof typeof einwilligungenPlaceholders], })) } } }, [placeholderValues, selectedDataPointsData]) // Search handler const handleSearch = useCallback(async () => { if (!searchQuery.trim()) return setIsSearching(true) try { const results = await searchTemplates({ query: searchQuery, templateType: selectedType || undefined, language: selectedLanguage || undefined, jurisdiction: selectedJurisdiction || undefined, limit: 20, }) setSearchResults(results) } catch (error) { console.error('Search failed:', error) } finally { setIsSearching(false) } }, [searchQuery, selectedType, selectedLanguage, selectedJurisdiction]) // Toggle template selection const toggleTemplate = (id: string) => { setSelectedTemplates((prev) => prev.includes(id) ? prev.filter((t) => t !== id) : [...prev, id] ) } // Get selected template objects const selectedTemplateObjects = searchResults.filter((r) => selectedTemplates.includes(r.id) ) // Get all unique placeholders from selected templates const allPlaceholders = Array.from( new Set(selectedTemplateObjects.flatMap((t) => t.placeholders || [])) ) // Combined content from selected templates const combinedContent = selectedTemplateObjects .map((t) => `## ${t.documentTitle || 'Abschnitt'}\n\n${t.text}`) .join('\n\n---\n\n') // Step info - using 'consent' as base since document-generator doesn't exist yet const stepInfo = STEP_EXPLANATIONS['consent'] || { title: 'Dokumentengenerator', description: 'Generieren Sie rechtliche Dokumente aus lizenzkonformen Vorlagen', explanation: 'Der Dokumentengenerator nutzt frei lizenzierte Textbausteine um Datenschutzerklaerungen, AGB und andere rechtliche Dokumente zu erstellen.', tips: ['Waehlen Sie passende Vorlagen aus der Suche', 'Fuellen Sie die Platzhalter mit Ihren Unternehmensdaten'], } if (isLoading) { return (
) } return (
{/* Step Header */} {/* Status Overview */}
Collection Status
Indexierte Chunks
{status?.stats?.points_count || 0}
Aktive Quellen
{sources.filter((s) => s.enabled).length}
Ausgewaehlt
{selectedTemplates.length}
{/* Tab Navigation */}
{(['search', 'compose', 'preview'] as const).map((tab) => ( ))}
{/* Search Tab */} {activeTab === 'search' && (
{/* Search Form */}
setSearchQuery(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} placeholder="z.B. Datenschutzerklaerung, Cookie-Banner, Widerruf..." className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500" />
{/* Search Results */} {searchResults.length > 0 && (

{searchResults.length} Ergebnisse

{selectedTemplates.length > 0 && ( )}
{searchResults.map((result) => ( toggleTemplate(result.id)} /> ))}
)} {searchResults.length === 0 && searchQuery && !isSearching && (

Keine Vorlagen gefunden

Versuchen Sie einen anderen Suchbegriff oder aendern Sie die Filter.

)} {/* Quick Start Templates */} {searchResults.length === 0 && !searchQuery && (

Schnellstart - Haeufig benoetigte Dokumente

{[ { query: 'Datenschutzerklaerung DSGVO', type: 'privacy_policy', icon: '🔒' }, { query: 'Cookie Banner', type: 'cookie_banner', icon: '🍪' }, { query: 'Impressum', type: 'impressum', icon: '📋' }, { query: 'AGB Nutzungsbedingungen', type: 'terms_of_service', icon: '📜' }, ].map((item) => ( ))}
)}
)} {/* Compose Tab */} {activeTab === 'compose' && selectedTemplates.length > 0 && (
{/* Main Content - 2/3 */}
{/* Selected Templates */}

Ausgewaehlte Bausteine ({selectedTemplates.length})

{selectedTemplateObjects.map((t, index) => (
{index + 1}. {t.documentTitle}
))}
{/* Placeholder Editor */} setPlaceholderValues((prev) => ({ ...prev, [key]: value })) } /> {/* Attribution Footer */} {/* Actions */}
{/* Sidebar - 1/3: Einwilligungen DataPoints */}
)} {/* Preview Tab */} {activeTab === 'preview' && selectedTemplates.length > 0 && (

Dokument-Vorschau

{/* Document Validation based on selected Einwilligungen */} {selectedDataPointsData && selectedDataPointsData.length > 0 && ( )} {/* Attribution */}
)} {/* Sources Info */} {activeTab === 'search' && sources.length > 0 && (

Verfuegbare Quellen

{sources.filter((s) => s.enabled).slice(0, 6).map((source) => (
{source.name}

{source.description}

{source.template_types.slice(0, 3).map((t: string) => ( {TEMPLATE_TYPE_LABELS[t as TemplateType] || t} ))}
))}
)}
) }