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:
Benjamin Admin
2026-02-09 09:51:32 +01:00
parent f7487ee240
commit bfdaf63ba9
2009 changed files with 749983 additions and 1731 deletions

View File

@@ -0,0 +1,154 @@
# Studio JavaScript Modules
Das monolithische studio.js (9.787 Zeilen) wurde in modulare ES6-Module aufgeteilt.
## Modul-Struktur
```
backend/frontend/static/js/
├── studio.js # Original (noch nicht aktualisiert)
└── modules/
├── theme.js # Dark/Light Mode (105 Zeilen)
├── translations.js # Übersetzungen DE/EN (971 Zeilen)
├── i18n.js # Internationalisierung (250 Zeilen)
├── lightbox.js # Bildvorschau (234 Zeilen)
├── api-helpers.js # API-Utilities (360 Zeilen)
├── file-manager.js # Dateiverwaltung (614 Zeilen)
├── learning-units-module.js # Lerneinheiten (517 Zeilen)
├── mc-module.js # Multiple Choice (474 Zeilen)
├── cloze-module.js # Lückentext (430 Zeilen)
├── mindmap-module.js # Mindmap (223 Zeilen)
└── qa-leitner-module.js # Q&A / Leitner (444 Zeilen)
```
## Module-Übersicht
### theme.js
- Dark/Light Mode Toggle
- Speichert Präferenz in localStorage
- Exports: `getCurrentTheme()`, `setTheme()`, `initThemeToggle()`
### translations.js
- Übersetzungswörterbuch für DE/EN
- Export: `translations` Objekt
### i18n.js
- Internationalisierungsfunktionen
- Exports: `t()`, `applyLanguage()`, `updateUITexts()`
### lightbox.js
- Bildvorschau-Modal
- Exports: `openLightbox()`, `closeLightbox()`
### api-helpers.js
- API-Fetch mit Fehlerbehandlung
- Status-Anzeige
- Exports: `apiFetch()`, `setStatus()`
### file-manager.js
- Arbeitsblatt-Upload und -Verwaltung
- Eingang-Dateien laden
- Exports: `loadEingangFiles()`, `renderEingangList()`, usw.
### learning-units-module.js
- Lerneinheiten CRUD
- Arbeitsblatt-Zuordnung
- Exports: `loadLearningUnits()`, `addUnitFromForm()`, usw.
### mc-module.js
- Multiple Choice Generierung
- Quiz-Vorschau und Bewertung
- Exports: `generateMcQuestions()`, `renderMcPreview()`, usw.
### cloze-module.js
- Lückentext-Generierung
- Interaktive Ausfüllung
- Exports: `generateClozeTexts()`, `renderClozePreview()`, usw.
### mindmap-module.js
- Mindmap-Generierung
- SVG-Rendering
- Exports: `generateMindmap()`, `renderMindmapPreview()`, usw.
### qa-leitner-module.js
- Frage-Antwort-Generierung
- Leitner-System Integration
- Exports: `generateQaQuestions()`, `renderQaPreview()`, usw.
## Verwendung
```javascript
// Als ES6 Modul importieren
import { getCurrentTheme, setTheme, initThemeToggle } from './modules/theme.js';
import { t, applyLanguage } from './modules/i18n.js';
import { openLightbox, closeLightbox } from './modules/lightbox.js';
// ...
// Theme initialisieren
initThemeToggle();
// Übersetzung abrufen
const label = t('btn_create');
```
## TODO
Die Haupt-studio.js sollte aktualisiert werden, um diese Module zu importieren:
```javascript
// In studio.js
import * as Theme from './modules/theme.js';
import * as I18n from './modules/i18n.js';
import * as FileManager from './modules/file-manager.js';
// ...
```
## Statistiken
| Komponente | Zeilen |
|------------|--------|
| theme.js | 105 |
| translations.js | 971 |
| i18n.js | 250 |
| lightbox.js | 234 |
| api-helpers.js | 360 |
| file-manager.js | 614 |
| learning-units-module.js | 517 |
| mc-module.js | 474 |
| cloze-module.js | 430 |
| mindmap-module.js | 223 |
| qa-leitner-module.js | 444 |
| **Gesamt Module** | **4.622** |
| studio.js (Original) | 9.787 |
## Remaining to Extract (~5,165 lines)
The following sections remain in studio.js and should be extracted:
| Section | Lines | Target Module |
|---------|-------|---------------|
| GDPR Functions | ~150 | gdpr-module.js |
| Legal Modal | ~200 | legal-module.js |
| Authentication | ~450 | auth-module.js |
| Notifications | ~400 | notifications-module.js |
| Word Upload | ~140 | upload-module.js |
| Admin Documents | ~940 | admin/documents.js |
| Cookie Categories Admin | ~130 | admin/cookies.js |
| Admin Stats | ~170 | admin/stats.js |
| User Data Export | ~55 | admin/export.js |
| DSR Management | ~450 | admin/dsr.js |
| DSMS Functions | ~520 | dsms-module.js |
| Email Templates | ~400 | admin/email-templates.js |
| Communication Panel | ~2,140 | communication-module.js |
## Refactoring-Historie
**03.02.2026**: Refactoring status documented
- Existing modules cover ~47% of original studio.js (4,622 of 9,787 lines)
- Remaining ~5,165 lines identified for future extraction
- Build tooling (Webpack/Vite) recommended for ES6 module bundling
**19.01.2026**: Module aus studio.js extrahiert:
- Alle funktionalen Bereiche in separate ES6-Module aufgeteilt
- Module verwenden Export/Import-Syntax
- Original studio.js noch nicht aktualisiert (backward compatibility)

View File

@@ -0,0 +1,360 @@
/**
* BreakPilot Studio - API Helpers Module
*
* Gemeinsame Funktionen für API-Aufrufe und Status-Verwaltung:
* - fetchJSON: Wrapper für fetch mit Error-Handling
* - postJSON: POST-Requests mit JSON-Body
* - setStatus: Status-Leiste aktualisieren
* - showNotification: Toast-Benachrichtigungen
*
* Refactored: 2026-01-19
*/
import { t } from './i18n.js';
// Status-Bar Element-Referenzen (werden bei init gesetzt)
let statusBar = null;
let statusDot = null;
let statusMain = null;
let statusSub = null;
/**
* Initialisiert die Status-Bar Referenzen
* Sollte beim DOMContentLoaded aufgerufen werden
*/
export function initStatusBar() {
statusBar = document.getElementById('status-bar');
statusDot = document.getElementById('status-dot');
statusMain = document.getElementById('status-main');
statusSub = document.getElementById('status-sub');
}
/**
* Setzt den Status in der Status-Leiste
* @param {string} type - 'ready'|'working'|'success'|'error'
* @param {string} main - Haupttext
* @param {string} [sub] - Optionaler Untertext
*/
export function setStatus(type, main, sub = '') {
if (!statusBar || !statusDot || !statusMain) {
console.log(`[Status ${type}]: ${main}`, sub);
return;
}
// Alle Status-Klassen entfernen
statusBar.classList.remove('status-ready', 'status-working', 'status-success', 'status-error');
statusDot.classList.remove('dot-ready', 'dot-working', 'dot-success', 'dot-error');
// Neue Status-Klasse setzen
statusBar.classList.add(`status-${type}`);
statusDot.classList.add(`dot-${type}`);
// Texte setzen
statusMain.textContent = main;
if (statusSub) {
statusSub.textContent = sub;
}
}
/**
* Setzt den Status auf "Bereit"
*/
export function setStatusReady() {
setStatus('ready', t('status_ready') || 'Bereit', '');
}
/**
* Setzt den Status auf "Arbeitet..."
* @param {string} message - Was gerade gemacht wird
*/
export function setStatusWorking(message) {
setStatus('working', message, '');
}
/**
* Setzt den Status auf "Erfolg"
* @param {string} message - Erfolgsmeldung
* @param {string} [details] - Optionale Details
*/
export function setStatusSuccess(message, details = '') {
setStatus('success', message, details);
}
/**
* Setzt den Status auf "Fehler"
* @param {string} message - Fehlermeldung
* @param {string} [details] - Optionale Details
*/
export function setStatusError(message, details = '') {
setStatus('error', message, details);
}
/**
* Führt einen GET-Request aus und parst JSON
* @param {string} url - Die URL
* @param {Object} [options] - Zusätzliche fetch-Optionen
* @returns {Promise<any>} - Das geparste JSON
* @throws {Error} - Bei Netzwerk- oder Parse-Fehlern
*/
export async function fetchJSON(url, options = {}) {
const response = await fetch(url, {
...options,
headers: {
'Accept': 'application/json',
...options.headers,
},
});
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
return response.json();
}
/**
* Führt einen POST-Request mit JSON-Body aus
* @param {string} url - Die URL
* @param {Object} data - Die zu sendenden Daten
* @param {Object} [options] - Zusätzliche fetch-Optionen
* @returns {Promise<any>} - Das geparste JSON
* @throws {Error} - Bei Netzwerk- oder Parse-Fehlern
*/
export async function postJSON(url, data = {}, options = {}) {
const response = await fetch(url, {
method: 'POST',
...options,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...options.headers,
},
body: JSON.stringify(data),
});
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
return response.json();
}
/**
* Führt einen POST-Request ohne Body aus (für Trigger-Endpoints)
* @param {string} url - Die URL
* @param {Object} [options] - Zusätzliche fetch-Optionen
* @returns {Promise<any>} - Das geparste JSON
*/
export async function postTrigger(url, options = {}) {
const response = await fetch(url, {
method: 'POST',
...options,
headers: {
'Accept': 'application/json',
...options.headers,
},
});
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
return response.json();
}
/**
* Führt einen DELETE-Request aus
* @param {string} url - Die URL
* @param {Object} [options] - Zusätzliche fetch-Optionen
* @returns {Promise<any>} - Das geparste JSON
*/
export async function deleteRequest(url, options = {}) {
const response = await fetch(url, {
method: 'DELETE',
...options,
headers: {
'Accept': 'application/json',
...options.headers,
},
});
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
return response.json();
}
/**
* Lädt eine Datei hoch
* @param {string} url - Die Upload-URL
* @param {File|FormData} file - Die Datei oder FormData
* @param {function} [onProgress] - Progress-Callback (0-100)
* @returns {Promise<any>} - Das geparste JSON
*/
export async function uploadFile(url, file, onProgress = null) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', url);
if (onProgress && xhr.upload) {
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
onProgress(percent);
}
});
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
resolve(JSON.parse(xhr.responseText));
} catch (e) {
resolve({ status: 'OK', message: xhr.responseText });
}
} else {
reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
}
};
xhr.onerror = () => reject(new Error('Network error during upload'));
// FormData erstellen falls nötig
let formData;
if (file instanceof FormData) {
formData = file;
} else {
formData = new FormData();
formData.append('file', file);
}
xhr.send(formData);
});
}
/**
* Zeigt eine kurze Benachrichtigung (Toast)
* @param {string} message - Die Nachricht
* @param {string} [type='info'] - 'info'|'success'|'error'|'warning'
* @param {number} [duration=3000] - Anzeigedauer in ms
*/
export function showNotification(message, type = 'info', duration = 3000) {
// Prüfe ob Toast-Container existiert, sonst erstellen
let container = document.getElementById('toast-container');
if (!container) {
container = document.createElement('div');
container.id = 'toast-container';
container.style.cssText = 'position:fixed;top:16px;right:16px;z-index:10000;display:flex;flex-direction:column;gap:8px;';
document.body.appendChild(container);
}
// Toast erstellen
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.style.cssText = `
padding: 12px 16px;
border-radius: 8px;
background: var(--bp-card-bg, #1e293b);
color: var(--bp-text, #e2e8f0);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
font-size: 13px;
animation: slideIn 0.3s ease;
border-left: 4px solid ${type === 'success' ? '#22c55e' : type === 'error' ? '#ef4444' : type === 'warning' ? '#f59e0b' : '#3b82f6'};
`;
toast.textContent = message;
container.appendChild(toast);
// Nach duration entfernen
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, duration);
}
/**
* Wrapper für API-Aufrufe mit Status-Anzeige und Error-Handling
* @param {function} apiCall - Die async API-Funktion
* @param {Object} options - Optionen
* @param {string} options.workingMessage - Nachricht während des Ladens
* @param {string} options.successMessage - Nachricht bei Erfolg
* @param {string} options.errorMessage - Nachricht bei Fehler
* @returns {Promise<any>} - Das Ergebnis oder null bei Fehler
*/
export async function withStatus(apiCall, options = {}) {
const {
workingMessage = 'Wird geladen...',
successMessage = 'Erfolgreich',
errorMessage = 'Fehler',
} = options;
setStatusWorking(workingMessage);
try {
const result = await apiCall();
setStatusSuccess(successMessage);
return result;
} catch (error) {
console.error(errorMessage, error);
setStatusError(errorMessage, String(error.message || error));
return null;
}
}
/**
* Debounce-Funktion für häufige Events
* @param {function} func - Die zu debouncende Funktion
* @param {number} wait - Wartezeit in ms
* @returns {function} - Die gedebouncte Funktion
*/
export function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* Throttle-Funktion für Rate-Limiting
* @param {function} func - Die zu throttlende Funktion
* @param {number} limit - Minimaler Abstand in ms
* @returns {function} - Die gethrottlete Funktion
*/
export function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// CSS für Toast-Animationen (einmal injizieren)
if (typeof document !== 'undefined' && !document.getElementById('toast-styles')) {
const style = document.createElement('style');
style.id = 'toast-styles';
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
}

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

View File

@@ -0,0 +1,614 @@
/**
* BreakPilot Studio - File Manager Module
*
* Datei-Verwaltung für den Arbeitsblatt-Editor:
* - Laden und Rendern der Dateiliste
* - Upload von Dateien
* - Löschen von Dateien
* - Vorschau-Funktionen
* - Navigation zwischen Dateien
*
* Refactored: 2026-01-19
*/
import { t } from './i18n.js';
import { setStatus, setStatusWorking, setStatusError, setStatusSuccess, fetchJSON } from './api-helpers.js';
import { openLightbox } from './lightbox.js';
// State
let allEingangFiles = [];
let eingangFiles = [];
let currentIndex = 0;
let currentSelectedFile = null;
let worksheetPairs = {};
let allWorksheetPairs = {};
let showOnlyUnitFiles = false;
let currentUnitId = null;
// DOM References (werden bei init gesetzt)
let eingangListEl = null;
let eingangCountEl = null;
let previewContainer = null;
let fileInput = null;
let btnUploadInline = null;
/**
* Initialisiert den File Manager
* @param {Object} options - Konfiguration
*/
export function initFileManager(options = {}) {
eingangListEl = document.getElementById('eingang-list') || options.listEl;
eingangCountEl = document.getElementById('eingang-count') || options.countEl;
previewContainer = document.getElementById('preview-container') || options.previewEl;
fileInput = document.getElementById('file-input') || options.fileInput;
btnUploadInline = document.getElementById('btn-upload-inline') || options.uploadBtn;
// Upload-Button Event
if (btnUploadInline) {
btnUploadInline.addEventListener('click', handleUpload);
}
// Initial load
loadEingangFiles();
loadWorksheetPairs();
}
/**
* Setzt die aktuelle Lerneinheit
* @param {string} unitId - Die Unit-ID
*/
export function setCurrentUnit(unitId) {
currentUnitId = unitId;
}
/**
* Setzt den Filter für Lerneinheit-Dateien
* @param {boolean} show - Nur Unit-Dateien anzeigen
*/
export function setShowOnlyUnitFiles(show) {
showOnlyUnitFiles = show;
}
/**
* Gibt die aktuelle Dateiliste zurück
* @returns {string[]} - Liste der Dateinamen
*/
export function getFiles() {
return eingangFiles.slice();
}
/**
* Gibt den aktuellen Index zurück
* @returns {number}
*/
export function getCurrentIndex() {
return currentIndex;
}
/**
* Setzt den aktuellen Index
* @param {number} idx
*/
export function setCurrentIndex(idx) {
currentIndex = idx;
renderEingangList();
renderPreviewForCurrent();
}
/**
* Gibt den aktuell ausgewählten Dateinamen zurück
* @returns {string|null}
*/
export function getCurrentFile() {
return eingangFiles[currentIndex] || null;
}
/**
* Lädt die Dateien aus dem Eingang
*/
export async function loadEingangFiles() {
try {
const data = await fetchJSON('/api/eingang-dateien');
allEingangFiles = data.eingang || [];
eingangFiles = allEingangFiles.slice();
currentIndex = 0;
renderEingangList();
} catch (e) {
console.error('Fehler beim Laden der Dateien:', e);
setStatusError(t('error') || 'Fehler', String(e));
}
}
/**
* Lädt die Worksheet-Pairs (Original → Bereinigt)
*/
export async function loadWorksheetPairs() {
try {
const data = await fetchJSON('/api/worksheet-pairs');
allWorksheetPairs = {};
(data.pairs || []).forEach((p) => {
allWorksheetPairs[p.original] = { clean_html: p.clean_html, clean_image: p.clean_image };
});
worksheetPairs = { ...allWorksheetPairs };
renderPreviewForCurrent();
} catch (e) {
console.error('Fehler beim Laden der Neuaufbau-Daten:', e);
setStatusError(t('error') || 'Fehler', String(e));
}
}
/**
* Rendert die Dateiliste
*/
export function renderEingangList() {
if (!eingangListEl) return;
eingangListEl.innerHTML = '';
if (!eingangFiles.length) {
const li = document.createElement('li');
li.className = 'file-empty';
li.textContent = t('no_files') || 'Noch keine Dateien vorhanden.';
eingangListEl.appendChild(li);
if (eingangCountEl) {
eingangCountEl.textContent = '0 ' + (t('files') || 'Dateien');
}
return;
}
eingangFiles.forEach((filename, idx) => {
const li = document.createElement('li');
li.className = 'file-item';
if (idx === currentIndex) {
li.classList.add('active');
}
const nameSpan = document.createElement('span');
nameSpan.className = 'file-item-name';
nameSpan.textContent = filename;
const actionsSpan = document.createElement('span');
actionsSpan.style.display = 'flex';
actionsSpan.style.gap = '6px';
// Button: Aus Lerneinheit entfernen
const removeFromUnitBtn = document.createElement('span');
removeFromUnitBtn.className = 'file-item-delete';
removeFromUnitBtn.textContent = '✕';
removeFromUnitBtn.title = t('remove_from_unit') || 'Aus Lerneinheit entfernen';
removeFromUnitBtn.addEventListener('click', (ev) => {
ev.stopPropagation();
if (!currentUnitId) {
alert(t('select_unit_first') || 'Zum Entfernen bitte zuerst eine Lerneinheit auswählen.');
return;
}
const ok = confirm(t('confirm_remove_from_unit') || 'Dieses Arbeitsblatt aus der aktuellen Lerneinheit entfernen? Die Datei selbst bleibt erhalten.');
if (!ok) return;
removeWorksheetFromCurrentUnit(eingangFiles[idx]);
});
// Button: Datei komplett löschen
const deleteFileBtn = document.createElement('span');
deleteFileBtn.className = 'file-item-delete';
deleteFileBtn.textContent = '🗑️';
deleteFileBtn.title = t('delete_file') || 'Datei komplett löschen';
deleteFileBtn.style.color = '#ef4444';
deleteFileBtn.addEventListener('click', async (ev) => {
ev.stopPropagation();
const ok = confirm(t('confirm_delete_file') || `Datei "${eingangFiles[idx]}" wirklich komplett löschen? Diese Aktion kann nicht rückgängig gemacht werden.`);
if (!ok) return;
await deleteFileCompletely(eingangFiles[idx]);
});
actionsSpan.appendChild(removeFromUnitBtn);
actionsSpan.appendChild(deleteFileBtn);
li.appendChild(nameSpan);
li.appendChild(actionsSpan);
li.addEventListener('click', () => {
currentIndex = idx;
currentSelectedFile = filename;
renderEingangList();
renderPreviewForCurrent();
// Event für andere Module
window.dispatchEvent(new CustomEvent('fileSelected', {
detail: { filename, index: idx }
}));
});
eingangListEl.appendChild(li);
});
if (eingangCountEl) {
eingangCountEl.textContent = eingangFiles.length + ' ' + (eingangFiles.length === 1 ? (t('file') || 'Datei') : (t('files') || 'Dateien'));
}
}
/**
* Rendert die Vorschau für die aktuelle Datei
*/
export function renderPreviewForCurrent() {
if (!previewContainer) return;
if (!eingangFiles.length) {
const message = showOnlyUnitFiles && currentUnitId
? (t('no_files_in_unit') || 'Dieser Lerneinheit sind noch keine Arbeitsblätter zugeordnet.')
: (t('no_files') || 'Keine Dateien vorhanden.');
previewContainer.innerHTML = `<div class="preview-placeholder">${message}</div>`;
return;
}
if (currentIndex < 0) currentIndex = 0;
if (currentIndex >= eingangFiles.length) currentIndex = eingangFiles.length - 1;
const filename = eingangFiles[currentIndex];
const entry = worksheetPairs[filename] || { clean_html: null, clean_image: null };
renderPreview(entry, currentIndex);
}
/**
* Rendert die Vorschau (Original vs. Bereinigt)
* @param {Object} entry - Die Worksheet-Pair-Daten
* @param {number} index - Der Index
*/
function renderPreview(entry, index) {
if (!previewContainer) return;
previewContainer.innerHTML = '';
const wrapper = document.createElement('div');
wrapper.className = 'compare-wrapper';
// Original-Sektion
const originalSection = createPreviewSection(
t('original_scan') || 'Original-Scan',
t('old_left') || 'Alt (links)',
() => {
const img = document.createElement('img');
img.className = 'preview-img';
const imgSrc = '/preview-file/' + encodeURIComponent(eingangFiles[index]);
img.src = imgSrc;
img.alt = 'Original ' + eingangFiles[index];
img.addEventListener('dblclick', () => openLightbox(imgSrc, eingangFiles[index]));
return img;
}
);
// Bereinigt-Sektion
const cleanSection = createPreviewSection(
t('rebuilt_worksheet') || 'Neu aufgebautes Arbeitsblatt',
createPrintButton(),
() => {
if (entry.clean_image) {
const imgClean = document.createElement('img');
imgClean.className = 'preview-img';
const cleanSrc = '/preview-clean-file/' + encodeURIComponent(entry.clean_image);
imgClean.src = cleanSrc;
imgClean.alt = 'Neu aufgebaut ' + eingangFiles[index];
imgClean.addEventListener('dblclick', () => openLightbox(cleanSrc, eingangFiles[index] + ' (neu)'));
return imgClean;
} else if (entry.clean_html) {
const frame = document.createElement('iframe');
frame.className = 'clean-frame';
frame.src = '/api/clean-html/' + encodeURIComponent(entry.clean_html);
frame.title = t('rebuilt_worksheet') || 'Neu aufgebautes Arbeitsblatt';
frame.addEventListener('dblclick', () => {
window.open('/api/clean-html/' + encodeURIComponent(entry.clean_html), '_blank');
});
return frame;
} else {
const placeholder = document.createElement('div');
placeholder.className = 'preview-placeholder';
placeholder.textContent = t('no_rebuild_data') || 'Noch keine Neuaufbau-Daten vorhanden.';
return placeholder;
}
}
);
// Thumbnails in der Mitte
const thumbsColumn = document.createElement('div');
thumbsColumn.className = 'preview-thumbnails';
thumbsColumn.id = 'preview-thumbnails-middle';
renderThumbnailsInColumn(thumbsColumn);
wrapper.appendChild(originalSection);
wrapper.appendChild(thumbsColumn);
wrapper.appendChild(cleanSection);
// Navigation
const navDiv = createNavigationButtons();
wrapper.appendChild(navDiv);
previewContainer.appendChild(wrapper);
}
/**
* Erstellt eine Vorschau-Sektion
*/
function createPreviewSection(title, rightContent, contentFactory) {
const section = document.createElement('div');
section.className = 'compare-section';
const header = document.createElement('div');
header.className = 'compare-header';
const titleSpan = document.createElement('span');
titleSpan.textContent = title;
const rightSpan = document.createElement('span');
rightSpan.style.display = 'flex';
rightSpan.style.alignItems = 'center';
rightSpan.style.gap = '8px';
if (typeof rightContent === 'string') {
rightSpan.textContent = rightContent;
} else if (rightContent instanceof Node) {
rightSpan.appendChild(rightContent);
}
header.appendChild(titleSpan);
header.appendChild(rightSpan);
const body = document.createElement('div');
body.className = 'compare-body';
const inner = document.createElement('div');
inner.className = 'compare-body-inner';
const content = contentFactory();
inner.appendChild(content);
body.appendChild(inner);
section.appendChild(header);
section.appendChild(body);
return section;
}
/**
* Erstellt den Druck-Button
*/
function createPrintButton() {
const container = document.createElement('span');
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.gap = '8px';
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn btn-sm btn-ghost no-print';
btn.style.padding = '4px 10px';
btn.style.fontSize = '11px';
btn.textContent = '🖨️ ' + (t('print') || 'Drucken');
btn.addEventListener('click', () => {
const currentFile = eingangFiles[currentIndex];
if (!currentFile) {
alert(t('worksheet_no_data') || 'Keine Arbeitsblatt-Daten vorhanden.');
return;
}
window.open('/api/print-worksheet/' + encodeURIComponent(currentFile), '_blank');
});
const label = document.createElement('span');
label.textContent = t('new_right') || 'Neu (rechts)';
container.appendChild(btn);
container.appendChild(label);
return container;
}
/**
* Rendert Thumbnails in einer Spalte
*/
function renderThumbnailsInColumn(container) {
container.innerHTML = '';
if (eingangFiles.length <= 1) return;
const maxThumbs = 5;
let thumbCount = 0;
for (let i = 0; i < eingangFiles.length && thumbCount < maxThumbs; i++) {
if (i === currentIndex) continue;
const filename = eingangFiles[i];
const thumb = document.createElement('div');
thumb.className = 'preview-thumb';
const img = document.createElement('img');
img.src = '/preview-file/' + encodeURIComponent(filename);
img.alt = filename;
const label = document.createElement('div');
label.className = 'preview-thumb-label';
label.textContent = `${i + 1}`;
thumb.appendChild(img);
thumb.appendChild(label);
thumb.addEventListener('click', () => {
currentIndex = i;
renderEingangList();
renderPreviewForCurrent();
});
container.appendChild(thumb);
thumbCount++;
}
}
/**
* Erstellt die Navigations-Buttons
*/
function createNavigationButtons() {
const navDiv = document.createElement('div');
navDiv.className = 'preview-nav';
const prevBtn = document.createElement('button');
prevBtn.type = 'button';
prevBtn.textContent = '';
prevBtn.disabled = currentIndex === 0;
prevBtn.addEventListener('click', () => {
if (currentIndex > 0) {
currentIndex--;
renderEingangList();
renderPreviewForCurrent();
}
});
const nextBtn = document.createElement('button');
nextBtn.type = 'button';
nextBtn.textContent = '';
nextBtn.disabled = currentIndex >= eingangFiles.length - 1;
nextBtn.addEventListener('click', () => {
if (currentIndex < eingangFiles.length - 1) {
currentIndex++;
renderEingangList();
renderPreviewForCurrent();
}
});
const positionSpan = document.createElement('span');
positionSpan.textContent = `${currentIndex + 1} ${t('of') || 'von'} ${eingangFiles.length}`;
navDiv.appendChild(prevBtn);
navDiv.appendChild(positionSpan);
navDiv.appendChild(nextBtn);
return navDiv;
}
/**
* Handle File Upload
*/
async function handleUpload(ev) {
ev.preventDefault();
ev.stopPropagation();
if (!fileInput) return;
const files = fileInput.files;
if (!files || !files.length) {
alert(t('select_files_first') || 'Bitte erst Dateien auswählen.');
return;
}
const formData = new FormData();
for (const file of files) {
formData.append('files', file);
}
try {
setStatusWorking(t('uploading') || 'Upload läuft …');
const resp = await fetch('/api/upload-multi', {
method: 'POST',
body: formData,
});
if (!resp.ok) {
console.error('Upload-Fehler: HTTP', resp.status);
setStatusError(t('upload_error') || 'Fehler beim Upload', 'HTTP ' + resp.status);
return;
}
setStatusSuccess(t('upload_complete') || 'Upload abgeschlossen');
fileInput.value = '';
// Liste neu laden
await loadEingangFiles();
await loadWorksheetPairs();
} catch (e) {
console.error('Netzwerkfehler beim Upload', e);
setStatusError(t('network_error') || 'Netzwerkfehler', String(e));
}
}
/**
* Entfernt ein Arbeitsblatt aus der aktuellen Lerneinheit
* @param {string} filename - Der Dateiname
*/
async function removeWorksheetFromCurrentUnit(filename) {
if (!currentUnitId) return;
try {
setStatusWorking(t('removing_from_unit') || 'Entferne aus Lerneinheit...');
const resp = await fetch(`/api/units/${currentUnitId}/worksheets/${encodeURIComponent(filename)}`, {
method: 'DELETE'
});
if (!resp.ok) {
throw new Error('HTTP ' + resp.status);
}
setStatusSuccess(t('removed_from_unit') || 'Aus Lerneinheit entfernt');
await loadEingangFiles();
} catch (e) {
console.error('Fehler beim Entfernen:', e);
setStatusError(t('error') || 'Fehler', String(e));
}
}
/**
* Löscht eine Datei komplett
* @param {string} filename - Der Dateiname
*/
async function deleteFileCompletely(filename) {
try {
setStatusWorking(t('deleting_file') || 'Lösche Datei...');
const resp = await fetch('/api/delete-file/' + encodeURIComponent(filename), {
method: 'DELETE'
});
if (!resp.ok) {
throw new Error('HTTP ' + resp.status);
}
setStatusSuccess(t('file_deleted') || 'Datei gelöscht');
await loadEingangFiles();
await loadWorksheetPairs();
} catch (e) {
console.error('Fehler beim Löschen:', e);
setStatusError(t('error') || 'Fehler', String(e));
}
}
/**
* Navigiert zur nächsten Datei
*/
export function nextFile() {
if (currentIndex < eingangFiles.length - 1) {
currentIndex++;
renderEingangList();
renderPreviewForCurrent();
}
}
/**
* Navigiert zur vorherigen Datei
*/
export function prevFile() {
if (currentIndex > 0) {
currentIndex--;
renderEingangList();
renderPreviewForCurrent();
}
}
/**
* Aktualisiert die Worksheet-Pairs für einen bestimmten Dateinamen
* @param {string} filename
* @param {Object} pair
*/
export function updateWorksheetPair(filename, pair) {
worksheetPairs[filename] = pair;
allWorksheetPairs[filename] = pair;
if (eingangFiles[currentIndex] === filename) {
renderPreviewForCurrent();
}
}

View File

@@ -0,0 +1,250 @@
/**
* BreakPilot Studio - i18n Module
*
* Internationalisierungs-Funktionen:
* - t(key): Übersetzungsfunktion
* - setLanguage(lang): Sprache wechseln
* - applyLanguage(): UI-Texte aktualisieren
* - getCurrentLang(): Aktuelle Sprache abrufen
*
* Refactored: 2026-01-19
*/
import { translations, rtlLanguages, defaultLanguage, availableLanguages } from './translations.js';
// Aktuelle Sprache (aus localStorage oder Standard)
let currentLang = localStorage.getItem('bp_language') || defaultLanguage;
/**
* Übersetzungsfunktion
* @param {string} key - Übersetzungsschlüssel
* @returns {string} - Übersetzter Text oder Fallback
*/
export function t(key) {
const lang = translations[currentLang] || translations[defaultLanguage];
return lang[key] || translations[defaultLanguage][key] || key;
}
/**
* Aktuelle Sprache abrufen
* @returns {string} - Sprachcode (de, en, tr, etc.)
*/
export function getCurrentLang() {
return currentLang;
}
/**
* Prüft ob aktuelle Sprache RTL ist
* @returns {boolean}
*/
export function isRTL() {
return rtlLanguages.includes(currentLang);
}
/**
* Sprache wechseln
* @param {string} lang - Neuer Sprachcode
*/
export function setLanguage(lang) {
if (translations[lang]) {
currentLang = lang;
localStorage.setItem('bp_language', lang);
applyLanguage();
return true;
}
console.warn(`Language '${lang}' not available`);
return false;
}
/**
* Wendet die aktuelle Sprache auf alle UI-Elemente an
*/
export function applyLanguage() {
// RTL-Unterstützung
if (isRTL()) {
document.body.classList.add('rtl');
document.documentElement.setAttribute('dir', 'rtl');
} else {
document.body.classList.remove('rtl');
document.documentElement.setAttribute('dir', 'ltr');
}
// Alle Elemente mit data-i18n-Attribut aktualisieren
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
const translated = t(key);
// Verschiedene Element-Typen behandeln
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
el.placeholder = translated;
} else {
el.textContent = translated;
}
});
// Elemente mit data-i18n-title für Tooltips
document.querySelectorAll('[data-i18n-title]').forEach(el => {
const key = el.getAttribute('data-i18n-title');
el.title = t(key);
});
// Elemente mit data-i18n-value für value-Attribute
document.querySelectorAll('[data-i18n-value]').forEach(el => {
const key = el.getAttribute('data-i18n-value');
el.value = t(key);
});
// Custom Event für andere Module
window.dispatchEvent(new CustomEvent('languageChanged', {
detail: { language: currentLang }
}));
}
/**
* Aktualisiert spezifische UI-Texte (Legacy-Kompatibilität)
* Diese Funktion wird von älterem Code verwendet der direkt UI-IDs referenziert
*/
export function updateUITexts() {
// Sidebar
const sidebarAreas = document.querySelector('.sidebar h4');
if (sidebarAreas) sidebarAreas.textContent = t('sidebar_areas');
// Breadcrumb / Brand
const brandSub = document.querySelector('.brand-sub');
if (brandSub) brandSub.textContent = t('brand_sub');
// Tab Labels
const navCompare = document.getElementById('nav-compare');
if (navCompare) navCompare.textContent = t('nav_compare');
const navTiles = document.getElementById('nav-tiles');
if (navTiles) navTiles.textContent = t('nav_tiles');
// Buttons
const uploadBtn = document.getElementById('uploadBtn');
if (uploadBtn) {
const textSpan = uploadBtn.querySelector('.btn-text');
if (textSpan) textSpan.textContent = t('btn_upload');
}
const deleteBtn = document.getElementById('deleteBtn');
if (deleteBtn) {
const textSpan = deleteBtn.querySelector('.btn-text');
if (textSpan) textSpan.textContent = t('btn_delete');
}
// Card Headers
document.querySelectorAll('.card-header').forEach(header => {
const icon = header.querySelector('i');
const iconHTML = icon ? icon.outerHTML : '';
// Original / Cleaned Sections
if (header.closest('.scan-section')?.classList.contains('original-scan')) {
header.innerHTML = iconHTML + ' ' + t('original_scan');
} else if (header.closest('.scan-section')?.classList.contains('cleaned-scan')) {
header.innerHTML = iconHTML + ' ' + t('cleaned_version');
}
});
// Tiles - MC
const mcTile = document.querySelector('.mc-tile');
if (mcTile) {
const title = mcTile.querySelector('.tile-content h3');
if (title) title.textContent = t('mc_title');
const desc = mcTile.querySelector('.tile-content p');
if (desc) desc.textContent = t('mc_desc');
}
// Tiles - Cloze
const clozeTile = document.querySelector('.cloze-tile');
if (clozeTile) {
const title = clozeTile.querySelector('.tile-content h3');
if (title) title.textContent = t('cloze_title');
const desc = clozeTile.querySelector('.tile-content p');
if (desc) desc.textContent = t('cloze_desc');
}
// Tiles - Q&A
const qaTile = document.querySelector('.qa-tile');
if (qaTile) {
const title = qaTile.querySelector('.tile-content h3');
if (title) title.textContent = t('qa_title');
const desc = qaTile.querySelector('.tile-content p');
if (desc) desc.textContent = t('qa_desc');
}
// Tiles - Mindmap
const mindmapTile = document.querySelector('.mindmap-tile');
if (mindmapTile) {
const title = mindmapTile.querySelector('.tile-content h3');
if (title) title.textContent = t('mindmap_title');
const desc = mindmapTile.querySelector('.tile-content p');
if (desc) desc.textContent = t('mindmap_desc');
}
// Footer
const imprintLink = document.querySelector('footer a[href*="imprint"]');
if (imprintLink) imprintLink.textContent = t('imprint');
const privacyLink = document.querySelector('footer a[href*="privacy"]');
if (privacyLink) privacyLink.textContent = t('privacy');
const contactLink = document.querySelector('footer a[href*="contact"]');
if (contactLink) contactLink.textContent = t('contact');
// Process Button
const fullProcessBtn = document.getElementById('fullProcessBtn');
if (fullProcessBtn) {
const textSpan = fullProcessBtn.querySelector('.btn-text');
if (textSpan) textSpan.textContent = t('btn_full_process');
}
// Status Bar
const statusText = document.getElementById('statusText');
if (statusText && statusText.textContent === 'Bereit' || statusText?.textContent === 'Ready') {
statusText.textContent = t('status_ready');
}
}
/**
* Initialisiert das Sprachwahl-UI
* @param {string} containerId - ID des Containers für Sprachauswahl
*/
export function initLanguageSelector(containerId = 'language-selector') {
const container = document.getElementById(containerId);
if (!container) return;
// Dropdown erstellen
container.innerHTML = `
<select id="language-select" class="language-select">
${Object.entries(availableLanguages).map(([code, name]) =>
`<option value="${code}" ${code === currentLang ? 'selected' : ''}>${name}</option>`
).join('')}
</select>
`;
// Event Handler
const select = document.getElementById('language-select');
if (select) {
select.addEventListener('change', (e) => {
setLanguage(e.target.value);
});
}
}
/**
* Formatiert einen Status-Text mit Platzhaltern
* @param {string} key - Übersetzungsschlüssel
* @param {Object} vars - Variablen zum Einsetzen
* @returns {string} - Formatierter Text
*/
export function tFormat(key, vars = {}) {
let text = t(key);
Object.entries(vars).forEach(([k, v]) => {
text = text.replace(new RegExp(`\\{${k}\\}`, 'g'), v);
});
return text;
}
// Re-export für Convenience
export { translations, rtlLanguages, defaultLanguage, availableLanguages };

View File

@@ -0,0 +1,517 @@
/**
* BreakPilot Studio - Learning Units Module
*
* Lerneinheiten-Verwaltung:
* - Laden, Erstellen, Löschen von Lerneinheiten
* - Zuordnung von Arbeitsblättern zu Lerneinheiten
* - Filter für Lerneinheiten-spezifische Ansicht
*
* Refactored: 2026-01-19
*/
import { t } from './i18n.js';
import { setStatus } from './api-helpers.js';
// State
let units = [];
let currentUnitId = null;
let showOnlyUnitFiles = false;
// DOM References
let unitListEl = null;
let unitHeading1 = null;
let unitHeading2 = null;
let btnAddUnit = null;
let btnToggleFilter = null;
let btnAttachCurrentToLu = null;
// Form Inputs
let unitStudentInput = null;
let unitSubjectInput = null;
let unitGradeInput = null;
let unitTitleInput = null;
// Callbacks für File-Manager Integration
let getEingangFilesCallback = null;
let getAllEingangFilesCallback = null;
let getAllWorksheetPairsCallback = null;
let setFilteredDataCallback = null;
let renderListCallback = null;
let renderPreviewCallback = null;
let getCurrentWorksheetBasenameCallback = null;
/**
* Initialisiert das Learning Units Modul
* @param {Object} options - Konfiguration
*/
export function initLearningUnitsModule(options = {}) {
// DOM-Referenzen
unitListEl = document.getElementById('unit-list') || options.unitListEl;
unitHeading1 = document.getElementById('unit-heading-1') || options.unitHeading1;
unitHeading2 = document.getElementById('unit-heading-2') || options.unitHeading2;
btnAddUnit = document.getElementById('btn-add-unit') || options.btnAddUnit;
btnToggleFilter = document.getElementById('btn-toggle-filter') || options.btnToggleFilter;
btnAttachCurrentToLu = document.getElementById('btn-attach-current-to-lu') || options.btnAttachCurrentToLu;
// Form Inputs
unitStudentInput = document.getElementById('unit-student') || options.unitStudentInput;
unitSubjectInput = document.getElementById('unit-subject') || options.unitSubjectInput;
unitGradeInput = document.getElementById('unit-grade') || options.unitGradeInput;
unitTitleInput = document.getElementById('unit-title') || options.unitTitleInput;
// Callbacks
getEingangFilesCallback = options.getEingangFiles || (() => []);
getAllEingangFilesCallback = options.getAllEingangFiles || (() => []);
getAllWorksheetPairsCallback = options.getAllWorksheetPairs || (() => ({}));
setFilteredDataCallback = options.setFilteredData || (() => {});
renderListCallback = options.renderList || (() => {});
renderPreviewCallback = options.renderPreview || (() => {});
getCurrentWorksheetBasenameCallback = options.getCurrentWorksheetBasename || (() => null);
// Event-Listener
if (btnAddUnit) {
btnAddUnit.addEventListener('click', (ev) => {
ev.preventDefault();
addUnitFromForm();
});
}
if (btnAttachCurrentToLu) {
btnAttachCurrentToLu.addEventListener('click', (ev) => {
ev.preventDefault();
attachCurrentWorksheetToUnit();
});
}
if (btnToggleFilter) {
btnToggleFilter.addEventListener('click', () => {
showOnlyUnitFiles = !showOnlyUnitFiles;
if (showOnlyUnitFiles) {
btnToggleFilter.textContent = t('only_unit') || 'Nur Lerneinheit';
btnToggleFilter.classList.add('btn-primary');
} else {
btnToggleFilter.textContent = t('all_files') || 'Alle Dateien';
btnToggleFilter.classList.remove('btn-primary');
}
applyUnitFilter();
});
}
// Initial laden
loadLearningUnits();
}
/**
* Aktualisiert die Überschrift mit dem Namen der aktuellen Lerneinheit
* @param {Object|null} unit - Lerneinheit oder null
*/
function updateUnitHeading(unit = null) {
if (!unit && currentUnitId && units && units.length) {
unit = units.find((u) => u.id === currentUnitId) || null;
}
let text = t('no_unit_selected') || 'Keine Lerneinheit ausgewählt';
if (unit) {
const name = unit.label || unit.title || t('learning_unit') || 'Lerneinheit';
text = (t('learning_unit') || 'Lerneinheit') + ': ' + name;
}
if (unitHeading1) unitHeading1.textContent = text;
if (unitHeading2) unitHeading2.textContent = text;
}
/**
* Wendet den Lerneinheiten-Filter an
* Zeigt nur Dateien der aktuellen Lerneinheit oder alle Dateien
*/
export function applyUnitFilter() {
let unit = null;
if (currentUnitId && units && units.length) {
unit = units.find((u) => u.id === currentUnitId) || null;
}
const allEingangFiles = getAllEingangFilesCallback();
const allWorksheetPairs = getAllWorksheetPairsCallback();
// Wenn Filter deaktiviert ODER keine Lerneinheit ausgewählt -> alle Dateien anzeigen
if (!showOnlyUnitFiles || !unit || !Array.isArray(unit.worksheet_files) || unit.worksheet_files.length === 0) {
setFilteredDataCallback(allEingangFiles.slice(), { ...allWorksheetPairs }, 0);
renderListCallback();
renderPreviewCallback();
updateUnitHeading(unit);
return;
}
// Filter aktiv: nur Dateien der aktuellen Lerneinheit anzeigen
const allowed = new Set(unit.worksheet_files || []);
const filteredFiles = allEingangFiles.filter((f) => allowed.has(f));
const filteredPairs = {};
Object.keys(allWorksheetPairs).forEach((key) => {
if (allowed.has(key)) {
filteredPairs[key] = allWorksheetPairs[key];
}
});
setFilteredDataCallback(filteredFiles, filteredPairs, 0);
renderListCallback();
renderPreviewCallback();
updateUnitHeading(unit);
}
/**
* Lädt alle Lerneinheiten vom Server
*/
export async function loadLearningUnits() {
try {
const resp = await fetch('/api/learning-units/');
if (!resp.ok) {
console.error('Fehler beim Laden der Lerneinheiten', resp.status);
return;
}
units = await resp.json();
if (units.length && !currentUnitId) {
currentUnitId = units[0].id;
}
renderUnits();
applyUnitFilter();
} catch (e) {
console.error('Netzwerkfehler beim Laden der Lerneinheiten', e);
}
}
/**
* Rendert die Lerneinheiten-Liste
*/
export function renderUnits() {
if (!unitListEl) return;
unitListEl.innerHTML = '';
if (!units.length) {
const li = document.createElement('li');
li.className = 'unit-item';
li.textContent = t('no_units_yet') || 'Noch keine Lerneinheiten angelegt.';
unitListEl.appendChild(li);
updateUnitHeading(null);
return;
}
units.forEach((u) => {
const li = document.createElement('li');
li.className = 'unit-item';
if (u.id === currentUnitId) {
li.classList.add('active');
}
const contentDiv = document.createElement('div');
contentDiv.style.flex = '1';
contentDiv.style.minWidth = '0';
const titleEl = document.createElement('div');
titleEl.textContent = u.label || u.title || t('learning_unit') || 'Lerneinheit';
const metaEl = document.createElement('div');
metaEl.className = 'unit-item-meta';
const metaParts = [];
if (u.meta) {
metaParts.push(u.meta);
}
if (Array.isArray(u.worksheet_files)) {
metaParts.push((t('worksheets') || 'Blätter') + ': ' + u.worksheet_files.length);
}
metaEl.textContent = metaParts.join(' · ');
contentDiv.appendChild(titleEl);
contentDiv.appendChild(metaEl);
// Delete-Button
const deleteBtn = document.createElement('span');
deleteBtn.textContent = '🗑️';
deleteBtn.style.cursor = 'pointer';
deleteBtn.style.fontSize = '12px';
deleteBtn.style.color = '#ef4444';
deleteBtn.title = t('delete_unit') || 'Lerneinheit löschen';
deleteBtn.addEventListener('click', async (ev) => {
ev.stopPropagation();
const confirmMsg = t('confirm_delete_unit') || 'Lerneinheit "{name}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.';
const ok = confirm(confirmMsg.replace('{name}', u.label || u.title || ''));
if (!ok) return;
await deleteLearningUnit(u.id);
});
li.appendChild(contentDiv);
li.appendChild(deleteBtn);
li.addEventListener('click', () => {
currentUnitId = u.id;
renderUnits();
applyUnitFilter();
// Event für andere Module
window.dispatchEvent(new CustomEvent('unitSelected', { detail: { unitId: u.id, unit: u } }));
});
unitListEl.appendChild(li);
});
}
/**
* Erstellt eine neue Lerneinheit aus dem Formular
*/
export async function addUnitFromForm() {
const student = (unitStudentInput && unitStudentInput.value || '').trim();
const subject = (unitSubjectInput && unitSubjectInput.value || '').trim();
const grade = (unitGradeInput && unitGradeInput.value || '').trim();
const title = (unitTitleInput && unitTitleInput.value || '').trim();
if (!student && !subject && !title) {
alert(t('unit_form_empty') || 'Bitte mindestens einen Wert (Schüler/in, Fach oder Thema) eintragen.');
return;
}
const payload = {
student,
subject,
title,
grade,
};
try {
const resp = await fetch('/api/learning-units/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!resp.ok) {
console.error('Fehler beim Anlegen der Lerneinheit', resp.status);
alert(t('unit_create_error') || 'Lerneinheit konnte nicht angelegt werden.');
return;
}
const created = await resp.json();
units.push(created);
currentUnitId = created.id;
// Formular leeren
if (unitStudentInput) unitStudentInput.value = '';
if (unitSubjectInput) unitSubjectInput.value = '';
if (unitTitleInput) unitTitleInput.value = '';
if (unitGradeInput) unitGradeInput.value = '';
renderUnits();
applyUnitFilter();
// Event für andere Module
window.dispatchEvent(new CustomEvent('unitCreated', { detail: { unit: created } }));
} catch (e) {
console.error('Netzwerkfehler beim Anlegen der Lerneinheit', e);
alert(t('network_error') || 'Netzwerkfehler beim Anlegen der Lerneinheit.');
}
}
/**
* Ordnet das aktuelle Arbeitsblatt der aktuellen Lerneinheit zu
*/
export async function attachCurrentWorksheetToUnit() {
if (!currentUnitId) {
alert(t('select_unit_first') || 'Bitte zuerst eine Lerneinheit auswählen oder anlegen.');
return;
}
const basename = getCurrentWorksheetBasenameCallback();
if (!basename) {
alert(t('select_worksheet_first') || 'Bitte zuerst ein Arbeitsblatt im linken Bereich auswählen.');
return;
}
const payload = { worksheet_files: [basename] };
try {
const resp = await fetch(`/api/learning-units/${currentUnitId}/attach-worksheets`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!resp.ok) {
console.error('Fehler beim Zuordnen des Arbeitsblatts', resp.status);
alert(t('attach_error') || 'Arbeitsblatt konnte nicht zugeordnet werden.');
return;
}
const updated = await resp.json();
const idx = units.findIndex((u) => u.id === updated.id);
if (idx !== -1) {
units[idx] = updated;
}
renderUnits();
applyUnitFilter();
// Event für andere Module
window.dispatchEvent(new CustomEvent('worksheetAttached', { detail: { unitId: currentUnitId, filename: basename } }));
} catch (e) {
console.error('Netzwerkfehler beim Zuordnen des Arbeitsblatts', e);
alert(t('network_error') || 'Netzwerkfehler beim Zuordnen des Arbeitsblatts.');
}
}
/**
* Entfernt ein Arbeitsblatt aus der aktuellen Lerneinheit
* @param {string} filename - Dateiname des Arbeitsblatts
*/
export async function removeWorksheetFromCurrentUnit(filename) {
if (!currentUnitId) {
alert(t('select_unit_first') || 'Bitte zuerst eine Lerneinheit auswählen.');
return;
}
if (!filename) {
alert(t('error_no_filename') || 'Fehler: kein Dateiname übergeben.');
return;
}
const payload = { worksheet_file: filename };
try {
const resp = await fetch(`/api/learning-units/${currentUnitId}/remove-worksheet`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!resp.ok) {
console.error('Fehler beim Entfernen des Arbeitsblatts', resp.status);
alert(t('remove_worksheet_error') || 'Arbeitsblatt konnte nicht aus der Lerneinheit entfernt werden.');
return;
}
const updated = await resp.json();
const idx = units.findIndex((u) => u.id === updated.id);
if (idx !== -1) {
units[idx] = updated;
}
renderUnits();
applyUnitFilter();
// Event für andere Module
window.dispatchEvent(new CustomEvent('worksheetRemoved', { detail: { unitId: currentUnitId, filename } }));
} catch (e) {
console.error('Netzwerkfehler beim Entfernen des Arbeitsblatts', e);
alert(t('network_error') || 'Netzwerkfehler beim Entfernen des Arbeitsblatts.');
}
}
/**
* Löscht eine Lerneinheit
* @param {string} unitId - ID der Lerneinheit
*/
export async function deleteLearningUnit(unitId) {
if (!unitId) {
alert(t('error_no_unit_id') || 'Fehler: keine Lerneinheit-ID übergeben.');
return;
}
try {
setStatus(t('deleting_unit') || 'Lösche Lerneinheit …', '', 'busy');
const resp = await fetch(`/api/learning-units/${unitId}`, {
method: 'DELETE',
});
if (!resp.ok) {
console.error('Fehler beim Löschen der Lerneinheit', resp.status);
setStatus(t('delete_error') || 'Fehler beim Löschen', '', 'error');
alert(t('unit_delete_error') || 'Lerneinheit konnte nicht gelöscht werden.');
return;
}
const result = await resp.json();
if (result.status === 'deleted') {
setStatus(t('unit_deleted') || 'Lerneinheit gelöscht', '');
// Lerneinheit aus der lokalen Liste entfernen
units = units.filter((u) => u.id !== unitId);
// Wenn die gelöschte Einheit ausgewählt war, Auswahl zurücksetzen
if (currentUnitId === unitId) {
currentUnitId = units.length > 0 ? units[0].id : null;
}
renderUnits();
applyUnitFilter();
// Event für andere Module
window.dispatchEvent(new CustomEvent('unitDeleted', { detail: { unitId } }));
} else {
setStatus(t('error') || 'Fehler', t('unknown_error') || 'Unbekannter Fehler', 'error');
alert(t('unit_delete_error') || 'Fehler beim Löschen der Lerneinheit.');
}
} catch (e) {
console.error('Netzwerkfehler beim Löschen der Lerneinheit', e);
setStatus(t('network_error') || 'Netzwerkfehler', String(e), 'error');
alert(t('network_error') || 'Netzwerkfehler beim Löschen der Lerneinheit.');
}
}
// === Getter und Setter ===
/**
* Gibt alle Lerneinheiten zurück
* @returns {Array}
*/
export function getUnits() {
return units;
}
/**
* Gibt die aktuelle Lerneinheit-ID zurück
* @returns {string|null}
*/
export function getCurrentUnitId() {
return currentUnitId;
}
/**
* Setzt die aktuelle Lerneinheit-ID
* @param {string|null} unitId
*/
export function setCurrentUnitId(unitId) {
currentUnitId = unitId;
renderUnits();
applyUnitFilter();
}
/**
* Gibt zurück, ob der Filter aktiv ist
* @returns {boolean}
*/
export function getShowOnlyUnitFiles() {
return showOnlyUnitFiles;
}
/**
* Setzt den Filter-Status
* @param {boolean} value
*/
export function setShowOnlyUnitFiles(value) {
showOnlyUnitFiles = value;
if (btnToggleFilter) {
if (showOnlyUnitFiles) {
btnToggleFilter.textContent = t('only_unit') || 'Nur Lerneinheit';
btnToggleFilter.classList.add('btn-primary');
} else {
btnToggleFilter.textContent = t('all_files') || 'Alle Dateien';
btnToggleFilter.classList.remove('btn-primary');
}
}
applyUnitFilter();
}
/**
* Gibt die aktuelle Lerneinheit zurück
* @returns {Object|null}
*/
export function getCurrentUnit() {
if (!currentUnitId || !units.length) return null;
return units.find((u) => u.id === currentUnitId) || null;
}

View File

@@ -0,0 +1,234 @@
/**
* BreakPilot Studio - Lightbox Module
*
* Vollbild-Bildvorschau und Modal-Funktionen:
* - Lightbox für Arbeitsblatt-Vorschauen
* - Keyboard-Navigation (Escape zum Schließen)
* - Click-outside zum Schließen
*
* Refactored: 2026-01-19
*/
// DOM-Referenzen
let lightboxEl = null;
let lightboxImg = null;
let lightboxCaption = null;
let lightboxClose = null;
// Callback für Close-Event
let onCloseCallback = null;
/**
* Initialisiert die Lightbox
* Sucht nach Standard-IDs oder erstellt das DOM
*/
export function initLightbox() {
lightboxEl = document.getElementById('lightbox');
lightboxImg = document.getElementById('lightbox-img');
lightboxCaption = document.getElementById('lightbox-caption');
lightboxClose = document.getElementById('lightbox-close');
// Falls keine Lightbox im DOM, erstelle sie
if (!lightboxEl) {
createLightboxDOM();
}
// Event-Listener
if (lightboxClose) {
lightboxClose.addEventListener('click', closeLightbox);
}
if (lightboxEl) {
lightboxEl.addEventListener('click', (ev) => {
// Schließen bei Klick auf Hintergrund
if (ev.target === lightboxEl) {
closeLightbox();
}
});
}
// Escape-Taste
document.addEventListener('keydown', (ev) => {
if (ev.key === 'Escape' && isLightboxOpen()) {
closeLightbox();
}
});
}
/**
* Erstellt das Lightbox-DOM dynamisch
*/
function createLightboxDOM() {
lightboxEl = document.createElement('div');
lightboxEl.id = 'lightbox';
lightboxEl.className = 'lightbox hidden';
lightboxEl.innerHTML = `
<div class="lightbox-content">
<button class="lightbox-close" id="lightbox-close">Schließen ✕</button>
<img id="lightbox-img" class="lightbox-img" src="" alt="Vorschau">
<div id="lightbox-caption" class="lightbox-caption"></div>
</div>
`;
document.body.appendChild(lightboxEl);
// Referenzen aktualisieren
lightboxImg = document.getElementById('lightbox-img');
lightboxCaption = document.getElementById('lightbox-caption');
lightboxClose = document.getElementById('lightbox-close');
// CSS injizieren falls nicht vorhanden
if (!document.getElementById('lightbox-styles')) {
const style = document.createElement('style');
style.id = 'lightbox-styles';
style.textContent = `
.lightbox {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
opacity: 1;
transition: opacity 0.3s ease;
}
.lightbox.hidden {
display: none;
opacity: 0;
}
.lightbox-content {
position: relative;
max-width: 90%;
max-height: 90%;
}
.lightbox-img {
max-width: 100%;
max-height: 85vh;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
}
.lightbox-close {
position: absolute;
top: -40px;
right: 0;
background: transparent;
border: none;
color: white;
font-size: 14px;
cursor: pointer;
padding: 8px 16px;
border-radius: 4px;
transition: background 0.2s;
}
.lightbox-close:hover {
background: rgba(255, 255, 255, 0.1);
}
.lightbox-caption {
color: white;
text-align: center;
margin-top: 12px;
font-size: 14px;
}
`;
document.head.appendChild(style);
}
}
/**
* Öffnet die Lightbox mit einem Bild
* @param {string} src - Bild-URL
* @param {string} [caption] - Optionale Bildunterschrift
*/
export function openLightbox(src, caption = '') {
if (!src) {
console.warn('openLightbox: No image source provided');
return;
}
// Lazy-Init falls noch nicht initialisiert
if (!lightboxEl) {
initLightbox();
}
if (lightboxImg) {
lightboxImg.src = src;
lightboxImg.alt = caption || 'Vorschau';
}
if (lightboxCaption) {
lightboxCaption.textContent = caption;
}
if (lightboxEl) {
lightboxEl.classList.remove('hidden');
// Body-Scroll verhindern
document.body.style.overflow = 'hidden';
}
}
/**
* Schließt die Lightbox
*/
export function closeLightbox() {
if (lightboxEl) {
lightboxEl.classList.add('hidden');
// Body-Scroll wiederherstellen
document.body.style.overflow = '';
}
if (lightboxImg) {
lightboxImg.src = '';
}
// Callback ausführen
if (onCloseCallback) {
onCloseCallback();
}
}
/**
* Prüft ob die Lightbox geöffnet ist
* @returns {boolean}
*/
export function isLightboxOpen() {
return lightboxEl && !lightboxEl.classList.contains('hidden');
}
/**
* Setzt einen Callback für das Close-Event
* @param {function} callback
*/
export function onClose(callback) {
onCloseCallback = callback;
}
/**
* Wechselt das Bild in der offenen Lightbox
* @param {string} src - Neue Bild-URL
* @param {string} [caption] - Neue Bildunterschrift
*/
export function changeLightboxImage(src, caption = '') {
if (lightboxImg) {
lightboxImg.src = src;
lightboxImg.alt = caption || 'Vorschau';
}
if (lightboxCaption) {
lightboxCaption.textContent = caption;
}
}
/**
* Setzt den Text des Close-Buttons
* @param {string} text - Der neue Text
*/
export function setCloseButtonText(text) {
if (lightboxClose) {
lightboxClose.textContent = text;
}
}

View File

@@ -0,0 +1,474 @@
/**
* 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;
}

View File

@@ -0,0 +1,223 @@
/**
* BreakPilot Studio - Mindmap Module
*
* Mindmap/Lernplakat-Generierung aus Arbeitsblättern:
* - Generieren von Mindmaps aus analysierten Arbeitsblättern
* - Vorschau und Anzeige
* - Druck-Funktion (A4/A3)
*
* Refactored: 2026-01-19
*/
import { t } from './i18n.js';
import { setStatus, setStatusWorking, setStatusError, setStatusSuccess, fetchJSON } from './api-helpers.js';
// State
let currentMindmapData = null;
// DOM References
let mindmapPreview = null;
let mindmapBadge = null;
let btnMindmapGenerate = null;
let btnMindmapShow = null;
let btnMindmapPrint = null;
// Callback für aktuelle Datei
let getCurrentFileCallback = null;
/**
* Initialisiert das Mindmap-Modul
* @param {Object} options - Konfiguration
* @param {Function} options.getCurrentFile - Callback um die aktuelle Datei zu bekommen
*/
export function initMindmapModule(options = {}) {
getCurrentFileCallback = options.getCurrentFile || (() => null);
mindmapPreview = document.getElementById('mindmap-preview') || options.previewEl;
mindmapBadge = document.getElementById('mindmap-badge') || options.badgeEl;
btnMindmapGenerate = document.getElementById('btn-mindmap-generate') || options.generateBtn;
btnMindmapShow = document.getElementById('btn-mindmap-show') || options.showBtn;
btnMindmapPrint = document.getElementById('btn-mindmap-print') || options.printBtn;
// Event-Listener
if (btnMindmapGenerate) {
btnMindmapGenerate.addEventListener('click', generateMindmap);
}
if (btnMindmapShow) {
btnMindmapShow.addEventListener('click', openMindmapView);
}
if (btnMindmapPrint) {
btnMindmapPrint.addEventListener('click', openMindmapPrint);
}
// Event für Datei-Wechsel
window.addEventListener('fileSelected', (ev) => {
loadMindmapData();
});
}
/**
* Generiert eine Mindmap für die aktuelle Datei
*/
export async function generateMindmap() {
const currentFile = getCurrentFileCallback();
if (!currentFile) {
alert(t('select_file_first') || 'Bitte zuerst eine Datei auswählen.');
return;
}
try {
setStatusWorking(t('mindmap_generating') || 'Generiere Mindmap...');
if (mindmapBadge) {
mindmapBadge.textContent = t('generating') || 'Generiert...';
mindmapBadge.className = 'card-badge';
}
const resp = await fetch('/api/generate-mindmap/' + encodeURIComponent(currentFile), {
method: 'POST'
});
if (!resp.ok) {
throw new Error('HTTP ' + resp.status);
}
const data = await resp.json();
if (data.status === 'OK') {
if (mindmapBadge) {
mindmapBadge.textContent = t('ready') || 'Fertig';
mindmapBadge.className = 'card-badge badge-success';
}
setStatusSuccess(t('mindmap_generated') || 'Mindmap erstellt!');
// Lade Mindmap-Daten
await loadMindmapData();
} else if (data.status === 'NOT_FOUND') {
if (mindmapBadge) {
mindmapBadge.textContent = t('no_analysis') || 'Keine Analyse';
mindmapBadge.className = 'card-badge badge-error';
}
setStatusError(t('analyze_first') || 'Bitte zuerst analysieren (Neuaufbau starten)');
} else {
throw new Error(data.message || 'Fehler bei der Mindmap-Generierung');
}
} catch (err) {
console.error('Mindmap error:', err);
if (mindmapBadge) {
mindmapBadge.textContent = t('error') || 'Fehler';
mindmapBadge.className = 'card-badge badge-error';
}
setStatusError(t('error') || 'Fehler', err.message);
}
}
/**
* Lädt die Mindmap-Daten für die aktuelle Datei
*/
export async function loadMindmapData() {
const currentFile = getCurrentFileCallback();
if (!currentFile) {
if (mindmapPreview) mindmapPreview.innerHTML = '';
return;
}
try {
const resp = await fetch('/api/mindmap-data/' + encodeURIComponent(currentFile));
const data = await resp.json();
if (data.status === 'OK' && data.data) {
currentMindmapData = data.data;
renderMindmapPreview();
if (btnMindmapShow) btnMindmapShow.style.display = 'inline-block';
if (btnMindmapPrint) btnMindmapPrint.style.display = 'inline-block';
if (mindmapBadge) {
mindmapBadge.textContent = t('ready') || 'Fertig';
mindmapBadge.className = 'card-badge badge-success';
}
} else {
currentMindmapData = null;
if (mindmapPreview) mindmapPreview.innerHTML = '';
if (btnMindmapShow) btnMindmapShow.style.display = 'none';
if (btnMindmapPrint) btnMindmapPrint.style.display = 'none';
if (mindmapBadge) {
mindmapBadge.textContent = t('ready') || 'Bereit';
mindmapBadge.className = 'card-badge';
}
}
} catch (err) {
console.error('Error loading mindmap:', err);
}
}
/**
* Rendert die Mindmap-Vorschau
*/
function renderMindmapPreview() {
if (!mindmapPreview) return;
if (!currentMindmapData) {
mindmapPreview.innerHTML = '';
return;
}
const topic = currentMindmapData.topic || 'Thema';
const categories = currentMindmapData.categories || [];
const categoryCount = categories.length;
const termCount = categories.reduce((sum, cat) => sum + (cat.terms ? cat.terms.length : 0), 0);
mindmapPreview.innerHTML = `
<div style="margin-top:10px;padding:12px;background:linear-gradient(135deg,#f0f9ff,#e0f2fe);border-radius:10px;text-align:center;">
<div style="font-size:18px;font-weight:bold;color:#0369a1;margin-bottom:8px;">${escapeHtml(topic)}</div>
<div style="font-size:12px;color:#64748b;">
${categoryCount} ${t('categories') || 'Kategorien'} | ${termCount} ${t('terms') || 'Begriffe'}
</div>
</div>
`;
}
/**
* Öffnet die Mindmap-Ansicht (A4)
*/
export function openMindmapView() {
const currentFile = getCurrentFileCallback();
if (!currentFile) return;
window.open('/api/mindmap-html/' + encodeURIComponent(currentFile) + '?format=a4', '_blank');
}
/**
* Öffnet die Mindmap-Druckansicht (A3)
*/
export function openMindmapPrint() {
const currentFile = getCurrentFileCallback();
if (!currentFile) return;
window.open('/api/mindmap-html/' + encodeURIComponent(currentFile) + '?format=a3', '_blank');
}
/**
* Gibt die aktuellen Mindmap-Daten zurück
* @returns {Object|null}
*/
export function getMindmapData() {
return currentMindmapData;
}
/**
* Setzt die Mindmap-Daten
* @param {Object} data
*/
export function setMindmapData(data) {
currentMindmapData = data;
renderMindmapPreview();
}
/**
* Helper: HTML-Escape
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

View File

@@ -0,0 +1,444 @@
/**
* 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;
}

View File

@@ -0,0 +1,105 @@
/**
* BreakPilot Studio - Theme Module
*
* Dark/Light Mode Toggle-Funktionalität:
* - Speichert Präferenz in localStorage
* - Unterstützt data-theme Attribut auf <html>
*
* Refactored: 2026-01-19
*/
// Initialisiere Theme sofort beim Laden (IIFE)
(function initializeTheme() {
const savedTheme = localStorage.getItem('bp-theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
console.log('Initial theme set to:', savedTheme);
})();
/**
* Holt das aktuelle Theme
* @returns {string} - 'dark' oder 'light'
*/
export function getCurrentTheme() {
return document.documentElement.getAttribute('data-theme') || 'dark';
}
/**
* Setzt das Theme
* @param {string} theme - 'dark' oder 'light'
*/
export function setTheme(theme) {
if (theme !== 'dark' && theme !== 'light') {
console.warn(`Invalid theme: ${theme}. Use 'dark' or 'light'.`);
return;
}
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('bp-theme', theme);
// Custom Event für andere Module
window.dispatchEvent(new CustomEvent('themeChanged', {
detail: { theme }
}));
}
/**
* Wechselt zwischen Dark und Light Mode
* @returns {string} - Das neue Theme
*/
export function toggleTheme() {
const current = getCurrentTheme();
const newTheme = current === 'dark' ? 'light' : 'dark';
setTheme(newTheme);
return newTheme;
}
/**
* Initialisiert den Theme-Toggle-Button
* Sucht nach Elements mit IDs: theme-toggle, theme-icon, theme-label
*/
export function initThemeToggle() {
const toggle = document.getElementById('theme-toggle');
const icon = document.getElementById('theme-icon');
const label = document.getElementById('theme-label');
if (!toggle || !icon || !label) {
console.warn('Theme toggle elements not found (theme-toggle, theme-icon, theme-label)');
return;
}
function updateToggleUI(theme) {
if (theme === 'light') {
icon.textContent = '☀️';
label.textContent = 'Light';
} else {
icon.textContent = '🌙';
label.textContent = 'Dark';
}
}
// Initialize UI based on current theme
updateToggleUI(getCurrentTheme());
// Click-Handler
toggle.addEventListener('click', function() {
console.log('Theme toggle clicked');
const newTheme = toggleTheme();
console.log('Switched to:', newTheme);
updateToggleUI(newTheme);
});
}
/**
* Prüft ob Dark Mode aktiv ist
* @returns {boolean}
*/
export function isDarkMode() {
return getCurrentTheme() === 'dark';
}
/**
* Prüft ob Light Mode aktiv ist
* @returns {boolean}
*/
export function isLightMode() {
return getCurrentTheme() === 'light';
}

View File

@@ -0,0 +1,971 @@
/**
* BreakPilot Studio - Translations Module
*
* Enthält alle UI-Übersetzungen für 7 Sprachen:
* - de: Deutsch (Standard)
* - en: English
* - tr: Türkisch
* - ar: Arabisch (RTL)
* - ru: Russisch
* - uk: Ukrainisch
* - pl: Polnisch
*
* Refactored: 2026-01-19
*/
export const translations = {
de: {
// Navigation & Header
brand_sub: "Studio",
nav_compare: "Arbeitsblätter",
nav_tiles: "Lern-Kacheln",
login: "Login / Anmeldung",
mvp_local: "MVP · Lokal auf deinem Mac",
// Sidebar
sidebar_areas: "Bereiche",
sidebar_studio: "Arbeitsblatt Studio",
sidebar_active: "aktiv",
sidebar_parents: "Eltern-Kanal",
sidebar_soon: "demnächst",
sidebar_correction: "Korrektur / Noten",
sidebar_units: "Lerneinheiten (lokal)",
input_student: "Schüler/in",
input_subject: "Fach",
input_grade: "Klasse (z.B. 7a)",
input_unit_title: "Lerneinheit / Thema",
btn_create: "Anlegen",
btn_add_current: "Aktuelles Arbeitsblatt hinzufügen",
btn_filter_unit: "Nur Lerneinheit",
btn_filter_all: "Alle Dateien",
// Screen 1 - Compare
uploaded_worksheets: "Hochgeladene Arbeitsblätter",
files: "Dateien",
btn_upload: "Hochladen",
btn_delete: "Löschen",
original_scan: "Original-Scan",
cleaned_version: "Bereinigt (Handschrift entfernt)",
no_cleaned: "Noch keine bereinigte Version vorhanden.",
process_hint: "Klicke auf 'Verarbeiten', um das Arbeitsblatt zu analysieren und zu bereinigen.",
worksheet_print: "Drucken",
worksheet_no_data: "Keine Arbeitsblatt-Daten vorhanden.",
btn_full_process: "Verarbeiten (Analyse + Bereinigung + HTML)",
btn_original_generate: "Nur Original-HTML generieren",
// Screen 2 - Tiles
learning_unit: "Lerneinheit",
no_unit_selected: "Keine Lerneinheit ausgewählt",
// MC Tile
mc_title: "Multiple Choice Test",
mc_ready: "Bereit",
mc_generating: "Generiert...",
mc_done: "Fertig",
mc_error: "Fehler",
mc_desc: "Erzeugt passende MC-Aufgaben zur ursprünglichen Schwierigkeit (z. B. Klasse 7), ohne das Niveau zu verändern.",
mc_generate: "MC generieren",
mc_show: "Fragen anzeigen",
mc_quiz_title: "Multiple Choice Quiz",
mc_evaluate: "Auswerten",
mc_correct: "Richtig!",
mc_incorrect: "Leider falsch.",
mc_not_answered: "Nicht beantwortet. Richtig wäre:",
mc_result: "von",
mc_result_correct: "richtig",
mc_percent: "korrekt",
mc_no_questions: "Noch keine MC-Fragen für dieses Arbeitsblatt generiert.",
mc_print: "Drucken",
mc_print_with_answers: "Mit Lösungen drucken?",
// Cloze Tile
cloze_title: "Lückentext",
cloze_desc: "Erzeugt Lückentexte mit mehreren sinnvollen Lücken pro Satz. Inkl. Übersetzung für Eltern.",
cloze_translation: "Übersetzung:",
cloze_generate: "Lückentext generieren",
cloze_start: "Übung starten",
cloze_exercise_title: "Lückentext-Übung",
cloze_instruction: "Fülle die Lücken aus und klicke auf 'Prüfen'.",
cloze_check: "Prüfen",
cloze_show_answers: "Lösungen zeigen",
cloze_no_texts: "Noch keine Lückentexte für dieses Arbeitsblatt generiert.",
cloze_sentences: "Sätze",
cloze_gaps: "Lücken",
cloze_gaps_total: "Lücken gesamt",
cloze_with_gaps: "(mit Lücken)",
cloze_print: "Drucken",
cloze_print_with_answers: "Mit Lösungen drucken?",
// QA Tile
qa_title: "Frage-Antwort-Blatt",
qa_desc: "Frage-Antwort-Paare mit Leitner-Box System. Wiederholung nach Schwierigkeitsgrad.",
qa_generate: "Q&A generieren",
qa_learn: "Lernen starten",
qa_print: "Drucken",
qa_no_questions: "Noch keine Q&A für dieses Arbeitsblatt generiert.",
qa_box_new: "Neu",
qa_box_learning: "Gelernt",
qa_box_mastered: "Gefestigt",
qa_show_answer: "Antwort zeigen",
qa_your_answer: "Deine Antwort",
qa_type_answer: "Schreibe deine Antwort hier...",
qa_check_answer: "Antwort prüfen",
qa_correct_answer: "Richtige Antwort",
qa_self_evaluate: "War deine Antwort richtig?",
qa_no_answer: "(keine Antwort eingegeben)",
qa_correct: "Richtig",
qa_incorrect: "Falsch",
qa_key_terms: "Schlüsselbegriffe",
qa_session_correct: "Richtig",
qa_session_incorrect: "Falsch",
qa_session_complete: "Lernrunde abgeschlossen!",
qa_result_correct: "richtig",
qa_restart: "Nochmal lernen",
qa_print_with_answers: "Mit Lösungen drucken?",
question: "Frage",
answer: "Antwort",
status_generating_qa: "Generiere Q&A …",
status_qa_generated: "Q&A generiert",
// Common
close: "Schließen",
subject: "Fach",
grade: "Stufe",
questions: "Fragen",
worksheet: "Arbeitsblatt",
loading: "Lädt...",
error: "Fehler",
success: "Erfolgreich",
// Footer
imprint: "Impressum",
privacy: "Datenschutz",
contact: "Kontakt",
// Status messages
status_ready: "Bereit",
status_processing: "Verarbeitet...",
status_generating_mc: "Generiere MC-Fragen …",
status_generating_cloze: "Generiere Lückentexte …",
status_please_wait: "Bitte warten, KI arbeitet.",
status_mc_generated: "MC-Fragen generiert",
status_cloze_generated: "Lückentexte generiert",
status_files_created: "Dateien erstellt",
// Mindmap Tile
mindmap_title: "Mindmap Lernposter",
mindmap_desc: "Erstellt eine kindgerechte Mindmap mit dem Hauptthema in der Mitte und allen Fachbegriffen in farbigen Kategorien.",
mindmap_generate: "Mindmap erstellen",
mindmap_show: "Ansehen",
mindmap_print_a3: "A3 Drucken",
generating_mindmap: "Erstelle Mindmap...",
mindmap_generated: "Mindmap erstellt!",
no_analysis: "Keine Analyse",
analyze_first: "Bitte zuerst analysieren (Verarbeiten starten)",
categories: "Kategorien",
terms: "Begriffe",
},
tr: {
brand_sub: "Stüdyo",
nav_compare: "Çalışma Sayfaları",
nav_tiles: "Öğrenme Kartları",
login: "Giriş / Kayıt",
mvp_local: "MVP · Mac'inizde Yerel",
sidebar_areas: "Alanlar",
sidebar_studio: "Çalışma Sayfası Stüdyosu",
sidebar_active: "aktif",
sidebar_parents: "Ebeveyn Kanalı",
sidebar_soon: "yakında",
sidebar_correction: "Düzeltme / Notlar",
sidebar_units: "Öğrenme Birimleri (yerel)",
input_student: "Öğrenci",
input_subject: "Ders",
input_grade: "Sınıf (örn. 7a)",
input_unit_title: "Öğrenme Birimi / Konu",
btn_create: "Oluştur",
btn_add_current: "Mevcut çalışma sayfasını ekle",
btn_filter_unit: "Sadece Birim",
btn_filter_all: "Tüm Dosyalar",
uploaded_worksheets: "Yüklenen Çalışma Sayfaları",
files: "Dosya",
btn_upload: "Yükle",
btn_delete: "Sil",
original_scan: "Orijinal Tarama",
cleaned_version: "Temizlenmiş (El yazısı kaldırıldı)",
no_cleaned: "Henüz temizlenmiş sürüm yok.",
process_hint: "Çalışma sayfasını analiz etmek ve temizlemek için 'İşle'ye tıklayın.",
worksheet_print: "Yazdır",
worksheet_no_data: "Çalışma sayfası verisi yok.",
btn_full_process: "İşle (Analiz + Temizleme + HTML)",
btn_original_generate: "Sadece Orijinal HTML Oluştur",
learning_unit: "Öğrenme Birimi",
no_unit_selected: "Öğrenme birimi seçilmedi",
mc_title: "Çoktan Seçmeli Test",
mc_ready: "Hazır",
mc_generating: "Oluşturuluyor...",
mc_done: "Tamamlandı",
mc_error: "Hata",
mc_desc: "Orijinal zorluğa uygun (örn. 7. sınıf) çoktan seçmeli sorular oluşturur.",
mc_generate: "ÇS Oluştur",
mc_show: "Soruları Göster",
mc_quiz_title: "Çoktan Seçmeli Quiz",
mc_evaluate: "Değerlendir",
mc_correct: "Doğru!",
mc_incorrect: "Maalesef yanlış.",
mc_not_answered: "Cevaplanmadı. Doğru cevap:",
mc_result: "/",
mc_result_correct: "doğru",
mc_percent: "doğru",
mc_no_questions: "Bu çalışma sayfası için henüz ÇS sorusu oluşturulmadı.",
mc_print: "Yazdır",
mc_print_with_answers: "Cevaplarla yazdır?",
cloze_title: "Boşluk Doldurma",
cloze_desc: "Her cümlede birden fazla anlamlı boşluk içeren metinler oluşturur. Ebeveynler için çeviri dahil.",
cloze_translation: "Çeviri:",
cloze_generate: "Boşluk Metni Oluştur",
cloze_start: "Alıştırmayı Başlat",
cloze_exercise_title: "Boşluk Doldurma Alıştırması",
cloze_instruction: "Boşlukları doldurun ve 'Kontrol Et'e tıklayın.",
cloze_check: "Kontrol Et",
cloze_show_answers: "Cevapları Göster",
cloze_no_texts: "Bu çalışma sayfası için henüz boşluk metni oluşturulmadı.",
cloze_sentences: "Cümle",
cloze_gaps: "Boşluk",
cloze_gaps_total: "Toplam boşluk",
cloze_with_gaps: "(boşluklu)",
cloze_print: "Yazdır",
cloze_print_with_answers: "Cevaplarla yazdır?",
qa_title: "Soru-Cevap Sayfası",
qa_desc: "Leitner kutu sistemiyle soru-cevap çiftleri. Zorluk derecesine göre tekrar.",
qa_generate: "S&C Oluştur",
qa_learn: "Öğrenmeye Başla",
qa_print: "Yazdır",
qa_no_questions: "Bu çalışma sayfası için henüz S&C oluşturulmadı.",
qa_box_new: "Yeni",
qa_box_learning: "Öğreniliyor",
qa_box_mastered: "Pekiştirildi",
qa_show_answer: "Cevabı Göster",
qa_your_answer: "Senin Cevabın",
qa_type_answer: "Cevabını buraya yaz...",
qa_check_answer: "Cevabı Kontrol Et",
qa_correct_answer: "Doğru Cevap",
qa_self_evaluate: "Cevabın doğru muydu?",
qa_no_answer: "(cevap girilmedi)",
qa_correct: "Doğru",
qa_incorrect: "Yanlış",
qa_key_terms: "Anahtar Kavramlar",
qa_session_correct: "Doğru",
qa_session_incorrect: "Yanlış",
qa_session_complete: "Öğrenme turu tamamlandı!",
qa_result_correct: "doğru",
qa_restart: "Tekrar Öğren",
qa_print_with_answers: "Cevaplarla yazdır?",
question: "Soru",
answer: "Cevap",
status_generating_qa: "S&C oluşturuluyor…",
status_qa_generated: "S&C oluşturuldu",
close: "Kapat",
subject: "Ders",
grade: "Seviye",
questions: "Soru",
worksheet: "Çalışma Sayfası",
loading: "Yükleniyor...",
error: "Hata",
success: "Başarılı",
imprint: "Künye",
privacy: "Gizlilik",
contact: "İletişim",
status_ready: "Hazır",
status_processing: "İşleniyor...",
status_generating_mc: "ÇS soruları oluşturuluyor…",
status_generating_cloze: "Boşluk metinleri oluşturuluyor…",
status_please_wait: "Lütfen bekleyin, yapay zeka çalışıyor.",
status_mc_generated: "ÇS soruları oluşturuldu",
status_cloze_generated: "Boşluk metinleri oluşturuldu",
status_files_created: "dosya oluşturuldu",
mindmap_title: "Zihin Haritası Poster",
mindmap_desc: "Ana konuyu ortada ve tüm terimleri renkli kategorilerde gösteren çocuk dostu bir zihin haritası oluşturur.",
mindmap_generate: "Zihin Haritası Oluştur",
mindmap_show: "Görüntüle",
mindmap_print_a3: "A3 Yazdır",
generating_mindmap: "Zihin haritası oluşturuluyor...",
mindmap_generated: "Zihin haritası oluşturuldu!",
no_analysis: "Analiz yok",
analyze_first: "Lütfen önce analiz edin (İşle'ye tıklayın)",
categories: "Kategoriler",
terms: "Terimler",
},
ar: {
brand_sub: "ستوديو",
nav_compare: "أوراق العمل",
nav_tiles: "بطاقات التعلم",
login: "تسجيل الدخول / التسجيل",
mvp_local: "MVP · محلي على جهازك",
sidebar_areas: "الأقسام",
sidebar_studio: "استوديو أوراق العمل",
sidebar_active: "نشط",
sidebar_parents: "قناة الوالدين",
sidebar_soon: "قريباً",
sidebar_correction: "التصحيح / الدرجات",
sidebar_units: "وحدات التعلم (محلية)",
input_student: "الطالب/ة",
input_subject: "المادة",
input_grade: "الصف (مثل 7أ)",
input_unit_title: "وحدة التعلم / الموضوع",
btn_create: "إنشاء",
btn_add_current: "إضافة ورقة العمل الحالية",
btn_filter_unit: "الوحدة فقط",
btn_filter_all: "جميع الملفات",
uploaded_worksheets: "أوراق العمل المحملة",
files: "ملفات",
btn_upload: "تحميل",
btn_delete: "حذف",
original_scan: "المسح الأصلي",
cleaned_version: "منظف (تم إزالة الكتابة اليدوية)",
no_cleaned: "لا توجد نسخة منظفة بعد.",
process_hint: "انقر على 'معالجة' لتحليل وتنظيف ورقة العمل.",
worksheet_print: "طباعة",
worksheet_no_data: "لا توجد بيانات ورقة العمل.",
btn_full_process: "معالجة (تحليل + تنظيف + HTML)",
btn_original_generate: "إنشاء HTML الأصلي فقط",
learning_unit: "وحدة التعلم",
no_unit_selected: "لم يتم اختيار وحدة تعلم",
mc_title: "اختبار متعدد الخيارات",
mc_ready: "جاهز",
mc_generating: "جاري الإنشاء...",
mc_done: "تم",
mc_error: "خطأ",
mc_desc: "ينشئ أسئلة اختيار من متعدد تناسب مستوى الصعوبة الأصلي (مثل الصف 7).",
mc_generate: "إنشاء أسئلة",
mc_show: "عرض الأسئلة",
mc_quiz_title: "اختبار متعدد الخيارات",
mc_evaluate: "تقييم",
mc_correct: "صحيح!",
mc_incorrect: "للأسف خطأ.",
mc_not_answered: "لم تتم الإجابة. الإجابة الصحيحة:",
mc_result: "من",
mc_result_correct: "صحيح",
mc_percent: "صحيح",
mc_no_questions: "لم يتم إنشاء أسئلة بعد لورقة العمل هذه.",
mc_print: "طباعة",
mc_print_with_answers: "طباعة مع الإجابات؟",
cloze_title: "ملء الفراغات",
cloze_desc: "ينشئ نصوصاً بفراغات متعددة في كل جملة. يشمل الترجمة للوالدين.",
cloze_translation: "الترجمة:",
cloze_generate: "إنشاء نص الفراغات",
cloze_start: "بدء التمرين",
cloze_exercise_title: "تمرين ملء الفراغات",
cloze_instruction: "املأ الفراغات وانقر على 'تحقق'.",
cloze_check: "تحقق",
cloze_show_answers: "عرض الإجابات",
cloze_no_texts: "لم يتم إنشاء نصوص فراغات بعد لورقة العمل هذه.",
cloze_sentences: "جمل",
cloze_gaps: "فراغات",
cloze_gaps_total: "إجمالي الفراغات",
cloze_with_gaps: "(مع فراغات)",
cloze_print: "طباعة",
cloze_print_with_answers: "طباعة مع الإجابات؟",
qa_title: "ورقة الأسئلة والأجوبة",
qa_desc: "أزواج أسئلة وأجوبة مع نظام صندوق لايتنر. التكرار حسب الصعوبة.",
qa_generate: "إنشاء س&ج",
qa_learn: "بدء التعلم",
qa_print: "طباعة",
qa_no_questions: "لم يتم إنشاء س&ج بعد لورقة العمل هذه.",
qa_box_new: "جديد",
qa_box_learning: "قيد التعلم",
qa_box_mastered: "متقن",
qa_show_answer: "عرض الإجابة",
qa_your_answer: "إجابتك",
qa_type_answer: "اكتب إجابتك هنا...",
qa_check_answer: "تحقق من الإجابة",
qa_correct_answer: "الإجابة الصحيحة",
qa_self_evaluate: "هل كانت إجابتك صحيحة؟",
qa_no_answer: "(لم يتم إدخال إجابة)",
qa_correct: "صحيح",
qa_incorrect: "خطأ",
qa_key_terms: "المصطلحات الرئيسية",
qa_session_correct: "صحيح",
qa_session_incorrect: "خطأ",
qa_session_complete: "اكتملت جولة التعلم!",
qa_result_correct: "صحيح",
qa_restart: "تعلم مرة أخرى",
qa_print_with_answers: "طباعة مع الإجابات؟",
question: "سؤال",
answer: "إجابة",
status_generating_qa: "جاري إنشاء س&ج…",
status_qa_generated: "تم إنشاء س&ج",
close: "إغلاق",
subject: "المادة",
grade: "المستوى",
questions: "أسئلة",
worksheet: "ورقة العمل",
loading: "جاري التحميل...",
error: "خطأ",
success: "نجاح",
imprint: "البصمة",
privacy: "الخصوصية",
contact: "اتصل بنا",
status_ready: "جاهز",
status_processing: "جاري المعالجة...",
status_generating_mc: "جاري إنشاء الأسئلة…",
status_generating_cloze: "جاري إنشاء نصوص الفراغات…",
status_please_wait: "يرجى الانتظار، الذكاء الاصطناعي يعمل.",
status_mc_generated: "تم إنشاء الأسئلة",
status_cloze_generated: "تم إنشاء نصوص الفراغات",
status_files_created: "ملفات تم إنشاؤها",
mindmap_title: "ملصق خريطة ذهنية",
mindmap_desc: "ينشئ خريطة ذهنية مناسبة للأطفال مع الموضوع الرئيسي في المنتصف وجميع المصطلحات في فئات ملونة.",
mindmap_generate: "إنشاء خريطة ذهنية",
mindmap_show: "عرض",
mindmap_print_a3: "طباعة A3",
generating_mindmap: "جاري إنشاء الخريطة الذهنية...",
mindmap_generated: "تم إنشاء الخريطة الذهنية!",
no_analysis: "لا يوجد تحليل",
analyze_first: "يرجى التحليل أولاً (انقر على معالجة)",
categories: "الفئات",
terms: "المصطلحات",
},
ru: {
brand_sub: "Студия",
nav_compare: "Рабочие листы",
nav_tiles: "Учебные карточки",
login: "Вход / Регистрация",
mvp_local: "MVP · Локально на вашем Mac",
sidebar_areas: "Разделы",
sidebar_studio: "Студия рабочих листов",
sidebar_active: "активно",
sidebar_parents: "Канал для родителей",
sidebar_soon: "скоро",
sidebar_correction: "Проверка / Оценки",
sidebar_units: "Учебные блоки (локально)",
input_student: "Ученик",
input_subject: "Предмет",
input_grade: "Класс (напр. 7а)",
input_unit_title: "Учебный блок / Тема",
btn_create: "Создать",
btn_add_current: "Добавить текущий лист",
btn_filter_unit: "Только блок",
btn_filter_all: "Все файлы",
uploaded_worksheets: "Загруженные рабочие листы",
files: "файлов",
btn_upload: "Загрузить",
btn_delete: "Удалить",
original_scan: "Оригинальный скан",
cleaned_version: "Очищено (рукопись удалена)",
no_cleaned: "Очищенная версия пока недоступна.",
process_hint: "Нажмите 'Обработать' для анализа и очистки листа.",
worksheet_print: "Печать",
worksheet_no_data: "Нет данных рабочего листа.",
btn_full_process: "Обработать (Анализ + Очистка + HTML)",
btn_original_generate: "Только оригинальный HTML",
learning_unit: "Учебный блок",
no_unit_selected: "Блок не выбран",
mc_title: "Тест с выбором ответа",
mc_ready: "Готово",
mc_generating: "Создается...",
mc_done: "Готово",
mc_error: "Ошибка",
mc_desc: "Создает вопросы с выбором ответа соответствующей сложности (напр. 7 класс).",
mc_generate: "Создать тест",
mc_show: "Показать вопросы",
mc_quiz_title: "Тест с выбором ответа",
mc_evaluate: "Оценить",
mc_correct: "Правильно!",
mc_incorrect: "К сожалению, неверно.",
mc_not_answered: "Нет ответа. Правильный ответ:",
mc_result: "из",
mc_result_correct: "правильно",
mc_percent: "верно",
mc_no_questions: "Вопросы для этого листа еще не созданы.",
mc_print: "Печать",
mc_print_with_answers: "Печатать с ответами?",
cloze_title: "Текст с пропусками",
cloze_desc: "Создает тексты с несколькими пропусками в каждом предложении. Включая перевод для родителей.",
cloze_translation: "Перевод:",
cloze_generate: "Создать текст",
cloze_start: "Начать упражнение",
cloze_exercise_title: "Упражнение с пропусками",
cloze_instruction: "Заполните пропуски и нажмите 'Проверить'.",
cloze_check: "Проверить",
cloze_show_answers: "Показать ответы",
cloze_no_texts: "Тексты для этого листа еще не созданы.",
cloze_sentences: "предложений",
cloze_gaps: "пропусков",
cloze_gaps_total: "Всего пропусков",
cloze_with_gaps: "(с пропусками)",
cloze_print: "Печать",
cloze_print_with_answers: "Печатать с ответами?",
qa_title: "Лист вопросов и ответов",
qa_desc: "Пары вопрос-ответ с системой Лейтнера. Повторение по уровню сложности.",
qa_generate: "Создать В&О",
qa_learn: "Начать обучение",
qa_print: "Печать",
qa_no_questions: "В&О для этого листа еще не созданы.",
qa_box_new: "Новый",
qa_box_learning: "Изучается",
qa_box_mastered: "Освоено",
qa_show_answer: "Показать ответ",
qa_your_answer: "Твой ответ",
qa_type_answer: "Напиши свой ответ здесь...",
qa_check_answer: "Проверить ответ",
qa_correct_answer: "Правильный ответ",
qa_self_evaluate: "Твой ответ был правильным?",
qa_no_answer: "(ответ не введён)",
qa_correct: "Правильно",
qa_incorrect: "Неправильно",
qa_key_terms: "Ключевые термины",
qa_session_correct: "Правильно",
qa_session_incorrect: "Неправильно",
qa_session_complete: "Раунд обучения завершен!",
qa_result_correct: "правильно",
qa_restart: "Учить снова",
qa_print_with_answers: "Печатать с ответами?",
question: "Вопрос",
answer: "Ответ",
status_generating_qa: "Создание В&О…",
status_qa_generated: "В&О созданы",
close: "Закрыть",
subject: "Предмет",
grade: "Уровень",
questions: "вопросов",
worksheet: "Рабочий лист",
loading: "Загрузка...",
error: "Ошибка",
success: "Успешно",
imprint: "Импрессум",
privacy: "Конфиденциальность",
contact: "Контакт",
status_ready: "Готово",
status_processing: "Обработка...",
status_generating_mc: "Создание вопросов…",
status_generating_cloze: "Создание текстов…",
status_please_wait: "Пожалуйста, подождите, ИИ работает.",
status_mc_generated: "Вопросы созданы",
status_cloze_generated: "Тексты созданы",
status_files_created: "файлов создано",
mindmap_title: "Плакат Майнд-карта",
mindmap_desc: "Создает детскую ментальную карту с главной темой в центре и всеми терминами в цветных категориях.",
mindmap_generate: "Создать карту",
mindmap_show: "Просмотр",
mindmap_print_a3: "Печать A3",
generating_mindmap: "Создание карты...",
mindmap_generated: "Карта создана!",
no_analysis: "Нет анализа",
analyze_first: "Сначала выполните анализ (нажмите Обработать)",
categories: "Категории",
terms: "Термины",
},
uk: {
brand_sub: "Студія",
nav_compare: "Робочі аркуші",
nav_tiles: "Навчальні картки",
login: "Вхід / Реєстрація",
mvp_local: "MVP · Локально на вашому Mac",
sidebar_areas: "Розділи",
sidebar_studio: "Студія робочих аркушів",
sidebar_active: "активно",
sidebar_parents: "Канал для батьків",
sidebar_soon: "незабаром",
sidebar_correction: "Перевірка / Оцінки",
sidebar_units: "Навчальні блоки (локально)",
input_student: "Учень",
input_subject: "Предмет",
input_grade: "Клас (напр. 7а)",
input_unit_title: "Навчальний блок / Тема",
btn_create: "Створити",
btn_add_current: "Додати поточний аркуш",
btn_filter_unit: "Лише блок",
btn_filter_all: "Усі файли",
uploaded_worksheets: "Завантажені робочі аркуші",
files: "файлів",
btn_upload: "Завантажити",
btn_delete: "Видалити",
original_scan: "Оригінальний скан",
cleaned_version: "Очищено (рукопис видалено)",
no_cleaned: "Очищена версія ще недоступна.",
process_hint: "Натисніть 'Обробити' для аналізу та очищення аркуша.",
worksheet_print: "Друк",
worksheet_no_data: "Немає даних робочого аркуша.",
btn_full_process: "Обробити (Аналіз + Очищення + HTML)",
btn_original_generate: "Лише оригінальний HTML",
learning_unit: "Навчальний блок",
no_unit_selected: "Блок не вибрано",
mc_title: "Тест з вибором відповіді",
mc_ready: "Готово",
mc_generating: "Створюється...",
mc_done: "Готово",
mc_error: "Помилка",
mc_desc: "Створює питання з вибором відповіді відповідної складності (напр. 7 клас).",
mc_generate: "Створити тест",
mc_show: "Показати питання",
mc_quiz_title: "Тест з вибором відповіді",
mc_evaluate: "Оцінити",
mc_correct: "Правильно!",
mc_incorrect: "На жаль, неправильно.",
mc_not_answered: "Немає відповіді. Правильна відповідь:",
mc_result: "з",
mc_result_correct: "правильно",
mc_percent: "вірно",
mc_no_questions: "Питання для цього аркуша ще не створені.",
mc_print: "Друк",
mc_print_with_answers: "Друкувати з відповідями?",
cloze_title: "Текст з пропусками",
cloze_desc: "Створює тексти з кількома пропусками в кожному реченні. Включаючи переклад для батьків.",
cloze_translation: "Переклад:",
cloze_generate: "Створити текст",
cloze_start: "Почати вправу",
cloze_exercise_title: "Вправа з пропусками",
cloze_instruction: "Заповніть пропуски та натисніть 'Перевірити'.",
cloze_check: "Перевірити",
cloze_show_answers: "Показати відповіді",
cloze_no_texts: "Тексти для цього аркуша ще не створені.",
cloze_sentences: "речень",
cloze_gaps: "пропусків",
cloze_gaps_total: "Всього пропусків",
cloze_with_gaps: "(з пропусками)",
cloze_print: "Друк",
cloze_print_with_answers: "Друкувати з відповідями?",
qa_title: "Аркуш питань і відповідей",
qa_desc: "Пари питання-відповідь з системою Лейтнера. Повторення за рівнем складності.",
qa_generate: "Створити П&В",
qa_learn: "Почати навчання",
qa_print: "Друк",
qa_no_questions: "П&В для цього аркуша ще не створені.",
qa_box_new: "Новий",
qa_box_learning: "Вивчається",
qa_box_mastered: "Засвоєно",
qa_show_answer: "Показати відповідь",
qa_your_answer: "Твоя відповідь",
qa_type_answer: "Напиши свою відповідь тут...",
qa_check_answer: "Перевірити відповідь",
qa_correct_answer: "Правильна відповідь",
qa_self_evaluate: "Твоя відповідь була правильною?",
qa_no_answer: "(відповідь не введена)",
qa_correct: "Правильно",
qa_incorrect: "Неправильно",
qa_key_terms: "Ключові терміни",
qa_session_correct: "Правильно",
qa_session_incorrect: "Неправильно",
qa_session_complete: "Раунд навчання завершено!",
qa_result_correct: "правильно",
qa_restart: "Вчити знову",
qa_print_with_answers: "Друкувати з відповідями?",
question: "Питання",
answer: "Відповідь",
status_generating_qa: "Створення П&В…",
status_qa_generated: "П&В створені",
close: "Закрити",
subject: "Предмет",
grade: "Рівень",
questions: "питань",
worksheet: "Робочий аркуш",
loading: "Завантаження...",
error: "Помилка",
success: "Успішно",
imprint: "Імпресум",
privacy: "Конфіденційність",
contact: "Контакт",
status_ready: "Готово",
status_processing: "Обробка...",
status_generating_mc: "Створення питань…",
status_generating_cloze: "Створення текстів…",
status_please_wait: "Будь ласка, зачекайте, ШІ працює.",
status_mc_generated: "Питання створені",
status_cloze_generated: "Тексти створені",
status_files_created: "файлів створено",
mindmap_title: "Плакат Інтелект-карта",
mindmap_desc: "Створює дитячу інтелект-карту з головною темою в центрі та всіма термінами в кольорових категоріях.",
mindmap_generate: "Створити карту",
mindmap_show: "Переглянути",
mindmap_print_a3: "Друк A3",
generating_mindmap: "Створення карти...",
mindmap_generated: "Карту створено!",
no_analysis: "Немає аналізу",
analyze_first: "Спочатку виконайте аналіз (натисніть Обробити)",
categories: "Категорії",
terms: "Терміни",
},
pl: {
brand_sub: "Studio",
nav_compare: "Karty pracy",
nav_tiles: "Karty nauki",
login: "Logowanie / Rejestracja",
mvp_local: "MVP · Lokalnie na Twoim Mac",
sidebar_areas: "Sekcje",
sidebar_studio: "Studio kart pracy",
sidebar_active: "aktywne",
sidebar_parents: "Kanał dla rodziców",
sidebar_soon: "wkrótce",
sidebar_correction: "Korekta / Oceny",
sidebar_units: "Jednostki nauki (lokalnie)",
input_student: "Uczeń",
input_subject: "Przedmiot",
input_grade: "Klasa (np. 7a)",
input_unit_title: "Jednostka nauki / Temat",
btn_create: "Utwórz",
btn_add_current: "Dodaj bieżącą kartę",
btn_filter_unit: "Tylko jednostka",
btn_filter_all: "Wszystkie pliki",
uploaded_worksheets: "Przesłane karty pracy",
files: "plików",
btn_upload: "Prześlij",
btn_delete: "Usuń",
original_scan: "Oryginalny skan",
cleaned_version: "Oczyszczone (pismo ręczne usunięte)",
no_cleaned: "Oczyszczona wersja jeszcze niedostępna.",
process_hint: "Kliknij 'Przetwórz', aby przeanalizować i oczyścić kartę.",
worksheet_print: "Drukuj",
worksheet_no_data: "Brak danych arkusza.",
btn_full_process: "Przetwórz (Analiza + Czyszczenie + HTML)",
btn_original_generate: "Tylko oryginalny HTML",
learning_unit: "Jednostka nauki",
no_unit_selected: "Nie wybrano jednostki",
mc_title: "Test wielokrotnego wyboru",
mc_ready: "Gotowe",
mc_generating: "Tworzenie...",
mc_done: "Gotowe",
mc_error: "Błąd",
mc_desc: "Tworzy pytania wielokrotnego wyboru o odpowiednim poziomie trudności (np. klasa 7).",
mc_generate: "Utwórz test",
mc_show: "Pokaż pytania",
mc_quiz_title: "Test wielokrotnego wyboru",
mc_evaluate: "Oceń",
mc_correct: "Dobrze!",
mc_incorrect: "Niestety źle.",
mc_not_answered: "Brak odpowiedzi. Poprawna odpowiedź:",
mc_result: "z",
mc_result_correct: "poprawnie",
mc_percent: "poprawnie",
mc_no_questions: "Pytania dla tej karty jeszcze nie zostały utworzone.",
mc_print: "Drukuj",
mc_print_with_answers: "Drukować z odpowiedziami?",
cloze_title: "Tekst z lukami",
cloze_desc: "Tworzy teksty z wieloma lukami w każdym zdaniu. W tym tłumaczenie dla rodziców.",
cloze_translation: "Tłumaczenie:",
cloze_generate: "Utwórz tekst",
cloze_start: "Rozpocznij ćwiczenie",
cloze_exercise_title: "Ćwiczenie z lukami",
cloze_instruction: "Wypełnij luki i kliknij 'Sprawdź'.",
cloze_check: "Sprawdź",
cloze_show_answers: "Pokaż odpowiedzi",
cloze_no_texts: "Teksty dla tej karty jeszcze nie zostały utworzone.",
cloze_sentences: "zdań",
cloze_gaps: "luk",
cloze_gaps_total: "Łącznie luk",
cloze_with_gaps: "(z lukami)",
cloze_print: "Drukuj",
cloze_print_with_answers: "Drukować z odpowiedziami?",
qa_title: "Arkusz pytań i odpowiedzi",
qa_desc: "Pary pytanie-odpowiedź z systemem Leitnera. Powtórki według poziomu trudności.",
qa_generate: "Utwórz P&O",
qa_learn: "Rozpocznij naukę",
qa_print: "Drukuj",
qa_no_questions: "P&O dla tej karty jeszcze nie zostały utworzone.",
qa_box_new: "Nowy",
qa_box_learning: "W nauce",
qa_box_mastered: "Opanowane",
qa_show_answer: "Pokaż odpowiedź",
qa_your_answer: "Twoja odpowiedź",
qa_type_answer: "Napisz swoją odpowiedź tutaj...",
qa_check_answer: "Sprawdź odpowiedź",
qa_correct_answer: "Prawidłowa odpowiedź",
qa_self_evaluate: "Czy twoja odpowiedź była poprawna?",
qa_no_answer: "(nie wprowadzono odpowiedzi)",
qa_correct: "Dobrze",
qa_incorrect: "Źle",
qa_key_terms: "Kluczowe pojęcia",
qa_session_correct: "Dobrze",
qa_session_incorrect: "Źle",
qa_session_complete: "Runda nauki zakończona!",
qa_result_correct: "poprawnie",
qa_restart: "Ucz się ponownie",
qa_print_with_answers: "Drukować z odpowiedziami?",
question: "Pytanie",
answer: "Odpowiedź",
status_generating_qa: "Tworzenie P&O…",
status_qa_generated: "P&O utworzone",
close: "Zamknij",
subject: "Przedmiot",
grade: "Poziom",
questions: "pytań",
worksheet: "Karta pracy",
loading: "Ładowanie...",
error: "Błąd",
success: "Sukces",
imprint: "Impressum",
privacy: "Prywatność",
contact: "Kontakt",
status_ready: "Gotowe",
status_processing: "Przetwarzanie...",
status_generating_mc: "Tworzenie pytań…",
status_generating_cloze: "Tworzenie tekstów…",
status_please_wait: "Proszę czekać, AI pracuje.",
status_mc_generated: "Pytania utworzone",
status_cloze_generated: "Teksty utworzone",
status_files_created: "plików utworzono",
mindmap_title: "Plakat Mapa myśli",
mindmap_desc: "Tworzy przyjazną dla dzieci mapę myśli z głównym tematem w centrum i wszystkimi terminami w kolorowych kategoriach.",
mindmap_generate: "Utwórz mapę",
mindmap_show: "Podgląd",
mindmap_print_a3: "Drukuj A3",
generating_mindmap: "Tworzenie mapy...",
mindmap_generated: "Mapa utworzona!",
no_analysis: "Brak analizy",
analyze_first: "Najpierw wykonaj analizę (kliknij Przetwórz)",
categories: "Kategorie",
terms: "Terminy",
},
en: {
brand_sub: "Studio",
nav_compare: "Worksheets",
nav_tiles: "Learning Tiles",
login: "Login / Sign Up",
mvp_local: "MVP · Local on your Mac",
sidebar_areas: "Areas",
sidebar_studio: "Worksheet Studio",
sidebar_active: "active",
sidebar_parents: "Parents Channel",
sidebar_soon: "coming soon",
sidebar_correction: "Correction / Grades",
sidebar_units: "Learning Units (local)",
input_student: "Student",
input_subject: "Subject",
input_grade: "Grade (e.g. 7a)",
input_unit_title: "Learning Unit / Topic",
btn_create: "Create",
btn_add_current: "Add current worksheet",
btn_filter_unit: "Unit only",
btn_filter_all: "All files",
uploaded_worksheets: "Uploaded Worksheets",
files: "files",
btn_upload: "Upload",
btn_delete: "Delete",
original_scan: "Original Scan",
cleaned_version: "Cleaned (handwriting removed)",
no_cleaned: "No cleaned version available yet.",
process_hint: "Click 'Process' to analyze and clean the worksheet.",
worksheet_print: "Print",
worksheet_no_data: "No worksheet data available.",
btn_full_process: "Process (Analysis + Cleaning + HTML)",
btn_original_generate: "Generate Original HTML Only",
learning_unit: "Learning Unit",
no_unit_selected: "No unit selected",
mc_title: "Multiple Choice Test",
mc_ready: "Ready",
mc_generating: "Generating...",
mc_done: "Done",
mc_error: "Error",
mc_desc: "Creates multiple choice questions matching the original difficulty level (e.g. Grade 7).",
mc_generate: "Generate MC",
mc_show: "Show Questions",
mc_quiz_title: "Multiple Choice Quiz",
mc_evaluate: "Evaluate",
mc_correct: "Correct!",
mc_incorrect: "Unfortunately wrong.",
mc_not_answered: "Not answered. Correct answer:",
mc_result: "of",
mc_result_correct: "correct",
mc_percent: "correct",
mc_no_questions: "No MC questions generated yet for this worksheet.",
mc_print: "Print",
mc_print_with_answers: "Print with answers?",
cloze_title: "Fill in the Blanks",
cloze_desc: "Creates texts with multiple meaningful gaps per sentence. Including translation for parents.",
cloze_translation: "Translation:",
cloze_generate: "Generate Cloze Text",
cloze_start: "Start Exercise",
cloze_exercise_title: "Fill in the Blanks Exercise",
cloze_instruction: "Fill in the blanks and click 'Check'.",
cloze_check: "Check",
cloze_show_answers: "Show Answers",
cloze_no_texts: "No cloze texts generated yet for this worksheet.",
cloze_sentences: "sentences",
cloze_gaps: "gaps",
cloze_gaps_total: "Total gaps",
cloze_with_gaps: "(with gaps)",
cloze_print: "Print",
cloze_print_with_answers: "Print with answers?",
qa_title: "Question & Answer Sheet",
qa_desc: "Q&A pairs with Leitner box system. Spaced repetition by difficulty level.",
qa_generate: "Generate Q&A",
qa_learn: "Start Learning",
qa_print: "Print",
qa_no_questions: "No Q&A generated yet for this worksheet.",
qa_box_new: "New",
qa_box_learning: "Learning",
qa_box_mastered: "Mastered",
qa_show_answer: "Show Answer",
qa_your_answer: "Your Answer",
qa_type_answer: "Write your answer here...",
qa_check_answer: "Check Answer",
qa_correct_answer: "Correct Answer",
qa_self_evaluate: "Was your answer correct?",
qa_no_answer: "(no answer entered)",
qa_correct: "Correct",
qa_incorrect: "Incorrect",
qa_key_terms: "Key Terms",
qa_session_correct: "Correct",
qa_session_incorrect: "Incorrect",
qa_session_complete: "Learning session complete!",
qa_result_correct: "correct",
qa_restart: "Learn Again",
qa_print_with_answers: "Print with answers?",
question: "Question",
answer: "Answer",
status_generating_qa: "Generating Q&A…",
status_qa_generated: "Q&A generated",
close: "Close",
subject: "Subject",
grade: "Level",
questions: "questions",
worksheet: "Worksheet",
loading: "Loading...",
error: "Error",
success: "Success",
imprint: "Imprint",
privacy: "Privacy",
contact: "Contact",
status_ready: "Ready",
status_processing: "Processing...",
status_generating_mc: "Generating MC questions…",
status_generating_cloze: "Generating cloze texts…",
status_please_wait: "Please wait, AI is working.",
status_mc_generated: "MC questions generated",
status_cloze_generated: "Cloze texts generated",
status_files_created: "files created",
mindmap_title: "Mindmap Learning Poster",
mindmap_desc: "Creates a child-friendly mindmap with the main topic in the center and all terms in colorful categories.",
mindmap_generate: "Create Mindmap",
mindmap_show: "View",
mindmap_print_a3: "Print A3",
generating_mindmap: "Creating mindmap...",
mindmap_generated: "Mindmap created!",
no_analysis: "No analysis",
analyze_first: "Please analyze first (click Process)",
categories: "Categories",
terms: "Terms",
}
};
// RTL-Sprachen (Right-to-Left)
export const rtlLanguages = ['ar'];
// Standard-Sprache
export const defaultLanguage = 'de';
// Verfügbare Sprachen mit Labels
export const availableLanguages = {
de: 'Deutsch',
en: 'English',
tr: 'Türkçe',
ar: 'العربية',
ru: 'Русский',
uk: 'Українська',
pl: 'Polski'
};