Remove duplicate compliance and DSGVO admin pages that have been superseded by the unified SDK pipeline. Update navigation, sidebar, roles, and module registry to reflect the new structure. Add DSFA corpus API proxy and source-policy components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
584 lines
21 KiB
TypeScript
584 lines
21 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useCallback, useEffect } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
|
|
// =============================================================================
|
|
// TYPES
|
|
// =============================================================================
|
|
|
|
export interface UploadedDocument {
|
|
id: string
|
|
name: string
|
|
type: string
|
|
size: number
|
|
uploadedAt: Date
|
|
extractedVersion?: string
|
|
extractedContent?: ExtractedContent
|
|
status: 'uploading' | 'processing' | 'ready' | 'error'
|
|
error?: string
|
|
}
|
|
|
|
export interface ExtractedContent {
|
|
title?: string
|
|
version?: string
|
|
lastModified?: string
|
|
sections?: ExtractedSection[]
|
|
metadata?: Record<string, string>
|
|
}
|
|
|
|
export interface ExtractedSection {
|
|
title: string
|
|
content: string
|
|
type?: string
|
|
}
|
|
|
|
export interface DocumentUploadSectionProps {
|
|
/** Type of document being uploaded (tom, dsfa, vvt, loeschfristen, etc.) */
|
|
documentType: 'tom' | 'dsfa' | 'vvt' | 'loeschfristen' | 'consent' | 'policy' | 'custom'
|
|
/** Title displayed in the upload section */
|
|
title?: string
|
|
/** Description text */
|
|
description?: string
|
|
/** Accepted file types */
|
|
acceptedTypes?: string
|
|
/** Callback when document is uploaded and processed */
|
|
onDocumentProcessed?: (doc: UploadedDocument) => void
|
|
/** Callback to open document in workflow editor */
|
|
onOpenInEditor?: (doc: UploadedDocument) => void
|
|
/** Session ID for QR upload */
|
|
sessionId?: string
|
|
/** Custom CSS classes */
|
|
className?: string
|
|
}
|
|
|
|
// =============================================================================
|
|
// ICONS
|
|
// =============================================================================
|
|
|
|
const UploadIcon = () => (
|
|
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
|
</svg>
|
|
)
|
|
|
|
const QRIcon = () => (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
|
|
</svg>
|
|
)
|
|
|
|
const DocumentIcon = () => (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
)
|
|
|
|
const CheckIcon = () => (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
)
|
|
|
|
const EditIcon = () => (
|
|
<svg className="w-4 h-4" 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>
|
|
)
|
|
|
|
const CloseIcon = () => (
|
|
<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>
|
|
)
|
|
|
|
// =============================================================================
|
|
// HELPER FUNCTIONS
|
|
// =============================================================================
|
|
|
|
function formatFileSize(bytes: number): string {
|
|
if (bytes === 0) return '0 B'
|
|
const k = 1024
|
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
|
}
|
|
|
|
function detectVersionFromFilename(filename: string): string | undefined {
|
|
// Common version patterns: v1.0, V2.1, _v3, -v1.2.3, version-2
|
|
const patterns = [
|
|
/[vV](\d+(?:\.\d+)*)/,
|
|
/version[_-]?(\d+(?:\.\d+)*)/i,
|
|
/[_-]v?(\d+\.\d+(?:\.\d+)?)[_-]/,
|
|
]
|
|
|
|
for (const pattern of patterns) {
|
|
const match = filename.match(pattern)
|
|
if (match) {
|
|
return match[1]
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function suggestNextVersion(currentVersion?: string): string {
|
|
if (!currentVersion) return '1.0'
|
|
|
|
const parts = currentVersion.split('.').map(Number)
|
|
if (parts.length >= 2) {
|
|
parts[parts.length - 1] += 1
|
|
} else {
|
|
parts.push(1)
|
|
}
|
|
return parts.join('.')
|
|
}
|
|
|
|
// =============================================================================
|
|
// QR CODE MODAL
|
|
// =============================================================================
|
|
|
|
interface QRModalProps {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
sessionId: string
|
|
onFileUploaded?: (file: File) => void
|
|
}
|
|
|
|
function QRCodeModal({ isOpen, onClose, sessionId }: QRModalProps) {
|
|
const [uploadUrl, setUploadUrl] = useState('')
|
|
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) return
|
|
|
|
let baseUrl = typeof window !== 'undefined' ? window.location.origin : ''
|
|
|
|
// Hostname to IP mapping for local network
|
|
const hostnameToIP: Record<string, string> = {
|
|
'macmini': '192.168.178.100',
|
|
'macmini.local': '192.168.178.100',
|
|
}
|
|
|
|
Object.entries(hostnameToIP).forEach(([hostname, ip]) => {
|
|
if (baseUrl.includes(hostname)) {
|
|
baseUrl = baseUrl.replace(hostname, ip)
|
|
}
|
|
})
|
|
|
|
// Force HTTP for mobile access (SSL cert is for hostname, not IP)
|
|
// This is safe because it's only used on the local network
|
|
if (baseUrl.startsWith('https://')) {
|
|
baseUrl = baseUrl.replace('https://', 'http://')
|
|
}
|
|
|
|
const uploadPath = `/upload/sdk/${sessionId}`
|
|
const fullUrl = `${baseUrl}${uploadPath}`
|
|
setUploadUrl(fullUrl)
|
|
|
|
const qrApiUrl = `https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${encodeURIComponent(fullUrl)}`
|
|
setQrCodeUrl(qrApiUrl)
|
|
}, [isOpen, sessionId])
|
|
|
|
const copyToClipboard = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(uploadUrl)
|
|
} catch (err) {
|
|
console.error('Copy failed:', err)
|
|
}
|
|
}
|
|
|
|
if (!isOpen) return null
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} />
|
|
<div className="relative bg-white rounded-2xl shadow-xl max-w-md w-full p-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-xl bg-purple-100 flex items-center justify-center">
|
|
<QRIcon />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-gray-900">Mit Handy hochladen</h3>
|
|
<p className="text-sm text-gray-500">QR-Code scannen</p>
|
|
</div>
|
|
</div>
|
|
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-lg">
|
|
<CloseIcon />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-center">
|
|
<div className="p-4 bg-white border border-gray-200 rounded-xl">
|
|
{qrCodeUrl ? (
|
|
<img src={qrCodeUrl} alt="QR Code" className="w-[200px] h-[200px]" />
|
|
) : (
|
|
<div className="w-[200px] h-[200px] flex items-center justify-center">
|
|
<div className="w-8 h-8 border-2 border-purple-500 border-t-transparent rounded-full animate-spin" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<p className="mt-4 text-center text-sm text-gray-600">
|
|
Scannen Sie den Code mit Ihrem Handy,<br />
|
|
um Dokumente hochzuladen.
|
|
</p>
|
|
|
|
<div className="mt-4 w-full">
|
|
<p className="text-xs text-gray-400 mb-2">Oder Link teilen:</p>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={uploadUrl}
|
|
readOnly
|
|
className="flex-1 px-3 py-2 text-sm border border-gray-200 rounded-lg bg-gray-50"
|
|
/>
|
|
<button
|
|
onClick={copyToClipboard}
|
|
className="px-4 py-2 text-sm bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
|
>
|
|
Kopieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 p-3 bg-amber-50 border border-amber-200 rounded-lg w-full">
|
|
<p className="text-xs text-amber-800">
|
|
<strong>Hinweis:</strong> Ihr Handy muss im gleichen Netzwerk sein.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// MAIN COMPONENT
|
|
// =============================================================================
|
|
|
|
export function DocumentUploadSection({
|
|
documentType,
|
|
title,
|
|
description,
|
|
acceptedTypes = '.pdf,.docx,.doc',
|
|
onDocumentProcessed,
|
|
onOpenInEditor,
|
|
sessionId,
|
|
className = '',
|
|
}: DocumentUploadSectionProps) {
|
|
const router = useRouter()
|
|
const [isDragging, setIsDragging] = useState(false)
|
|
const [uploadedDocs, setUploadedDocs] = useState<UploadedDocument[]>([])
|
|
const [showQRModal, setShowQRModal] = useState(false)
|
|
const [isExpanded, setIsExpanded] = useState(false)
|
|
|
|
const effectiveSessionId = sessionId || `sdk-${documentType}-${Date.now()}`
|
|
|
|
const defaultTitles: Record<string, string> = {
|
|
tom: 'Bestehende TOMs hochladen',
|
|
dsfa: 'Bestehende DSFA hochladen',
|
|
vvt: 'Bestehendes VVT hochladen',
|
|
loeschfristen: 'Bestehende Löschfristen hochladen',
|
|
consent: 'Bestehende Einwilligungsdokumente hochladen',
|
|
policy: 'Bestehende Richtlinie hochladen',
|
|
custom: 'Dokument hochladen',
|
|
}
|
|
|
|
const defaultDescriptions: Record<string, string> = {
|
|
tom: 'Laden Sie Ihre bestehenden technischen und organisatorischen Maßnahmen hoch. Wir erkennen die Version und zeigen Ihnen, was aktualisiert werden sollte.',
|
|
dsfa: 'Laden Sie Ihre bestehende Datenschutz-Folgenabschätzung hoch. Wir analysieren den Inhalt und schlagen Ergänzungen vor.',
|
|
vvt: 'Laden Sie Ihr bestehendes Verarbeitungsverzeichnis hoch. Wir erkennen die Struktur und helfen bei der Aktualisierung.',
|
|
loeschfristen: 'Laden Sie Ihre bestehenden Löschfristen-Dokumente hoch. Wir analysieren die Fristen und prüfen auf Vollständigkeit.',
|
|
consent: 'Laden Sie Ihre bestehenden Einwilligungsdokumente hoch.',
|
|
policy: 'Laden Sie Ihre bestehende Richtlinie hoch.',
|
|
custom: 'Laden Sie ein bestehendes Dokument hoch.',
|
|
}
|
|
|
|
const displayTitle = title || defaultTitles[documentType]
|
|
const displayDescription = description || defaultDescriptions[documentType]
|
|
|
|
// Drag & Drop handlers
|
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault()
|
|
setIsDragging(true)
|
|
}, [])
|
|
|
|
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault()
|
|
setIsDragging(false)
|
|
}, [])
|
|
|
|
const processFile = useCallback(async (file: File) => {
|
|
const docId = `doc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
|
|
const newDoc: UploadedDocument = {
|
|
id: docId,
|
|
name: file.name,
|
|
type: file.type,
|
|
size: file.size,
|
|
uploadedAt: new Date(),
|
|
extractedVersion: detectVersionFromFilename(file.name),
|
|
status: 'uploading',
|
|
}
|
|
|
|
setUploadedDocs(prev => [...prev, newDoc])
|
|
setIsExpanded(true)
|
|
|
|
try {
|
|
// Upload to API
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
formData.append('documentType', documentType)
|
|
formData.append('sessionId', effectiveSessionId)
|
|
|
|
const response = await fetch('/api/sdk/v1/documents/upload', {
|
|
method: 'POST',
|
|
body: formData,
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Upload fehlgeschlagen')
|
|
}
|
|
|
|
const result = await response.json()
|
|
|
|
const updatedDoc: UploadedDocument = {
|
|
...newDoc,
|
|
status: 'ready',
|
|
extractedVersion: result.extractedVersion || newDoc.extractedVersion,
|
|
extractedContent: result.extractedContent,
|
|
}
|
|
|
|
setUploadedDocs(prev => prev.map(d => d.id === docId ? updatedDoc : d))
|
|
|
|
if (onDocumentProcessed) {
|
|
onDocumentProcessed(updatedDoc)
|
|
}
|
|
} catch (error) {
|
|
setUploadedDocs(prev => prev.map(d =>
|
|
d.id === docId
|
|
? { ...d, status: 'error', error: error instanceof Error ? error.message : 'Fehler' }
|
|
: d
|
|
))
|
|
}
|
|
}, [documentType, effectiveSessionId, onDocumentProcessed])
|
|
|
|
const handleDrop = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault()
|
|
setIsDragging(false)
|
|
|
|
const files = Array.from(e.dataTransfer.files).filter(f =>
|
|
f.type === 'application/pdf' ||
|
|
f.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
|
f.type === 'application/msword'
|
|
)
|
|
|
|
files.forEach(processFile)
|
|
}, [processFile])
|
|
|
|
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (!e.target.files) return
|
|
Array.from(e.target.files).forEach(processFile)
|
|
e.target.value = '' // Reset input
|
|
}, [processFile])
|
|
|
|
const removeDocument = useCallback((docId: string) => {
|
|
setUploadedDocs(prev => prev.filter(d => d.id !== docId))
|
|
}, [])
|
|
|
|
const handleOpenInEditor = useCallback((doc: UploadedDocument) => {
|
|
if (onOpenInEditor) {
|
|
onOpenInEditor(doc)
|
|
} else {
|
|
// Default: navigate to workflow editor
|
|
router.push(`/sdk/workflow?documentType=${documentType}&documentId=${doc.id}`)
|
|
}
|
|
}, [documentType, onOpenInEditor, router])
|
|
|
|
return (
|
|
<div className={`bg-white border border-gray-200 rounded-xl overflow-hidden ${className}`}>
|
|
{/* Header */}
|
|
<button
|
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
className="w-full flex items-center justify-between p-4 hover:bg-gray-50 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-lg bg-blue-50 flex items-center justify-center text-blue-600">
|
|
<DocumentIcon />
|
|
</div>
|
|
<div className="text-left">
|
|
<h3 className="font-medium text-gray-900">{displayTitle}</h3>
|
|
<p className="text-sm text-gray-500">
|
|
{uploadedDocs.length > 0
|
|
? `${uploadedDocs.length} Dokument(e) hochgeladen`
|
|
: 'Optional - bestehende Dokumente importieren'
|
|
}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<svg
|
|
className={`w-5 h-5 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
{/* Expanded Content */}
|
|
{isExpanded && (
|
|
<div className="p-4 pt-0 space-y-4">
|
|
<p className="text-sm text-gray-600">{displayDescription}</p>
|
|
|
|
{/* Upload Area */}
|
|
<div
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
className={`relative p-6 rounded-lg border-2 border-dashed transition-colors ${
|
|
isDragging
|
|
? 'border-purple-400 bg-purple-50'
|
|
: 'border-gray-200 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<input
|
|
type="file"
|
|
accept={acceptedTypes}
|
|
multiple
|
|
onChange={handleFileSelect}
|
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
|
/>
|
|
<div className="text-center">
|
|
<div className="text-gray-400 mb-2">
|
|
<UploadIcon />
|
|
</div>
|
|
<p className="text-sm font-medium text-gray-700">
|
|
{isDragging ? 'Dateien hier ablegen' : 'Dateien hierher ziehen'}
|
|
</p>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
oder klicken zum Auswählen (PDF, DOCX)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* QR Upload Button */}
|
|
<button
|
|
onClick={() => setShowQRModal(true)}
|
|
className="w-full flex items-center justify-center gap-2 px-4 py-2 text-sm text-purple-600 bg-purple-50 hover:bg-purple-100 rounded-lg transition-colors"
|
|
>
|
|
<QRIcon />
|
|
<span>Mit Handy hochladen (QR-Code)</span>
|
|
</button>
|
|
|
|
{/* Uploaded Documents */}
|
|
{uploadedDocs.length > 0 && (
|
|
<div className="space-y-2">
|
|
<p className="text-sm font-medium text-gray-700">Hochgeladene Dokumente:</p>
|
|
{uploadedDocs.map(doc => (
|
|
<div
|
|
key={doc.id}
|
|
className={`flex items-center gap-3 p-3 rounded-lg border ${
|
|
doc.status === 'error'
|
|
? 'bg-red-50 border-red-200'
|
|
: doc.status === 'ready'
|
|
? 'bg-green-50 border-green-200'
|
|
: 'bg-gray-50 border-gray-200'
|
|
}`}
|
|
>
|
|
<div className="flex-shrink-0">
|
|
{doc.status === 'uploading' || doc.status === 'processing' ? (
|
|
<div className="w-5 h-5 border-2 border-purple-500 border-t-transparent rounded-full animate-spin" />
|
|
) : doc.status === 'ready' ? (
|
|
<div className="w-5 h-5 rounded-full bg-green-500 text-white flex items-center justify-center">
|
|
<CheckIcon />
|
|
</div>
|
|
) : (
|
|
<div className="w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center text-xs">!</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-gray-900 truncate">{doc.name}</p>
|
|
<div className="flex items-center gap-2 text-xs text-gray-500">
|
|
<span>{formatFileSize(doc.size)}</span>
|
|
{doc.extractedVersion && (
|
|
<>
|
|
<span>•</span>
|
|
<span className="text-purple-600">Version {doc.extractedVersion}</span>
|
|
<span>→</span>
|
|
<span className="text-green-600 font-medium">
|
|
Vorschlag: v{suggestNextVersion(doc.extractedVersion)}
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
{doc.error && (
|
|
<p className="text-xs text-red-600 mt-1">{doc.error}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
{doc.status === 'ready' && (
|
|
<button
|
|
onClick={() => handleOpenInEditor(doc)}
|
|
className="p-2 text-purple-600 hover:bg-purple-100 rounded-lg transition-colors"
|
|
title="Im Editor öffnen"
|
|
>
|
|
<EditIcon />
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => removeDocument(doc.id)}
|
|
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
|
title="Entfernen"
|
|
>
|
|
<CloseIcon />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Extracted Content Preview */}
|
|
{uploadedDocs.some(d => d.status === 'ready' && d.extractedContent) && (
|
|
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
<h4 className="text-sm font-medium text-blue-900 mb-2">Erkannte Inhalte</h4>
|
|
{uploadedDocs
|
|
.filter(d => d.status === 'ready' && d.extractedContent)
|
|
.map(doc => (
|
|
<div key={doc.id} className="text-sm text-blue-800">
|
|
{doc.extractedContent?.title && (
|
|
<p><strong>Titel:</strong> {doc.extractedContent.title}</p>
|
|
)}
|
|
{doc.extractedContent?.sections && (
|
|
<p><strong>Abschnitte:</strong> {doc.extractedContent.sections.length} gefunden</p>
|
|
)}
|
|
</div>
|
|
))
|
|
}
|
|
<button
|
|
onClick={() => uploadedDocs.find(d => d.status === 'ready') && handleOpenInEditor(uploadedDocs.find(d => d.status === 'ready')!)}
|
|
className="mt-3 w-full px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors"
|
|
>
|
|
Im Änderungsmodus öffnen
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* QR Modal */}
|
|
<QRCodeModal
|
|
isOpen={showQRModal}
|
|
onClose={() => setShowQRModal(false)}
|
|
sessionId={effectiveSessionId}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default DocumentUploadSection
|