Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
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
|