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>
This commit is contained in:
474
backend/frontend/static/js/modules/mc-module.js
Normal file
474
backend/frontend/static/js/modules/mc-module.js
Normal file
@@ -0,0 +1,474 @@
|
||||
/**
|
||||
* BreakPilot Studio - Multiple Choice Module
|
||||
*
|
||||
* Multiple Choice Quiz-Funktionalität:
|
||||
* - Generierung von MC-Fragen aus Arbeitsblättern
|
||||
* - Interaktives Quiz mit Auswertung
|
||||
* - Druckfunktion (mit/ohne Lösungen)
|
||||
*
|
||||
* Refactored: 2026-01-19
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
import { setStatus } from './api-helpers.js';
|
||||
|
||||
// State
|
||||
let currentMcData = null;
|
||||
let mcAnswers = {};
|
||||
|
||||
// DOM References
|
||||
let mcPreview = null;
|
||||
let mcBadge = null;
|
||||
let mcModal = null;
|
||||
let mcModalBody = null;
|
||||
let mcModalClose = null;
|
||||
let btnMcGenerate = null;
|
||||
let btnMcShow = null;
|
||||
let btnMcPrint = null;
|
||||
|
||||
// Callback für aktuelle Datei
|
||||
let getCurrentFileCallback = null;
|
||||
let getEingangFilesCallback = null;
|
||||
let getCurrentIndexCallback = null;
|
||||
|
||||
/**
|
||||
* Initialisiert das Multiple Choice Modul
|
||||
* @param {Object} options - Konfiguration
|
||||
*/
|
||||
export function initMcModule(options = {}) {
|
||||
getCurrentFileCallback = options.getCurrentFile || (() => null);
|
||||
getEingangFilesCallback = options.getEingangFiles || (() => []);
|
||||
getCurrentIndexCallback = options.getCurrentIndex || (() => 0);
|
||||
|
||||
// DOM References
|
||||
mcPreview = document.getElementById('mc-preview') || options.previewEl;
|
||||
mcBadge = document.getElementById('mc-badge') || options.badgeEl;
|
||||
mcModal = document.getElementById('mc-modal') || options.modalEl;
|
||||
mcModalBody = document.getElementById('mc-modal-body') || options.modalBodyEl;
|
||||
mcModalClose = document.getElementById('mc-modal-close') || options.modalCloseEl;
|
||||
btnMcGenerate = document.getElementById('btn-mc-generate') || options.generateBtn;
|
||||
btnMcShow = document.getElementById('btn-mc-show') || options.showBtn;
|
||||
btnMcPrint = document.getElementById('btn-mc-print') || options.printBtn;
|
||||
|
||||
// Event-Listener
|
||||
if (btnMcGenerate) {
|
||||
btnMcGenerate.addEventListener('click', generateMcQuestions);
|
||||
}
|
||||
|
||||
if (btnMcShow) {
|
||||
btnMcShow.addEventListener('click', openMcModal);
|
||||
}
|
||||
|
||||
if (btnMcPrint) {
|
||||
btnMcPrint.addEventListener('click', openMcPrintDialog);
|
||||
}
|
||||
|
||||
if (mcModalClose) {
|
||||
mcModalClose.addEventListener('click', closeMcModal);
|
||||
}
|
||||
|
||||
if (mcModal) {
|
||||
mcModal.addEventListener('click', (ev) => {
|
||||
if (ev.target === mcModal) {
|
||||
closeMcModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Event für Datei-Wechsel
|
||||
window.addEventListener('fileSelected', () => {
|
||||
loadMcPreviewForCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert MC-Fragen für alle Arbeitsblätter
|
||||
*/
|
||||
export async function generateMcQuestions() {
|
||||
try {
|
||||
setStatus(t('mc_generating') || 'Generiere MC-Fragen …', t('ai_working') || 'Bitte warten, KI arbeitet.', 'busy');
|
||||
if (mcBadge) mcBadge.textContent = t('generating') || 'Generiert...';
|
||||
|
||||
const resp = await fetch('/api/generate-mc', { method: 'POST' });
|
||||
if (!resp.ok) {
|
||||
throw new Error('HTTP ' + resp.status);
|
||||
}
|
||||
|
||||
const result = await resp.json();
|
||||
if (result.status === 'OK' && result.generated.length > 0) {
|
||||
setStatus(t('mc_generated') || 'MC-Fragen generiert', result.generated.length + ' ' + (t('files_created') || 'Dateien erstellt'));
|
||||
if (mcBadge) mcBadge.textContent = t('ready') || 'Fertig';
|
||||
if (btnMcShow) btnMcShow.style.display = 'inline-block';
|
||||
if (btnMcPrint) btnMcPrint.style.display = 'inline-block';
|
||||
|
||||
// Lade die erste MC-Datei für Vorschau
|
||||
await loadMcPreviewForCurrent();
|
||||
} else if (result.errors && result.errors.length > 0) {
|
||||
setStatus(t('mc_error') || 'Fehler bei MC-Generierung', result.errors[0].error, 'error');
|
||||
if (mcBadge) mcBadge.textContent = t('error') || 'Fehler';
|
||||
} else {
|
||||
setStatus(t('no_mc_generated') || 'Keine MC-Fragen generiert', t('analysis_missing') || 'Möglicherweise fehlen Analyse-Daten.', 'error');
|
||||
if (mcBadge) mcBadge.textContent = t('ready') || 'Bereit';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('MC-Generierung fehlgeschlagen:', e);
|
||||
setStatus(t('mc_error') || 'Fehler bei MC-Generierung', String(e), 'error');
|
||||
if (mcBadge) mcBadge.textContent = t('error') || 'Fehler';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt MC-Vorschau für die aktuelle Datei
|
||||
*/
|
||||
export async function loadMcPreviewForCurrent() {
|
||||
const eingangFiles = getEingangFilesCallback();
|
||||
const currentIndex = getCurrentIndexCallback();
|
||||
|
||||
if (!eingangFiles.length) {
|
||||
if (mcPreview) mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + (t('no_worksheets') || 'Keine Arbeitsblätter vorhanden.') + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFile = eingangFiles[currentIndex];
|
||||
if (!currentFile) return;
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/mc-data/' + encodeURIComponent(currentFile));
|
||||
const result = await resp.json();
|
||||
|
||||
if (result.status === 'OK' && result.data) {
|
||||
currentMcData = result.data;
|
||||
renderMcPreview(result.data);
|
||||
if (btnMcShow) btnMcShow.style.display = 'inline-block';
|
||||
if (btnMcPrint) btnMcPrint.style.display = 'inline-block';
|
||||
} else {
|
||||
if (mcPreview) mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + (t('no_mc_for_worksheet') || 'Noch keine MC-Fragen für dieses Arbeitsblatt generiert.') + '</div>';
|
||||
currentMcData = null;
|
||||
if (btnMcPrint) btnMcPrint.style.display = 'none';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der MC-Daten:', e);
|
||||
if (mcPreview) mcPreview.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die MC-Vorschau
|
||||
* @param {Object} mcData - MC-Daten
|
||||
*/
|
||||
function renderMcPreview(mcData) {
|
||||
if (!mcPreview) return;
|
||||
if (!mcData || !mcData.questions || mcData.questions.length === 0) {
|
||||
mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + (t('no_questions') || 'Keine Fragen vorhanden.') + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const questions = mcData.questions;
|
||||
const metadata = mcData.metadata || {};
|
||||
|
||||
let html = '';
|
||||
|
||||
// Zeige Metadaten
|
||||
if (metadata.grade_level || metadata.subject) {
|
||||
html += '<div class="mc-stats">';
|
||||
if (metadata.subject) {
|
||||
html += '<div class="mc-stats-item"><strong>' + (t('subject') || 'Fach') + ':</strong> ' + escapeHtml(metadata.subject) + '</div>';
|
||||
}
|
||||
if (metadata.grade_level) {
|
||||
html += '<div class="mc-stats-item"><strong>' + (t('grade') || 'Stufe') + ':</strong> ' + escapeHtml(metadata.grade_level) + '</div>';
|
||||
}
|
||||
html += '<div class="mc-stats-item"><strong>' + (t('questions') || 'Fragen') + ':</strong> ' + questions.length + '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Zeige erste 2 Fragen als Vorschau
|
||||
const previewQuestions = questions.slice(0, 2);
|
||||
previewQuestions.forEach((q, idx) => {
|
||||
html += '<div class="mc-question">';
|
||||
html += '<div class="mc-question-text">' + (idx + 1) + '. ' + escapeHtml(q.question) + '</div>';
|
||||
html += '<div class="mc-options">';
|
||||
q.options.forEach(opt => {
|
||||
html += '<div class="mc-option" data-qid="' + q.id + '" data-opt="' + opt.id + '">';
|
||||
html += '<span class="mc-option-label">' + opt.id + ')</span> ' + escapeHtml(opt.text);
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
if (questions.length > 2) {
|
||||
html += '<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:8px;">+ ' + (questions.length - 2) + ' ' + (t('more_questions') || 'weitere Fragen') + '</div>';
|
||||
}
|
||||
|
||||
mcPreview.innerHTML = html;
|
||||
|
||||
// Event-Listener für Antwort-Auswahl
|
||||
mcPreview.querySelectorAll('.mc-option').forEach(optEl => {
|
||||
optEl.addEventListener('click', () => handleMcOptionClick(optEl));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Behandelt Klick auf eine MC-Option
|
||||
* @param {Element} optEl - Angeklicktes Option-Element
|
||||
*/
|
||||
function handleMcOptionClick(optEl) {
|
||||
const qid = optEl.getAttribute('data-qid');
|
||||
const optId = optEl.getAttribute('data-opt');
|
||||
|
||||
if (!currentMcData) return;
|
||||
|
||||
// Finde die Frage
|
||||
const question = currentMcData.questions.find(q => q.id === qid);
|
||||
if (!question) return;
|
||||
|
||||
// Markiere alle Optionen dieser Frage
|
||||
const questionEl = optEl.closest('.mc-question');
|
||||
const allOptions = questionEl.querySelectorAll('.mc-option');
|
||||
|
||||
allOptions.forEach(opt => {
|
||||
opt.classList.remove('selected', 'correct', 'incorrect');
|
||||
const thisOptId = opt.getAttribute('data-opt');
|
||||
if (thisOptId === question.correct_answer) {
|
||||
opt.classList.add('correct');
|
||||
} else if (thisOptId === optId) {
|
||||
opt.classList.add('incorrect');
|
||||
}
|
||||
});
|
||||
|
||||
// Speichere Antwort
|
||||
mcAnswers[qid] = optId;
|
||||
|
||||
// Zeige Feedback
|
||||
const isCorrect = optId === question.correct_answer;
|
||||
let feedbackEl = questionEl.querySelector('.mc-feedback');
|
||||
if (!feedbackEl) {
|
||||
feedbackEl = document.createElement('div');
|
||||
feedbackEl.className = 'mc-feedback';
|
||||
questionEl.appendChild(feedbackEl);
|
||||
}
|
||||
|
||||
if (isCorrect) {
|
||||
feedbackEl.style.background = 'rgba(34,197,94,0.1)';
|
||||
feedbackEl.style.borderColor = 'rgba(34,197,94,0.3)';
|
||||
feedbackEl.style.color = 'var(--bp-accent)';
|
||||
feedbackEl.textContent = (t('correct') || 'Richtig!') + ' ' + (question.explanation || '');
|
||||
} else {
|
||||
feedbackEl.style.background = 'rgba(239,68,68,0.1)';
|
||||
feedbackEl.style.borderColor = 'rgba(239,68,68,0.3)';
|
||||
feedbackEl.style.color = '#ef4444';
|
||||
feedbackEl.textContent = (t('incorrect') || 'Leider falsch.') + ' ' + (question.explanation || '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet das MC-Quiz-Modal
|
||||
*/
|
||||
export function openMcModal() {
|
||||
if (!currentMcData || !currentMcData.questions) {
|
||||
alert(t('no_mc_questions') || 'Keine MC-Fragen vorhanden. Bitte zuerst generieren.');
|
||||
return;
|
||||
}
|
||||
|
||||
mcAnswers = {}; // Reset Antworten
|
||||
renderMcModal(currentMcData);
|
||||
if (mcModal) mcModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Schließt das MC-Modal
|
||||
*/
|
||||
export function closeMcModal() {
|
||||
if (mcModal) mcModal.classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert den Modal-Inhalt
|
||||
* @param {Object} mcData - MC-Daten
|
||||
*/
|
||||
function renderMcModal(mcData) {
|
||||
if (!mcModalBody) return;
|
||||
|
||||
const questions = mcData.questions;
|
||||
const metadata = mcData.metadata || {};
|
||||
|
||||
let html = '';
|
||||
|
||||
// Header mit Metadaten
|
||||
html += '<div class="mc-stats" style="margin-bottom:16px;">';
|
||||
if (metadata.source_title) {
|
||||
html += '<div class="mc-stats-item"><strong>' + (t('worksheet') || 'Arbeitsblatt') + ':</strong> ' + escapeHtml(metadata.source_title) + '</div>';
|
||||
}
|
||||
if (metadata.subject) {
|
||||
html += '<div class="mc-stats-item"><strong>' + (t('subject') || 'Fach') + ':</strong> ' + escapeHtml(metadata.subject) + '</div>';
|
||||
}
|
||||
if (metadata.grade_level) {
|
||||
html += '<div class="mc-stats-item"><strong>' + (t('grade') || 'Stufe') + ':</strong> ' + escapeHtml(metadata.grade_level) + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// Alle Fragen
|
||||
questions.forEach((q, idx) => {
|
||||
html += '<div class="mc-question" data-qid="' + q.id + '">';
|
||||
html += '<div class="mc-question-text">' + (idx + 1) + '. ' + escapeHtml(q.question) + '</div>';
|
||||
html += '<div class="mc-options">';
|
||||
q.options.forEach(opt => {
|
||||
html += '<div class="mc-option" data-qid="' + q.id + '" data-opt="' + opt.id + '">';
|
||||
html += '<span class="mc-option-label">' + opt.id + ')</span> ' + escapeHtml(opt.text);
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
// Auswertungs-Button
|
||||
html += '<div style="margin-top:16px;text-align:center;">';
|
||||
html += '<button class="btn btn-primary" id="btn-mc-evaluate">' + (t('evaluate') || 'Auswerten') + '</button>';
|
||||
html += '</div>';
|
||||
|
||||
mcModalBody.innerHTML = html;
|
||||
|
||||
// Event-Listener
|
||||
mcModalBody.querySelectorAll('.mc-option').forEach(optEl => {
|
||||
optEl.addEventListener('click', () => {
|
||||
const qid = optEl.getAttribute('data-qid');
|
||||
const optId = optEl.getAttribute('data-opt');
|
||||
|
||||
// Deselektiere andere Optionen der gleichen Frage
|
||||
const questionEl = optEl.closest('.mc-question');
|
||||
questionEl.querySelectorAll('.mc-option').forEach(o => o.classList.remove('selected'));
|
||||
optEl.classList.add('selected');
|
||||
|
||||
mcAnswers[qid] = optId;
|
||||
});
|
||||
});
|
||||
|
||||
const btnEvaluate = document.getElementById('btn-mc-evaluate');
|
||||
if (btnEvaluate) {
|
||||
btnEvaluate.addEventListener('click', evaluateMcQuiz);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wertet das Quiz aus
|
||||
*/
|
||||
function evaluateMcQuiz() {
|
||||
if (!currentMcData || !mcModalBody) return;
|
||||
|
||||
let correct = 0;
|
||||
let total = currentMcData.questions.length;
|
||||
|
||||
currentMcData.questions.forEach(q => {
|
||||
const questionEl = mcModalBody.querySelector('.mc-question[data-qid="' + q.id + '"]');
|
||||
if (!questionEl) return;
|
||||
|
||||
const userAnswer = mcAnswers[q.id];
|
||||
const allOptions = questionEl.querySelectorAll('.mc-option');
|
||||
|
||||
allOptions.forEach(opt => {
|
||||
opt.classList.remove('correct', 'incorrect');
|
||||
const optId = opt.getAttribute('data-opt');
|
||||
if (optId === q.correct_answer) {
|
||||
opt.classList.add('correct');
|
||||
} else if (optId === userAnswer && userAnswer !== q.correct_answer) {
|
||||
opt.classList.add('incorrect');
|
||||
}
|
||||
});
|
||||
|
||||
// Zeige Erklärung
|
||||
let feedbackEl = questionEl.querySelector('.mc-feedback');
|
||||
if (!feedbackEl) {
|
||||
feedbackEl = document.createElement('div');
|
||||
feedbackEl.className = 'mc-feedback';
|
||||
questionEl.appendChild(feedbackEl);
|
||||
}
|
||||
|
||||
if (userAnswer === q.correct_answer) {
|
||||
correct++;
|
||||
feedbackEl.style.background = 'rgba(34,197,94,0.1)';
|
||||
feedbackEl.style.borderColor = 'rgba(34,197,94,0.3)';
|
||||
feedbackEl.style.color = 'var(--bp-accent)';
|
||||
feedbackEl.textContent = (t('correct') || 'Richtig!') + ' ' + (q.explanation || '');
|
||||
} else if (userAnswer) {
|
||||
feedbackEl.style.background = 'rgba(239,68,68,0.1)';
|
||||
feedbackEl.style.borderColor = 'rgba(239,68,68,0.3)';
|
||||
feedbackEl.style.color = '#ef4444';
|
||||
feedbackEl.textContent = (t('incorrect') || 'Falsch.') + ' ' + (q.explanation || '');
|
||||
} else {
|
||||
feedbackEl.style.background = 'rgba(148,163,184,0.1)';
|
||||
feedbackEl.style.borderColor = 'rgba(148,163,184,0.3)';
|
||||
feedbackEl.style.color = 'var(--bp-text-muted)';
|
||||
feedbackEl.textContent = (t('not_answered') || 'Nicht beantwortet.') + ' ' + (t('correct_was') || 'Richtig wäre:') + ' ' + q.correct_answer.toUpperCase();
|
||||
}
|
||||
});
|
||||
|
||||
// Zeige Gesamtergebnis
|
||||
const percentage = Math.round(correct / total * 100);
|
||||
const resultHtml = '<div style="margin-top:16px;padding:12px;background:rgba(15,23,42,0.6);border-radius:8px;text-align:center;">' +
|
||||
'<div style="font-size:18px;font-weight:600;">' + correct + ' ' + (t('of') || 'von') + ' ' + total + ' ' + (t('correct_answers') || 'richtig') + '</div>' +
|
||||
'<div style="font-size:12px;color:var(--bp-text-muted);margin-top:4px;">' + percentage + '% ' + (t('percent_correct') || 'korrekt') + '</div>' +
|
||||
'</div>';
|
||||
|
||||
const existingResult = mcModalBody.querySelector('.mc-result');
|
||||
if (existingResult) {
|
||||
existingResult.remove();
|
||||
}
|
||||
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.className = 'mc-result';
|
||||
resultDiv.innerHTML = resultHtml;
|
||||
mcModalBody.appendChild(resultDiv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet den Druck-Dialog
|
||||
*/
|
||||
export function openMcPrintDialog() {
|
||||
if (!currentMcData) {
|
||||
alert(t('no_mc_questions') || 'Keine MC-Fragen vorhanden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const eingangFiles = getEingangFilesCallback();
|
||||
const currentIndex = getCurrentIndexCallback();
|
||||
const currentFile = eingangFiles[currentIndex];
|
||||
|
||||
const confirmMsg = (t('mc_print_with_answers') || 'Mit Lösungen drucken?') +
|
||||
'\n\nOK = ' + (t('solution_sheet') || 'Lösungsblatt mit markierten Antworten') +
|
||||
'\n' + (t('cancel') || 'Abbrechen') + ' = ' + (t('exercise_sheet') || 'Übungsblatt ohne Lösungen');
|
||||
|
||||
const choice = confirm(confirmMsg);
|
||||
const url = '/api/print-mc/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
// === Getter und Setter ===
|
||||
|
||||
/**
|
||||
* Gibt die aktuellen MC-Daten zurück
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
export function getMcData() {
|
||||
return currentMcData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die MC-Daten
|
||||
* @param {Object} data
|
||||
*/
|
||||
export function setMcData(data) {
|
||||
currentMcData = data;
|
||||
if (data) {
|
||||
renderMcPreview(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: HTML-Escape
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
Reference in New Issue
Block a user