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
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

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;
}