""" 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 """
Dateien hierher ziehen oder klicken
Lade Klausuren hoch und starte die automatische Korrektur, um Ergebnisse zu sehen.
${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 = `Die Korrektur wurde abgeschlossen, aber es wurden keine Ergebnisse gefunden.