# HTML-Kurs erstellen: Allgemeine Anleitung

Diese Anleitung erklärt, wie ein KI-Agent eine eigenständige HTML-Kursdatei für das LMS erstellt — für Themen die weder Python noch Scratch sind (z.B. KI, Medienbildung, Ethik, Informatik-Theorie).

> **Alternative:** Für die meisten Kurse ist der **JSON-Import** (`ki-json-import.md`) einfacher und wartbarer. HTML-Kurse nur dann erstellen, wenn spezielle Interaktivität gebraucht wird, die der JSON-Editor nicht unterstützt.

---

## Wann HTML statt JSON?

| Situation | Empfehlung |
|---|---|
| Standardkurs mit Text, Video, MC-Fragen, Code | **JSON-Import** |
| Komplexe interaktive Visualisierungen (Canvas, Animationen) | **HTML** |
| Eingebettete externe Werkzeuge (außer Scratch/TurboWarp) | **HTML** |
| Schritt-für-Schritt Python-Visualizer | **HTML (python-Stil)** |
| Scratch/TurboWarp-Kurs | **HTML (scratch-Stil)** |
| Hybridkurs: Theorie + interaktives Element | **JSON** mit `ai-chat`-Block |

---

## Pflicht-Struktur jeder HTML-Kursdatei

### 1. Datei benennen & verlinken

- Dateiname: lowercase, keine Leerzeichen, z.B. `ki-ethik.html`
- In `index.html` als neuer Kurs-Button eintragen:

```html
<!-- In index.html, bei den anderen Kurs-Buttons -->
<button class="course-btn" onclick="openCourse('ki-ethik')">
  🤖 KI & Ethik
</button>
```

Und in der Kurs-Konfiguration in `index.html`:
```js
const COURSES = {
  // ... bestehende Kurse ...
  'ki-ethik': { label: 'KI & Ethik', file: 'ki-ethik.html' }
};
```

### 2. CSS-Variablen

Immer von den LMS-Variablen ausgehen, kein hardcodiertes Styling. **Keine Google Fonts** einbinden (DSGVO) — System-Font-Stack verwenden.

```css
:root {
  /* Akzentfarbe — hier anpassen (Indigo: #4f46e5, Rose: #e11d48, etc.) */
  --primary:      #4f46e5;
  --primary-dark: #3730a3;
  --primary-mid:  #6366f1;
  --primary-light:#eef2ff;

  /* Surfaces */
  --bg:           #f4f5f9;
  --surface:      #ffffff;
  --surface-2:    #f9fafb;
  --border:       #e3e6ef;
  --border-strong:#c8cdd9;

  /* Semantik */
  --green:        #16a34a;
  --green-light:  #f0fdf4;
  --yellow:       #d97706;
  --yellow-light: #fffbeb;
  --red:          #dc2626;
  --red-light:    #fef2f2;

  /* Text */
  --text:         #111827;
  --text-2:       #374151;
  --muted:        #6b7280;

  --mono: 'JetBrains Mono','Fira Code',monospace;

  /* Shape & Shadow */
  --r-sm: 6px;
  --r-md: 10px;
  --r-lg: 16px;
  --shadow-sm: 0 1px 4px rgba(0,0,0,.07), 0 0 0 1px rgba(0,0,0,.03);
  --shadow-md: 0 4px 12px rgba(0,0,0,.08), 0 0 0 1px rgba(0,0,0,.04);
  --t: .18s ease;
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
  font-family: Inter, 'Segoe UI', system-ui, -apple-system, sans-serif;
  background: var(--bg);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
}
```

**Header** — immer Gradient, kein Flat-Color:

```css
.lms-header {
  background: linear-gradient(135deg, var(--primary) 0%, var(--primary-mid) 100%);
  color: #fff;
  height: 60px;
  padding: 0 2rem;
  display: flex;
  align-items: center;
  gap: 1rem;
  box-shadow: 0 2px 12px rgba(0,0,0,.15);
}
.lms-header a {
  color: rgba(255,255,255,.8);
  text-decoration: none;
  font-size: .875rem;
  font-weight: 500;
  padding: .3rem .6rem;
  border-radius: var(--r-sm);
  border: 1px solid rgba(255,255,255,.25);
}
.lms-header a:hover { background: rgba(255,255,255,.15); color: #fff; }
```

### 3. XSS-Schutz

**Immer** die `esc()`-Funktion für alle dynamisch eingefügten Texte verwenden:

```js
function esc(s) {
  return String(s ?? '')
    .replace(/&/g,'&amp;')
    .replace(/</g,'&lt;')
    .replace(/>/g,'&gt;')
    .replace(/"/g,'&quot;');
}
```

### 4. Fortschritts-System

Jeder Kurs muss Fortschritt an die API senden. Pflicht-Felder:

> **Hinweis:** `COURSE_ID` wird beim Import automatisch vom Server überschrieben. Der Wert im Code ist nur ein Platzhalter — nach dem Upload enthält die Datei die korrekte, eindeutige ID.

```js
const COURSE_ID = 'ki-ethik';  // wird beim Import durch die echte ID ersetzt
let IDENTITY = null;
let PB = new Uint8Array(N_LEKTIONEN);  // 8 Bit pro Lektion

// Bit lesen
function pbGet(lessonIdx, bit) { return !!(PB[lessonIdx] & (1 << bit)); }
// Bit setzen
function pbSet(lessonIdx, bit, v) {
  if (v) PB[lessonIdx] |= (1 << bit);
  else   PB[lessonIdx] &= ~(1 << bit);
}
// Base64url-Encoding
function progEncode() {
  return btoa(String.fromCharCode(...PB)).replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'');
}
// Base64url-Decoding
function progDecode(b64) {
  const s = b64.replace(/-/g,'+').replace(/_/g,'/');
  const bin = atob(s.padEnd(s.length + (4 - s.length % 4) % 4, '='));
  PB = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) PB[i] = bin.charCodeAt(i);
}

async function hashPhrase(phrase) {
  const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(phrase));
  return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2,'0')).join('');
}

async function reportHash() {
  if (!IDENTITY) return;
  await fetch('api.php', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ action: 'hash/report', identity: IDENTITY, course: COURSE_ID, hash: progEncode() })
  });
}
```

### 5. Auth-Flow (Boot)

**Wichtig:** Kein eigenes Login-Formular oder Auth-Guard einbauen! Schüler sind bereits über `index.html` eingeloggt — die Passphrase liegt in `localStorage['lms_passphrase']`. Der Kurs lädt einfach direkt.

```js
(async function boot() {
  const savedPhrase = localStorage.getItem('lms_passphrase');
  if (savedPhrase) {
    const identity = await hashPhrase(savedPhrase);
    const res = await fetch('api.php', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ action: 'hash/get', identity, course: COURSE_ID })
    });
    const data = await res.json();
    if (data.ok) {
      IDENTITY = identity;
      if (data.data?.hash) progDecode(data.data.hash);
      // Antworten + Canvas für erste Lektion laden
      await loadAnswers(_currentLessonKey);
    }
  }
  // Kurs immer rendern — kein Auth-Guard, kein Login-Formular
  renderCourse();
})();
```

---

## Standard-Layout-Muster

### A: Tab-basierter Kurs (wie python.html, ki.html)

Lektionen als horizontale Tabs, innerhalb jeder Lektion Unter-Tabs (Erklärung / Aufgaben / Quiz):

```html
<div class="concept-tabs" id="cTabs">
  <button class="concept-tab active" data-c="einheit-1">📡 Einheit 1</button>
  <button class="concept-tab" data-c="einheit-2">🔒 Einheit 2</button>
</div>

<main>
  <div class="concept-section active" id="sec-einheit-1">
    <div class="mode-tabs">
      <button class="mode-tab active" data-c="einheit-1" data-m="erkl">Erklärung</button>
      <button class="mode-tab" data-c="einheit-1" data-m="aufg">Aufgaben</button>
    </div>
    <div class="mode-panel active" id="einheit-1-erkl">...</div>
    <div class="mode-panel" id="einheit-1-aufg">...</div>
  </div>
</main>
```

```js
// Tab-Navigation
document.getElementById('cTabs').addEventListener('click', e => {
  const t = e.target.closest('.concept-tab');
  if (!t) return;
  const key = t.dataset.c;
  document.querySelectorAll('.concept-tab').forEach(b => b.classList.toggle('active', b.dataset.c === key));
  document.querySelectorAll('.concept-section').forEach(s => s.classList.toggle('active', s.id === 'sec-' + key));
});
document.addEventListener('click', e => {
  const t = e.target.closest('.mode-tab');
  if (!t) return;
  const c = t.dataset.c, m = t.dataset.m;
  document.querySelectorAll(`.mode-tab[data-c="${c}"]`).forEach(b => b.classList.toggle('active', b.dataset.m === m));
  document.querySelectorAll(`#sec-${c} .mode-panel`).forEach(p => p.classList.toggle('active', p.id === `${c}-${m}`));
});
```

### B: Sidebar-basierter Kurs (wie scratch.html)

Aufgaben als Liste links, Hauptinhalt rechts (gut für viel Platz brauchendes Hauptelement):

```html
<div style="display:flex;height:calc(100vh - 90px)">
  <aside style="width:260px;overflow-y:auto;border-right:1px solid var(--border);background:var(--surface)">
    <!-- Aufgabenliste -->
  </aside>
  <div style="flex:1;overflow-y:auto">
    <!-- Hauptinhalt -->
  </div>
</div>
```

---

## Aufgaben-Typen

Alle Aufgaben sollen folgendes gemeinsame Grundgerüst haben:

```html
<div class="task-card" id="tcard-AUFG_ID">
  <div class="task-num">Aufgabe 1</div>
  <div class="task-title">Titel der Aufgabe</div>
  <div class="task-desc">Beschreibung / Aufgabenstellung</div>
  <!-- Aufgaben-Inhalt je nach Typ -->
</div>
```

```css
.task-card { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 1.2rem 1.4rem; margin-bottom: 1rem; }
.task-num { font-size: .75rem; font-weight: 700; color: var(--primary); text-transform: uppercase; letter-spacing: .06em; margin-bottom: .3rem; }
.task-title { font-size: 1rem; font-weight: 700; margin-bottom: .35rem; }
.task-desc { font-size: .9rem; color: var(--muted); margin-bottom: .75rem; line-height: 1.6; }
```

---

### Freitext (Tippen + Handschrift)

Der wichtigste Aufgabentyp. Schüler können entweder tippen **oder** mit der Hand zeichnen/schreiben.

```html
<div class="task-card" id="tcard-a1">
  <div class="task-num">Aufgabe 1</div>
  <div class="task-title">Meine Frage</div>
  <div class="task-desc">Aufgabenstellung hier.</div>
  <div class="wb-expand-row">
    <button class="wb-expand-btn" id="wb-txt-a1" onclick="expandWB('a1','text')">✏️ Tippen</button>
    <button class="wb-expand-btn" id="wb-draw-a1" onclick="expandWB('a1','draw')">🖊 Handschrift</button>
  </div>
  <div id="wbs-txt-a1" style="display:none">
    <textarea class="freitext-ta" id="ft-a1" placeholder="Deine Antwort…" oninput="saveFreitext('a1',this.value)"></textarea>
  </div>
  <div id="wbs-draw-a1" style="display:none">
    <canvas class="wb-canvas" id="wbc-a1" height="400"></canvas>
    <button class="btn sm" style="margin:.4rem 0 .5rem" onclick="clearCanvas('a1')">Zeichnung löschen</button>
  </div>
  <button class="btn grn task-done-btn" id="done-a1" onclick="markTaskDone('lektion-1', 0, 1)" style="width:100%">Antwort speichern</button>
</div>
```

```css
.wb-expand-row { display: flex; gap: .5rem; margin-bottom: .5rem; flex-wrap: wrap; }
.wb-expand-btn { padding: .3rem .8rem; font-size: .8rem; border-radius: 6px; border: 1px solid var(--border); background: var(--surface); cursor: pointer; color: var(--muted); }
.wb-expand-btn.open { border-color: var(--primary); color: var(--primary); background: var(--primary-light); }
.freitext-ta { width: 100%; min-height: 120px; padding: .75rem; border: 1px solid var(--border); border-radius: 8px; font-size: .9rem; font-family: inherit; resize: vertical; }
.wb-canvas { width: 100%; border: 1px solid var(--border); border-radius: 8px; cursor: crosshair; touch-action: none; }
.btn.grn { background: var(--green); color: #fff; border: none; border-radius: 8px; padding: .5rem 1rem; cursor: pointer; font-size: .88rem; font-weight: 600; }
.task-done-btn.done { opacity: .7; cursor: default; }
```

```js
const FREITEXT = {};
const CANVAS_DATA = {};

// Antworten vom Server laden (im boot() nach IDENTITY gesetzt aufrufen!)
async function loadAnswers(lessonKey) {
  if (!IDENTITY) return;
  try {
    const r = await fetch('api.php', { method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({ action:'answers/get', identity:IDENTITY, courseId:COURSE_ID, lessonKey }) });
    const d = await r.json();
    if (!d.ok) return;
    const shareState = {};
    for (const task of d.data?.answers || []) {
      if (task.type === 'canvas') CANVAS_DATA[task.taskId] = task.answer;
      else FREITEXT[task.taskId] = task.answer;
      if (task.shared) shareState[lessonKey] = true;
    }
    // Freigabe-Zustand an Footer melden
    if (Object.keys(shareState).length) LMSFooter.setShareState(shareState);
  } catch {}
}

function expandWB(id, mode) {
  const txtBtn  = document.getElementById('wb-txt-' + id);
  const drawBtn = document.getElementById('wb-draw-' + id);
  const txtBox  = document.getElementById('wbs-txt-' + id);
  const drawBox = document.getElementById('wbs-draw-' + id);
  if (mode === 'text') {
    const open = txtBox.style.display === 'none';
    txtBox.style.display = open ? '' : 'none';
    txtBtn.classList.toggle('open', open);
  } else {
    const open = drawBox.style.display === 'none';
    drawBox.style.display = open ? '' : 'none';
    drawBtn.classList.toggle('open', open);
    if (open) initCanvas(id);
  }
}

function saveFreitext(id, val) {
  FREITEXT[id] = val;
  if (!IDENTITY) return;
  fetch('api.php', { method:'POST', headers:{'Content-Type':'application/json'},
    body: JSON.stringify({ action:'answers/save', identity:IDENTITY, courseId:COURSE_ID,
      lessonKey: _currentLessonKey, taskId: id, type:'freitext', answer: val }) }).catch(()=>{});
}

function initCanvas(id) {
  const cv = document.getElementById('wbc-' + id);
  if (cv._init) return; cv._init = true;
  const cssW = cv.offsetWidth || 600, cssH = 400;
  cv.width = cssW * devicePixelRatio; cv.height = cssH * devicePixelRatio;
  const ctx = cv.getContext('2d');
  ctx.scale(devicePixelRatio, devicePixelRatio);
  ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, cssW, cssH);
  const canvasKey = id + '__canvas';
  if (CANVAS_DATA[canvasKey]) {
    const img = new Image();
    img.onload = () => { ctx.save(); ctx.setTransform(1,0,0,1,0,0); ctx.drawImage(img,0,0,cv.width,cv.height); ctx.restore(); };
    img.src = CANVAS_DATA[canvasKey];
  }
  ctx.strokeStyle='#1e293b'; ctx.lineWidth=2; ctx.lineCap='round'; ctx.lineJoin='round';
  let drawing=false;
  function pos(e){const r=cv.getBoundingClientRect();return[(e.clientX-r.left)*(cssW/r.width),(e.clientY-r.top)*(cssH/r.height)];}
  cv.addEventListener('pointerdown',e=>{drawing=true;ctx.beginPath();const[x,y]=pos(e);ctx.moveTo(x,y);cv.setPointerCapture(e.pointerId);});
  cv.addEventListener('pointermove',e=>{if(!drawing)return;const[x,y]=pos(e);ctx.lineTo(x,y);ctx.stroke();});
  cv.addEventListener('pointerup',()=>{
    drawing=false;
    const d=cv.toDataURL('image/png');
    CANVAS_DATA[canvasKey]=d;
    if(!IDENTITY) return;
    fetch('api.php',{method:'POST',headers:{'Content-Type':'application/json'},
      body:JSON.stringify({action:'answers/save',identity:IDENTITY,courseId:COURSE_ID,
        lessonKey:_currentLessonKey,taskId:canvasKey,type:'canvas',answer:d})}).catch(()=>{});
  });
  cv.addEventListener('pointercancel',()=>{drawing=false;});
}

function clearCanvas(id) {
  const cv = document.getElementById('wbc-' + id);
  if (!cv) return;
  const dpr = devicePixelRatio;
  const ctx = cv.getContext('2d');
  ctx.fillStyle='#fff'; ctx.fillRect(0,0,cv.width,cv.height);
  const canvasKey = id + '__canvas';
  delete CANVAS_DATA[canvasKey];
  if (!IDENTITY) return;
  fetch('api.php',{method:'POST',headers:{'Content-Type':'application/json'},
    body:JSON.stringify({action:'answers/save',identity:IDENTITY,courseId:COURSE_ID,
      lessonKey:_currentLessonKey,taskId:canvasKey,type:'canvas',answer:''})}).catch(()=>{});
}
```

---

### Multiple Choice

```html
<div class="task-card" id="tcard-a2">
  <div class="task-num">Aufgabe 2</div>
  <div class="task-title">Welche Aussage ist richtig?</div>
  <div class="task-desc">Wähle die richtige Antwort.</div>
  <div class="choice-opts" id="mcopts-a2">
    <button class="choice-opt" onclick="checkMC('lektion-1',1,0,'a2')">Option A</button>
    <button class="choice-opt" onclick="checkMC('lektion-1',1,1,'a2')">Option B</button>
    <button class="choice-opt" onclick="checkMC('lektion-1',1,2,'a2')">Option C</button>
  </div>
  <div class="choice-feedback" id="mcfb-a2" style="display:none"></div>
</div>
```

```js
const MC_CORRECT = { 'a2': 1 };  // Index der richtigen Antwort (0-basiert)

function checkMC(lessonKey, taskIdx, chosen, id) {
  const correct = MC_CORRECT[id];
  document.querySelectorAll(`#mcopts-${id} .choice-opt`).forEach((btn, i) => {
    btn.disabled = true;
    btn.classList.toggle('ok', i === correct);
    btn.classList.toggle('err', i === chosen && i !== correct);
  });
  const fb = document.getElementById('mcfb-' + id);
  fb.style.display = '';
  fb.className = 'choice-feedback ' + (chosen === correct ? 'ok' : 'err');
  fb.textContent = chosen === correct ? '✓ Richtig!' : '✗ Leider falsch.';
  if (chosen === correct) markTaskDone(lessonKey, taskIdx, 2);  // bit 2 für zweite Aufgabe
}
```

```css
.choice-opts { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .75rem; }
.choice-opt { padding: .65rem 1rem; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); cursor: pointer; text-align: left; font-size: .9rem; transition: border-color .15s, background .15s; }
.choice-opt:hover { border-color: var(--primary); background: var(--primary-light); }
.choice-opt.ok { background: #dcfce7; border-color: var(--green); color: var(--green); }
.choice-opt.err { background: #fee2e2; border-color: var(--red); color: var(--red); }
.choice-feedback { padding: .5rem .75rem; border-radius: 8px; font-size: .88rem; font-weight: 600; }
.choice-feedback.ok { background: #dcfce7; color: var(--green); }
.choice-feedback.err { background: #fee2e2; color: var(--red); }
```

---

### Wahr/Falsch (True/False)

```html
<div class="task-card" id="tcard-a3">
  <div class="task-num">Aufgabe 3</div>
  <div class="task-title">Aussage: KI kann immer die Wahrheit sagen.</div>
  <div class="tf-opts">
    <label class="tf-label" onclick="selectTF('a3','true')">
      <input type="radio" name="tf-a3" value="true"> ✓ Richtig
    </label>
    <label class="tf-label" onclick="selectTF('a3','false')">
      <input type="radio" name="tf-a3" value="false"> ✗ Falsch
    </label>
  </div>
  <button class="btn pri" onclick="checkTF('lektion-1', 2, 'a3', false)">Antwort prüfen</button>
  <div class="choice-feedback" id="tffb-a3" style="display:none"></div>
</div>
```

```js
// WICHTIG: Radio-Buttons bei TF immer explizit setzen (label onclick bricht native behavior)
function selectTF(id, val) {
  const radio = document.querySelector(`input[name="tf-${id}"][value="${val}"]`);
  if (radio) radio.checked = true;
}

function checkTF(lessonKey, taskIdx, id, correct) {
  // correct = true/false (die richtige Antwort)
  const selected = document.querySelector(`input[name="tf-${id}"]:checked`);
  if (!selected) return;
  const chosen = selected.value === 'true';
  const fb = document.getElementById('tffb-' + id);
  fb.style.display = '';
  fb.className = 'choice-feedback ' + (chosen === correct ? 'ok' : 'err');
  fb.textContent = chosen === correct ? '✓ Richtig!' : '✗ Leider falsch.';
  document.querySelectorAll(`input[name="tf-${id}"]`).forEach(r => r.disabled = true);
  if (chosen === correct) markTaskDone(lessonKey, taskIdx, 3);
}
```

```css
.tf-opts { display: flex; gap: 1rem; margin-bottom: .75rem; }
.tf-label { display: flex; align-items: center; gap: .5rem; padding: .65rem 1.2rem; border: 1px solid var(--border); border-radius: 8px; cursor: pointer; font-size: .9rem; background: var(--surface); }
.tf-label:hover { border-color: var(--primary); }
```

---

### Checkbox (Mehrfachauswahl)

```html
<div class="task-card" id="tcard-a4">
  <div class="task-num">Aufgabe 4</div>
  <div class="task-title">Welche dieser Aussagen stimmen?</div>
  <div style="display:flex;flex-direction:column;gap:.4rem;margin-bottom:.75rem" id="cbopts-a4">
    <label style="display:flex;align-items:center;gap:.6rem;padding:.6rem 1rem;border:1px solid var(--border);border-radius:8px;cursor:pointer;font-size:.9rem;background:var(--surface)">
      <input type="checkbox" id="cbopt-a4-0" style="accent-color:var(--primary)"> Aussage A
    </label>
    <label style="display:flex;align-items:center;gap:.6rem;padding:.6rem 1rem;border:1px solid var(--border);border-radius:8px;cursor:pointer;font-size:.9rem;background:var(--surface)">
      <input type="checkbox" id="cbopt-a4-1" style="accent-color:var(--primary)"> Aussage B
    </label>
    <label style="display:flex;align-items:center;gap:.6rem;padding:.6rem 1rem;border:1px solid var(--border);border-radius:8px;cursor:pointer;font-size:.9rem;background:var(--surface)">
      <input type="checkbox" id="cbopt-a4-2" style="accent-color:var(--primary)"> Aussage C
    </label>
  </div>
  <button class="btn pri" onclick="checkCheckbox('lektion-1', 3, 'a4', [0,2])">Prüfen</button>
  <div class="choice-feedback" id="cbfb-a4" style="display:none"></div>
</div>
```

```js
function checkCheckbox(lessonKey, taskIdx, id, correctIndices) {
  const n = document.querySelectorAll(`[id^="cbopt-${id}-"]`).length;
  const chosen = Array.from({length: n}, (_, i) => document.getElementById(`cbopt-${id}-${i}`)?.checked);
  const correct = Array.from({length: n}, (_, i) => correctIndices.includes(i));
  const allRight = chosen.every((v, i) => v === correct[i]);
  const fb = document.getElementById('cbfb-' + id);
  fb.style.display = '';
  fb.className = 'choice-feedback ' + (allRight ? 'ok' : 'err');
  fb.textContent = allRight ? '✓ Alle richtigen Antworten ausgewählt!' : '✗ Nicht ganz richtig.';
  if (allRight) markTaskDone(lessonKey, taskIdx, 4);
}
```

---

### KI-Chat Block (ai-chat)

Ein eingebetteter KI-Chatbot als Lernhilfe innerhalb eines Inhaltsbereichs (nicht der globale Tutor).

```html
<div class="ai-chat-widget" id="aichat-1">
  <div class="ai-chat-messages" id="aichat-msgs-1"></div>
  <div class="ai-chat-input">
    <textarea id="aichat-inp-1" placeholder="Stelle eine Frage…" rows="1"
      onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendAiChat('1')}"></textarea>
    <button onclick="sendAiChat('1')">Senden</button>
  </div>
</div>
```

```js
const AI_CHAT_HISTORIES = {};
const AI_CHAT_PROMPTS = {
  '1': 'Du bist ein hilfreicher Assistent für diesen Kurs. Antworte auf Deutsch, kurz und verständlich.'
};

async function sendAiChat(chatId) {
  const inp = document.getElementById('aichat-inp-' + chatId);
  const text = inp.value.trim();
  if (!text) return;
  inp.value = '';
  if (!AI_CHAT_HISTORIES[chatId]) AI_CHAT_HISTORIES[chatId] = [];
  addAiChatMsg(chatId, 'user', text);
  AI_CHAT_HISTORIES[chatId].push({ role:'user', content:text });
  const thinking = addAiChatMsg(chatId, 'assistant', '…');
  try {
    const res = await fetch('api.php', { method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({ action:'ai/chat', identity:IDENTITY, course:COURSE_ID,
        messages: AI_CHAT_HISTORIES[chatId], systemprompt: AI_CHAT_PROMPTS[chatId] || '' }) });
    const data = await res.json();
    const reply = data.data?.content || 'Keine Antwort.';
    thinking.textContent = reply;
    AI_CHAT_HISTORIES[chatId].push({ role:'assistant', content:reply });
  } catch { thinking.textContent = 'Verbindungsfehler.'; }
}

function addAiChatMsg(chatId, role, text) {
  const box = document.getElementById('aichat-msgs-' + chatId);
  const el = document.createElement('div');
  el.className = 'ai-msg ' + (role === 'user' ? 'user' : 'assistant');
  el.textContent = text;
  box.appendChild(el);
  box.scrollTop = box.scrollHeight;
  return el;
}
```

---

### Fortschritt markieren (markTaskDone)

```js
function markTaskDone(lessonKey, taskIdx, bit) {
  const li = LESSON_KEYS.indexOf(lessonKey);
  if (li < 0) return;
  pbSet(li, bit, 1);
  // Lektion als done markieren wenn alle Aufgaben erledigt
  const allDone = LESSON_TASKS[lessonKey].every((_, i) => pbGet(li, i + 1));
  if (allDone) pbSet(li, 0, 1);
  updateProgPanel();
  reportHash();
  const btn = document.getElementById('done-' + LESSON_TASKS[lessonKey][taskIdx]?.id);
  if (btn) { btn.textContent = '✓ Erledigt'; btn.disabled = true; btn.classList.add('done'); }
}
```

---

## Pflicht: Fixed Footer via `lms-footer.js`

**Jeder HTML-Kurs bindet `lms-footer.js` ein** — das Skript injiziert automatisch `#progPanel` (Fortschritts-Dots, PDF-Export-Button, KI-Tutor-FAB) und `#tutorChat` ins DOM. Kein manuelles HTML/CSS nötig.

### Einbindung

```html
<!-- Im <head> oder vor </body> -->
<script src="lms-footer.js"></script>
```

### Boot-Integration

```js
// LESSON_KEYS = ['lektion-1', 'lektion-2', ...]
// LESSONS = [{ key: 'lektion-1', title: '...', tutorEnabled: true }, ...]

let _currentLessonKey = LESSON_KEYS[0];
let _currentMode = 'erkl';  // 'erkl' | 'aufg'

(async function boot() {
  const savedPhrase = localStorage.getItem('lms_passphrase');
  if (savedPhrase) {
    const identity = await hashPhrase(savedPhrase);
    const res = await fetch('api.php', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ action: 'hash/get', identity, course: COURSE_ID })
    });
    const data = await res.json();
    if (data.ok) {
      IDENTITY = identity;
      if (data.data?.hash) progDecode(data.data.hash);
      await loadAnswers(_currentLessonKey);
    }
  }
  renderCourse();

  // Footer initialisieren
  await LMSFooter.init({
    courseId: COURSE_ID,
    identity: IDENTITY,
    lessons: LESSONS,
    getProgress: (li) => ({ done: pbGet(li, 0), partial: [1,2,3,4,5].some(b => pbGet(li, b)) }),
    onNavigate: (key) => switchTo(key),
    onExport: () => exportAllPDF(),    // optional — Button erscheint nur wenn angegeben
    getCurrentLesson: () => _currentLessonKey,
    getCurrentTab: () => _currentMode, // 'aufg' zeigt den Tutor-FAB
    getTaskIds: (lessonKey) => LESSON_TASKS[lessonKey]?.map(t => t.id) || [],  // für Freigabe
  });
})();
```

### Nach Fortschrittsänderung

```js
function markTaskDone(lessonKey, taskIdx, bit) {
  const li = LESSON_KEYS.indexOf(lessonKey);
  if (li < 0) return;
  pbSet(li, bit, 1);
  const allDone = LESSON_TASKS[lessonKey].every((_, i) => pbGet(li, i + 1));
  if (allDone) pbSet(li, 0, 1);
  LMSFooter.update();  // Dots aktualisieren
  reportHash();
  // ... Button-Status setzen
}
```

### Nach Tab/Lektionswechsel

```js
function switchTo(key) {
  _currentLessonKey = key;
  // ... DOM-Switching ...
  LMSFooter.onTabChange();  // Tutor-FAB-Sichtbarkeit aktualisieren
}

document.addEventListener('click', e => {
  const t = e.target.closest('.mode-tab');
  if (!t) return;
  _currentMode = t.dataset.m;
  // ... Tab-Switching ...
  LMSFooter.onTabChange();
});
```

---

## Aufgaben-Freigabe (lessonShare)

Die Freigabe-Checkbox (`🔓 Aufgaben & Übungen für Lehrkraft freigeben`) wird von `lms-footer.js` automatisch in den Footer injiziert und verwaltet — kein manuelles HTML oder JS nötig.

Der Lehrer sieht freigegebene Antworten in teacher.html unter "📝 Antworten ansehen" beim jeweiligen Schüler.

### Was der Kurs bereitstellen muss

**1. `getTaskIds`-Callback in `LMSFooter.init()`** — damit der Footer alle Task-IDs einer Lektion kennt:

```js
await LMSFooter.init({
  // ...
  getTaskIds: (lessonKey) => LESSON_TASKS[lessonKey]?.map(t => t.id) || [],
});
```

**2. Freigabe-Zustand nach `loadAnswers()` melden:**

```js
async function loadAnswers(lessonKey) {
  // ... (Antworten laden wie oben)
  // Wenn eine Antwort shared=true hat:
  if (task.shared) LMSFooter.setShareState({ [lessonKey]: true });
}
```

**3. `LMSFooter.onTabChange()` bei Lektionswechsel** — aktualisiert die Checkbox automatisch (bereits im Tab-Switching nötig, s.o.).

---

## KI-Tutor (optional)

Der KI-Tutor wird vollständig von `lms-footer.js` gehandhabt — kein manuelles HTML oder JS nötig.

`lms-footer.js` prüft beim Init automatisch `llm/hasconfig` und zeigt den FAB nur, wenn:
- Der Tutor serverseitig aktiviert ist (`tutor_enabled: true`)
- Die aktuelle Lektion `tutorEnabled: true` hat (im LESSONS-Array)
- Der aktuelle Tab `'aufg'` oder `'aufgaben'` ist (via `getCurrentTab`-Callback)

### Konfiguration in LESSONS

```js
const LESSONS = [
  { key: 'lektion-1', title: 'Einführung', tutorEnabled: false },
  { key: 'lektion-2', title: 'Praxis', tutorEnabled: true },  // Tutor aktiv
];
```

### Optionaler eigener System-Prompt

```js
await LMSFooter.init({
  // ...
  tutorPrompt: 'Du bist ein spezialisierter Tutor für diesen Robotik-Kurs. Erkläre auf Deutsch...',
});
```

Ohne `tutorPrompt` verwendet `lms-footer.js` einen Standard-Prompt auf Deutsch.

---

## Pinnwand (optional)

Diskussions-Pinnwand kann in jeden Kurs eingebettet werden. Vollständige API-Referenz:

```js
// Beiträge laden
async function loadBoard() {
  const res = await fetch('api.php', { method:'POST', headers:{'Content-Type':'application/json'},
    body: JSON.stringify({ action:'cboard/list', courseId: COURSE_ID, identity: IDENTITY }) });
  const data = await res.json();
  // data.data.posts → Array von Posts rendern
}

// Beitrag posten
async function postToBoard(text) {
  await fetch('api.php', { method:'POST', headers:{'Content-Type':'application/json'},
    body: JSON.stringify({ action:'cboard/post', courseId: COURSE_ID, identity: IDENTITY, text }) });
}
```

---

## Polling / Abstimmungen (optional)

Echtzeit-Abstimmungen, die pro Kurs-Instanz getrennt funktionieren (jede Klasse hat eigene Ergebnisse).

### HTML

```html
<div class="poll-block" id="poll-meine-umfrage">
  <div class="poll-question">Welches Thema interessiert euch am meisten?</div>
  <div class="poll-opts">
    <button class="poll-opt" onclick="castVote('meine-umfrage', 0)">KI & Ethik</button>
    <button class="poll-opt" onclick="castVote('meine-umfrage', 1)">Robotik</button>
    <button class="poll-opt" onclick="castVote('meine-umfrage', 2)">Webentwicklung</button>
  </div>
  <div class="poll-result" id="poll-result-meine-umfrage" style="display:none"></div>
</div>
```

### CSS

```css
.poll-block { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 1.2rem; margin: 1rem 0; }
.poll-question { font-weight: 700; font-size: 1rem; margin-bottom: .8rem; }
.poll-opts { display: flex; flex-direction: column; gap: .4rem; }
.poll-opt { padding: .6rem 1rem; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); cursor: pointer; text-align: left; font-size: .9rem; transition: all .2s; }
.poll-opt:hover { border-color: var(--primary); background: var(--primary-light, #eef2ff); }
.poll-bar { height: 28px; border-radius: 6px; background: var(--primary-light, #eef2ff); margin-bottom: .3rem; display: flex; align-items: center; padding: 0 .6rem; font-size: .82rem; position: relative; overflow: hidden; }
.poll-bar-fill { position: absolute; left: 0; top: 0; bottom: 0; background: var(--primary); opacity: .2; border-radius: 6px; }
```

### JS

```js
async function castVote(pollId, optIndex) {
  if (!IDENTITY) return;
  try {
    const res = await fetch('api.php', { method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({ action:'poll/vote', identity:IDENTITY, courseId:COURSE_ID, pollId, vote:optIndex }) });
    const data = await res.json();
    if (data.ok) renderPollResults(pollId, data.data);
  } catch {}
}

async function loadPollResults(pollId) {
  if (!IDENTITY) return;
  try {
    const res = await fetch('api.php', { method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({ action:'poll/results', identity:IDENTITY, courseId:COURSE_ID, pollId }) });
    const data = await res.json();
    if (data.ok && data.data.hasVoted) renderPollResults(pollId, data.data);
  } catch {}
}

function renderPollResults(pollId, data) {
  const block = document.getElementById('poll-' + pollId);
  if (!block) return;
  const opts = block.querySelector('.poll-opts');
  const result = block.querySelector('.poll-result') || document.createElement('div');
  if (opts) opts.style.display = 'none';
  result.style.display = '';
  const votes = data.votes || {};
  const total = data.total || 0;
  const labels = [...block.querySelectorAll('.poll-opt')].map(b => b.textContent);
  result.innerHTML = labels.map((label, i) => {
    const count = votes[String(i)] || 0;
    const pct = total > 0 ? Math.round(count / total * 100) : 0;
    return `<div class="poll-bar"><div class="poll-bar-fill" style="width:${pct}%"></div><span style="position:relative;z-index:1">${esc(label)} — ${count} (${pct}%)</span></div>`;
  }).join('');
}
```

**Wichtig:** Beim Kurs-Boot jedes vorhandene Poll mit `loadPollResults(pollId)` laden, damit bereits abgestimmte Schüler sofort die Ergebnisse sehen.

---

## Link-Element (optional)

Modernes, klickbares Link-Element mit Icon und Beschriftung — öffnet immer in einem neuen Tab.

### HTML

```html
<a href="https://docs.python.org/3/" target="_blank" rel="noopener noreferrer" class="link-block">
  <span class="link-block-icon">🐍</span>
  <span class="link-block-label">Offizielle Python-Dokumentation</span>
  <span class="link-block-arrow">↗</span>
</a>
```

### CSS

```css
a.link-block {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .85rem 1.1rem;
  margin: .6rem 0;
  background: var(--surface);
  border: 1.5px solid var(--border);
  border-radius: var(--r-md);
  text-decoration: none;
  color: var(--primary);
  font-weight: 500;
  font-size: .95rem;
  transition: border-color var(--t), box-shadow var(--t), background var(--t);
  box-shadow: 0 1px 4px rgba(0,0,0,.06);
}
a.link-block:hover {
  border-color: var(--primary);
  background: var(--primary-light);
}
.link-block-icon { font-size: 1.25rem; flex-shrink: 0; }
.link-block-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.link-block-arrow { font-size: 1rem; color: var(--muted); }
```

Mehrere Links hintereinander — z.B. als Linkliste für weiterführende Ressourcen:

```html
<div style="display:flex;flex-direction:column;gap:.4rem;margin:1rem 0">
  <a href="https://scratch.mit.edu/" target="_blank" rel="noopener noreferrer" class="link-block">
    <span class="link-block-icon">🐱</span>
    <span class="link-block-label">Scratch — Offizielle Website</span>
    <span class="link-block-arrow">↗</span>
  </a>
  <a href="https://turbowarp.org/" target="_blank" rel="noopener noreferrer" class="link-block">
    <span class="link-block-icon">⚡</span>
    <span class="link-block-label">TurboWarp — Schnellere Scratch-Engine</span>
    <span class="link-block-arrow">↗</span>
  </a>
</div>
```

---

## PDF-Export (Pflicht)

Jeder Kurs muss einen PDF-Export anbieten. Der "📄 Export"-Button im Footer wird von `lms-footer.js` automatisch hinzugefügt, wenn `onExport` in `LMSFooter.init()` übergeben wird.

```js
await LMSFooter.init({
  // ...
  onExport: () => exportAllPDF(),  // eigene exportAllPDF-Funktion im Kurs
});
```

### exportLessonPDF(lessonKey)

```js
function exportLessonPDF(lessonKey) {
  const lesson = LESSONS.find(l => l.key === lessonKey); // oder dein Lektion-Array
  if (!lesson) return;
  const color = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#4f46e5';
  const today = new Date().toLocaleDateString('de-DE');
  const tasksHTML = buildPDFTasks(lesson);
  const html = buildPDFDoc(lesson.title, `${COURSE_TITLE} · ${today}`, tasksHTML, color);
  openPDFWindow(html);
}

function exportAllPDF() {
  const color = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#4f46e5';
  const today = new Date().toLocaleDateString('de-DE');
  const allHTML = LESSONS.map(lesson => {
    const t = buildPDFTasks(lesson);
    if (!t) return '';
    return `<div class="lesson-section"><h2 class="lesson-heading">${esc(lesson.title||'')}</h2>${t}</div>`;
  }).filter(Boolean).join('');
  if (!allHTML) { alert('Keine Aufgaben vorhanden.'); return; }
  const html = buildPDFDoc(`${COURSE_TITLE} – Alle Aufgaben`, `Alle Lektionen · ${today}`, allHTML, color);
  openPDFWindow(html);
}

// Aufgaben für eine Lektion als PDF-HTML aufbauen
function buildPDFTasks(lesson) {
  return (lesson.tasks || []).map((t, ti) => {
    const num = `Aufgabe ${ti+1}`;
    if (t.type === 'freitext') {
      const txt = FREITEXT[t.id] || '';
      const img = CANVAS_DATA[t.id + '__canvas'];
      let ans = '';
      if (txt) ans += `<div class="pdf-ans">${esc(txt)}</div>`;
      if (img) ans += `<img src="${img}" style="max-width:100%;border-radius:4px;border:1px solid #dde1ea;margin-top:.3rem">`;
      if (!ans) ans = `<div class="pdf-ans-empty">(keine Antwort)</div>`;
      return `<div class="pdf-task"><div class="pdf-num">${num}</div><div class="pdf-title">${esc(t.title)}</div><div class="pdf-desc">${esc(t.desc||'')}</div>${ans}</div>`;
    }
    // MC, TF, Checkbox analog ergänzen
    return '';
  }).filter(Boolean).join('');
}

function buildPDFDoc(title, subtitle, bodyHTML, color) {
  return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${esc(title)}</title>
<style>
body{font-family:system-ui,sans-serif;max-width:780px;margin:2rem auto;padding:0 1.5rem;color:#1e293b;line-height:1.6}
h1{font-size:1.5rem;font-weight:800;color:${color};margin-bottom:.2rem}
.sub{color:#64748b;font-size:.9rem;margin-bottom:2rem}
.lesson-section{margin-bottom:2.5rem}
.lesson-heading{font-size:1.15rem;font-weight:700;color:${color};border-bottom:2px solid ${color};padding-bottom:.3rem;margin-bottom:1rem}
.pdf-task{margin-bottom:1rem;padding:.9rem 1rem;border:1px solid #dde1ea;border-radius:8px;background:#f9fafb;break-inside:avoid}
.pdf-num{font-size:.72rem;color:#94a3b8;margin-bottom:.15rem;text-transform:uppercase;letter-spacing:.04em}
.pdf-title{font-weight:700;margin-bottom:.3rem}
.pdf-desc{font-size:.88rem;color:#64748b;margin-bottom:.6rem}
.pdf-ans{white-space:pre-wrap;font-size:.92rem;line-height:1.6;border-left:3px solid ${color};padding-left:.7rem}
.pdf-ans-empty{color:#94a3b8;font-size:.82rem;font-style:italic}
@media print{button{display:none}}
</style></head><body>
<h1>${esc(title)}</h1><div class="sub">${esc(subtitle)}</div>
${bodyHTML}
<button onclick="window.print()" style="margin-top:2rem;padding:.6rem 1.2rem;background:${color};color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:1rem">Als PDF drucken / speichern</button>
</body></html>`;
}

function openPDFWindow(html) {
  const win = window.open('', '_blank');
  if (!win) { alert('Bitte erlaube Pop-ups.'); return; }
  win.document.write(html); win.document.close();
}
```

---

## Checkliste für den KI-Agenten

- [ ] `COURSE_ID` eindeutig, nur `[a-z0-9-]`
- [ ] In `index.html` verlinkt
- [ ] `esc()` für alle dynamischen Texte
- [ ] CSS-Variablen aus `:root` (kein hardcoding)
- [ ] Auth-Flow implementiert (Passphrase aus localStorage, **kein eigenes Login-Formular**)
- [ ] Fortschritt per `hash/report` gemeldet
- [ ] Fortschritt beim Laden per `hash/get` wiederhergestellt
- [ ] `<script src="lms-footer.js"></script>` eingebunden
- [ ] `LMSFooter.init({...})` im Boot aufgerufen (nach IDENTITY gesetzt)
- [ ] `LMSFooter.update()` nach jedem Fortschrittsschritt aufgerufen
- [ ] `LMSFooter.onTabChange()` bei Tab- und Lektionswechsel aufgerufen
- [ ] `onExport: () => exportAllPDF()` übergeben (zeigt "📄 Export"-Button)
- [ ] `getCurrentTab` gibt `'aufg'` zurück wenn Aufgaben-Tab aktiv (für Tutor-FAB)
- [ ] Kein `console.log` im Produktionscode
- [ ] Alle Texte auf Deutsch
- [ ] `onclick`-Attribute: keine Sonderzeichen direkt einbetten → Daten aus JS-Objekt lesen
- [ ] Keine externen CDN-Links außer Pyodide (wenn Python benötigt)
