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>
475 lines
16 KiB
JavaScript
475 lines
16 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|