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:
430
backend/frontend/static/js/modules/cloze-module.js
Normal file
430
backend/frontend/static/js/modules/cloze-module.js
Normal file
@@ -0,0 +1,430 @@
|
||||
/**
|
||||
* BreakPilot Studio - Cloze (Lückentext) Module
|
||||
*
|
||||
* Lückentext-Funktionalität mit Übersetzung:
|
||||
* - Generierung von Lückentexten aus Arbeitsblättern
|
||||
* - Mehrsprachige Übersetzungen (TR, AR, RU, UK, PL, EN)
|
||||
* - Interaktives Übungsmodul mit Auswertung
|
||||
* - Druckfunktion (mit/ohne Lösungen)
|
||||
*
|
||||
* Refactored: 2026-01-19
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
import { setStatus } from './api-helpers.js';
|
||||
|
||||
// State
|
||||
let currentClozeData = null;
|
||||
let clozeAnswers = {};
|
||||
|
||||
// DOM References
|
||||
let clozePreview = null;
|
||||
let clozeBadge = null;
|
||||
let clozeLanguageSelect = null;
|
||||
let clozeModal = null;
|
||||
let clozeModalBody = null;
|
||||
let clozeModalClose = null;
|
||||
let btnClozeGenerate = null;
|
||||
let btnClozeShow = null;
|
||||
let btnClozePrint = null;
|
||||
|
||||
// Callbacks
|
||||
let getEingangFilesCallback = null;
|
||||
let getCurrentIndexCallback = null;
|
||||
|
||||
/**
|
||||
* Initialisiert das Cloze-Modul
|
||||
* @param {Object} options - Konfiguration
|
||||
*/
|
||||
export function initClozeModule(options = {}) {
|
||||
getEingangFilesCallback = options.getEingangFiles || (() => []);
|
||||
getCurrentIndexCallback = options.getCurrentIndex || (() => 0);
|
||||
|
||||
// DOM References
|
||||
clozePreview = document.getElementById('cloze-preview') || options.previewEl;
|
||||
clozeBadge = document.getElementById('cloze-badge') || options.badgeEl;
|
||||
clozeLanguageSelect = document.getElementById('cloze-language') || options.languageSelectEl;
|
||||
clozeModal = document.getElementById('cloze-modal') || options.modalEl;
|
||||
clozeModalBody = document.getElementById('cloze-modal-body') || options.modalBodyEl;
|
||||
clozeModalClose = document.getElementById('cloze-modal-close') || options.modalCloseEl;
|
||||
btnClozeGenerate = document.getElementById('btn-cloze-generate') || options.generateBtn;
|
||||
btnClozeShow = document.getElementById('btn-cloze-show') || options.showBtn;
|
||||
btnClozePrint = document.getElementById('btn-cloze-print') || options.printBtn;
|
||||
|
||||
// Event-Listener
|
||||
if (btnClozeGenerate) {
|
||||
btnClozeGenerate.addEventListener('click', generateClozeTexts);
|
||||
}
|
||||
|
||||
if (btnClozeShow) {
|
||||
btnClozeShow.addEventListener('click', openClozeModal);
|
||||
}
|
||||
|
||||
if (btnClozePrint) {
|
||||
btnClozePrint.addEventListener('click', openClozePrintDialog);
|
||||
}
|
||||
|
||||
if (clozeModalClose) {
|
||||
clozeModalClose.addEventListener('click', closeClozeModal);
|
||||
}
|
||||
|
||||
if (clozeModal) {
|
||||
clozeModal.addEventListener('click', (ev) => {
|
||||
if (ev.target === clozeModal) {
|
||||
closeClozeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Event für Datei-Wechsel
|
||||
window.addEventListener('fileSelected', () => {
|
||||
loadClozePreviewForCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Lückentexte für alle Arbeitsblätter
|
||||
*/
|
||||
export async function generateClozeTexts() {
|
||||
const targetLang = clozeLanguageSelect ? clozeLanguageSelect.value : 'tr';
|
||||
|
||||
try {
|
||||
setStatus(t('cloze_generating') || 'Generiere Lückentexte …', t('ai_working') || 'Bitte warten, KI arbeitet.', 'busy');
|
||||
if (clozeBadge) clozeBadge.textContent = t('generating') || 'Generiert...';
|
||||
|
||||
const resp = await fetch('/api/generate-cloze?target_language=' + targetLang, { 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('cloze_generated') || 'Lückentexte generiert', result.generated.length + ' ' + (t('files_created') || 'Dateien erstellt'));
|
||||
if (clozeBadge) clozeBadge.textContent = t('ready') || 'Fertig';
|
||||
if (btnClozeShow) btnClozeShow.style.display = 'inline-block';
|
||||
if (btnClozePrint) btnClozePrint.style.display = 'inline-block';
|
||||
|
||||
// Lade Vorschau für aktuelle Datei
|
||||
await loadClozePreviewForCurrent();
|
||||
} else if (result.errors && result.errors.length > 0) {
|
||||
setStatus(t('cloze_error') || 'Fehler bei Lückentext-Generierung', result.errors[0].error, 'error');
|
||||
if (clozeBadge) clozeBadge.textContent = t('error') || 'Fehler';
|
||||
} else {
|
||||
setStatus(t('no_cloze_generated') || 'Keine Lückentexte generiert', t('analysis_missing') || 'Möglicherweise fehlen Analyse-Daten.', 'error');
|
||||
if (clozeBadge) clozeBadge.textContent = t('ready') || 'Bereit';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Lückentext-Generierung fehlgeschlagen:', e);
|
||||
setStatus(t('cloze_error') || 'Fehler bei Lückentext-Generierung', String(e), 'error');
|
||||
if (clozeBadge) clozeBadge.textContent = t('error') || 'Fehler';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Cloze-Vorschau für die aktuelle Datei
|
||||
*/
|
||||
export async function loadClozePreviewForCurrent() {
|
||||
const eingangFiles = getEingangFilesCallback();
|
||||
const currentIndex = getCurrentIndexCallback();
|
||||
|
||||
if (!eingangFiles.length) {
|
||||
if (clozePreview) clozePreview.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/cloze-data/' + encodeURIComponent(currentFile));
|
||||
const result = await resp.json();
|
||||
|
||||
if (result.status === 'OK' && result.data) {
|
||||
currentClozeData = result.data;
|
||||
renderClozePreview(result.data);
|
||||
if (btnClozeShow) btnClozeShow.style.display = 'inline-block';
|
||||
if (btnClozePrint) btnClozePrint.style.display = 'inline-block';
|
||||
} else {
|
||||
if (clozePreview) clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + (t('no_cloze_for_worksheet') || 'Noch keine Lückentexte für dieses Arbeitsblatt generiert.') + '</div>';
|
||||
currentClozeData = null;
|
||||
if (btnClozeShow) btnClozeShow.style.display = 'none';
|
||||
if (btnClozePrint) btnClozePrint.style.display = 'none';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Lückentext-Daten:', e);
|
||||
if (clozePreview) clozePreview.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Cloze-Vorschau
|
||||
* @param {Object} clozeData - Cloze-Daten
|
||||
*/
|
||||
function renderClozePreview(clozeData) {
|
||||
if (!clozePreview) return;
|
||||
if (!clozeData || !clozeData.cloze_items || clozeData.cloze_items.length === 0) {
|
||||
clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + (t('no_cloze_texts') || 'Keine Lückentexte vorhanden.') + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const items = clozeData.cloze_items;
|
||||
const metadata = clozeData.metadata || {};
|
||||
|
||||
let html = '';
|
||||
|
||||
// Statistiken
|
||||
html += '<div class="cloze-stats">';
|
||||
if (metadata.subject) {
|
||||
html += '<div><strong>' + (t('subject') || 'Fach') + ':</strong> ' + escapeHtml(metadata.subject) + '</div>';
|
||||
}
|
||||
if (metadata.grade_level) {
|
||||
html += '<div><strong>' + (t('grade') || 'Stufe') + ':</strong> ' + escapeHtml(metadata.grade_level) + '</div>';
|
||||
}
|
||||
html += '<div><strong>' + (t('sentences') || 'Sätze') + ':</strong> ' + items.length + '</div>';
|
||||
if (metadata.total_gaps) {
|
||||
html += '<div><strong>' + (t('gaps') || 'Lücken') + ':</strong> ' + metadata.total_gaps + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// Zeige erste 2 Sätze als Vorschau
|
||||
const previewItems = items.slice(0, 2);
|
||||
previewItems.forEach((item, idx) => {
|
||||
html += '<div class="cloze-item">';
|
||||
html += '<div class="cloze-sentence">' + (idx + 1) + '. ' + item.sentence_with_gaps.replace(/___/g, '<span class="cloze-gap">___</span>') + '</div>';
|
||||
|
||||
// Übersetzung anzeigen
|
||||
if (item.translation && item.translation.full_sentence) {
|
||||
html += '<div class="cloze-translation">';
|
||||
html += '<div class="cloze-translation-label">' + (item.translation.language_name || t('translation') || 'Übersetzung') + ':</div>';
|
||||
html += escapeHtml(item.translation.full_sentence);
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
if (items.length > 2) {
|
||||
html += '<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:8px;">+ ' + (items.length - 2) + ' ' + (t('more_sentences') || 'weitere Sätze') + '</div>';
|
||||
}
|
||||
|
||||
clozePreview.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet das Cloze-Modal
|
||||
*/
|
||||
export function openClozeModal() {
|
||||
if (!currentClozeData || !currentClozeData.cloze_items) {
|
||||
alert(t('no_cloze_texts') || 'Keine Lückentexte vorhanden. Bitte zuerst generieren.');
|
||||
return;
|
||||
}
|
||||
|
||||
clozeAnswers = {}; // Reset Antworten
|
||||
renderClozeModal(currentClozeData);
|
||||
if (clozeModal) clozeModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Schließt das Cloze-Modal
|
||||
*/
|
||||
export function closeClozeModal() {
|
||||
if (clozeModal) clozeModal.classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert den Modal-Inhalt
|
||||
* @param {Object} clozeData - Cloze-Daten
|
||||
*/
|
||||
function renderClozeModal(clozeData) {
|
||||
if (!clozeModalBody) return;
|
||||
|
||||
const items = clozeData.cloze_items;
|
||||
const metadata = clozeData.metadata || {};
|
||||
|
||||
let html = '';
|
||||
|
||||
// Header
|
||||
html += '<div class="cloze-stats" style="margin-bottom:16px;">';
|
||||
if (metadata.source_title) {
|
||||
html += '<div><strong>' + (t('worksheet') || 'Arbeitsblatt') + ':</strong> ' + escapeHtml(metadata.source_title) + '</div>';
|
||||
}
|
||||
if (metadata.total_gaps) {
|
||||
html += '<div><strong>' + (t('total_gaps') || 'Lücken gesamt') + ':</strong> ' + metadata.total_gaps + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
html += '<div style="font-size:12px;color:var(--bp-text-muted);margin-bottom:12px;">' + (t('cloze_instruction') || 'Fülle die Lücken aus und klicke auf "Prüfen".') + '</div>';
|
||||
|
||||
// Alle Sätze mit Eingabefeldern
|
||||
items.forEach((item, idx) => {
|
||||
html += '<div class="cloze-item" data-cid="' + item.id + '">';
|
||||
|
||||
// Satz mit Eingabefeldern statt ___
|
||||
let sentenceHtml = item.sentence_with_gaps;
|
||||
const gaps = item.gaps || [];
|
||||
|
||||
// Ersetze ___ durch Eingabefelder
|
||||
let gapIndex = 0;
|
||||
sentenceHtml = sentenceHtml.replace(/___/g, () => {
|
||||
const gap = gaps[gapIndex] || { id: 'g' + gapIndex, word: '' };
|
||||
const inputId = item.id + '_' + gap.id;
|
||||
gapIndex++;
|
||||
return '<input type="text" class="cloze-gap-input" data-cid="' + item.id + '" data-gid="' + gap.id + '" data-answer="' + escapeHtml(gap.word) + '" id="input_' + inputId + '" autocomplete="off">';
|
||||
});
|
||||
|
||||
html += '<div class="cloze-sentence">' + (idx + 1) + '. ' + sentenceHtml + '</div>';
|
||||
|
||||
// Übersetzung als Hilfe
|
||||
if (item.translation && item.translation.sentence_with_gaps) {
|
||||
html += '<div class="cloze-translation">';
|
||||
html += '<div class="cloze-translation-label">' + (item.translation.language_name || t('translation') || 'Übersetzung') + ' (' + (t('with_gaps') || 'mit Lücken') + '):</div>';
|
||||
html += escapeHtml(item.translation.sentence_with_gaps);
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
// Buttons
|
||||
html += '<div style="margin-top:16px;text-align:center;display:flex;gap:8px;justify-content:center;">';
|
||||
html += '<button class="btn btn-primary" id="btn-cloze-check">' + (t('check') || 'Prüfen') + '</button>';
|
||||
html += '<button class="btn btn-ghost" id="btn-cloze-show-answers">' + (t('show_answers') || 'Lösungen zeigen') + '</button>';
|
||||
html += '</div>';
|
||||
|
||||
clozeModalBody.innerHTML = html;
|
||||
|
||||
// Event-Listener für Prüfen-Button
|
||||
const btnCheck = document.getElementById('btn-cloze-check');
|
||||
if (btnCheck) {
|
||||
btnCheck.addEventListener('click', checkClozeAnswers);
|
||||
}
|
||||
|
||||
// Event-Listener für Lösungen zeigen
|
||||
const btnShowAnswers = document.getElementById('btn-cloze-show-answers');
|
||||
if (btnShowAnswers) {
|
||||
btnShowAnswers.addEventListener('click', showClozeAnswers);
|
||||
}
|
||||
|
||||
// Enter-Taste zum Prüfen
|
||||
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
|
||||
input.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
checkClozeAnswers();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Überprüft die Cloze-Antworten
|
||||
*/
|
||||
function checkClozeAnswers() {
|
||||
if (!clozeModalBody) return;
|
||||
|
||||
let correct = 0;
|
||||
let total = 0;
|
||||
|
||||
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
|
||||
const userAnswer = input.value.trim().toLowerCase();
|
||||
const correctAnswer = input.getAttribute('data-answer').toLowerCase();
|
||||
total++;
|
||||
|
||||
// Entferne vorherige Klassen
|
||||
input.classList.remove('correct', 'incorrect');
|
||||
|
||||
if (userAnswer === correctAnswer) {
|
||||
input.classList.add('correct');
|
||||
correct++;
|
||||
} else if (userAnswer !== '') {
|
||||
input.classList.add('incorrect');
|
||||
}
|
||||
});
|
||||
|
||||
// Zeige Ergebnis
|
||||
let existingResult = clozeModalBody.querySelector('.cloze-result');
|
||||
if (existingResult) existingResult.remove();
|
||||
|
||||
const percentage = Math.round(correct / total * 100);
|
||||
const resultHtml = '<div class="cloze-result" 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 resultDiv = document.createElement('div');
|
||||
resultDiv.innerHTML = resultHtml;
|
||||
clozeModalBody.appendChild(resultDiv.firstChild);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt alle Cloze-Lösungen
|
||||
*/
|
||||
function showClozeAnswers() {
|
||||
if (!clozeModalBody) return;
|
||||
|
||||
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
|
||||
const correctAnswer = input.getAttribute('data-answer');
|
||||
input.value = correctAnswer;
|
||||
input.classList.remove('incorrect');
|
||||
input.classList.add('correct');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet den Druck-Dialog
|
||||
*/
|
||||
export function openClozePrintDialog() {
|
||||
if (!currentClozeData) {
|
||||
alert(t('no_cloze_texts') || 'Keine Lückentexte vorhanden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const eingangFiles = getEingangFilesCallback();
|
||||
const currentIndex = getCurrentIndexCallback();
|
||||
const currentFile = eingangFiles[currentIndex];
|
||||
|
||||
const confirmMsg = (t('cloze_print_with_answers') || 'Mit Lösungen drucken?') +
|
||||
'\n\nOK = ' + (t('with_filled_gaps') || 'Mit ausgefüllten Lücken') +
|
||||
'\n' + (t('cancel') || 'Abbrechen') + ' = ' + (t('exercise_with_wordbank') || 'Übungsblatt mit Wortbank');
|
||||
|
||||
const choice = confirm(confirmMsg);
|
||||
const url = '/api/print-cloze/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
// === Getter und Setter ===
|
||||
|
||||
/**
|
||||
* Gibt die aktuellen Cloze-Daten zurück
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
export function getClozeData() {
|
||||
return currentClozeData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Cloze-Daten
|
||||
* @param {Object} data
|
||||
*/
|
||||
export function setClozeData(data) {
|
||||
currentClozeData = data;
|
||||
if (data) {
|
||||
renderClozePreview(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die aktuelle Zielsprache zurück
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getTargetLanguage() {
|
||||
return clozeLanguageSelect ? clozeLanguageSelect.value : 'tr';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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