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>
445 lines
16 KiB
JavaScript
445 lines
16 KiB
JavaScript
/**
|
|
* BreakPilot Studio - Q&A Leitner Module
|
|
*
|
|
* Frage-Antwort Lernkarten mit Leitner-System:
|
|
* - Generieren von Q&A aus analysierten Arbeitsblättern
|
|
* - Leitner-Box-System (Neu, Gelernt, Gefestigt)
|
|
* - Lern-Session mit Selbstbewertung
|
|
* - Fortschrittsspeicherung
|
|
*
|
|
* Refactored: 2026-01-19
|
|
*/
|
|
|
|
import { t } from './i18n.js';
|
|
import { setStatus, setStatusWorking, setStatusError, setStatusSuccess, fetchJSON } from './api-helpers.js';
|
|
|
|
// State
|
|
let currentQaData = null;
|
|
let currentQaIndex = 0;
|
|
let qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
|
|
|
|
// DOM References
|
|
let qaPreview = null;
|
|
let qaBadge = null;
|
|
let qaModal = null;
|
|
let qaModalBody = null;
|
|
let qaModalClose = null;
|
|
let btnQaGenerate = null;
|
|
let btnQaLearn = null;
|
|
let btnQaPrint = null;
|
|
|
|
// Callback für aktuelle Datei
|
|
let getCurrentFileCallback = null;
|
|
let getFilesCallback = null;
|
|
let getCurrentIndexCallback = null;
|
|
|
|
/**
|
|
* Initialisiert das Q&A Leitner-Modul
|
|
* @param {Object} options - Konfiguration
|
|
*/
|
|
export function initQaModule(options = {}) {
|
|
getCurrentFileCallback = options.getCurrentFile || (() => null);
|
|
getFilesCallback = options.getFiles || (() => []);
|
|
getCurrentIndexCallback = options.getCurrentIndex || (() => 0);
|
|
|
|
qaPreview = document.getElementById('qa-preview') || options.previewEl;
|
|
qaBadge = document.getElementById('qa-badge') || options.badgeEl;
|
|
qaModal = document.getElementById('qa-modal') || options.modalEl;
|
|
qaModalBody = document.getElementById('qa-modal-body') || options.modalBodyEl;
|
|
qaModalClose = document.getElementById('qa-modal-close') || options.closeBtn;
|
|
btnQaGenerate = document.getElementById('btn-qa-generate') || options.generateBtn;
|
|
btnQaLearn = document.getElementById('btn-qa-learn') || options.learnBtn;
|
|
btnQaPrint = document.getElementById('btn-qa-print') || options.printBtn;
|
|
|
|
// Event-Listener
|
|
if (btnQaGenerate) {
|
|
btnQaGenerate.addEventListener('click', generateQaQuestions);
|
|
}
|
|
|
|
if (btnQaLearn) {
|
|
btnQaLearn.addEventListener('click', openQaModal);
|
|
}
|
|
|
|
if (btnQaPrint) {
|
|
btnQaPrint.addEventListener('click', openQaPrintDialog);
|
|
}
|
|
|
|
if (qaModalClose) {
|
|
qaModalClose.addEventListener('click', closeQaModal);
|
|
}
|
|
|
|
if (qaModal) {
|
|
qaModal.addEventListener('click', (ev) => {
|
|
if (ev.target === qaModal) {
|
|
closeQaModal();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Event für Datei-Wechsel
|
|
window.addEventListener('fileSelected', () => {
|
|
loadQaPreviewForCurrent();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generiert Q&A für alle Dateien
|
|
*/
|
|
export async function generateQaQuestions() {
|
|
try {
|
|
setStatusWorking(t('status_generating_qa') || 'Generiere Q&A ...');
|
|
if (qaBadge) qaBadge.textContent = t('mc_generating') || 'Generiert...';
|
|
|
|
const resp = await fetch('/api/generate-qa', { method: 'POST' });
|
|
if (!resp.ok) {
|
|
throw new Error('HTTP ' + resp.status);
|
|
}
|
|
|
|
const result = await resp.json();
|
|
if (result.status === 'OK' && result.generated.length > 0) {
|
|
setStatusSuccess(
|
|
t('status_qa_generated') || 'Q&A generiert',
|
|
result.generated.length + ' ' + (t('status_files_created') || 'Dateien erstellt')
|
|
);
|
|
if (qaBadge) qaBadge.textContent = t('mc_done') || 'Fertig';
|
|
if (btnQaLearn) btnQaLearn.style.display = 'inline-block';
|
|
if (btnQaPrint) btnQaPrint.style.display = 'inline-block';
|
|
|
|
await loadQaPreviewForCurrent();
|
|
} else if (result.errors && result.errors.length > 0) {
|
|
setStatusError(t('error') || 'Fehler', result.errors[0].error);
|
|
if (qaBadge) qaBadge.textContent = t('mc_error') || 'Fehler';
|
|
} else {
|
|
setStatusError(t('error') || 'Fehler', 'Keine Q&A generiert.');
|
|
if (qaBadge) qaBadge.textContent = t('mc_ready') || 'Bereit';
|
|
}
|
|
} catch (e) {
|
|
console.error('Q&A-Generierung fehlgeschlagen:', e);
|
|
setStatusError(t('error') || 'Fehler', String(e));
|
|
if (qaBadge) qaBadge.textContent = t('mc_error') || 'Fehler';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lädt die Q&A-Vorschau für die aktuelle Datei
|
|
*/
|
|
export async function loadQaPreviewForCurrent() {
|
|
const files = getFilesCallback();
|
|
if (!files.length) {
|
|
if (qaPreview) {
|
|
qaPreview.innerHTML = `<div style="font-size:11px;color:var(--bp-text-muted);">${t('qa_no_questions') || 'Noch keine Q&A vorhanden.'}</div>`;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const currentFile = getCurrentFileCallback();
|
|
if (!currentFile) return;
|
|
|
|
try {
|
|
const resp = await fetch('/api/qa-data/' + encodeURIComponent(currentFile));
|
|
const result = await resp.json();
|
|
|
|
if (result.status === 'OK' && result.data) {
|
|
currentQaData = result.data;
|
|
renderQaPreview(result.data);
|
|
if (btnQaLearn) btnQaLearn.style.display = 'inline-block';
|
|
if (btnQaPrint) btnQaPrint.style.display = 'inline-block';
|
|
} else {
|
|
if (qaPreview) {
|
|
qaPreview.innerHTML = `<div style="font-size:11px;color:var(--bp-text-muted);">${t('qa_no_questions') || 'Noch keine Q&A für dieses Arbeitsblatt generiert.'}</div>`;
|
|
}
|
|
currentQaData = null;
|
|
if (btnQaLearn) btnQaLearn.style.display = 'none';
|
|
if (btnQaPrint) btnQaPrint.style.display = 'none';
|
|
}
|
|
} catch (e) {
|
|
console.error('Fehler beim Laden der Q&A-Daten:', e);
|
|
if (qaPreview) qaPreview.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rendert die Q&A-Vorschau
|
|
*/
|
|
function renderQaPreview(qaData) {
|
|
if (!qaPreview) return;
|
|
if (!qaData || !qaData.qa_items || qaData.qa_items.length === 0) {
|
|
qaPreview.innerHTML = `<div style="font-size:11px;color:var(--bp-text-muted);">${t('qa_no_questions') || 'Keine Fragen vorhanden.'}</div>`;
|
|
return;
|
|
}
|
|
|
|
const items = qaData.qa_items;
|
|
|
|
// Zähle Fragen nach Box
|
|
let box0 = 0, box1 = 0, box2 = 0;
|
|
items.forEach(item => {
|
|
const box = item.leitner ? item.leitner.box : 0;
|
|
if (box === 0) box0++;
|
|
else if (box === 1) box1++;
|
|
else box2++;
|
|
});
|
|
|
|
let html = `
|
|
<div class="mc-stats" style="margin-bottom:8px;">
|
|
<div style="display:flex;gap:12px;font-size:11px;">
|
|
<div style="color:#ef4444;">${t('qa_box_new') || 'Neu'}: ${box0}</div>
|
|
<div style="color:#f59e0b;">${t('qa_box_learning') || 'Lernt'}: ${box1}</div>
|
|
<div style="color:#22c55e;">${t('qa_box_mastered') || 'Gefestigt'}: ${box2}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Zeige erste 2 Fragen als Vorschau
|
|
const previewItems = items.slice(0, 2);
|
|
previewItems.forEach((item, idx) => {
|
|
html += `
|
|
<div class="mc-question" style="padding:8px;margin-bottom:6px;background:rgba(255,255,255,0.03);border-radius:6px;">
|
|
<div style="font-size:12px;font-weight:500;margin-bottom:4px;">${idx + 1}. ${escapeHtml(item.question)}</div>
|
|
<div style="font-size:11px;color:var(--bp-text-muted);">→ ${escapeHtml(item.answer.substring(0, 60))}${item.answer.length > 60 ? '...' : ''}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
if (items.length > 2) {
|
|
html += `<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:4px;">+ ${items.length - 2} ${t('questions') || 'weitere Fragen'}</div>`;
|
|
}
|
|
|
|
qaPreview.innerHTML = html;
|
|
}
|
|
|
|
/**
|
|
* Öffnet das Lern-Modal
|
|
*/
|
|
export function openQaModal() {
|
|
if (!currentQaData || !currentQaData.qa_items) {
|
|
alert(t('qa_no_questions') || 'Keine Q&A vorhanden. Bitte zuerst generieren.');
|
|
return;
|
|
}
|
|
|
|
currentQaIndex = 0;
|
|
qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
|
|
renderQaLearningCard();
|
|
if (qaModal) qaModal.classList.remove('hidden');
|
|
}
|
|
|
|
/**
|
|
* Schließt das Lern-Modal
|
|
*/
|
|
export function closeQaModal() {
|
|
if (qaModal) qaModal.classList.add('hidden');
|
|
}
|
|
|
|
/**
|
|
* Rendert die aktuelle Lernkarte
|
|
*/
|
|
function renderQaLearningCard() {
|
|
const items = currentQaData.qa_items;
|
|
|
|
if (currentQaIndex >= items.length) {
|
|
renderQaSessionSummary();
|
|
return;
|
|
}
|
|
|
|
const item = items[currentQaIndex];
|
|
const leitner = item.leitner || { box: 0 };
|
|
const boxNames = [t('qa_box_new') || 'Neu', t('qa_box_learning') || 'Gelernt', t('qa_box_mastered') || 'Gefestigt'];
|
|
const boxColors = ['#ef4444', '#f59e0b', '#22c55e'];
|
|
|
|
let html = `
|
|
<!-- Fortschrittsanzeige -->
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
|
|
<div style="font-size:12px;color:var(--bp-text-muted);">${t('question') || 'Frage'} ${currentQaIndex + 1} / ${items.length}</div>
|
|
<div style="display:flex;align-items:center;gap:6px;">
|
|
<span style="font-size:10px;color:${boxColors[leitner.box]};background:${boxColors[leitner.box]}22;padding:2px 8px;border-radius:10px;">${boxNames[leitner.box]}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Frage -->
|
|
<div style="background:rgba(255,255,255,0.05);padding:20px;border-radius:12px;margin-bottom:16px;">
|
|
<div style="font-size:11px;color:var(--bp-text-muted);margin-bottom:8px;">${t('question') || 'Frage'}:</div>
|
|
<div style="font-size:16px;font-weight:500;line-height:1.5;">${escapeHtml(item.question)}</div>
|
|
</div>
|
|
|
|
<!-- Eingabefeld -->
|
|
<div id="qa-input-container" style="margin-bottom:16px;">
|
|
<div style="font-size:11px;color:var(--bp-text-muted);margin-bottom:8px;">${t('qa_your_answer') || 'Deine Antwort'}:</div>
|
|
<textarea id="qa-user-answer" style="width:100%;min-height:80px;padding:12px;border:1px solid rgba(255,255,255,0.2);border-radius:8px;background:rgba(255,255,255,0.05);color:var(--bp-text);font-size:14px;resize:vertical;font-family:inherit;" placeholder="${t('qa_type_answer') || 'Schreibe deine Antwort hier...'}"></textarea>
|
|
</div>
|
|
|
|
<!-- Prüfen-Button -->
|
|
<div id="qa-check-btn-container" style="text-align:center;margin-bottom:16px;">
|
|
<button class="btn btn-primary" id="btn-qa-check-answer" style="padding:12px 32px;">${t('qa_check_answer') || 'Antwort prüfen'}</button>
|
|
</div>
|
|
|
|
<!-- Vergleichs-Container (versteckt) -->
|
|
<div id="qa-comparison-container" style="display:none;">
|
|
<div id="qa-user-answer-display" style="background:rgba(59,130,246,0.1);padding:16px;border-radius:12px;margin-bottom:12px;border-left:3px solid #3b82f6;">
|
|
<div style="font-size:11px;color:#3b82f6;margin-bottom:8px;">${t('qa_your_answer') || 'Deine Antwort'}:</div>
|
|
<div id="qa-user-answer-text" style="font-size:14px;line-height:1.5;"></div>
|
|
</div>
|
|
|
|
<div style="background:rgba(34,197,94,0.1);padding:16px;border-radius:12px;margin-bottom:16px;border-left:3px solid #22c55e;">
|
|
<div style="font-size:11px;color:#22c55e;margin-bottom:8px;">${t('qa_correct_answer') || 'Richtige Antwort'}:</div>
|
|
<div style="font-size:14px;line-height:1.5;">${escapeHtml(item.answer)}</div>
|
|
${item.key_terms && item.key_terms.length > 0 ? `
|
|
<div style="margin-top:12px;font-size:11px;color:var(--bp-text-muted);">
|
|
${t('qa_key_terms') || 'Schlüsselbegriffe'}: <span style="color:#22c55e;">${item.key_terms.join(', ')}</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
|
|
<!-- Selbstbewertung -->
|
|
<div style="text-align:center;margin-bottom:8px;">
|
|
<div style="font-size:12px;color:var(--bp-text-muted);margin-bottom:12px;">${t('qa_self_evaluate') || 'War deine Antwort richtig?'}</div>
|
|
<div style="display:flex;gap:12px;justify-content:center;">
|
|
<button class="btn" id="btn-qa-incorrect" style="background:#ef4444;padding:12px 24px;">${t('qa_incorrect') || 'Falsch'}</button>
|
|
<button class="btn" id="btn-qa-correct" style="background:#22c55e;padding:12px 24px;">${t('qa_correct') || 'Richtig'}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Session-Statistik -->
|
|
<div style="margin-top:16px;padding-top:12px;border-top:1px solid rgba(255,255,255,0.1);display:flex;justify-content:center;gap:20px;font-size:11px;">
|
|
<div style="color:#22c55e;">${t('qa_session_correct') || 'Richtig'}: ${qaSessionStats.correct}</div>
|
|
<div style="color:#ef4444;">${t('qa_session_incorrect') || 'Falsch'}: ${qaSessionStats.incorrect}</div>
|
|
</div>
|
|
`;
|
|
|
|
if (qaModalBody) {
|
|
qaModalBody.innerHTML = html;
|
|
|
|
// Event Listener
|
|
document.getElementById('btn-qa-check-answer')?.addEventListener('click', () => {
|
|
const userAnswer = document.getElementById('qa-user-answer')?.value.trim() || '';
|
|
document.getElementById('qa-user-answer-text').textContent = userAnswer || (t('qa_no_answer') || '(keine Antwort eingegeben)');
|
|
document.getElementById('qa-input-container').style.display = 'none';
|
|
document.getElementById('qa-check-btn-container').style.display = 'none';
|
|
document.getElementById('qa-comparison-container').style.display = 'block';
|
|
});
|
|
|
|
// Enter zum Prüfen
|
|
document.getElementById('qa-user-answer')?.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
document.getElementById('btn-qa-check-answer')?.click();
|
|
}
|
|
});
|
|
|
|
// Fokus
|
|
setTimeout(() => {
|
|
document.getElementById('qa-user-answer')?.focus();
|
|
}, 100);
|
|
|
|
document.getElementById('btn-qa-correct')?.addEventListener('click', () => handleQaAnswer(true));
|
|
document.getElementById('btn-qa-incorrect')?.addEventListener('click', () => handleQaAnswer(false));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verarbeitet die Antwort
|
|
*/
|
|
async function handleQaAnswer(correct) {
|
|
const item = currentQaData.qa_items[currentQaIndex];
|
|
|
|
qaSessionStats.total++;
|
|
if (correct) qaSessionStats.correct++;
|
|
else qaSessionStats.incorrect++;
|
|
|
|
// Speichere Fortschritt
|
|
try {
|
|
const currentFile = getCurrentFileCallback();
|
|
if (currentFile) {
|
|
await fetch(`/api/qa-progress?filename=${encodeURIComponent(currentFile)}&item_id=${encodeURIComponent(item.id)}&correct=${correct}`, {
|
|
method: 'POST'
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.error('Fehler beim Speichern des Fortschritts:', e);
|
|
}
|
|
|
|
currentQaIndex++;
|
|
renderQaLearningCard();
|
|
}
|
|
|
|
/**
|
|
* Rendert die Session-Zusammenfassung
|
|
*/
|
|
function renderQaSessionSummary() {
|
|
const percent = qaSessionStats.total > 0 ? Math.round(qaSessionStats.correct / qaSessionStats.total * 100) : 0;
|
|
const emoji = percent >= 80 ? '🎉' : percent >= 50 ? '👍' : '💪';
|
|
|
|
let html = `
|
|
<div style="text-align:center;padding:20px;">
|
|
<div style="font-size:48px;margin-bottom:16px;">${emoji}</div>
|
|
<div style="font-size:24px;font-weight:600;margin-bottom:8px;">${t('qa_session_complete') || 'Lernrunde abgeschlossen!'}</div>
|
|
<div style="font-size:18px;margin-bottom:24px;">
|
|
${qaSessionStats.correct} / ${qaSessionStats.total} ${t('qa_result_correct') || 'richtig'} (${percent}%)
|
|
</div>
|
|
|
|
<div style="display:flex;justify-content:center;gap:24px;margin-bottom:24px;">
|
|
<div style="text-align:center;">
|
|
<div style="font-size:32px;color:#22c55e;">${qaSessionStats.correct}</div>
|
|
<div style="font-size:11px;color:var(--bp-text-muted);">${t('qa_correct') || 'Richtig'}</div>
|
|
</div>
|
|
<div style="text-align:center;">
|
|
<div style="font-size:32px;color:#ef4444;">${qaSessionStats.incorrect}</div>
|
|
<div style="font-size:11px;color:var(--bp-text-muted);">${t('qa_incorrect') || 'Falsch'}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:flex;gap:12px;justify-content:center;">
|
|
<button class="btn btn-primary" id="btn-qa-restart">${t('qa_restart') || 'Nochmal lernen'}</button>
|
|
<button class="btn btn-ghost" id="btn-qa-close-summary">${t('close') || 'Schließen'}</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
if (qaModalBody) {
|
|
qaModalBody.innerHTML = html;
|
|
|
|
document.getElementById('btn-qa-restart')?.addEventListener('click', () => {
|
|
currentQaIndex = 0;
|
|
qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
|
|
renderQaLearningCard();
|
|
});
|
|
|
|
document.getElementById('btn-qa-close-summary')?.addEventListener('click', closeQaModal);
|
|
|
|
// Aktualisiere Preview
|
|
loadQaPreviewForCurrent();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Öffnet den Druck-Dialog
|
|
*/
|
|
function openQaPrintDialog() {
|
|
if (!currentQaData) {
|
|
alert(t('qa_no_questions') || 'Keine Q&A vorhanden.');
|
|
return;
|
|
}
|
|
|
|
const currentFile = getCurrentFileCallback();
|
|
if (!currentFile) return;
|
|
|
|
const choice = confirm((t('qa_print_with_answers') || 'Mit Lösungen drucken?') + '\n\nOK = Mit Lösungen\nAbbrechen = Nur Fragen');
|
|
const url = '/api/print-qa/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
|
|
window.open(url, '_blank');
|
|
}
|
|
|
|
/**
|
|
* Gibt die aktuellen Q&A-Daten zurück
|
|
*/
|
|
export function getQaData() {
|
|
return currentQaData;
|
|
}
|
|
|
|
/**
|
|
* Helper: HTML-Escape
|
|
*/
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text || '';
|
|
return div.innerHTML;
|
|
}
|