""" 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

Pre-Check

Post-Check

Unit Vorschau
📝

Fuegen Sie Metadaten und Stops hinzu, um eine Vorschau zu sehen.

""" @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) => `
${obj}
`).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 = `
`; // 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 = `
Parameter: Ziel treffen
`; break; case 'slider_adjust': html = `
Parameter: Slider einstellen
`; break; case 'slider_equivalence': const eqPairs = params.pairs || [{ left: 50, right: 50 }]; html = `
Parameter: Werte synchronisieren
${eqPairs.map((p, i) => `
`).join('')}
`; break; case 'sequence_arrange': const items = params.items || ['Item 1', 'Item 2', 'Item 3']; html = `
Parameter: Reihenfolge (korrekte Reihenfolge eingeben)
${items.map((item, i) => `
${i + 1}.
`).join('')}
`; break; case 'toggle_switch': const options = params.options || [{ value: 'a', label: 'Option A', correct: true }, { value: 'b', label: 'Option B', correct: false }]; html = `
Parameter: Auswahloptionen
${options.map((opt, i) => `
`).join('')}
`; break; case 'drag_match': const matchPairs = params.pairs || [{ left: 'Begriff 1', right: 'Definition 1' }]; html = `
Parameter: Zuordnungspaare
${matchPairs.map((p, i) => `
`).join('')}
`; break; case 'error_find': const errors = params.errors || ['Fehler 1']; html = `
Parameter: Fehler finden
${errors.map((err, i) => `
`).join('')}
`; break; case 'transfer_apply': html = `
Parameter: Konzept anwenden
`; break; default: html = '

Waehlen Sie einen Interaktionstyp, um Parameter zu konfigurieren.

'; } 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 = `
`; 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 = ` ${count + 1}. `; 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 = ` `; 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 = ` `; 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 = ` `; 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 = '

Keine Vokabeln

'; return; } container.innerHTML = vocab.map((v, i) => `
`).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 = `
📋

Noch keine Stops. Klicken Sie unten, um einen Stop hinzuzufuegen.

`; return; } container.innerHTML = ucUnitData.stops.map((stop, i) => `
${i + 1} ${stop.label['de-DE'] || stop.stop_id || 'Unbenannt'}
${UC_INTERACTION_TYPES.find(t => t.value === stop.interaction.type)?.label || 'Nicht gesetzt'}
${stop.stop_id}
`).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 = `
📝

Fuegen Sie Stops hinzu, um eine Vorschau zu sehen.

`; } else { let html = '
'; html += `

Template: ${ucUnitData.template || 'Nicht gesetzt'}

`; html += `

Fach: ${ucUnitData.subject || 'Nicht gesetzt'} - ${ucUnitData.topic || ''}

`; html += `

Dauer: ${ucUnitData.duration_minutes} Min | Klassen: ${ucUnitData.grade_band.join(', ')}

`; html += '
'; html += '
'; ucUnitData.stops.forEach((stop, i) => { if (i > 0) html += ''; html += `${stop.label['de-DE'] || stop.stop_id}`; }); html += '
'; 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 = `
✅ Validierung erfolgreich
${result.warnings.length > 0 ? ` ` : ''}
`; } else { container.innerHTML = `
❌ Validierung fehlgeschlagen
`; } } catch (e) { container.innerHTML = `
❌ Validierung nicht moeglich

Backend nicht erreichbar

`; } } // ============================================== // 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 = ''; units.forEach(unit => { select.innerHTML += ``; }); 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 '
'; }).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() }