This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/frontend/modules/correction.py
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

1482 lines
38 KiB
Python

"""
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 """
<!-- Correction Panel -->
<div id="panel-correction" class="panel-correction">
<!-- Header -->
<div class="correction-header">
<div class="correction-title">Klausurkorrektur</div>
<div class="correction-actions">
<div class="export-options">
<button class="export-btn" onclick="exportCorrectionResults('pdf')">
<span class="icon">📄</span>PDF
</button>
<button class="export-btn" onclick="exportCorrectionResults('csv')">
<span class="icon">📊</span>CSV
</button>
<button class="export-btn" onclick="exportCorrectionResults('excel')">
<span class="icon">📗</span>Excel
</button>
</div>
</div>
</div>
<!-- Content -->
<div class="correction-content">
<!-- Linke Spalte: Setup -->
<div class="correction-setup-panel">
<!-- Upload Section -->
<div class="correction-section">
<h3>Klausuren hochladen</h3>
<div class="correction-upload-area" id="exam-upload-area">
<div class="upload-icon">📝</div>
<h3>Klausuren hochladen</h3>
<p>Dateien hierher ziehen oder klicken</p>
<div class="file-types">PDF, JPG, PNG - max. 50MB</div>
<input type="file" id="exam-file-input" multiple accept=".pdf,.jpg,.jpeg,.png" hidden>
</div>
</div>
<!-- Exam Info -->
<div class="correction-section">
<h3>Klausur-Informationen</h3>
<div class="correction-form">
<div class="form-group">
<label>Fach</label>
<select id="exam-subject">
<option value="">Fach waehlen...</option>
<option value="deutsch">Deutsch</option>
<option value="mathe">Mathematik</option>
<option value="englisch">Englisch</option>
<option value="physik">Physik</option>
<option value="chemie">Chemie</option>
<option value="biologie">Biologie</option>
<option value="geschichte">Geschichte</option>
<option value="erdkunde">Erdkunde</option>
<option value="andere">Andere</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label>Klasse</label>
<input type="text" id="exam-class" placeholder="z.B. 10a">
</div>
<div class="form-group">
<label>Max. Punkte</label>
<input type="number" id="exam-max-points" placeholder="100" value="100">
</div>
</div>
<div class="form-group">
<label>Titel/Thema</label>
<input type="text" id="exam-title" placeholder="z.B. Klassenarbeit Nr. 3">
</div>
</div>
</div>
<!-- Start Button -->
<div class="correction-section">
<button class="correction-start-btn" id="start-correction-btn" disabled onclick="startCorrectionJob()">
Korrektur starten
</button>
</div>
</div>
<!-- Mittlere Spalte: Pipeline -->
<div class="correction-pipeline-panel">
<div class="pipeline-title">Verarbeitungs-Pipeline</div>
<div class="pipeline-steps">
<div class="pipeline-step" data-step="upload">
<div class="pipeline-step-icon">📤</div>
<div class="pipeline-step-content">
<div class="pipeline-step-label">Upload</div>
<div class="pipeline-step-status">Dateien hochladen</div>
</div>
</div>
<div class="pipeline-step" data-step="preprocess">
<div class="pipeline-step-icon">🔧</div>
<div class="pipeline-step-content">
<div class="pipeline-step-label">Vorverarbeitung</div>
<div class="pipeline-step-status">Bildoptimierung</div>
</div>
</div>
<div class="pipeline-step" data-step="ocr">
<div class="pipeline-step-icon">👁️</div>
<div class="pipeline-step-content">
<div class="pipeline-step-label">OCR</div>
<div class="pipeline-step-status">Texterkennung</div>
</div>
</div>
<div class="pipeline-step" data-step="segment">
<div class="pipeline-step-icon">✂️</div>
<div class="pipeline-step-content">
<div class="pipeline-step-label">Segmentierung</div>
<div class="pipeline-step-status">Aufgaben erkennen</div>
</div>
</div>
<div class="pipeline-step" data-step="grade">
<div class="pipeline-step-icon">🤖</div>
<div class="pipeline-step-content">
<div class="pipeline-step-label">AI-Bewertung</div>
<div class="pipeline-step-status">Automatisch benoten</div>
</div>
</div>
<div class="pipeline-step" data-step="review">
<div class="pipeline-step-icon">✅</div>
<div class="pipeline-step-content">
<div class="pipeline-step-label">Review</div>
<div class="pipeline-step-status">Ueberpruefen</div>
</div>
</div>
</div>
</div>
<!-- Rechte Spalte: Results -->
<div class="correction-results-panel">
<div class="results-header">
<div class="results-title">Ergebnisse</div>
<div class="results-stats">
<div class="stat-item">
<div class="stat-value" id="stat-total">0</div>
<div class="stat-label">Klausuren</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-corrected">0</div>
<div class="stat-label">Korrigiert</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-average">-</div>
<div class="stat-label">Durchschnitt</div>
</div>
</div>
</div>
<!-- Results/Review Content -->
<div class="correction-results" id="correction-results">
<div class="correction-empty">
<div class="icon">📝</div>
<h2>Keine Klausuren geladen</h2>
<p>Lade Klausuren hoch und starte die automatische Korrektur, um Ergebnisse zu sehen.</p>
</div>
</div>
<!-- Review Interface (hidden by default) -->
<div class="review-interface" id="review-interface">
<div class="review-left">
<div class="review-image-container">
<img id="review-image" class="review-image" src="" alt="Klausur">
</div>
</div>
<div class="review-right">
<!-- OCR Text -->
<div class="review-section">
<div class="review-section-title">Erkannter Text (OCR)</div>
<div class="ocr-text-container" id="ocr-text">
Hier erscheint der erkannte Text...
</div>
</div>
<!-- Answers -->
<div class="review-section">
<div class="review-section-title">Antworten & Bewertung</div>
<div id="answers-container">
<!-- Wird dynamisch gefuellt -->
</div>
</div>
<!-- Total Score -->
<div class="total-score-section">
<div class="total-score">
<div>
<div class="total-score-label">Gesamtpunkte</div>
<div class="total-score-value" id="total-points">0 / 100</div>
</div>
<select class="grade-select" id="grade-select">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
</select>
</div>
</div>
<!-- Review Actions -->
<div class="review-actions">
<button class="btn btn-secondary" onclick="previousExam()">Zurueck</button>
<button class="btn btn-primary" onclick="saveAndNextExam()">Speichern & Weiter</button>
</div>
</div>
</div>
</div>
</div>
</div>
"""
@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 = `
<div style="color: #10b981; font-size: 48px;">✓</div>
<h3>${examUploadedFiles.length} Datei(en) ausgewaehlt</h3>
<p>${examUploadedFiles.map(f => f.name).slice(0, 3).join(', ')}${examUploadedFiles.length > 3 ? '...' : ''}</p>
<button onclick="document.getElementById('exam-file-input').click()"
style="margin-top: 16px; padding: 8px 16px; background: var(--bg-tertiary); border: none; border-radius: 8px; color: var(--text-primary); cursor: pointer;">
Andere Dateien waehlen
</button>
`;
}
// 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 = `
<div class="correction-empty">
<div class="icon">📝</div>
<h2>Keine Ergebnisse</h2>
<p>Die Korrektur wurde abgeschlossen, aber es wurden keine Ergebnisse gefunden.</p>
</div>
`;
return;
}
container.innerHTML = `
<div class="exam-list">
${correctedExams.map((exam, idx) => `
<div class="exam-item${idx === currentExamIndex ? ' active' : ''}" onclick="selectExam(${idx})">
<div class="exam-thumb">
<span style="font-size: 24px;">📄</span>
</div>
<div class="exam-info">
<div class="exam-name">${exam.student_name || exam.filename}</div>
<div class="exam-meta">${exam.points} / ${exam.max_points} Punkte</div>
</div>
<div class="exam-grade ${getGradeClass(exam.grade)}">${exam.grade}</div>
</div>
`).join('')}
</div>
`;
}
// 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) => `
<div class="answer-section">
<div class="answer-header">
<span class="answer-task">${ans.task}</span>
<div class="answer-points">
<input type="number" id="answer-points-${idx}" value="${ans.points}" min="0" max="${ans.max}">
<span>/ ${ans.max}</span>
</div>
</div>
<div class="answer-text">${ans.text || 'Keine Antwort erkannt'}</div>
<div class="ai-feedback">${ans.feedback || 'AI-Feedback wird generiert...'}</div>
</div>
`).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');
}
}
"""