This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/frontend/modules/unit_creator.py
Benjamin Admin 21a844cb8a 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>
2026-02-09 09:51:32 +01:00

2142 lines
65 KiB
Python

"""
BreakPilot Studio - Unit Creator Module
Ermoeglicht Lehrern das Erstellen und Bearbeiten von Learning Units.
Features:
- Metadaten-Editor (Template, Fach, Klassenstufe)
- Stop-Editor mit Drag & Drop
- Interaktionstyp-Konfiguration
- Validierung in Echtzeit
- JSON-Import/Export
"""
class UnitCreatorModule:
"""Unit Creator Modul fuer das BreakPilot Studio."""
@staticmethod
def get_css() -> str:
"""CSS fuer Unit Creator."""
return """
/* ==============================================
UNIT CREATOR MODULE STYLES
============================================== */
#panel-unit-creator {
padding: 24px;
display: none;
flex-direction: column;
gap: 20px;
}
/* Header */
.uc-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 16px;
border-bottom: 1px solid var(--bp-border);
}
.uc-title {
font-size: 1.5rem;
font-weight: 700;
}
.uc-actions {
display: flex;
gap: 12px;
}
/* Tabs */
.uc-tabs {
display: flex;
gap: 4px;
background: var(--bp-surface-elevated);
padding: 4px;
border-radius: 8px;
width: fit-content;
}
.uc-tab {
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
color: var(--bp-text-muted);
background: transparent;
border: none;
transition: all 0.2s;
}
.uc-tab:hover {
color: var(--bp-text);
}
.uc-tab.active {
background: var(--bp-primary);
color: white;
}
/* Tab Content */
.uc-content {
flex: 1;
overflow-y: auto;
}
.uc-tab-panel {
display: none;
}
.uc-tab-panel.active {
display: block;
}
/* Form Styles */
.uc-form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.uc-form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.uc-form-group.full-width {
grid-column: span 2;
}
.uc-form-row {
display: flex;
gap: 12px;
align-items: flex-start;
}
.uc-params-section {
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 8px;
padding: 16px;
}
.uc-params-title {
font-weight: 600;
font-size: 14px;
color: var(--bp-primary);
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--bp-border);
}
.uc-label {
font-size: 13px;
font-weight: 500;
color: var(--bp-text);
}
.uc-input,
.uc-select,
.uc-textarea {
padding: 10px 12px;
border-radius: 8px;
border: 1px solid var(--bp-border);
background: var(--bp-surface-elevated);
color: var(--bp-text);
font-size: 14px;
font-family: inherit;
}
.uc-input:focus,
.uc-select:focus,
.uc-textarea:focus {
outline: none;
border-color: var(--bp-primary);
}
.uc-textarea {
min-height: 100px;
resize: vertical;
}
/* Checkbox Group */
.uc-checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.uc-checkbox-item {
display: flex;
align-items: center;
gap: 6px;
}
.uc-checkbox-item input {
width: 18px;
height: 18px;
accent-color: var(--bp-primary);
}
/* Radio Group */
.uc-radio-group {
display: flex;
gap: 16px;
}
.uc-radio-item {
display: flex;
align-items: center;
gap: 6px;
}
.uc-radio-item input {
width: 18px;
height: 18px;
accent-color: var(--bp-primary);
}
/* Stops List */
.uc-stops-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.uc-stop-card {
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border);
border-radius: 12px;
padding: 16px;
cursor: grab;
}
.uc-stop-card:active {
cursor: grabbing;
}
.uc-stop-card.dragging {
opacity: 0.5;
border-color: var(--bp-primary);
}
.uc-stop-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.uc-stop-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
.uc-stop-order {
width: 24px;
height: 24px;
background: var(--bp-primary-soft);
color: var(--bp-primary);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
}
.uc-stop-actions {
display: flex;
gap: 8px;
}
.uc-stop-btn {
padding: 4px 8px;
border-radius: 4px;
border: 1px solid var(--bp-border);
background: transparent;
color: var(--bp-text-muted);
font-size: 12px;
cursor: pointer;
}
.uc-stop-btn:hover {
background: var(--bp-surface);
color: var(--bp-text);
}
.uc-stop-btn.delete:hover {
background: rgba(239, 68, 68, 0.1);
color: var(--bp-danger);
border-color: var(--bp-danger);
}
.uc-stop-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.uc-stop-field {
display: flex;
flex-direction: column;
gap: 4px;
}
.uc-stop-field.full {
grid-column: span 2;
}
.uc-stop-field label {
font-size: 11px;
color: var(--bp-text-muted);
text-transform: uppercase;
}
/* Add Stop Button */
.uc-add-stop {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 16px;
border: 2px dashed var(--bp-border);
border-radius: 12px;
background: transparent;
color: var(--bp-text-muted);
cursor: pointer;
transition: all 0.2s;
}
.uc-add-stop:hover {
border-color: var(--bp-primary);
color: var(--bp-primary);
background: var(--bp-primary-soft);
}
/* JSON Editor */
.uc-json-editor {
width: 100%;
min-height: 400px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 12px;
line-height: 1.5;
padding: 16px;
border-radius: 8px;
border: 1px solid var(--bp-border);
background: #1e1e1e;
color: #d4d4d4;
resize: vertical;
}
/* Preview */
.uc-preview {
background: var(--bp-surface-elevated);
border-radius: 12px;
padding: 20px;
}
.uc-preview-header {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--bp-border);
}
.uc-preview-flow {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.uc-preview-stop {
padding: 8px 12px;
background: var(--bp-primary-soft);
color: var(--bp-primary);
border-radius: 8px;
font-size: 12px;
font-weight: 500;
}
.uc-preview-arrow {
color: var(--bp-text-muted);
}
/* Validation */
.uc-validation {
padding: 16px;
border-radius: 8px;
margin-top: 16px;
}
.uc-validation.valid {
background: rgba(34, 197, 94, 0.1);
border: 1px solid var(--bp-success);
}
.uc-validation.invalid {
background: rgba(239, 68, 68, 0.1);
border: 1px solid var(--bp-danger);
}
.uc-validation-title {
font-weight: 600;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.uc-validation-list {
font-size: 13px;
list-style: none;
padding-left: 24px;
}
.uc-validation-list li {
margin-bottom: 4px;
}
.uc-validation-list li.error {
color: var(--bp-danger);
}
.uc-validation-list li.warning {
color: var(--bp-warning);
}
/* Footer */
.uc-footer {
display: flex;
justify-content: space-between;
padding-top: 16px;
border-top: 1px solid var(--bp-border);
}
/* Multi-Input */
.uc-multi-input {
display: flex;
flex-direction: column;
gap: 8px;
}
.uc-multi-input-items {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.uc-multi-input-item {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 4px;
font-size: 12px;
}
.uc-multi-input-item button {
background: none;
border: none;
color: var(--bp-text-muted);
cursor: pointer;
padding: 0;
font-size: 14px;
}
.uc-multi-input-item button:hover {
color: var(--bp-danger);
}
.uc-multi-input-add {
display: flex;
gap: 8px;
}
.uc-multi-input-add input {
flex: 1;
}
/* Empty State */
.uc-empty {
text-align: center;
padding: 40px;
color: var(--bp-text-muted);
}
.uc-empty-icon {
font-size: 48px;
margin-bottom: 12px;
}
/* ==========================================
WIZARD - Interaktive Bedienungsanleitung
========================================== */
.uc-wizard-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 9998;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
}
.uc-wizard-overlay.active {
opacity: 1;
visibility: visible;
}
.uc-wizard-highlight {
position: absolute;
box-shadow: 0 0 0 4px var(--bp-primary), 0 0 0 9999px rgba(0, 0, 0, 0.7);
border-radius: 8px;
z-index: 9999;
pointer-events: none;
transition: all 0.4s ease;
}
.uc-wizard-tooltip {
position: fixed;
background: white;
border-radius: 12px;
padding: 24px;
max-width: 400px;
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
z-index: 10000;
animation: ucWizardFadeIn 0.3s ease;
}
@keyframes ucWizardFadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.uc-wizard-step {
font-size: 12px;
color: var(--bp-primary);
font-weight: 600;
margin-bottom: 8px;
}
.uc-wizard-title {
font-size: 18px;
font-weight: 700;
color: #1a1a1a;
margin-bottom: 12px;
}
.uc-wizard-text {
font-size: 14px;
line-height: 1.6;
color: #444;
margin-bottom: 20px;
}
.uc-wizard-actions {
display: flex;
gap: 12px;
justify-content: space-between;
align-items: center;
}
.uc-wizard-btn {
padding: 10px 20px;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.uc-wizard-btn-primary {
background: var(--bp-primary);
color: white;
}
.uc-wizard-btn-primary:hover {
background: var(--bp-primary-hover);
}
.uc-wizard-btn-secondary {
background: #f0f0f0;
color: #333;
}
.uc-wizard-btn-secondary:hover {
background: #e0e0e0;
}
.uc-wizard-btn-skip {
background: transparent;
color: #888;
font-size: 13px;
}
.uc-wizard-btn-skip:hover {
color: #555;
}
.uc-wizard-progress {
display: flex;
gap: 6px;
margin-top: 16px;
}
.uc-wizard-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ddd;
}
.uc-wizard-dot.active {
background: var(--bp-primary);
}
.uc-wizard-dot.done {
background: var(--bp-primary);
opacity: 0.5;
}
/* Wizard Start Button */
.uc-wizard-start-btn {
position: fixed;
bottom: 24px;
right: 24px;
background: var(--bp-primary);
color: white;
border: none;
padding: 12px 20px;
border-radius: 30px;
font-weight: 500;
cursor: pointer;
box-shadow: 0 4px 12px rgba(108, 27, 27, 0.3);
z-index: 1000;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
}
.uc-wizard-start-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(108, 27, 27, 0.4);
}
"""
@staticmethod
def get_html() -> str:
"""HTML fuer Unit Creator Panel."""
return """
<!-- UNIT CREATOR PANEL -->
<div id="panel-unit-creator" class="uc-panel">
<!-- Header -->
<div class="uc-header">
<h2 class="uc-title">Unit Creator</h2>
<div class="uc-actions">
<button class="btn btn-ghost" onclick="ucValidate()">Validieren</button>
<button class="btn btn-ghost" onclick="ucImportJson()">JSON Import</button>
<button class="btn btn-ghost" onclick="ucExportJson()">JSON Export</button>
</div>
</div>
<!-- Tabs -->
<div class="uc-tabs">
<button class="uc-tab active" data-tab="metadata" onclick="ucSwitchTab('metadata')">Metadaten</button>
<button class="uc-tab" data-tab="stops" onclick="ucSwitchTab('stops')">Stops</button>
<button class="uc-tab" data-tab="checks" onclick="ucSwitchTab('checks')">Pre/Post-Check</button>
<button class="uc-tab" data-tab="preview" onclick="ucSwitchTab('preview')">Vorschau</button>
<button class="uc-tab" data-tab="json" onclick="ucSwitchTab('json')">JSON</button>
</div>
<!-- Tab Content -->
<div class="uc-content">
<!-- Metadata Tab -->
<div id="uc-tab-metadata" class="uc-tab-panel active">
<div class="uc-form-grid">
<div class="uc-form-group">
<label class="uc-label">Unit ID *</label>
<input type="text" class="uc-input" id="uc-unit-id" placeholder="z.B. bio_eye_lightpath_v1" onchange="ucUpdateField('unit_id', this.value)">
</div>
<div class="uc-form-group">
<label class="uc-label">Template *</label>
<select class="uc-select" id="uc-template" onchange="ucUpdateField('template', this.value)">
<option value="">-- Waehlen --</option>
<option value="flight_path">Flight Path (Lineare Reise)</option>
<option value="station_loop">Station Loop (Hub mit Stationen)</option>
</select>
</div>
<div class="uc-form-group">
<label class="uc-label">Fach</label>
<input type="text" class="uc-input" id="uc-subject" placeholder="z.B. Biologie" onchange="ucUpdateField('subject', this.value)">
</div>
<div class="uc-form-group">
<label class="uc-label">Thema</label>
<input type="text" class="uc-input" id="uc-topic" placeholder="z.B. Das Auge" onchange="ucUpdateField('topic', this.value)">
</div>
<div class="uc-form-group">
<label class="uc-label">Klassenstufen</label>
<div class="uc-checkbox-group">
<label class="uc-checkbox-item"><input type="checkbox" value="2" onchange="ucToggleGrade('2')"> 2</label>
<label class="uc-checkbox-item"><input type="checkbox" value="3" onchange="ucToggleGrade('3')"> 3</label>
<label class="uc-checkbox-item"><input type="checkbox" value="4" onchange="ucToggleGrade('4')"> 4</label>
<label class="uc-checkbox-item"><input type="checkbox" value="5" onchange="ucToggleGrade('5')" checked> 5</label>
<label class="uc-checkbox-item"><input type="checkbox" value="6" onchange="ucToggleGrade('6')" checked> 6</label>
<label class="uc-checkbox-item"><input type="checkbox" value="7" onchange="ucToggleGrade('7')" checked> 7</label>
<label class="uc-checkbox-item"><input type="checkbox" value="8" onchange="ucToggleGrade('8')"> 8</label>
</div>
</div>
<div class="uc-form-group">
<label class="uc-label">Schwierigkeit</label>
<div class="uc-radio-group">
<label class="uc-radio-item"><input type="radio" name="difficulty" value="base" checked onchange="ucUpdateField('difficulty', 'base')"> Basis</label>
<label class="uc-radio-item"><input type="radio" name="difficulty" value="advanced" onchange="ucUpdateField('difficulty', 'advanced')"> Fortgeschritten</label>
</div>
</div>
<div class="uc-form-group">
<label class="uc-label">Dauer (Minuten): <span id="uc-duration-value">8</span></label>
<input type="range" class="uc-input" id="uc-duration" min="3" max="20" value="8" oninput="ucUpdateDuration(this.value)">
</div>
<div class="uc-form-group">
<label class="uc-label">Version</label>
<input type="text" class="uc-input" id="uc-version" value="1.0.0" onchange="ucUpdateField('version', this.value)">
</div>
<div class="uc-form-group full-width">
<label class="uc-label">Lernziele</label>
<div class="uc-multi-input" id="uc-objectives-container">
<div class="uc-multi-input-items" id="uc-objectives-items"></div>
<div class="uc-multi-input-add">
<input type="text" class="uc-input" id="uc-objective-input" placeholder="Lernziel eingeben...">
<button class="btn btn-ghost" onclick="ucAddObjective()">+</button>
</div>
</div>
</div>
</div>
</div>
<!-- Stops Tab -->
<div id="uc-tab-stops" class="uc-tab-panel">
<div class="uc-stops-list" id="uc-stops-list">
<!-- Stops werden dynamisch eingefuegt -->
</div>
<button class="uc-add-stop" onclick="ucAddStop()">
<span>+</span>
<span>Stop hinzufuegen</span>
</button>
</div>
<!-- Pre/Post-Check Tab -->
<div id="uc-tab-checks" class="uc-tab-panel">
<div class="uc-form-grid">
<div class="uc-form-group full-width">
<h3 style="margin-bottom: 16px;">Pre-Check</h3>
</div>
<div class="uc-form-group">
<label class="uc-label">Fragen-Set ID</label>
<input type="text" class="uc-input" id="uc-precheck-id" placeholder="z.B. bio_eye_precheck_v1" onchange="ucUpdatePrecheck('question_set_id', this.value)">
</div>
<div class="uc-form-group">
<label class="uc-label">Zeit-Limit (Sekunden)</label>
<input type="number" class="uc-input" id="uc-precheck-time" value="120" onchange="ucUpdatePrecheck('time_limit_seconds', parseInt(this.value))">
</div>
<div class="uc-form-group">
<label class="uc-checkbox-item">
<input type="checkbox" id="uc-precheck-required" checked onchange="ucUpdatePrecheck('required', this.checked)">
Pre-Check erforderlich
</label>
</div>
<div class="uc-form-group full-width" style="margin-top: 24px;">
<h3 style="margin-bottom: 16px;">Post-Check</h3>
</div>
<div class="uc-form-group">
<label class="uc-label">Fragen-Set ID</label>
<input type="text" class="uc-input" id="uc-postcheck-id" placeholder="z.B. bio_eye_postcheck_v1" onchange="ucUpdatePostcheck('question_set_id', this.value)">
</div>
<div class="uc-form-group">
<label class="uc-label">Zeit-Limit (Sekunden)</label>
<input type="number" class="uc-input" id="uc-postcheck-time" value="180" onchange="ucUpdatePostcheck('time_limit_seconds', parseInt(this.value))">
</div>
<div class="uc-form-group">
<label class="uc-checkbox-item">
<input type="checkbox" id="uc-postcheck-required" checked onchange="ucUpdatePostcheck('required', this.checked)">
Post-Check erforderlich
</label>
</div>
</div>
</div>
<!-- Preview Tab -->
<div id="uc-tab-preview" class="uc-tab-panel">
<div class="uc-preview">
<div class="uc-preview-header" id="uc-preview-title">Unit Vorschau</div>
<div id="uc-preview-content">
<div class="uc-empty">
<div class="uc-empty-icon">&#128221;</div>
<p>Fuegen Sie Metadaten und Stops hinzu, um eine Vorschau zu sehen.</p>
</div>
</div>
<div id="uc-validation-result"></div>
</div>
</div>
<!-- JSON Tab -->
<div id="uc-tab-json" class="uc-tab-panel">
<textarea class="uc-json-editor" id="uc-json-editor" spellcheck="false"></textarea>
<div style="margin-top: 12px; display: flex; gap: 12px;">
<button class="btn btn-ghost" onclick="ucApplyJson()">JSON anwenden</button>
<button class="btn btn-ghost" onclick="ucFormatJson()">Formatieren</button>
<button class="btn btn-ghost" onclick="ucCopyJson()">Kopieren</button>
</div>
</div>
</div>
<!-- Footer -->
<div class="uc-footer">
<div>
<button class="btn btn-ghost" onclick="ucLoadExisting()">Bestehende Unit laden</button>
</div>
<div style="display: flex; gap: 12px;">
<button class="btn btn-ghost" onclick="ucSaveDraft()">Als Entwurf speichern</button>
<button class="btn btn-primary" onclick="ucPublish()">Veroeffentlichen</button>
</div>
</div>
</div>
<!-- Stop Edit Modal -->
<div class="modal-overlay" id="uc-stop-modal">
<div class="modal-content" style="max-width: 700px;">
<div class="modal-header">
<h2 class="modal-title">Stop bearbeiten</h2>
<button class="modal-close" onclick="ucCloseStopModal()">&times;</button>
</div>
<div id="uc-stop-modal-content">
<!-- Dynamisch gefuellt -->
</div>
<div style="padding: 16px; border-top: 1px solid var(--bp-border); display: flex; justify-content: flex-end; gap: 12px;">
<button class="btn btn-ghost" onclick="ucCloseStopModal()">Abbrechen</button>
<button class="btn btn-primary" onclick="ucSaveStop()">Speichern</button>
</div>
</div>
</div>
<!-- Load Unit Modal -->
<div class="modal-overlay" id="uc-load-modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">Unit laden</h2>
<button class="modal-close" onclick="ucCloseLoadModal()">&times;</button>
</div>
<div style="padding: 16px;">
<select class="uc-select" id="uc-load-select" style="width: 100%;">
<option value="">-- Unit waehlen --</option>
</select>
</div>
<div style="padding: 16px; border-top: 1px solid var(--bp-border); display: flex; justify-content: flex-end; gap: 12px;">
<button class="btn btn-ghost" onclick="ucCloseLoadModal()">Abbrechen</button>
<button class="btn btn-primary" onclick="ucLoadSelectedUnit()">Laden</button>
</div>
</div>
</div>
<!-- WIZARD -->
<div class="uc-wizard-overlay" id="uc-wizard-overlay"></div>
<div class="uc-wizard-highlight" id="uc-wizard-highlight" style="display:none;"></div>
<div class="uc-wizard-tooltip" id="uc-wizard-tooltip" style="display:none;">
<div class="uc-wizard-step" id="uc-wizard-step-num">Schritt 1 von 7</div>
<div class="uc-wizard-title" id="uc-wizard-title">Willkommen</div>
<div class="uc-wizard-text" id="uc-wizard-text">Text hier</div>
<div class="uc-wizard-actions">
<button class="uc-wizard-btn uc-wizard-btn-skip" onclick="ucWizardSkip()">Ueberspringen</button>
<div style="display:flex;gap:8px;">
<button class="uc-wizard-btn uc-wizard-btn-secondary" id="uc-wizard-back" onclick="ucWizardBack()">Zurueck</button>
<button class="uc-wizard-btn uc-wizard-btn-primary" id="uc-wizard-next" onclick="ucWizardNext()">Weiter</button>
</div>
</div>
<div class="uc-wizard-progress" id="uc-wizard-progress"></div>
</div>
<button class="uc-wizard-start-btn" id="uc-wizard-start-btn" onclick="ucWizardStart()" style="display:none;">
<span>&#10067;</span> Anleitung starten
</button>
"""
@staticmethod
def get_js() -> str:
"""JavaScript fuer Unit Creator."""
return """
// ==============================================
// UNIT CREATOR MODULE
// ==============================================
console.log('Unit Creator Module loaded');
// Current unit data
let ucUnitData = {
unit_id: '',
template: '',
version: '1.0.0',
locale: ['de-DE'],
grade_band: ['5', '6', '7'],
duration_minutes: 8,
difficulty: 'base',
subject: '',
topic: '',
learning_objectives: [],
stops: [],
precheck: {
question_set_id: '',
required: true,
time_limit_seconds: 120
},
postcheck: {
question_set_id: '',
required: true,
time_limit_seconds: 180
},
teacher_controls: {
allow_skip: true,
allow_replay: true,
max_time_per_stop_sec: 90,
show_hints: true,
require_precheck: true,
require_postcheck: true
},
assets: {},
metadata: {}
};
// Current editing stop index
let ucEditingStopIndex = -1;
// Interaction types
const UC_INTERACTION_TYPES = [
{ value: 'aim_and_pass', label: 'Ziel treffen (Aim & Pass)' },
{ value: 'slider_adjust', label: 'Wert einstellen (Slider)' },
{ value: 'slider_equivalence', label: 'Werte synchronisieren (Equivalence)' },
{ value: 'sequence_arrange', label: 'Reihenfolge sortieren (Sequence)' },
{ value: 'toggle_switch', label: 'Auswahl treffen (Toggle)' },
{ value: 'drag_match', label: 'Zuordnung (Drag & Match)' },
{ value: 'error_find', label: 'Fehler finden (Error Find)' },
{ value: 'transfer_apply', label: 'Konzept anwenden (Transfer)' }
];
// ==============================================
// INITIALIZATION
// ==============================================
function loadUnitCreatorModule() {
console.log('Initializing Unit Creator...');
ucRenderStops();
ucUpdateJsonEditor();
ucUpdatePreview();
}
// ==============================================
// TAB NAVIGATION
// ==============================================
function ucSwitchTab(tabId) {
// Update tab buttons
document.querySelectorAll('.uc-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabId);
});
// Update tab panels
document.querySelectorAll('.uc-tab-panel').forEach(panel => {
panel.classList.toggle('active', panel.id === 'uc-tab-' + tabId);
});
// Special handling
if (tabId === 'json') {
ucUpdateJsonEditor();
} else if (tabId === 'preview') {
ucUpdatePreview();
}
}
// ==============================================
// FIELD UPDATES
// ==============================================
function ucUpdateField(field, value) {
ucUnitData[field] = value;
console.log('Updated field:', field, value);
}
function ucUpdateDuration(value) {
ucUnitData.duration_minutes = parseInt(value);
document.getElementById('uc-duration-value').textContent = value;
}
function ucToggleGrade(grade) {
const index = ucUnitData.grade_band.indexOf(grade);
if (index > -1) {
ucUnitData.grade_band.splice(index, 1);
} else {
ucUnitData.grade_band.push(grade);
ucUnitData.grade_band.sort();
}
console.log('Grade band:', ucUnitData.grade_band);
}
function ucUpdatePrecheck(field, value) {
ucUnitData.precheck[field] = value;
}
function ucUpdatePostcheck(field, value) {
ucUnitData.postcheck[field] = value;
}
// ==============================================
// LEARNING OBJECTIVES
// ==============================================
function ucAddObjective() {
const input = document.getElementById('uc-objective-input');
const value = input.value.trim();
if (value) {
ucUnitData.learning_objectives.push(value);
input.value = '';
ucRenderObjectives();
}
}
function ucRemoveObjective(index) {
ucUnitData.learning_objectives.splice(index, 1);
ucRenderObjectives();
}
function ucRenderObjectives() {
const container = document.getElementById('uc-objectives-items');
container.innerHTML = ucUnitData.learning_objectives.map((obj, i) => `
<div class="uc-multi-input-item">
<span>${obj}</span>
<button onclick="ucRemoveObjective(${i})">&times;</button>
</div>
`).join('');
}
// ==============================================
// STOPS MANAGEMENT
// ==============================================
function ucAddStop() {
const newStop = {
stop_id: 'stop_' + (ucUnitData.stops.length + 1),
order: ucUnitData.stops.length,
label: { 'de-DE': '' },
narration: { 'de-DE': '' },
interaction: { type: '', params: {} },
concept: { why: { 'de-DE': '' }, common_misconception: { 'de-DE': '' } },
vocab: [],
telemetry_tags: []
};
ucUnitData.stops.push(newStop);
ucRenderStops();
}
function ucRemoveStop(index) {
if (confirm('Stop wirklich loeschen?')) {
ucUnitData.stops.splice(index, 1);
// Update order
ucUnitData.stops.forEach((stop, i) => stop.order = i);
ucRenderStops();
}
}
function ucEditStop(index) {
ucEditingStopIndex = index;
const stop = ucUnitData.stops[index];
const content = document.getElementById('uc-stop-modal-content');
content.innerHTML = `
<div style="padding: 16px; display: flex; flex-direction: column; gap: 16px; max-height: 70vh; overflow-y: auto;">
<div class="uc-form-group">
<label class="uc-label">Stop ID</label>
<input type="text" class="uc-input" id="uc-edit-stop-id" value="${stop.stop_id}">
</div>
<div class="uc-form-group">
<label class="uc-label">Label (DE)</label>
<input type="text" class="uc-input" id="uc-edit-stop-label" value="${stop.label['de-DE'] || ''}">
</div>
<div class="uc-form-group">
<label class="uc-label">Interaktionstyp</label>
<select class="uc-select" id="uc-edit-stop-interaction" onchange="ucUpdateInteractionParams()">
<option value="">-- Waehlen --</option>
${UC_INTERACTION_TYPES.map(t => `
<option value="${t.value}" ${stop.interaction.type === t.value ? 'selected' : ''}>${t.label}</option>
`).join('')}
</select>
</div>
<!-- Dynamische Parameter je nach Interaktionstyp -->
<div id="uc-interaction-params-container"></div>
<div class="uc-form-group">
<label class="uc-label">Narration (DE)</label>
<textarea class="uc-textarea" id="uc-edit-stop-narration" rows="4">${stop.narration['de-DE'] || ''}</textarea>
</div>
<div class="uc-form-group">
<label class="uc-label">Warum ist das wichtig? (DE)</label>
<textarea class="uc-textarea" id="uc-edit-stop-why" rows="2">${stop.concept?.why?.['de-DE'] || ''}</textarea>
</div>
<div class="uc-form-group">
<label class="uc-label">Haeufige Fehlvorstellung (DE)</label>
<textarea class="uc-textarea" id="uc-edit-stop-misconception" rows="2">${stop.concept?.common_misconception?.['de-DE'] || ''}</textarea>
</div>
<!-- Vokabeln -->
<div class="uc-form-group">
<label class="uc-label">Vokabeln</label>
<div id="uc-vocab-list"></div>
<button type="button" class="btn btn-ghost" onclick="ucAddVocab()" style="margin-top: 8px;">+ Vokabel hinzufuegen</button>
</div>
</div>
`;
// Interaktions-Parameter und Vokabeln rendern
ucUpdateInteractionParams();
ucRenderVocabList();
document.getElementById('uc-stop-modal').classList.add('active');
}
// Dynamische Parameter je nach Interaktionstyp
function ucUpdateInteractionParams() {
const container = document.getElementById('uc-interaction-params-container');
const type = document.getElementById('uc-edit-stop-interaction').value;
const stop = ucUnitData.stops[ucEditingStopIndex];
const params = stop.interaction.params || {};
let html = '';
switch (type) {
case 'aim_and_pass':
html = `
<div class="uc-params-section">
<div class="uc-params-title">Parameter: Ziel treffen</div>
<div class="uc-form-row">
<div class="uc-form-group" style="flex:1">
<label class="uc-label">Ziel (Target)</label>
<input type="text" class="uc-input" id="uc-param-target" value="${params.target || ''}" placeholder="z.B. pupil">
</div>
<div class="uc-form-group" style="flex:1">
<label class="uc-label">Toleranz</label>
<input type="number" class="uc-input" id="uc-param-tolerance" value="${params.tolerance || 5}" min="0" max="100">
</div>
</div>
</div>
`;
break;
case 'slider_adjust':
html = `
<div class="uc-params-section">
<div class="uc-params-title">Parameter: Slider einstellen</div>
<div class="uc-form-row">
<div class="uc-form-group" style="flex:1">
<label class="uc-label">Minimum</label>
<input type="number" class="uc-input" id="uc-param-min" value="${params.min ?? 0}">
</div>
<div class="uc-form-group" style="flex:1">
<label class="uc-label">Maximum</label>
<input type="number" class="uc-input" id="uc-param-max" value="${params.max ?? 100}">
</div>
</div>
<div class="uc-form-row">
<div class="uc-form-group" style="flex:1">
<label class="uc-label">Korrekter Wert</label>
<input type="number" class="uc-input" id="uc-param-correct" value="${params.correct ?? 50}">
</div>
<div class="uc-form-group" style="flex:1">
<label class="uc-label">Toleranz</label>
<input type="number" class="uc-input" id="uc-param-tolerance" value="${params.tolerance ?? 5}">
</div>
</div>
</div>
`;
break;
case 'slider_equivalence':
const eqPairs = params.pairs || [{ left: 50, right: 50 }];
html = `
<div class="uc-params-section">
<div class="uc-params-title">Parameter: Werte synchronisieren</div>
<div id="uc-equivalence-pairs">
${eqPairs.map((p, i) => `
<div class="uc-form-row uc-pair-row" data-index="${i}">
<div class="uc-form-group" style="flex:1">
<label class="uc-label">Links</label>
<input type="number" class="uc-input uc-eq-left" value="${p.left ?? 50}">
</div>
<div class="uc-form-group" style="flex:1">
<label class="uc-label">Rechts</label>
<input type="number" class="uc-input uc-eq-right" value="${p.right ?? 50}">
</div>
<button type="button" class="btn btn-ghost" onclick="this.parentElement.remove()" style="margin-top:24px;">X</button>
</div>
`).join('')}
</div>
<button type="button" class="btn btn-ghost" onclick="ucAddEquivalencePair()">+ Paar hinzufuegen</button>
</div>
`;
break;
case 'sequence_arrange':
const items = params.items || ['Item 1', 'Item 2', 'Item 3'];
html = `
<div class="uc-params-section">
<div class="uc-params-title">Parameter: Reihenfolge (korrekte Reihenfolge eingeben)</div>
<div id="uc-sequence-items">
${items.map((item, i) => `
<div class="uc-form-row uc-sequence-row" data-index="${i}">
<span style="padding:8px;color:var(--text-muted);">${i + 1}.</span>
<input type="text" class="uc-input uc-seq-item" value="${item}" style="flex:1">
<button type="button" class="btn btn-ghost" onclick="this.parentElement.remove()">X</button>
</div>
`).join('')}
</div>
<button type="button" class="btn btn-ghost" onclick="ucAddSequenceItem()">+ Element hinzufuegen</button>
</div>
`;
break;
case 'toggle_switch':
const options = params.options || [{ value: 'a', label: 'Option A', correct: true }, { value: 'b', label: 'Option B', correct: false }];
html = `
<div class="uc-params-section">
<div class="uc-params-title">Parameter: Auswahloptionen</div>
<div id="uc-toggle-options">
${options.map((opt, i) => `
<div class="uc-form-row uc-toggle-row" data-index="${i}">
<input type="text" class="uc-input uc-opt-value" value="${opt.value}" placeholder="Wert" style="width:80px">
<input type="text" class="uc-input uc-opt-label" value="${opt.label}" placeholder="Label" style="flex:1">
<label style="display:flex;align-items:center;gap:4px;">
<input type="checkbox" class="uc-opt-correct" ${opt.correct ? 'checked' : ''}> Korrekt
</label>
<button type="button" class="btn btn-ghost" onclick="this.parentElement.remove()">X</button>
</div>
`).join('')}
</div>
<button type="button" class="btn btn-ghost" onclick="ucAddToggleOption()">+ Option hinzufuegen</button>
</div>
`;
break;
case 'drag_match':
const matchPairs = params.pairs || [{ left: 'Begriff 1', right: 'Definition 1' }];
html = `
<div class="uc-params-section">
<div class="uc-params-title">Parameter: Zuordnungspaare</div>
<div id="uc-match-pairs">
${matchPairs.map((p, i) => `
<div class="uc-form-row uc-match-row" data-index="${i}">
<input type="text" class="uc-input uc-match-left" value="${p.left}" placeholder="Begriff" style="flex:1">
<span style="padding:0 8px;">&#8594;</span>
<input type="text" class="uc-input uc-match-right" value="${p.right}" placeholder="Definition" style="flex:1">
<button type="button" class="btn btn-ghost" onclick="this.parentElement.remove()">X</button>
</div>
`).join('')}
</div>
<button type="button" class="btn btn-ghost" onclick="ucAddMatchPair()">+ Paar hinzufuegen</button>
</div>
`;
break;
case 'error_find':
const errors = params.errors || ['Fehler 1'];
html = `
<div class="uc-params-section">
<div class="uc-params-title">Parameter: Fehler finden</div>
<div class="uc-form-group">
<label class="uc-label">Korrekte Antwort</label>
<input type="text" class="uc-input" id="uc-param-correct" value="${params.correct || ''}">
</div>
<div class="uc-form-group">
<label class="uc-label">Fehler (zu finden)</label>
<div id="uc-error-items">
${errors.map((err, i) => `
<div class="uc-form-row uc-error-row" data-index="${i}">
<input type="text" class="uc-input uc-error-item" value="${err}" style="flex:1">
<button type="button" class="btn btn-ghost" onclick="this.parentElement.remove()">X</button>
</div>
`).join('')}
</div>
<button type="button" class="btn btn-ghost" onclick="ucAddErrorItem()">+ Fehler hinzufuegen</button>
</div>
</div>
`;
break;
case 'transfer_apply':
html = `
<div class="uc-params-section">
<div class="uc-params-title">Parameter: Konzept anwenden</div>
<div class="uc-form-group">
<label class="uc-label">Kontext</label>
<textarea class="uc-textarea" id="uc-param-context" rows="3">${params.context || ''}</textarea>
</div>
<div class="uc-form-group">
<label class="uc-label">Template / Vorlage</label>
<textarea class="uc-textarea" id="uc-param-template" rows="3">${params.template || ''}</textarea>
</div>
</div>
`;
break;
default:
html = '<div class="uc-params-section"><p style="color:var(--text-muted)">Waehlen Sie einen Interaktionstyp, um Parameter zu konfigurieren.</p></div>';
}
container.innerHTML = html;
}
// Helper-Funktionen fuer dynamische Listen
function ucAddEquivalencePair() {
const container = document.getElementById('uc-equivalence-pairs');
const div = document.createElement('div');
div.className = 'uc-form-row uc-pair-row';
div.innerHTML = `
<div class="uc-form-group" style="flex:1">
<label class="uc-label">Links</label>
<input type="number" class="uc-input uc-eq-left" value="50">
</div>
<div class="uc-form-group" style="flex:1">
<label class="uc-label">Rechts</label>
<input type="number" class="uc-input uc-eq-right" value="50">
</div>
<button type="button" class="btn btn-ghost" onclick="this.parentElement.remove()" style="margin-top:24px;">X</button>
`;
container.appendChild(div);
}
function ucAddSequenceItem() {
const container = document.getElementById('uc-sequence-items');
const count = container.querySelectorAll('.uc-sequence-row').length;
const div = document.createElement('div');
div.className = 'uc-form-row uc-sequence-row';
div.innerHTML = `
<span style="padding:8px;color:var(--text-muted);">${count + 1}.</span>
<input type="text" class="uc-input uc-seq-item" value="" style="flex:1" placeholder="Neues Element">
<button type="button" class="btn btn-ghost" onclick="this.parentElement.remove()">X</button>
`;
container.appendChild(div);
}
function ucAddToggleOption() {
const container = document.getElementById('uc-toggle-options');
const div = document.createElement('div');
div.className = 'uc-form-row uc-toggle-row';
div.innerHTML = `
<input type="text" class="uc-input uc-opt-value" value="" placeholder="Wert" style="width:80px">
<input type="text" class="uc-input uc-opt-label" value="" placeholder="Label" style="flex:1">
<label style="display:flex;align-items:center;gap:4px;">
<input type="checkbox" class="uc-opt-correct"> Korrekt
</label>
<button type="button" class="btn btn-ghost" onclick="this.parentElement.remove()">X</button>
`;
container.appendChild(div);
}
function ucAddMatchPair() {
const container = document.getElementById('uc-match-pairs');
const div = document.createElement('div');
div.className = 'uc-form-row uc-match-row';
div.innerHTML = `
<input type="text" class="uc-input uc-match-left" value="" placeholder="Begriff" style="flex:1">
<span style="padding:0 8px;">&#8594;</span>
<input type="text" class="uc-input uc-match-right" value="" placeholder="Definition" style="flex:1">
<button type="button" class="btn btn-ghost" onclick="this.parentElement.remove()">X</button>
`;
container.appendChild(div);
}
function ucAddErrorItem() {
const container = document.getElementById('uc-error-items');
const div = document.createElement('div');
div.className = 'uc-form-row uc-error-row';
div.innerHTML = `
<input type="text" class="uc-input uc-error-item" value="" style="flex:1" placeholder="Neuer Fehler">
<button type="button" class="btn btn-ghost" onclick="this.parentElement.remove()">X</button>
`;
container.appendChild(div);
}
// Vokabeln-Editor
function ucRenderVocabList() {
const container = document.getElementById('uc-vocab-list');
const stop = ucUnitData.stops[ucEditingStopIndex];
const vocab = stop.vocab || [];
if (vocab.length === 0) {
container.innerHTML = '<p style="color:var(--text-muted);font-size:0.9em;">Keine Vokabeln</p>';
return;
}
container.innerHTML = vocab.map((v, i) => `
<div class="uc-form-row uc-vocab-row" data-index="${i}">
<input type="text" class="uc-input uc-vocab-term" value="${v.term?.['de-DE'] || ''}" placeholder="Begriff" style="flex:1">
<input type="text" class="uc-input uc-vocab-hint" value="${v.hint?.['de-DE'] || ''}" placeholder="Hinweis" style="flex:1">
<button type="button" class="btn btn-ghost" onclick="ucRemoveVocab(${i})">X</button>
</div>
`).join('');
}
function ucAddVocab() {
const stop = ucUnitData.stops[ucEditingStopIndex];
if (!stop.vocab) stop.vocab = [];
stop.vocab.push({ term: { 'de-DE': '' }, hint: { 'de-DE': '' } });
ucRenderVocabList();
}
function ucRemoveVocab(index) {
const stop = ucUnitData.stops[ucEditingStopIndex];
stop.vocab.splice(index, 1);
ucRenderVocabList();
}
// Interaktions-Parameter aus dem UI auslesen
function ucCollectInteractionParams() {
const type = document.getElementById('uc-edit-stop-interaction').value;
const params = {};
switch (type) {
case 'aim_and_pass':
params.target = document.getElementById('uc-param-target')?.value || '';
params.tolerance = parseFloat(document.getElementById('uc-param-tolerance')?.value) || 5;
break;
case 'slider_adjust':
params.min = parseFloat(document.getElementById('uc-param-min')?.value) ?? 0;
params.max = parseFloat(document.getElementById('uc-param-max')?.value) ?? 100;
params.correct = parseFloat(document.getElementById('uc-param-correct')?.value) ?? 50;
params.tolerance = parseFloat(document.getElementById('uc-param-tolerance')?.value) ?? 5;
break;
case 'slider_equivalence':
params.pairs = [];
document.querySelectorAll('.uc-pair-row').forEach(row => {
params.pairs.push({
left: parseFloat(row.querySelector('.uc-eq-left')?.value) || 50,
right: parseFloat(row.querySelector('.uc-eq-right')?.value) || 50
});
});
break;
case 'sequence_arrange':
params.items = [];
document.querySelectorAll('.uc-seq-item').forEach(input => {
if (input.value.trim()) params.items.push(input.value.trim());
});
break;
case 'toggle_switch':
params.options = [];
document.querySelectorAll('.uc-toggle-row').forEach(row => {
params.options.push({
value: row.querySelector('.uc-opt-value')?.value || '',
label: row.querySelector('.uc-opt-label')?.value || '',
correct: row.querySelector('.uc-opt-correct')?.checked || false
});
});
break;
case 'drag_match':
params.pairs = [];
document.querySelectorAll('.uc-match-row').forEach(row => {
params.pairs.push({
left: row.querySelector('.uc-match-left')?.value || '',
right: row.querySelector('.uc-match-right')?.value || ''
});
});
break;
case 'error_find':
params.correct = document.getElementById('uc-param-correct')?.value || '';
params.errors = [];
document.querySelectorAll('.uc-error-item').forEach(input => {
if (input.value.trim()) params.errors.push(input.value.trim());
});
break;
case 'transfer_apply':
params.context = document.getElementById('uc-param-context')?.value || '';
params.template = document.getElementById('uc-param-template')?.value || '';
break;
}
return params;
}
// Vokabeln aus dem UI auslesen
function ucCollectVocab() {
const vocab = [];
document.querySelectorAll('.uc-vocab-row').forEach(row => {
const term = row.querySelector('.uc-vocab-term')?.value || '';
const hint = row.querySelector('.uc-vocab-hint')?.value || '';
if (term || hint) {
vocab.push({
term: { 'de-DE': term },
hint: { 'de-DE': hint }
});
}
});
return vocab;
}
function ucSaveStop() {
if (ucEditingStopIndex < 0) return;
const stop = ucUnitData.stops[ucEditingStopIndex];
stop.stop_id = document.getElementById('uc-edit-stop-id').value;
stop.label['de-DE'] = document.getElementById('uc-edit-stop-label').value;
stop.interaction.type = document.getElementById('uc-edit-stop-interaction').value;
stop.interaction.params = ucCollectInteractionParams();
stop.narration['de-DE'] = document.getElementById('uc-edit-stop-narration').value;
stop.concept = stop.concept || { why: {}, common_misconception: {} };
stop.concept.why['de-DE'] = document.getElementById('uc-edit-stop-why').value;
stop.concept.common_misconception['de-DE'] = document.getElementById('uc-edit-stop-misconception').value;
stop.vocab = ucCollectVocab();
ucCloseStopModal();
ucRenderStops();
}
function ucCloseStopModal() {
document.getElementById('uc-stop-modal').classList.remove('active');
ucEditingStopIndex = -1;
}
function ucRenderStops() {
const container = document.getElementById('uc-stops-list');
if (ucUnitData.stops.length === 0) {
container.innerHTML = `
<div class="uc-empty">
<div class="uc-empty-icon">&#128203;</div>
<p>Noch keine Stops. Klicken Sie unten, um einen Stop hinzuzufuegen.</p>
</div>
`;
return;
}
container.innerHTML = ucUnitData.stops.map((stop, i) => `
<div class="uc-stop-card" draggable="true" data-index="${i}">
<div class="uc-stop-header">
<div class="uc-stop-title">
<span class="uc-stop-order">${i + 1}</span>
<span>${stop.label['de-DE'] || stop.stop_id || 'Unbenannt'}</span>
</div>
<div class="uc-stop-actions">
<button class="uc-stop-btn" onclick="ucEditStop(${i})">Bearbeiten</button>
<button class="uc-stop-btn delete" onclick="ucRemoveStop(${i})">Loeschen</button>
</div>
</div>
<div class="uc-stop-content">
<div class="uc-stop-field">
<label>Interaktion</label>
<span>${UC_INTERACTION_TYPES.find(t => t.value === stop.interaction.type)?.label || 'Nicht gesetzt'}</span>
</div>
<div class="uc-stop-field">
<label>Stop ID</label>
<span>${stop.stop_id}</span>
</div>
</div>
</div>
`).join('');
// Add drag and drop
ucInitStopDragDrop();
}
function ucInitStopDragDrop() {
const cards = document.querySelectorAll('.uc-stop-card');
cards.forEach(card => {
card.addEventListener('dragstart', ucHandleDragStart);
card.addEventListener('dragend', ucHandleDragEnd);
card.addEventListener('dragover', ucHandleDragOver);
card.addEventListener('drop', ucHandleDrop);
});
}
let ucDraggedIndex = -1;
function ucHandleDragStart(e) {
ucDraggedIndex = parseInt(e.target.dataset.index);
e.target.classList.add('dragging');
}
function ucHandleDragEnd(e) {
e.target.classList.remove('dragging');
}
function ucHandleDragOver(e) {
e.preventDefault();
}
function ucHandleDrop(e) {
e.preventDefault();
const dropIndex = parseInt(e.target.closest('.uc-stop-card').dataset.index);
if (ucDraggedIndex !== dropIndex) {
const [draggedStop] = ucUnitData.stops.splice(ucDraggedIndex, 1);
ucUnitData.stops.splice(dropIndex, 0, draggedStop);
// Update order
ucUnitData.stops.forEach((stop, i) => stop.order = i);
ucRenderStops();
}
}
// ==============================================
// JSON EDITOR
// ==============================================
function ucUpdateJsonEditor() {
const editor = document.getElementById('uc-json-editor');
editor.value = JSON.stringify(ucUnitData, null, 2);
}
function ucApplyJson() {
const editor = document.getElementById('uc-json-editor');
try {
ucUnitData = JSON.parse(editor.value);
ucRenderStops();
ucRenderObjectives();
ucPopulateFormFromData();
alert('JSON erfolgreich angewendet!');
} catch (e) {
alert('Ungueltiges JSON: ' + e.message);
}
}
function ucFormatJson() {
const editor = document.getElementById('uc-json-editor');
try {
const data = JSON.parse(editor.value);
editor.value = JSON.stringify(data, null, 2);
} catch (e) {
alert('Ungueltiges JSON: ' + e.message);
}
}
function ucCopyJson() {
const editor = document.getElementById('uc-json-editor');
navigator.clipboard.writeText(editor.value).then(() => {
alert('JSON in Zwischenablage kopiert!');
});
}
function ucPopulateFormFromData() {
document.getElementById('uc-unit-id').value = ucUnitData.unit_id || '';
document.getElementById('uc-template').value = ucUnitData.template || '';
document.getElementById('uc-subject').value = ucUnitData.subject || '';
document.getElementById('uc-topic').value = ucUnitData.topic || '';
document.getElementById('uc-version').value = ucUnitData.version || '1.0.0';
document.getElementById('uc-duration').value = ucUnitData.duration_minutes || 8;
document.getElementById('uc-duration-value').textContent = ucUnitData.duration_minutes || 8;
// Difficulty
document.querySelector(`input[name="difficulty"][value="${ucUnitData.difficulty || 'base'}"]`).checked = true;
// Grade band checkboxes
document.querySelectorAll('.uc-checkbox-group input[type="checkbox"]').forEach(cb => {
cb.checked = ucUnitData.grade_band.includes(cb.value);
});
// Pre/Post check
document.getElementById('uc-precheck-id').value = ucUnitData.precheck?.question_set_id || '';
document.getElementById('uc-precheck-time').value = ucUnitData.precheck?.time_limit_seconds || 120;
document.getElementById('uc-precheck-required').checked = ucUnitData.precheck?.required !== false;
document.getElementById('uc-postcheck-id').value = ucUnitData.postcheck?.question_set_id || '';
document.getElementById('uc-postcheck-time').value = ucUnitData.postcheck?.time_limit_seconds || 180;
document.getElementById('uc-postcheck-required').checked = ucUnitData.postcheck?.required !== false;
}
// ==============================================
// PREVIEW
// ==============================================
function ucUpdatePreview() {
const title = document.getElementById('uc-preview-title');
const content = document.getElementById('uc-preview-content');
const validation = document.getElementById('uc-validation-result');
title.textContent = ucUnitData.unit_id || ucUnitData.topic || 'Unit Vorschau';
if (ucUnitData.stops.length === 0) {
content.innerHTML = `
<div class="uc-empty">
<div class="uc-empty-icon">&#128221;</div>
<p>Fuegen Sie Stops hinzu, um eine Vorschau zu sehen.</p>
</div>
`;
} else {
let html = '<div style="margin-bottom: 16px;">';
html += `<p><strong>Template:</strong> ${ucUnitData.template || 'Nicht gesetzt'}</p>`;
html += `<p><strong>Fach:</strong> ${ucUnitData.subject || 'Nicht gesetzt'} - ${ucUnitData.topic || ''}</p>`;
html += `<p><strong>Dauer:</strong> ${ucUnitData.duration_minutes} Min | <strong>Klassen:</strong> ${ucUnitData.grade_band.join(', ')}</p>`;
html += '</div>';
html += '<div class="uc-preview-flow">';
ucUnitData.stops.forEach((stop, i) => {
if (i > 0) html += '<span class="uc-preview-arrow">→</span>';
html += `<span class="uc-preview-stop">${stop.label['de-DE'] || stop.stop_id}</span>`;
});
html += '</div>';
content.innerHTML = html;
}
// Validate
ucValidateAndShow(validation);
}
async function ucValidateAndShow(container) {
try {
const response = await fetch('/api/units/definitions/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ucUnitData)
});
const result = await response.json();
if (result.valid) {
container.innerHTML = `
<div class="uc-validation valid">
<div class="uc-validation-title">&#9989; Validierung erfolgreich</div>
${result.warnings.length > 0 ? `
<ul class="uc-validation-list">
${result.warnings.map(w => `<li class="warning">&#9888; ${w.message}</li>`).join('')}
</ul>
` : ''}
</div>
`;
} else {
container.innerHTML = `
<div class="uc-validation invalid">
<div class="uc-validation-title">&#10060; Validierung fehlgeschlagen</div>
<ul class="uc-validation-list">
${result.errors.map(e => `<li class="error">&#10060; ${e.message}</li>`).join('')}
${result.warnings.map(w => `<li class="warning">&#9888; ${w.message}</li>`).join('')}
</ul>
</div>
`;
}
} catch (e) {
container.innerHTML = `
<div class="uc-validation invalid">
<div class="uc-validation-title">&#10060; Validierung nicht moeglich</div>
<p>Backend nicht erreichbar</p>
</div>
`;
}
}
// ==============================================
// VALIDATION
// ==============================================
async function ucValidate() {
ucSwitchTab('preview');
}
// ==============================================
// IMPORT / EXPORT
// ==============================================
function ucImportJson() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = e => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = event => {
try {
ucUnitData = JSON.parse(event.target.result);
ucRenderStops();
ucRenderObjectives();
ucPopulateFormFromData();
ucUpdateJsonEditor();
alert('JSON importiert!');
} catch (err) {
alert('Ungueltiges JSON: ' + err.message);
}
};
reader.readAsText(file);
};
input.click();
}
function ucExportJson() {
const blob = new Blob([JSON.stringify(ucUnitData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = (ucUnitData.unit_id || 'unit') + '.json';
a.click();
URL.revokeObjectURL(url);
}
// ==============================================
// LOAD EXISTING
// ==============================================
async function ucLoadExisting() {
const select = document.getElementById('uc-load-select');
try {
const response = await fetch('/api/units/definitions');
const units = await response.json();
select.innerHTML = '<option value="">-- Unit waehlen --</option>';
units.forEach(unit => {
select.innerHTML += `<option value="${unit.unit_id}">${unit.unit_id}</option>`;
});
document.getElementById('uc-load-modal').classList.add('active');
} catch (e) {
alert('Fehler beim Laden der Units: ' + e.message);
}
}
async function ucLoadSelectedUnit() {
const select = document.getElementById('uc-load-select');
const unitId = select.value;
if (!unitId) {
alert('Bitte eine Unit waehlen');
return;
}
try {
const response = await fetch('/api/units/definitions/' + unitId);
const data = await response.json();
ucUnitData = data.definition || data;
ucRenderStops();
ucRenderObjectives();
ucPopulateFormFromData();
ucUpdateJsonEditor();
ucCloseLoadModal();
alert('Unit geladen: ' + unitId);
} catch (e) {
alert('Fehler beim Laden: ' + e.message);
}
}
function ucCloseLoadModal() {
document.getElementById('uc-load-modal').classList.remove('active');
}
// ==============================================
// SAVE / PUBLISH
// ==============================================
async function ucSaveDraft() {
ucUnitData.status = 'draft';
await ucSaveUnit();
}
async function ucPublish() {
if (!confirm('Unit wirklich veroeffentlichen? Sie kann dann von Lehrern zugewiesen werden.')) {
return;
}
ucUnitData.status = 'published';
await ucSaveUnit();
}
async function ucSaveUnit() {
// Auto-fill pre/post check IDs if empty
if (!ucUnitData.precheck.question_set_id) {
ucUnitData.precheck.question_set_id = ucUnitData.unit_id + '_precheck';
}
if (!ucUnitData.postcheck.question_set_id) {
ucUnitData.postcheck.question_set_id = ucUnitData.unit_id + '_postcheck';
}
try {
// Check if unit exists
let method = 'POST';
let url = '/api/units/definitions';
try {
const checkResponse = await fetch('/api/units/definitions/' + ucUnitData.unit_id);
if (checkResponse.ok) {
method = 'PUT';
url = '/api/units/definitions/' + ucUnitData.unit_id;
}
} catch (e) {
// Unit doesn't exist, use POST
}
const response = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ucUnitData)
});
if (response.ok) {
const result = await response.json();
alert('Unit erfolgreich gespeichert: ' + result.unit_id);
} else {
const error = await response.json();
alert('Fehler: ' + (error.detail || 'Unbekannter Fehler'));
}
} catch (e) {
alert('Fehler beim Speichern: ' + e.message);
}
}
// ==============================================
// WIZARD - Interaktive Bedienungsanleitung
// ==============================================
const UC_WIZARD_STEPS = [
{
title: 'Willkommen im Unit Creator!',
text: 'Mit diesem Tool koennen Sie eigene Lerneinheiten (Units) fuer BreakpilotDrive erstellen. Der Wizard fuehrt Sie durch die wichtigsten Funktionen.',
element: null, // Kein Element hervorheben
position: 'center'
},
{
title: 'Metadaten eingeben',
text: 'Hier geben Sie die Grundinformationen ein: Unit-ID (eindeutiger Name), Template-Typ, Fach, Thema und Klassenstufen. Diese Daten helfen beim Filtern und Zuweisen.',
element: '#uc-tab-metadata',
position: 'right'
},
{
title: 'Stops hinzufuegen',
text: 'Jede Unit besteht aus mehreren "Stops" - das sind die einzelnen Lernstationen. Wechseln Sie zum Stops-Tab und fuegen Sie Ihre Stationen hinzu.',
element: '[data-tab="stops"]',
position: 'bottom'
},
{
title: 'Interaktionstypen waehlen',
text: 'Jeder Stop hat einen Interaktionstyp: Slider einstellen, Reihenfolge sortieren, Zuordnungen treffen, etc. Im Stop-Editor waehlen Sie den Typ und konfigurieren die Parameter.',
element: '#uc-stops-list',
position: 'left'
},
{
title: 'Pre/Post-Check konfigurieren',
text: 'Units haben einen Vortest (Precheck) und Nachtest (Postcheck) um den Lernerfolg zu messen. Konfigurieren Sie diese im dritten Tab.',
element: '[data-tab="checks"]',
position: 'bottom'
},
{
title: 'Vorschau und JSON',
text: 'Im Vorschau-Tab sehen Sie eine Zusammenfassung. Im JSON-Tab koennen Sie das Rohdatenformat bearbeiten oder Units importieren/exportieren.',
element: '[data-tab="preview"]',
position: 'bottom'
},
{
title: 'Validieren und Speichern',
text: 'Klicken Sie auf "Validieren" um Fehler zu pruefen. Mit "Als Entwurf speichern" sichern Sie Ihre Arbeit. "Veroeffentlichen" macht die Unit fuer Lehrer verfuegbar.',
element: '.uc-footer',
position: 'top'
},
{
title: 'Geschafft!',
text: 'Sie kennen jetzt die wichtigsten Funktionen. Der "Anleitung"-Button unten rechts startet diesen Wizard erneut. Viel Erfolg beim Erstellen Ihrer ersten Unit!',
element: null,
position: 'center'
}
];
let ucWizardCurrentStep = 0;
let ucWizardActive = false;
function ucWizardInit() {
// Pruefen ob Wizard schon gesehen wurde
const seen = localStorage.getItem('uc_wizard_seen');
if (!seen) {
// Beim ersten Mal automatisch starten
setTimeout(() => ucWizardStart(), 500);
}
// Start-Button immer anzeigen
document.getElementById('uc-wizard-start-btn').style.display = 'flex';
}
function ucWizardStart() {
ucWizardCurrentStep = 0;
ucWizardActive = true;
document.getElementById('uc-wizard-overlay').classList.add('active');
document.getElementById('uc-wizard-tooltip').style.display = 'block';
document.getElementById('uc-wizard-start-btn').style.display = 'none';
ucWizardShowStep();
}
function ucWizardShowStep() {
const step = UC_WIZARD_STEPS[ucWizardCurrentStep];
const total = UC_WIZARD_STEPS.length;
// Step-Nummer und Text
document.getElementById('uc-wizard-step-num').textContent = 'Schritt ' + (ucWizardCurrentStep + 1) + ' von ' + total;
document.getElementById('uc-wizard-title').textContent = step.title;
document.getElementById('uc-wizard-text').textContent = step.text;
// Back-Button
document.getElementById('uc-wizard-back').style.display = ucWizardCurrentStep > 0 ? 'inline-block' : 'none';
// Next-Button Text
const nextBtn = document.getElementById('uc-wizard-next');
nextBtn.textContent = ucWizardCurrentStep === total - 1 ? 'Fertig' : 'Weiter';
// Progress dots
const progress = document.getElementById('uc-wizard-progress');
progress.innerHTML = UC_WIZARD_STEPS.map((_, i) => {
let cls = 'uc-wizard-dot';
if (i < ucWizardCurrentStep) cls += ' done';
if (i === ucWizardCurrentStep) cls += ' active';
return '<div class="' + cls + '"></div>';
}).join('');
// Element hervorheben
const highlight = document.getElementById('uc-wizard-highlight');
const tooltip = document.getElementById('uc-wizard-tooltip');
if (step.element) {
const el = document.querySelector(step.element);
if (el) {
const rect = el.getBoundingClientRect();
highlight.style.display = 'block';
highlight.style.top = (rect.top + window.scrollY - 4) + 'px';
highlight.style.left = (rect.left + window.scrollX - 4) + 'px';
highlight.style.width = (rect.width + 8) + 'px';
highlight.style.height = (rect.height + 8) + 'px';
// Tooltip positionieren
const tooltipRect = tooltip.getBoundingClientRect();
let top, left;
switch (step.position) {
case 'right':
top = rect.top;
left = rect.right + 20;
break;
case 'left':
top = rect.top;
left = rect.left - tooltipRect.width - 20;
break;
case 'bottom':
top = rect.bottom + 20;
left = rect.left;
break;
case 'top':
top = rect.top - tooltipRect.height - 20;
left = rect.left;
break;
default:
top = window.innerHeight / 2 - 100;
left = window.innerWidth / 2 - 200;
}
// Grenzen pruefen
if (left < 20) left = 20;
if (left + 400 > window.innerWidth) left = window.innerWidth - 420;
if (top < 20) top = 20;
tooltip.style.top = top + 'px';
tooltip.style.left = left + 'px';
} else {
highlight.style.display = 'none';
tooltip.style.top = '50%';
tooltip.style.left = '50%';
tooltip.style.transform = 'translate(-50%, -50%)';
}
} else {
// Zentriert anzeigen
highlight.style.display = 'none';
tooltip.style.top = '50%';
tooltip.style.left = '50%';
tooltip.style.transform = 'translate(-50%, -50%)';
}
}
function ucWizardNext() {
if (ucWizardCurrentStep < UC_WIZARD_STEPS.length - 1) {
ucWizardCurrentStep++;
document.getElementById('uc-wizard-tooltip').style.transform = '';
ucWizardShowStep();
} else {
ucWizardEnd();
}
}
function ucWizardBack() {
if (ucWizardCurrentStep > 0) {
ucWizardCurrentStep--;
document.getElementById('uc-wizard-tooltip').style.transform = '';
ucWizardShowStep();
}
}
function ucWizardSkip() {
ucWizardEnd();
}
function ucWizardEnd() {
ucWizardActive = false;
document.getElementById('uc-wizard-overlay').classList.remove('active');
document.getElementById('uc-wizard-tooltip').style.display = 'none';
document.getElementById('uc-wizard-highlight').style.display = 'none';
document.getElementById('uc-wizard-start-btn').style.display = 'flex';
localStorage.setItem('uc_wizard_seen', 'true');
}
// Wizard beim Laden des Moduls initialisieren
document.addEventListener('DOMContentLoaded', function() {
// Nur starten wenn Unit Creator Panel aktiv wird
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
const panel = document.getElementById('panel-unit-creator');
if (panel && panel.classList.contains('active') && !localStorage.getItem('uc_wizard_seen')) {
setTimeout(() => ucWizardInit(), 300);
observer.disconnect();
}
});
});
const panel = document.getElementById('panel-unit-creator');
if (panel) {
observer.observe(panel, { attributes: true, attributeFilter: ['class'] });
}
});
// Alternativ: Manueller Aufruf wenn loadModule aufgerufen wird
if (typeof window.ucWizardInitCalled === 'undefined') {
window.ucWizardInitCalled = false;
const origLoadModule = window.loadModule;
if (origLoadModule) {
window.loadModule = function(moduleName) {
origLoadModule(moduleName);
if (moduleName === 'unit-creator' && !window.ucWizardInitCalled) {
window.ucWizardInitCalled = true;
setTimeout(() => ucWizardInit(), 500);
}
};
}
}
"""
def get_unit_creator_module() -> dict:
"""Gibt das komplette Unit Creator Modul als Dictionary zurueck."""
module = UnitCreatorModule()
return {
'css': module.get_css(),
'html': module.get_html(),
'js': module.get_js()
}