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>
431 lines
14 KiB
JavaScript
431 lines
14 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|