diff --git a/admin-v2/app/(sdk)/sdk/document-generator/components/index.ts b/admin-v2/app/(sdk)/sdk/document-generator/components/index.ts new file mode 100644 index 0000000..20b97f3 --- /dev/null +++ b/admin-v2/app/(sdk)/sdk/document-generator/components/index.ts @@ -0,0 +1,9 @@ +/** + * Document Generator Components + * + * Diese Komponenten integrieren die Einwilligungen-Datenpunkte + * in den Dokumentengenerator. + */ + +export { DataPointsPreview } from './DataPointsPreview' +export { DocumentValidation } from './DocumentValidation' diff --git a/admin-v2/app/(sdk)/sdk/document-generator/page.tsx b/admin-v2/app/(sdk)/sdk/document-generator/page.tsx new file mode 100644 index 0000000..e240fbe --- /dev/null +++ b/admin-v2/app/(sdk)/sdk/document-generator/page.tsx @@ -0,0 +1,793 @@ +'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} + + ))} +
+
+ ))} +
+
+ )} +
+ ) +}