Restore missing document-generator files

Restores index.ts (component exports) and page.tsx (793 lines)
for the SDK document generator module.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-02-09 11:34:35 +01:00
parent f72be6acf9
commit 62a5635246
2 changed files with 802 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
/**
* Document Generator Components
*
* Diese Komponenten integrieren die Einwilligungen-Datenpunkte
* in den Dokumentengenerator.
*/
export { DataPointsPreview } from './DataPointsPreview'
export { DocumentValidation } from './DocumentValidation'

View File

@@ -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<LegalTemplateResult[]> {
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<any> {
const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/status`)
if (!response.ok) return null
return response.json()
}
async function getSources(): Promise<any[]> {
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<string, string> = {
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 (
<span className={`px-2 py-1 text-xs rounded-full ${colors[status] || 'bg-gray-100 text-gray-700'}`}>
{status}
</span>
)
}
function LicenseBadge({ licenseId, small = false }: { licenseId: LicenseType | null; small?: boolean }) {
if (!licenseId) return null
const colors: Record<LicenseType, string> = {
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 (
<span className={`${small ? 'px-1.5 py-0.5 text-[10px]' : 'px-2 py-1 text-xs'} rounded border ${colors[licenseId]}`}>
{LICENSE_TYPE_LABELS[licenseId] || licenseId}
</span>
)
}
function TemplateCard({
template,
selected,
onSelect,
}: {
template: LegalTemplateResult
selected: boolean
onSelect: () => void
}) {
return (
<div
onClick={onSelect}
className={`p-4 rounded-xl border-2 cursor-pointer transition-all ${
selected
? 'border-purple-500 bg-purple-50'
: 'border-gray-200 hover:border-purple-300 bg-white'
}`}
>
<div className="flex items-start justify-between mb-2">
<div className="flex-1">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium text-gray-900">
{template.documentTitle || 'Untitled'}
</span>
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-0.5 rounded">
{template.score.toFixed(2)}
</span>
</div>
<div className="flex items-center gap-2 mt-1 flex-wrap">
{template.templateType && (
<span className="text-xs text-purple-600 bg-purple-100 px-2 py-0.5 rounded">
{TEMPLATE_TYPE_LABELS[template.templateType as TemplateType] || template.templateType}
</span>
)}
<LicenseBadge licenseId={template.licenseId as LicenseType} small />
<span className="text-xs text-gray-500 uppercase">
{template.language}
</span>
</div>
</div>
<input
type="checkbox"
checked={selected}
onChange={onSelect}
className="w-5 h-5 text-purple-600 rounded border-gray-300"
/>
</div>
<p className="text-sm text-gray-600 line-clamp-3 mt-2">
{template.text}
</p>
{template.attributionRequired && template.attributionText && (
<div className="mt-2 text-xs text-orange-600 bg-orange-50 p-2 rounded">
Attribution: {template.attributionText}
</div>
)}
{template.placeholders && template.placeholders.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{template.placeholders.slice(0, 5).map((p, i) => (
<span key={i} className="text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded">
{p}
</span>
))}
{template.placeholders.length > 5 && (
<span className="text-xs text-gray-500">
+{template.placeholders.length - 5} more
</span>
)}
</div>
)}
<div className="mt-3 text-xs text-gray-500">
Source: {template.sourceName}
</div>
</div>
)
}
function PlaceholderEditor({
placeholders,
values,
onChange,
}: {
placeholders: string[]
values: Record<string, string>
onChange: (key: string, value: string) => void
}) {
if (placeholders.length === 0) return null
return (
<div className="bg-blue-50 rounded-xl p-4 border border-blue-200">
<h4 className="font-medium text-blue-900 mb-3">Platzhalter ausfuellen</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{placeholders.map((placeholder) => (
<div key={placeholder}>
<label className="block text-sm text-blue-700 mb-1">{placeholder}</label>
<input
type="text"
value={values[placeholder] || ''}
onChange={(e) => 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"
/>
</div>
))}
</div>
</div>
)
}
function AttributionFooter({ templates }: { templates: LegalTemplateResult[] }) {
const attributionTemplates = templates.filter((t) => t.attributionRequired)
if (attributionTemplates.length === 0) return null
return (
<div className="bg-gray-50 rounded-xl p-4 border border-gray-200">
<h4 className="font-medium text-gray-900 mb-2">Quellenangaben (werden automatisch hinzugefuegt)</h4>
<div className="text-sm text-gray-600 space-y-1">
<p>Dieses Dokument wurde unter Verwendung folgender Quellen erstellt:</p>
<ul className="list-disc list-inside ml-2">
{attributionTemplates.map((t, i) => (
<li key={i}>
{t.attributionText || `${t.sourceName} (${t.licenseName})`}
</li>
))}
</ul>
</div>
</div>
)
}
function DocumentPreview({
content,
placeholders,
}: {
content: string
placeholders: Record<string, string>
}) {
// 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 (
<div className="bg-white rounded-xl border border-gray-200 p-6 prose prose-sm max-w-none">
<div className="whitespace-pre-wrap">{processedContent}</div>
</div>
)
}
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function DocumentGeneratorPage() {
const { state } = useSDK()
const { selectedDataPointsData } = useEinwilligungen()
// Status state
const [status, setStatus] = useState<any>(null)
const [sources, setSources] = useState<any[]>([])
const [isLoading, setIsLoading] = useState(true)
// Search state
const [searchQuery, setSearchQuery] = useState('')
const [selectedType, setSelectedType] = useState<TemplateType | ''>('')
const [selectedLanguage, setSelectedLanguage] = useState<'de' | 'en' | ''>('')
const [selectedJurisdiction, setSelectedJurisdiction] = useState<Jurisdiction | ''>('')
const [searchResults, setSearchResults] = useState<LegalTemplateResult[]>([])
const [isSearching, setIsSearching] = useState(false)
// Selection state
const [selectedTemplates, setSelectedTemplates] = useState<string[]>([])
// Editor state
const [placeholderValues, setPlaceholderValues] = useState<Record<string, string>>({})
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 (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
</div>
)
}
return (
<div className="space-y-6">
{/* Step Header */}
<StepHeader
stepId="document-generator"
title="Dokumentengenerator"
description="Generieren Sie rechtliche Dokumente aus lizenzkonformen Vorlagen"
explanation={stepInfo.explanation}
tips={stepInfo.tips}
>
<button
onClick={() => setActiveTab('compose')}
disabled={selectedTemplates.length === 0}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<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>
Dokument erstellen ({selectedTemplates.length})
</button>
</StepHeader>
{/* Status Overview */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">Collection Status</div>
<div className="flex items-center gap-2 mt-1">
<StatusBadge status={status?.stats?.status || 'unknown'} />
</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">Indexierte Chunks</div>
<div className="text-3xl font-bold text-gray-900">
{status?.stats?.points_count || 0}
</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">Aktive Quellen</div>
<div className="text-3xl font-bold text-purple-600">
{sources.filter((s) => s.enabled).length}
</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">Ausgewaehlt</div>
<div className="text-3xl font-bold text-blue-600">
{selectedTemplates.length}
</div>
</div>
</div>
{/* Tab Navigation */}
<div className="flex gap-2 border-b border-gray-200">
{(['search', 'compose', 'preview'] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
disabled={tab !== 'search' && selectedTemplates.length === 0}
className={`px-4 py-2 font-medium transition-colors ${
activeTab === tab
? 'text-purple-600 border-b-2 border-purple-600'
: 'text-gray-500 hover:text-gray-700 disabled:text-gray-300 disabled:cursor-not-allowed'
}`}
>
{tab === 'search' && 'Vorlagen suchen'}
{tab === 'compose' && 'Zusammenstellen'}
{tab === 'preview' && 'Vorschau'}
</button>
))}
</div>
{/* Search Tab */}
{activeTab === 'search' && (
<div className="space-y-4">
{/* Search Form */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex gap-4 items-end flex-wrap">
<div className="flex-1 min-w-[200px]">
<label className="block text-sm font-medium text-gray-700 mb-1">
Suche
</label>
<input
type="text"
value={searchQuery}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Dokumenttyp
</label>
<select
value={selectedType}
onChange={(e) => setSelectedType(e.target.value as TemplateType | '')}
className="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Alle Typen</option>
{Object.entries(TEMPLATE_TYPE_LABELS).map(([key, label]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Sprache
</label>
<select
value={selectedLanguage}
onChange={(e) => setSelectedLanguage(e.target.value as 'de' | 'en' | '')}
className="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Alle</option>
<option value="de">Deutsch</option>
<option value="en">English</option>
</select>
</div>
<button
onClick={handleSearch}
disabled={isSearching || !searchQuery.trim()}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50"
>
{isSearching ? 'Suche...' : 'Suchen'}
</button>
</div>
</div>
{/* Search Results */}
{searchResults.length > 0 && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-gray-900">
{searchResults.length} Ergebnisse
</h3>
{selectedTemplates.length > 0 && (
<button
onClick={() => setSelectedTemplates([])}
className="text-sm text-gray-500 hover:text-gray-700"
>
Auswahl aufheben
</button>
)}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{searchResults.map((result) => (
<TemplateCard
key={result.id}
template={result}
selected={selectedTemplates.includes(result.id)}
onSelect={() => toggleTemplate(result.id)}
/>
))}
</div>
</div>
)}
{searchResults.length === 0 && searchQuery && !isSearching && (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Keine Vorlagen gefunden</h3>
<p className="mt-2 text-gray-500">
Versuchen Sie einen anderen Suchbegriff oder aendern Sie die Filter.
</p>
</div>
)}
{/* Quick Start Templates */}
{searchResults.length === 0 && !searchQuery && (
<div className="bg-gradient-to-r from-purple-50 to-blue-50 rounded-xl border border-purple-200 p-6">
<h3 className="font-semibold text-gray-900 mb-4">Schnellstart - Haeufig benoetigte Dokumente</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[
{ 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) => (
<button
key={item.type}
onClick={() => {
setSearchQuery(item.query)
setSelectedType(item.type as TemplateType)
setTimeout(handleSearch, 100)
}}
className="p-4 bg-white rounded-lg border border-gray-200 hover:border-purple-300 hover:shadow transition-all text-center"
>
<span className="text-3xl mb-2 block">{item.icon}</span>
<span className="text-sm font-medium text-gray-900">
{TEMPLATE_TYPE_LABELS[item.type as TemplateType]}
</span>
</button>
))}
</div>
</div>
)}
</div>
)}
{/* Compose Tab */}
{activeTab === 'compose' && selectedTemplates.length > 0 && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Content - 2/3 */}
<div className="lg:col-span-2 space-y-6">
{/* Selected Templates */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="font-semibold text-gray-900 mb-4">
Ausgewaehlte Bausteine ({selectedTemplates.length})
</h3>
<div className="space-y-2">
{selectedTemplateObjects.map((t, index) => (
<div
key={t.id}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div className="flex items-center gap-3">
<span className="text-gray-400 font-mono">{index + 1}.</span>
<span className="font-medium">{t.documentTitle}</span>
<LicenseBadge licenseId={t.licenseId as LicenseType} small />
</div>
<button
onClick={() => toggleTemplate(t.id)}
className="text-red-500 hover:text-red-700"
>
<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>
</div>
{/* Placeholder Editor */}
<PlaceholderEditor
placeholders={allPlaceholders}
values={placeholderValues}
onChange={(key, value) =>
setPlaceholderValues((prev) => ({ ...prev, [key]: value }))
}
/>
{/* Attribution Footer */}
<AttributionFooter templates={selectedTemplateObjects} />
{/* Actions */}
<div className="flex justify-end gap-4">
<button
onClick={() => setActiveTab('search')}
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
>
Zurueck zur Suche
</button>
<button
onClick={() => setActiveTab('preview')}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
Vorschau anzeigen
</button>
</div>
</div>
{/* Sidebar - 1/3: Einwilligungen DataPoints */}
<div className="lg:col-span-1">
<DataPointsPreview
dataPoints={selectedDataPointsData || []}
onInsertPlaceholder={handleInsertPlaceholder}
language="de"
/>
</div>
</div>
)}
{/* Preview Tab */}
{activeTab === 'preview' && selectedTemplates.length > 0 && (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-gray-900">Dokument-Vorschau</h3>
<div className="flex gap-2">
<button
onClick={() => setActiveTab('compose')}
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
>
Bearbeiten
</button>
<button
onClick={() => {
// Copy to clipboard
let content = combinedContent
for (const [key, value] of Object.entries(placeholderValues)) {
if (value) {
content = content.replace(new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), value)
}
}
navigator.clipboard.writeText(content)
}}
className="px-4 py-2 text-purple-700 bg-purple-100 rounded-lg hover:bg-purple-200 transition-colors"
>
Kopieren
</button>
<button
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
Als PDF exportieren
</button>
</div>
</div>
{/* Document Validation based on selected Einwilligungen */}
{selectedDataPointsData && selectedDataPointsData.length > 0 && (
<DocumentValidation
dataPoints={selectedDataPointsData}
documentContent={combinedContent}
language="de"
onInsertPlaceholder={handleInsertPlaceholder}
/>
)}
<DocumentPreview
content={combinedContent}
placeholders={placeholderValues}
/>
{/* Attribution */}
<AttributionFooter templates={selectedTemplateObjects} />
</div>
)}
{/* Sources Info */}
{activeTab === 'search' && sources.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="font-semibold text-gray-900 mb-4">Verfuegbare Quellen</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{sources.filter((s) => s.enabled).slice(0, 6).map((source) => (
<div key={source.name} className="p-4 bg-gray-50 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<span className="font-medium text-gray-900">{source.name}</span>
<LicenseBadge licenseId={source.license_type as LicenseType} small />
</div>
<p className="text-sm text-gray-600 line-clamp-2">{source.description}</p>
<div className="mt-2 flex flex-wrap gap-1">
{source.template_types.slice(0, 3).map((t: string) => (
<span key={t} className="text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded">
{TEMPLATE_TYPE_LABELS[t as TemplateType] || t}
</span>
))}
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}