/** * BYOEH Upload Wizard Component * * 5-step wizard for uploading Erwartungshorizonte with client-side encryption: * 1. File Selection - Choose PDF file * 2. Metadata - Title, Subject, Niveau, Year * 3. Rights Confirmation - Legal acknowledgment (required) * 4. Encryption - Set passphrase (2x confirmation) * 5. Summary & Upload - Review and confirm */ import { useState, useEffect, useCallback } from 'react' import { encryptFile, generateSalt, isEncryptionSupported } from '../services/encryption' import { ehApi } from '../services/api' interface EHMetadata { title: string subject: string niveau: 'eA' | 'gA' year: number aufgaben_nummer?: string } interface EHUploadWizardProps { onClose: () => void onComplete?: (ehId: string) => void onSuccess?: (ehId: string) => void // Legacy alias for onComplete klausurSubject?: string klausurYear?: number defaultSubject?: string // Alias for klausurSubject defaultYear?: number // Alias for klausurYear klausurId?: string // If provided, automatically link EH to this Klausur } type WizardStep = 'file' | 'metadata' | 'rights' | 'encryption' | 'summary' const WIZARD_STEPS: WizardStep[] = ['file', 'metadata', 'rights', 'encryption', 'summary'] const STEP_LABELS: Record = { file: 'Datei', metadata: 'Metadaten', rights: 'Rechte', encryption: 'Verschluesselung', summary: 'Zusammenfassung' } const SUBJECTS = [ 'deutsch', 'englisch', 'mathematik', 'physik', 'chemie', 'biologie', 'geschichte', 'politik', 'erdkunde', 'kunst', 'musik', 'sport', 'informatik', 'latein', 'franzoesisch', 'spanisch' ] const RIGHTS_TEXT = `Ich bestaetige hiermit, dass: 1. Ich das Urheberrecht oder die notwendigen Nutzungsrechte an diesem Erwartungshorizont besitze. 2. Breakpilot diesen Erwartungshorizont NICHT fuer KI-Training verwendet, sondern ausschliesslich fuer RAG-gestuetzte Korrekturvorschlaege in meinem persoenlichen Arbeitsbereich. 3. Der Inhalt verschluesselt gespeichert wird und Breakpilot-Mitarbeiter keinen Zugriff auf den Klartext haben. 4. Ich diesen Erwartungshorizont jederzeit loeschen kann.` function EHUploadWizard({ onClose, onComplete, onSuccess, klausurSubject, klausurYear, defaultSubject, defaultYear, klausurId }: EHUploadWizardProps) { // Resolve aliases const effectiveSubject = klausurSubject || defaultSubject || 'deutsch' const effectiveYear = klausurYear || defaultYear || new Date().getFullYear() const handleComplete = onComplete || onSuccess || (() => {}) // Step state const [currentStep, setCurrentStep] = useState('file') // File step const [selectedFile, setSelectedFile] = useState(null) const [fileError, setFileError] = useState(null) // Metadata step const [metadata, setMetadata] = useState({ title: '', subject: effectiveSubject, niveau: 'eA', year: effectiveYear, aufgaben_nummer: '' }) // Rights step const [rightsConfirmed, setRightsConfirmed] = useState(false) // Encryption step const [passphrase, setPassphrase] = useState('') const [passphraseConfirm, setPassphraseConfirm] = useState('') const [showPassphrase, setShowPassphrase] = useState(false) const [passphraseStrength, setPassphraseStrength] = useState<'weak' | 'medium' | 'strong'>('weak') // Upload state const [uploading, setUploading] = useState(false) const [uploadProgress, setUploadProgress] = useState(0) const [uploadError, setUploadError] = useState(null) // Check encryption support const encryptionSupported = isEncryptionSupported() // Calculate passphrase strength useEffect(() => { if (passphrase.length < 8) { setPassphraseStrength('weak') } else if (passphrase.length < 12 || !/\d/.test(passphrase) || !/[A-Z]/.test(passphrase)) { setPassphraseStrength('medium') } else { setPassphraseStrength('strong') } }, [passphrase]) // Step navigation const currentStepIndex = WIZARD_STEPS.indexOf(currentStep) const isFirstStep = currentStepIndex === 0 const isLastStep = currentStepIndex === WIZARD_STEPS.length - 1 const goNext = useCallback(() => { if (!isLastStep) { setCurrentStep(WIZARD_STEPS[currentStepIndex + 1]) } }, [currentStepIndex, isLastStep]) const goBack = useCallback(() => { if (!isFirstStep) { setCurrentStep(WIZARD_STEPS[currentStepIndex - 1]) } }, [currentStepIndex, isFirstStep]) // Validation const isStepValid = useCallback((step: WizardStep): boolean => { switch (step) { case 'file': return selectedFile !== null && fileError === null case 'metadata': return metadata.title.trim().length > 0 && metadata.subject.length > 0 case 'rights': return rightsConfirmed case 'encryption': return passphrase.length >= 8 && passphrase === passphraseConfirm case 'summary': return true default: return false } }, [selectedFile, fileError, metadata, rightsConfirmed, passphrase, passphraseConfirm]) // File handling const handleFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file) { if (file.type !== 'application/pdf') { setFileError('Nur PDF-Dateien sind erlaubt') setSelectedFile(null) return } if (file.size > 50 * 1024 * 1024) { // 50MB limit setFileError('Datei ist zu gross (max. 50MB)') setSelectedFile(null) return } setFileError(null) setSelectedFile(file) // Auto-fill title from filename if (!metadata.title) { const name = file.name.replace(/\.pdf$/i, '').replace(/[_-]/g, ' ') setMetadata(prev => ({ ...prev, title: name })) } } } // Upload handler const handleUpload = async () => { if (!selectedFile || !encryptionSupported) return setUploading(true) setUploadProgress(10) setUploadError(null) try { // Step 1: Generate salt (used via encrypted.salt below) generateSalt() setUploadProgress(20) // Step 2: Encrypt file client-side const encrypted = await encryptFile(selectedFile, passphrase) setUploadProgress(50) // Step 3: Create form data const formData = new FormData() const encryptedBlob = new Blob([encrypted.encryptedData], { type: 'application/octet-stream' }) formData.append('file', encryptedBlob, 'encrypted.bin') const metadataJson = JSON.stringify({ metadata: { title: metadata.title, subject: metadata.subject, niveau: metadata.niveau, year: metadata.year, aufgaben_nummer: metadata.aufgaben_nummer || null }, encryption_key_hash: encrypted.keyHash, salt: encrypted.salt, rights_confirmed: true, original_filename: selectedFile.name }) formData.append('metadata_json', metadataJson) setUploadProgress(70) // Step 4: Upload to server const response = await ehApi.uploadEH(formData) setUploadProgress(90) // Step 5: Link to Klausur if klausurId provided if (klausurId && response.id) { try { await ehApi.linkToKlausur(response.id, klausurId) } catch (linkError) { console.warn('Failed to auto-link EH to Klausur:', linkError) // Don't fail the whole upload if linking fails } } setUploadProgress(100) // Success! handleComplete(response.id) } catch (error) { console.error('Upload failed:', error) setUploadError(error instanceof Error ? error.message : 'Upload fehlgeschlagen') } finally { setUploading(false) } } // Render step content const renderStepContent = () => { switch (currentStep) { case 'file': return (

Erwartungshorizont hochladen

Waehlen Sie die PDF-Datei Ihres Erwartungshorizonts aus. Die Datei wird verschluesselt und kann nur von Ihnen entschluesselt werden.

{fileError &&

{fileError}

} {!encryptionSupported && (

Ihr Browser unterstuetzt keine Verschluesselung. Bitte verwenden Sie einen modernen Browser (Chrome, Firefox, Safari, Edge).

)}
) case 'metadata': return (

Metadaten

Geben Sie Informationen zum Erwartungshorizont ein.

setMetadata(prev => ({ ...prev, title: e.target.value }))} placeholder="z.B. Deutsch Abitur 2024 Aufgabe 1" />
setMetadata(prev => ({ ...prev, year: parseInt(e.target.value) }))} />
setMetadata(prev => ({ ...prev, aufgaben_nummer: e.target.value }))} placeholder="z.B. 1a, 2.1" />
) case 'rights': return (

Rechte-Bestaetigung

Bitte lesen und bestaetigen Sie die folgenden Bedingungen.

{RIGHTS_TEXT}
setRightsConfirmed(e.target.checked)} />
Wichtig: Ihr Erwartungshorizont wird niemals fuer KI-Training verwendet. Er dient ausschliesslich als Referenz fuer Ihre persoenlichen Korrekturvorschlaege.
) case 'encryption': return (

Verschluesselung

Waehlen Sie ein sicheres Passwort fuer Ihren Erwartungshorizont. Dieses Passwort wird niemals an den Server gesendet.

setPassphrase(e.target.value)} placeholder="Mindestens 8 Zeichen" />
Staerke: {passphraseStrength === 'weak' ? 'Schwach' : passphraseStrength === 'medium' ? 'Mittel' : 'Stark'}
setPassphraseConfirm(e.target.value)} placeholder="Passwort wiederholen" /> {passphraseConfirm && passphrase !== passphraseConfirm && (

Passwoerter stimmen nicht ueberein

)}
Achtung: Merken Sie sich dieses Passwort gut! Ohne das Passwort kann der Erwartungshorizont nicht fuer Korrekturvorschlaege verwendet werden. Breakpilot kann Ihr Passwort nicht wiederherstellen.
) case 'summary': return (

Zusammenfassung

Pruefen Sie Ihre Eingaben und starten Sie den Upload.

Datei: {selectedFile?.name}
Titel: {metadata.title}
Fach: {metadata.subject.charAt(0).toUpperCase() + metadata.subject.slice(1)}
Niveau: {metadata.niveau}
Jahr: {metadata.year}
Verschluesselung: AES-256-GCM
Rechte bestaetigt: Ja
{uploading && (
{uploadProgress}%
)} {uploadError && (

{uploadError}

)}
) default: return null } } return (
{/* Header */}

Erwartungshorizont hochladen

{/* Progress */}
{WIZARD_STEPS.map((step, index) => (
{index < currentStepIndex ? '\u2713' : index + 1}
{STEP_LABELS[step]}
))}
{/* Content */}
{renderStepContent()}
{/* Footer */}
{isLastStep ? ( ) : ( )}
) } export default EHUploadWizard