""" BreakPilot Studio - Klausurkorrektur Modul Funktionen: - Klausuren hochladen (PDF/Bilder) - OCR-Verarbeitung - Automatische Bewertung mit AI - Pipeline-Visualisierung - Review-Interface - Export (PDF/CSV/Excel) """ class CorrectionModule: """Modul fuer Klausurkorrektur mit OCR und AI-Bewertung.""" @staticmethod def get_css() -> str: """CSS fuer das Correction-Modul.""" return """ /* ============================================= CORRECTION MODULE - Klausurkorrektur ============================================= */ /* Panel Layout */ .panel-correction { display: none; flex-direction: column; height: 100%; background: var(--bg-primary); } .panel-correction.active { display: flex; } /* Header */ .correction-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; border-bottom: 1px solid var(--border-color); background: var(--bg-secondary); } .correction-title { font-size: 18px; font-weight: 600; color: var(--text-primary); } .correction-actions { display: flex; gap: 12px; } /* Content Layout */ .correction-content { display: flex; flex: 1; overflow: hidden; } /* Linke Spalte - Setup */ .correction-setup-panel { width: 320px; border-right: 1px solid var(--border-color); display: flex; flex-direction: column; background: var(--bg-secondary); overflow-y: auto; } .correction-section { padding: 20px; border-bottom: 1px solid var(--border-color); } .correction-section h3 { font-size: 14px; font-weight: 600; color: var(--text-secondary); margin-bottom: 16px; } /* Upload Area */ .correction-upload-area { border: 2px dashed var(--border-color); border-radius: 12px; padding: 32px 24px; text-align: center; cursor: pointer; transition: all 0.2s; background: var(--bg-primary); } .correction-upload-area:hover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); } .correction-upload-area.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.1); } .correction-upload-area .upload-icon { font-size: 48px; margin-bottom: 16px; } .correction-upload-area h3 { margin-bottom: 8px; color: var(--text-primary); } .correction-upload-area p { color: var(--text-secondary); font-size: 13px; } .correction-upload-area .file-types { margin-top: 12px; font-size: 11px; color: var(--text-tertiary); } /* Form Fields */ .correction-form { display: flex; flex-direction: column; gap: 12px; } .correction-form label { font-size: 12px; color: var(--text-secondary); margin-bottom: 4px; } .correction-form input, .correction-form select { padding: 10px 12px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--bg-primary); color: var(--text-primary); font-size: 13px; } .correction-form input:focus, .correction-form select:focus { outline: none; border-color: var(--accent-primary); } .correction-form .form-group { display: flex; flex-direction: column; } .correction-form .form-row { display: flex; gap: 12px; } .correction-form .form-row .form-group { flex: 1; } /* Start Button */ .correction-start-btn { padding: 14px 24px; background: var(--accent-primary); color: white; border: none; border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; margin-top: 8px; } .correction-start-btn:hover:not(:disabled) { background: var(--accent-hover); transform: translateY(-1px); } .correction-start-btn:disabled { opacity: 0.5; cursor: not-allowed; } /* Mittlere Spalte - Pipeline */ .correction-pipeline-panel { width: 200px; border-right: 1px solid var(--border-color); padding: 24px 16px; background: var(--bg-primary); } .pipeline-title { font-size: 12px; font-weight: 600; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 20px; } .pipeline-steps { display: flex; flex-direction: column; gap: 8px; } .pipeline-step { display: flex; align-items: center; gap: 12px; padding: 12px; border-radius: 8px; background: var(--bg-secondary); transition: all 0.3s; } .pipeline-step-icon { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: var(--bg-tertiary); color: var(--text-tertiary); font-size: 14px; transition: all 0.3s; } .pipeline-step-content { flex: 1; } .pipeline-step-label { font-size: 13px; font-weight: 500; color: var(--text-primary); } .pipeline-step-status { font-size: 11px; color: var(--text-tertiary); } .pipeline-step.active { background: rgba(59, 130, 246, 0.1); } .pipeline-step.active .pipeline-step-icon { background: var(--accent-primary); color: white; animation: pulse 1.5s infinite; } .pipeline-step.completed .pipeline-step-icon { background: #10b981; color: white; } .pipeline-step.error .pipeline-step-icon { background: #ef4444; color: white; } @keyframes pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.05); opacity: 0.8; } } /* Rechte Spalte - Results/Review */ .correction-results-panel { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .results-header { padding: 16px 24px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; background: var(--bg-secondary); } .results-title { font-size: 16px; font-weight: 600; } .results-stats { display: flex; gap: 24px; } .stat-item { text-align: center; } .stat-value { font-size: 24px; font-weight: 700; color: var(--accent-primary); } .stat-label { font-size: 11px; color: var(--text-tertiary); text-transform: uppercase; } /* Results Content */ .correction-results { flex: 1; overflow-y: auto; padding: 24px; } .results-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-tertiary); } .results-placeholder .icon { font-size: 64px; margin-bottom: 16px; opacity: 0.5; } .results-placeholder h3 { margin-bottom: 8px; color: var(--text-secondary); } /* Exam List */ .exam-list { display: flex; flex-direction: column; gap: 12px; } .exam-item { display: flex; align-items: center; gap: 16px; padding: 16px; background: var(--bg-secondary); border-radius: 12px; border: 1px solid var(--border-color); cursor: pointer; transition: all 0.2s; } .exam-item:hover { border-color: var(--accent-primary); transform: translateX(4px); } .exam-item.active { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); } .exam-thumb { width: 60px; height: 80px; border-radius: 6px; background: var(--bg-tertiary); display: flex; align-items: center; justify-content: center; overflow: hidden; } .exam-thumb img { width: 100%; height: 100%; object-fit: cover; } .exam-info { flex: 1; } .exam-name { font-weight: 500; margin-bottom: 4px; } .exam-meta { font-size: 12px; color: var(--text-secondary); } .exam-grade { padding: 8px 16px; border-radius: 20px; font-weight: 600; font-size: 14px; } .exam-grade.excellent { background: rgba(16, 185, 129, 0.1); color: #10b981; } .exam-grade.good { background: rgba(59, 130, 246, 0.1); color: #3b82f6; } .exam-grade.average { background: rgba(245, 158, 11, 0.1); color: #f59e0b; } .exam-grade.poor { background: rgba(239, 68, 68, 0.1); color: #ef4444; } /* Review Interface */ .review-interface { display: none; flex: 1; overflow: hidden; } .review-interface.active { display: flex; } .review-left { flex: 1; border-right: 1px solid var(--border-color); overflow: hidden; display: flex; flex-direction: column; } .review-image-container { flex: 1; overflow: auto; padding: 16px; display: flex; justify-content: center; align-items: flex-start; background: var(--bg-tertiary); } .review-image { max-width: 100%; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } .review-right { width: 400px; display: flex; flex-direction: column; overflow-y: auto; } .review-section { padding: 20px; border-bottom: 1px solid var(--border-color); } .review-section-title { font-size: 12px; font-weight: 600; color: var(--text-tertiary); text-transform: uppercase; margin-bottom: 12px; } /* OCR Text */ .ocr-text-container { background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 8px; padding: 16px; font-family: 'Monaco', 'Consolas', monospace; font-size: 13px; line-height: 1.6; max-height: 200px; overflow-y: auto; } /* Answer Sections */ .answer-section { background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 8px; padding: 16px; margin-bottom: 12px; } .answer-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } .answer-task { font-weight: 600; } .answer-points { display: flex; align-items: center; gap: 8px; } .answer-points input { width: 50px; padding: 6px 8px; border: 1px solid var(--border-color); border-radius: 6px; text-align: center; font-weight: 600; background: var(--bg-secondary); color: var(--text-primary); } .answer-text { font-size: 13px; color: var(--text-secondary); margin-bottom: 12px; padding: 12px; background: var(--bg-secondary); border-radius: 6px; } .ai-feedback { font-size: 12px; color: var(--text-tertiary); padding: 10px; background: rgba(59, 130, 246, 0.05); border-left: 3px solid var(--accent-primary); border-radius: 0 6px 6px 0; } /* Total Score */ .total-score-section { padding: 20px; background: var(--bg-secondary); } .total-score { display: flex; justify-content: space-between; align-items: center; padding: 16px; background: var(--bg-primary); border-radius: 12px; } .total-score-label { font-size: 14px; color: var(--text-secondary); } .total-score-value { font-size: 28px; font-weight: 700; color: var(--accent-primary); } .grade-select { padding: 10px 16px; font-size: 18px; font-weight: 600; border: 2px solid var(--accent-primary); border-radius: 8px; background: var(--bg-primary); color: var(--accent-primary); cursor: pointer; } /* Review Actions */ .review-actions { display: flex; gap: 12px; padding: 16px; border-top: 1px solid var(--border-color); background: var(--bg-secondary); } .review-actions .btn { flex: 1; } /* Export Options */ .export-options { display: flex; gap: 8px; flex-wrap: wrap; } .export-btn { padding: 8px 16px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-primary); color: var(--text-primary); font-size: 12px; cursor: pointer; transition: all 0.2s; } .export-btn:hover { background: var(--bg-hover); border-color: var(--accent-primary); } .export-btn .icon { margin-right: 6px; } /* Empty State */ .correction-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; padding: 48px; text-align: center; } .correction-empty .icon { font-size: 80px; margin-bottom: 24px; opacity: 0.3; } .correction-empty h2 { margin-bottom: 12px; color: var(--text-secondary); } .correction-empty p { color: var(--text-tertiary); max-width: 400px; } """ @staticmethod def get_html() -> str: """HTML fuer das Correction-Modul.""" return """
Klausurkorrektur

Klausuren hochladen

📝

Klausuren hochladen

Dateien hierher ziehen oder klicken

PDF, JPG, PNG - max. 50MB

Klausur-Informationen

Verarbeitungs-Pipeline
📤
Upload
Dateien hochladen
🔧
Vorverarbeitung
Bildoptimierung
👁️
OCR
Texterkennung
✂️
Segmentierung
Aufgaben erkennen
🤖
AI-Bewertung
Automatisch benoten
Review
Ueberpruefen
Ergebnisse
0
Klausuren
0
Korrigiert
-
Durchschnitt
📝

Keine Klausuren geladen

Lade Klausuren hoch und starte die automatische Korrektur, um Ergebnisse zu sehen.

Klausur
Erkannter Text (OCR)
Hier erscheint der erkannte Text...
Antworten & Bewertung
Gesamtpunkte
0 / 100
""" @staticmethod def get_js() -> str: """JavaScript fuer das Correction-Modul.""" return """ // ============================================= // CORRECTION MODULE - Klausurkorrektur // ============================================= let correctionInitialized = false; let currentExamJob = null; let examUploadedFiles = []; let correctedExams = []; let currentExamIndex = 0; function loadCorrectionModule() { if (correctionInitialized) { console.log('Correction module already initialized'); return; } console.log('Loading Correction Module...'); // DOM Elements const uploadArea = document.getElementById('exam-upload-area'); const fileInput = document.getElementById('exam-file-input'); const startBtn = document.getElementById('start-correction-btn'); const resultsContainer = document.getElementById('correction-results'); const reviewInterface = document.getElementById('review-interface'); // --- Upload Handler --- if (uploadArea && fileInput) { uploadArea.addEventListener('click', () => fileInput.click()); uploadArea.addEventListener('dragover', (ev) => { ev.preventDefault(); uploadArea.classList.add('dragover'); }); uploadArea.addEventListener('dragleave', () => { uploadArea.classList.remove('dragover'); }); uploadArea.addEventListener('drop', (ev) => { ev.preventDefault(); uploadArea.classList.remove('dragover'); handleExamFiles(ev.dataTransfer.files); }); fileInput.addEventListener('change', () => { handleExamFiles(fileInput.files); }); } correctionInitialized = true; console.log('Correction Module loaded successfully'); } // Handle exam file upload function handleExamFiles(files) { if (!files.length) return; examUploadedFiles = Array.from(files); console.log(`${examUploadedFiles.length} Dateien fuer Klausur-Korrektur ausgewaehlt`); // Update UI const uploadArea = document.getElementById('exam-upload-area'); if (uploadArea) { uploadArea.innerHTML = `

${examUploadedFiles.length} Datei(en) ausgewaehlt

${examUploadedFiles.map(f => f.name).slice(0, 3).join(', ')}${examUploadedFiles.length > 3 ? '...' : ''}

`; } // Enable start button const startBtn = document.getElementById('start-correction-btn'); if (startBtn) { startBtn.disabled = false; } // Update stats document.getElementById('stat-total').textContent = examUploadedFiles.length; } // Start correction job - connects to /api/corrections API async function startCorrectionJob() { if (!examUploadedFiles.length) { alert('Bitte waehle zuerst Dateien aus.'); return; } const subject = document.getElementById('exam-subject')?.value || 'unbekannt'; const className = document.getElementById('exam-class')?.value || ''; const title = document.getElementById('exam-title')?.value || 'Klausur'; const maxPoints = parseInt(document.getElementById('exam-max-points')?.value) || 100; console.log('Starte Korrektur-Job:', { subject, className, title, files: examUploadedFiles.length }); // Show pipeline progress updatePipelineStep('upload', 'active'); // Reset state correctedExams = []; currentExamIndex = 0; try { // Process each file: create correction -> upload -> analyze for (let i = 0; i < examUploadedFiles.length; i++) { const file = examUploadedFiles[i]; console.log(`Verarbeite Datei ${i + 1}/${examUploadedFiles.length}: ${file.name}`); // Step 1: Create correction entry const createResp = await fetch('/api/corrections/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ student_id: `student-${i + 1}`, student_name: file.name.replace(/\\.[^.]+$/, ''), // Use filename as student name class_name: className, exam_title: title, subject: subject, max_points: maxPoints }) }); if (!createResp.ok) { throw new Error(`Korrektur erstellen fehlgeschlagen fuer ${file.name}`); } const createResult = await createResp.json(); if (!createResult.success) { throw new Error(createResult.error || 'Korrektur erstellen fehlgeschlagen'); } const correctionId = createResult.correction.id; updatePipelineStep('preprocess', 'active'); updatePipelineStep('upload', 'completed'); // Step 2: Upload file const formData = new FormData(); formData.append('file', file); const uploadResp = await fetch(`/api/corrections/${correctionId}/upload`, { method: 'POST', body: formData }); if (!uploadResp.ok) { throw new Error(`Upload fehlgeschlagen fuer ${file.name}`); } updatePipelineStep('ocr', 'active'); updatePipelineStep('preprocess', 'completed'); // Step 3: Poll for OCR completion let correction = await pollForOCRCompletion(correctionId); updatePipelineStep('segment', 'active'); updatePipelineStep('ocr', 'completed'); // Step 4: Analyze updatePipelineStep('grade', 'active'); updatePipelineStep('segment', 'completed'); const analyzeResp = await fetch(`/api/corrections/${correctionId}/analyze`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); if (analyzeResp.ok) { const analysisResult = await analyzeResp.json(); if (analysisResult.success) { // Refresh correction data const getResp = await fetch(`/api/corrections/${correctionId}`); if (getResp.ok) { const getResult = await getResp.json(); if (getResult.success) { correction = getResult.correction; } } } } // Add to results correctedExams.push({ id: correction.id, filename: file.name, student_name: correction.student_name, points: correction.total_points, max_points: correction.max_points, percentage: correction.percentage, grade: correction.grade || _calculateLocalGrade(correction.percentage), ocr_text: correction.extracted_text || '', answers: (correction.evaluations || []).map((e, idx) => ({ task: `Aufgabe ${e.question_number}`, points: e.points_awarded, max: e.points_possible, text: e.extracted_text, feedback: e.feedback })), image_url: correction.file_path ? `/static/corrections/${correction.id}` : null }); // Update progress document.getElementById('stat-corrected').textContent = correctedExams.length; } // All done updatePipelineStep('review', 'active'); updatePipelineStep('grade', 'completed'); showCorrectionResults(correctedExams); updatePipelineStep('review', 'completed'); } catch (e) { console.error('Korrektur-Job fehlgeschlagen:', e); alert('Korrektur fehlgeschlagen: ' + e.message); // Mark current step as error document.querySelectorAll('.pipeline-step.active').forEach(step => { step.classList.remove('active'); step.classList.add('error'); }); } } // Poll for OCR completion async function pollForOCRCompletion(correctionId, maxAttempts = 30) { for (let attempt = 0; attempt < maxAttempts; attempt++) { await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second const resp = await fetch(`/api/corrections/${correctionId}`); if (!resp.ok) continue; const result = await resp.json(); if (!result.success) continue; const correction = result.correction; // Check status if (correction.status === 'ocr_complete' || correction.status === 'analyzed' || correction.status === 'completed') { return correction; } if (correction.status === 'error') { throw new Error('OCR-Verarbeitung fehlgeschlagen'); } // Still processing, continue polling } throw new Error('OCR-Verarbeitung Timeout'); } // Calculate grade locally (fallback) function _calculateLocalGrade(percentage) { if (percentage >= 92) return '1'; if (percentage >= 81) return '2'; if (percentage >= 67) return '3'; if (percentage >= 50) return '4'; if (percentage >= 30) return '5'; return '6'; } // Reset pipeline visualization function resetPipeline() { const steps = ['upload', 'preprocess', 'ocr', 'segment', 'grade', 'review']; steps.forEach(step => updatePipelineStep(step, null)); } // Update pipeline step visualization function updatePipelineStep(stepId, status) { const step = document.querySelector(`[data-step="${stepId}"]`); if (!step) return; step.classList.remove('active', 'completed', 'error'); if (status) { step.classList.add(status); } } // Show correction results function showCorrectionResults(results) { correctedExams = results || []; // Update stats document.getElementById('stat-corrected').textContent = correctedExams.length; const avg = correctedExams.length ? (correctedExams.reduce((sum, e) => sum + e.points, 0) / correctedExams.length).toFixed(1) : '-'; document.getElementById('stat-average').textContent = avg; // Render exam list const container = document.getElementById('correction-results'); if (!container) return; if (!correctedExams.length) { container.innerHTML = `
📝

Keine Ergebnisse

Die Korrektur wurde abgeschlossen, aber es wurden keine Ergebnisse gefunden.

`; return; } container.innerHTML = `
${correctedExams.map((exam, idx) => `
📄
${exam.student_name || exam.filename}
${exam.points} / ${exam.max_points} Punkte
${exam.grade}
`).join('')}
`; } // Get grade class for styling function getGradeClass(grade) { const g = parseInt(grade); if (g === 1) return 'excellent'; if (g === 2) return 'good'; if (g === 3 || g === 4) return 'average'; return 'poor'; } // Select exam for review function selectExam(index) { currentExamIndex = index; const exam = correctedExams[index]; if (!exam) return; // Update active state in list document.querySelectorAll('.exam-item').forEach((item, idx) => { item.classList.toggle('active', idx === index); }); showReviewInterface(exam); } // Show review interface function showReviewInterface(exam) { const resultsContainer = document.getElementById('correction-results'); const reviewInterface = document.getElementById('review-interface'); if (resultsContainer) resultsContainer.style.display = 'none'; if (reviewInterface) { reviewInterface.classList.add('active'); reviewInterface.style.display = 'flex'; } // Populate review data const reviewImage = document.getElementById('review-image'); if (reviewImage && exam.image_url) { reviewImage.src = exam.image_url; } const ocrText = document.getElementById('ocr-text'); if (ocrText) { ocrText.textContent = exam.ocr_text || 'OCR-Text wird geladen...'; } // Render answers const answersContainer = document.getElementById('answers-container'); if (answersContainer && exam.answers) { answersContainer.innerHTML = exam.answers.map((ans, idx) => `
${ans.task}
/ ${ans.max}
${ans.text || 'Keine Antwort erkannt'}
${ans.feedback || 'AI-Feedback wird generiert...'}
`).join(''); } // Update total updateTotalPoints(exam); } // Update total points display function updateTotalPoints(exam) { const totalEl = document.getElementById('total-points'); if (totalEl) { totalEl.textContent = `${exam.points} / ${exam.max_points}`; } const gradeSelect = document.getElementById('grade-select'); if (gradeSelect) { gradeSelect.value = exam.grade; } } // Navigate exams function previousExam() { if (currentExamIndex > 0) { selectExam(currentExamIndex - 1); } } async function saveAndNextExam() { // Save current exam data const exam = correctedExams[currentExamIndex]; if (exam && exam.answers) { exam.answers.forEach((ans, idx) => { const input = document.getElementById(`answer-points-${idx}`); if (input) { ans.points = parseInt(input.value) || 0; } }); exam.points = exam.answers.reduce((sum, a) => sum + a.points, 0); exam.percentage = (exam.points / exam.max_points * 100) || 0; exam.grade = document.getElementById('grade-select')?.value || exam.grade; // Save to API if we have a correction ID if (exam.id) { try { const evaluations = exam.answers.map((ans, idx) => ({ question_number: idx + 1, extracted_text: ans.text || '', points_possible: ans.max, points_awarded: ans.points, feedback: ans.feedback || '', is_correct: ans.points >= ans.max * 0.5, confidence: 0.8 })); await fetch(`/api/corrections/${exam.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ evaluations: evaluations, total_points: exam.points, grade: exam.grade, status: 'reviewing' }) }); console.log(`Korrektur ${exam.id} gespeichert`); } catch (e) { console.error('Speichern fehlgeschlagen:', e); } } } // Move to next if (currentExamIndex < correctedExams.length - 1) { selectExam(currentExamIndex + 1); } else { // All done - mark as completed if we have IDs for (const exam of correctedExams) { if (exam.id) { try { await fetch(`/api/corrections/${exam.id}/complete`, { method: 'POST' }); } catch (e) { console.error(`Complete failed for ${exam.id}:`, e); } } } // Show summary hideReviewInterface(); showCorrectionResults(correctedExams); // Update stats const avg = correctedExams.length ? (correctedExams.reduce((sum, e) => sum + (e.percentage || 0), 0) / correctedExams.length).toFixed(1) : '-'; document.getElementById('stat-average').textContent = avg + '%'; } } function hideReviewInterface() { const resultsContainer = document.getElementById('correction-results'); const reviewInterface = document.getElementById('review-interface'); if (reviewInterface) { reviewInterface.classList.remove('active'); reviewInterface.style.display = 'none'; } if (resultsContainer) { resultsContainer.style.display = 'block'; } } // Export correction results - uses /api/corrections API async function exportCorrectionResults(format) { console.log(`Exportiere Ergebnisse als ${format}...`); if (!correctedExams.length) { alert('Keine Ergebnisse zum Exportieren vorhanden.'); return; } if (format === 'csv') { // CSV export - client-side const csv = [ 'Name,Punkte,MaxPunkte,Prozent,Note', ...correctedExams.map(e => `"${e.student_name || e.filename}",${e.points},${e.max_points},${e.percentage?.toFixed(1) || 0},${e.grade}` ) ].join('\\n'); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'klausur-ergebnisse.csv'; a.click(); URL.revokeObjectURL(url); } else if (format === 'pdf') { // PDF export - via API for each correction try { for (const exam of correctedExams) { if (!exam.id) continue; const resp = await fetch(`/api/corrections/${exam.id}/export-pdf`); if (!resp.ok) { console.error(`PDF export failed for ${exam.student_name}`); continue; } const blob = await resp.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `korrektur_${exam.student_name || 'unbekannt'}.pdf`; a.click(); URL.revokeObjectURL(url); // Small delay between downloads await new Promise(resolve => setTimeout(resolve, 300)); } alert(`${correctedExams.length} PDF(s) wurden heruntergeladen.`); } catch (e) { console.error('PDF export error:', e); alert('PDF-Export fehlgeschlagen: ' + e.message); } } else if (format === 'excel') { // Excel export - generate XLSX-like structure const headers = ['Name', 'Punkte', 'MaxPunkte', 'Prozent', 'Note']; const rows = correctedExams.map(e => [ e.student_name || e.filename, e.points, e.max_points, e.percentage?.toFixed(1) || 0, e.grade ]); // Create TSV (Tab-separated) which Excel can open const tsv = [ headers.join('\\t'), ...rows.map(row => row.join('\\t')) ].join('\\n'); const blob = new Blob([tsv], { type: 'application/vnd.ms-excel;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'klausur-ergebnisse.xls'; a.click(); URL.revokeObjectURL(url); } } // Show Correction Panel function showCorrectionPanel() { console.log('showCorrectionPanel called'); hideAllPanels(); hideStudioSubMenu(); const panel = document.getElementById('panel-correction'); if (panel) { panel.style.display = 'flex'; loadCorrectionModule(); console.log('Correction panel shown'); } else { console.error('panel-correction not found'); } } """