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