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>
2142 lines
65 KiB
Python
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">📝</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()">×</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()">×</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>❓</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})">×</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;">→</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;">→</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">📋</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">📝</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">✅ Validierung erfolgreich</div>
|
|
${result.warnings.length > 0 ? `
|
|
<ul class="uc-validation-list">
|
|
${result.warnings.map(w => `<li class="warning">⚠ ${w.message}</li>`).join('')}
|
|
</ul>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
} else {
|
|
container.innerHTML = `
|
|
<div class="uc-validation invalid">
|
|
<div class="uc-validation-title">❌ Validierung fehlgeschlagen</div>
|
|
<ul class="uc-validation-list">
|
|
${result.errors.map(e => `<li class="error">❌ ${e.message}</li>`).join('')}
|
|
${result.warnings.map(w => `<li class="warning">⚠ ${w.message}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
`;
|
|
}
|
|
} catch (e) {
|
|
container.innerHTML = `
|
|
<div class="uc-validation invalid">
|
|
<div class="uc-validation-title">❌ 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()
|
|
}
|