"""
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 """
📝
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 = `
`;
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
`;
break;
case 'drag_match':
const matchPairs = params.pairs || [{ left: 'Begriff 1', right: 'Definition 1' }];
html = `
Parameter: Zuordnungspaare
`;
break;
case 'error_find':
const errors = params.errors || ['Fehler 1'];
html = `
`;
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) => `
${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 ? `
${result.warnings.map(w => `- ⚠ ${w.message}
`).join('')}
` : ''}
`;
} else {
container.innerHTML = `
❌ Validierung fehlgeschlagen
${result.errors.map(e => `- ❌ ${e.message}
`).join('')}
${result.warnings.map(w => `- ⚠ ${w.message}
`).join('')}
`;
}
} 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()
}