/* Nora — Deeskalations-Coach Widget
* Vanilla JS, no build, no framework, keine externen Fonts/Analytics.
* Chat | Quiz | Flashcards | Fortschritt | Toolkit — localStorage only.
*/
(() => {
'use strict';
// ==== Config ====
const API = 'https://llm.qognio.com/api/bots/deesk-coach/chat';
const RAW_KEY = window.__NORA_KEY__ || '';
const KEY = /^qb_[a-zA-Z0-9]{6,}$/.test(RAW_KEY) ? RAW_KEY : '';
const LS_KEY = 'nora.state.v1';
const LS_CHAT = 'nora.chat.v1';
const LS_FLASH = 'nora.flash.v1';
// ==== State ====
let CURRICULA = null;
let state = loadState();
let chatHistory = loadChatHistory();
let flashCards = loadFlashCards(); // { [topicId]: [{front, back, hint, ef, interval, due, reps}] }
// ==== Utils ====
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => Array.from(r.querySelectorAll(s));
const now = () => Date.now();
const today = () => {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
};
function daysBetween(dateA, dateB) {
const a = new Date(dateA); const b = new Date(dateB);
return Math.round((b - a) / 86400000);
}
// ==== Persistence ====
function loadState() {
try {
const s = JSON.parse(localStorage.getItem(LS_KEY) || '{}');
return {
xp: s.xp || 0,
totalAnswers: s.totalAnswers || 0,
correctAnswers: s.correctAnswers || 0,
currentStreak: s.currentStreak || 0,
maxStreak: s.maxStreak || 0,
lastActive: s.lastActive || null,
quizStreak: s.quizStreak || 0,
maxQuizStreak: s.maxQuizStreak || 0,
mastery: s.mastery || {}, // { [curriculumId]: { correct, total } }
moduleCorrect: s.moduleCorrect || {}, // { [moduleId]: correctCount }
moduleTotal: s.moduleTotal || {}, // { [moduleId]: totalAnswered }
modulePassedFlash: s.modulePassedFlash || {}, // { [moduleId]: true when all cards rated >= "gut"}
completedQuizzes: s.completedQuizzes || 0,
flashCardsRated: s.flashCardsRated || 0,
badges: s.badges || {},
seenWelcome: s.seenWelcome || false,
completedCurricula: s.completedCurricula || []
};
} catch (e) {
return {
xp: 0, totalAnswers: 0, correctAnswers: 0,
currentStreak: 0, maxStreak: 0, lastActive: null,
quizStreak: 0, maxQuizStreak: 0, mastery: {},
moduleCorrect: {}, moduleTotal: {}, modulePassedFlash: {},
completedQuizzes: 0, flashCardsRated: 0,
badges: {}, seenWelcome: false, completedCurricula: []
};
}
}
function saveState() { localStorage.setItem(LS_KEY, JSON.stringify(state)); }
function loadChatHistory() {
try { return JSON.parse(localStorage.getItem(LS_CHAT) || '[]'); } catch (e) { return []; }
}
function saveChatHistory() {
// Cap at last 40 exchanges to keep API payload small
const trimmed = chatHistory.slice(-40);
localStorage.setItem(LS_CHAT, JSON.stringify(trimmed));
}
function loadFlashCards() {
try { return JSON.parse(localStorage.getItem(LS_FLASH) || '{}'); } catch (e) { return {}; }
}
function saveFlashCards() { localStorage.setItem(LS_FLASH, JSON.stringify(flashCards)); }
// ==== Streak / activity tracking ====
function touchActivity() {
const t = today();
if (state.lastActive === t) return;
if (state.lastActive) {
const diff = daysBetween(state.lastActive, t);
if (diff === 1) state.currentStreak += 1;
else if (diff > 1) state.currentStreak = 1;
else state.currentStreak = Math.max(1, state.currentStreak);
} else {
state.currentStreak = 1;
}
state.lastActive = t;
if (state.currentStreak > state.maxStreak) state.maxStreak = state.currentStreak;
checkBadges();
saveState();
}
// ==== XP / Level ====
function addXP(n, reason = '') {
state.xp += n;
saveState();
showXPGain(`+${n} XP${reason ? ' · ' + reason : ''}`);
}
function levelInfo() {
const levels = (CURRICULA && CURRICULA.levels) || [
{ min: 0, title: 'Einsteiger:in' }, { min: 50, title: 'Praktiker:in' },
{ min: 200, title: 'Erfahrene:r' }, { min: 500, title: 'Profi' },
{ min: 1250, title: 'Multiplikator:in' }, { min: 2500, title: 'Coach' },
{ min: 5000, title: 'Mediator:in' }
];
let cur = levels[0];
for (const l of levels) if (state.xp >= l.min) cur = l;
const idx = levels.indexOf(cur);
const next = levels[idx + 1] || null;
const pct = next ? Math.min(100, ((state.xp - cur.min) / (next.min - cur.min)) * 100) : 100;
return { levelNum: idx + 1, title: cur.title, pct, next };
}
// ==== Badges ====
function unlockBadge(id) {
if (state.badges[id]) return false;
state.badges[id] = today();
saveState();
const badge = (CURRICULA && CURRICULA.badges || []).find(b => b.id === id);
if (badge) toast('🏆 Neues Abzeichen: ' + badge.title, 'success', 4500);
return true;
}
function checkBadges() {
// Erster Dialog — 1 Quiz bestanden (irgendein Modul)
if ((state.completedQuizzes || 0) >= 1) unlockBadge('erste_deeskalation');
// GFK-Meister:in — Rosenberg-Modul komplett (Modul 'gfk' in completedCurricula)
if ((state.completedCurricula || []).includes('gfk')) unlockBadge('gfk_meister');
// Glasl-Detektiv — Grundlagen-Modul komplett
if ((state.completedCurricula || []).includes('grundlagen')) unlockBadge('glasl_expert');
// CPI-zertifiziert — Deeskalations-Phasen-Modul komplett
if ((state.completedCurricula || []).includes('phasen')) unlockBadge('cpi_certified');
// Active Listener — Aktiv-Zuhören-Modul komplett
if ((state.completedCurricula || []).includes('zuhoeren')) unlockBadge('active_listener');
// Körpersprache-Pro — Körpersprache-Modul komplett
if ((state.completedCurricula || []).includes('koerpersprache')) unlockBadge('body_language_pro');
// 2-Wochen-Disziplin — 14-Tage-Streak
if (state.maxStreak >= 14) unlockBadge('streak_14');
// Deeskalations-Profi — alle 10 Module complete
if ((state.completedCurricula || []).length >= 10) unlockBadge('all_rounder');
// Night Owl (late-night Lerner)
const h = new Date().getHours();
if (h >= 22) unlockBadge('night_owl');
}
// ==== Toast ====
function toast(msg, kind = '', ms = 3200) {
const stack = $('#toast-stack');
const t = document.createElement('div');
t.className = 'toast ' + kind;
t.textContent = msg;
stack.appendChild(t);
setTimeout(() => {
t.style.opacity = '0';
t.style.transition = 'opacity .25s';
setTimeout(() => t.remove(), 260);
}, ms);
}
function showXPGain(txt) {
const el = document.createElement('div');
el.className = 'xp-gain';
el.textContent = txt;
document.body.appendChild(el);
setTimeout(() => el.remove(), 1600);
}
// ==== Simple markdown renderer ====
function renderMD(md) {
if (!md) return '';
let s = md;
// Escape HTML first
s = s.replace(/&/g, '&').replace(//g, '>');
// Code fences
s = s.replace(/```(\w+)?\n([\s\S]*?)```/g, (_, lang, code) =>
`
${TYPE_BADGE[obj.type] || '📄 Strukturierte Antwort'}
`;
if (obj.topic) html += `
${esc(obj.topic)}
`;
if (obj.type === 'case') {
if (obj.scenario) html += `
Szenario:
${esc(obj.scenario)}
`;
const fragen = obj.fragen || obj.questions || [];
fragen.forEach((f, i) => {
html += `
Frage ${i+1}: ${esc(f.frage || f.q || '')}`;
const opts = f.options || [];
if (opts.length) {
html += '
';
opts.forEach((o, j) => {
const correct = (f.correct === j);
html += `- ${label[j]||(j+1)}) ${esc(o)}${correct ? ' ✓' : ''}
`;
});
html += '
';
}
const ex = f.explain || f.explanation;
if (ex) html += `
Erklärung: ${esc(ex)}
`;
html += '
';
});
const lessons = obj.lessons || [];
if (lessons.length) {
html += '
Lessons:';
lessons.forEach(l => { html += `- ${esc(l)}
`; });
html += '
';
}
const norm = obj.paragraphen || obj.normen || obj.artikel || [];
if (norm.length) {
html += '
Rechtsnormen: ' + norm.map(n => `${esc(n)}`).join(' · ') + '
';
}
} else if (obj.type === 'quiz' || obj.type === 'exam') {
// Exam-spezifisch: Dauer-Anzeige + HF-Tag pro Frage (IDA AdA-Schein)
if (obj.type === 'exam' && obj.duration_min) {
html += `
Dauer: ${esc(obj.duration_min)} Min · Fragen: ${(obj.questions||[]).length}
`;
}
const qs = obj.questions || [];
qs.forEach((q, i) => {
const hfTag = (obj.type === 'exam' && q.hf != null) ? `
[HF ${esc(q.hf)}]` : '';
html += `
Frage ${i+1}:${hfTag} ${esc(q.q || q.frage || '')}`;
const opts = q.options || [];
if (opts.length) {
html += '
';
opts.forEach((o, j) => {
const correct = (q.correct === j);
html += `- ${label[j]||(j+1)}) ${esc(o)}${correct ? ' ✓' : ''}
`;
});
html += '
';
}
const ex = q.explain || q.explanation;
if (ex) html += `
Erklärung: ${esc(ex)}
`;
html += '
';
});
} else if (obj.type === 'flashcards') {
(obj.cards || []).forEach((c, i) => {
html += `
Karte ${i+1}: ${esc(c.front || '')}
${esc(c.back || '')}
`;
if (c.hint) html += `
Hinweis: ${esc(c.hint)}
`;
html += '
';
});
} else if (obj.type === 'lesson' || obj.type === 'presentation') {
if (obj.objectives || obj.learning_objectives) {
const objs = obj.objectives || obj.learning_objectives;
html += '
Lernziele:';
objs.forEach(o => { html += `- ${esc(o)}
`; });
html += '
';
}
(obj.slides || []).forEach((s, i) => {
html += `
${i+1}. ${esc(s.title || '')}`;
if (s.content_md || s.content) html += `
${renderMD(s.content_md || s.content || '')}
`;
if (s.key_point) html += `
💡 ${esc(s.key_point)}
`;
html += '
';
});
} else if (obj.type === 'audit') {
// KURT / VESTIGIA — AI-Act Audit-Trail
if (obj.system) html += `
System: ${esc(obj.system)}
`;
const cls = obj.ai_act_risk_class || obj.risk_class;
if (cls) {
const clsColor = { prohibited:'#dc2626', high:'#dc2626', limited:'#eab308', minimal:'#22c55e' }[cls] || '#8b8a99';
html += `
Risiko-Klasse: ${esc(cls)}
`;
}
if (obj.role) html += `
Rolle: ${esc(obj.role)}
`;
if (obj.dsgvo_relevant != null) html += `
DSGVO-relevant: ${obj.dsgvo_relevant ? 'ja' : 'nein'}
`;
if (obj.art22_check) html += `
Art. 22 DSGVO: ${esc(obj.art22_check)}
`;
const arts = obj.required_artifacts || [];
if (arts.length) {
html += '
Erforderliche Artefakte:| Artefakt | Status | Basis |
';
arts.forEach(a => {
const sColor = { required:'#dc2626', optional:'#eab308', 'not-required':'#22c55e' }[a.status] || '#8b8a99';
html += `| ${esc(a.name||'')} | ${esc(a.status||'')} | ${esc(a.based_on||'')} |
`;
});
html += '
';
}
const cw = obj.crosswalk_savings || [];
if (cw.length) {
html += '
Crosswalk-Einsparung:';
cw.forEach(c => { html += `- ${esc(c)}
`; });
html += '
';
}
const dl = obj.deadlines || [];
if (dl.length) {
html += '
Fristen:';
dl.forEach(d => { html += `${esc(d.date||'')} — ${esc(d.what||'')} `; });
html += '
';
}
const ws = obj.warnings || obj.warnung || [];
if (ws.length) {
html += '
⚠ Warnungen:';
ws.forEach(w => { html += `- ${esc(w)}
`; });
html += '
';
}
} else if (obj.type === 'privacy_check') {
// PAUL — Pflege-Datenschutz-Ampel
const ampel = obj.ampel || 'yellow';
const ampelColor = { green:'#22c55e', yellow:'#eab308', red:'#dc2626' }[ampel] || '#8b8a99';
const ampelLabel = { green:'🟢 GRÜN — OK', yellow:'🟡 GELB — Vorsicht', red:'🔴 ROT — Akute Gefahr' }[ampel] || ampel;
html += `
${ampelLabel}
`;
if (obj.situation) html += `
Situation: ${esc(obj.situation)}
`;
if (obj.schweigepflicht) html += `
Schweigepflicht (§203 StGB): ${esc(obj.schweigepflicht)}
`;
if (obj.dsgvo_basis) html += `
DSGVO-Basis: ${esc(obj.dsgvo_basis)}
`;
const acts = obj.handlung || obj.handlungen || [];
if (acts.length) {
html += '
Handlung:';
acts.forEach(a => { html += `- ${esc(a)}
`; });
html += '
';
}
const wns = obj.warnung || obj.warnungen || [];
if (wns.length) {
html += `
⚠ Achtung:`;
wns.forEach(w => { html += `- ${esc(w)}
`; });
html += '
';
}
} else if (obj.type === 'mail_check') {
// Pia — Phishing-Prüferin Mail-Bewertung
const ampel = obj.ampel || 'yellow';
const ampelColor = { green:'#22c55e', yellow:'#eab308', red:'#dc2626' }[ampel] || '#8b8a99';
const ampelLabel = { green:'🟢 GRÜN — vermutlich legitim', yellow:'🟡 GELB — verdächtig, zweite Meinung holen', red:'🔴 ROT — sehr wahrscheinlich Phishing/BEC' }[ampel] || ampel;
html += `
${ampelLabel}
`;
if (obj.kurz_begruendung) html += `
Kurz: ${esc(obj.kurz_begruendung)}
`;
if (obj.pattern) html += `
Pattern: ${esc(obj.pattern)}
`;
const flags = obj.red_flags || [];
if (flags.length) {
html += '
Red Flags:';
flags.forEach(f => { html += `- ${esc(f)}
`; });
html += '
';
}
const recs = obj.empfohlene_aktion || obj.aktionen || [];
if (recs.length) {
html += '
Empfohlene Aktion:';
recs.forEach(a => { html += `- ${esc(a)}
`; });
html += '
';
}
if (obj.weiterleiten_an) html += `
Weiterleiten an: ${esc(obj.weiterleiten_an)}
`;
} else if (obj.type === 'plan') {
// Otto — 90-Tage-Onboarding-Plan
if (obj.role) html += `
Rolle: ${esc(obj.role)}
`;
const items = obj.weeks || obj.days || obj.steps || obj.phases || [];
if (items.length) {
items.forEach((it, i) => {
const label = it.day || it.week || it.phase || `Phase ${i+1}`;
const focus = it.focus || it.title || '';
html += `
${esc(label)}${focus?` — ${esc(focus)}`:''}
`;
const tasks = it.tasks || it.aufgaben || [];
if (tasks.length) {
html += '
';
tasks.forEach(t => { html += `- ${esc(typeof t === 'string' ? t : (t.task || t.text || JSON.stringify(t)))}
`; });
html += '
';
}
if (it.success_signal || it.success) html += `
✓ ${esc(it.success_signal || it.success)}
`;
html += '
';
});
}
} else if (obj.type === 'validate') {
// Eli — E-Rechnungs-Validator
const status = obj.status || 'ok';
const statusColor = { ok:'#22c55e', warnings:'#eab308', errors:'#dc2626' }[status] || '#8b8a99';
const statusLabel = { ok:'✅ Validierung OK', warnings:'⚠ Warnungen', errors:'❌ Fehler — nicht konform' }[status] || status;
html += `
${statusLabel}
`;
if (obj.file || obj.dateiname) html += `
Datei: ${esc(obj.file || obj.dateiname)}
`;
if (obj.format) html += `
Format: ${esc(obj.format)}
`;
const issues = obj.issues || obj.findings || [];
if (issues.length) {
html += '
Befunde:| Severity | Meldung | Fix |
';
issues.forEach(i => {
const sev = i.severity || 'info';
const sevColor = { error:'#dc2626', warning:'#eab308', info:'#06b6d4' }[sev] || '#8b8a99';
html += `| ${esc(sev)} | ${esc(i.message||i.msg||'')} | ${esc(i.fix||'')} |
`;
});
html += '
';
}
} else if (obj.type === 'interview') {
// LIMEN — Wissens-Interview entlang Achse
if (obj.achse) html += `
Wissens-Achse: ${esc(obj.achse)}
`;
const fs = obj.fragen || obj.questions || [];
fs.forEach((f, i) => {
html += `
Frage ${i+1}: ${esc(f.f || f.frage || f.q || '')}`;
if (f.tipp_aktiv_zuhören || f.tipp_zuhoeren) html += `
👂 Tipp Aktiv-Zuhören: ${esc(f.tipp_aktiv_zuhören || f.tipp_zuhoeren)}
`;
if (f.tipp_nachfass) html += `
↳ Tipp Nachfass: ${esc(f.tipp_nachfass)}
`;
html += '
';
});
} else if (obj.type === 'decode') {
// Zita — Zeugnis-Decoder
if (obj.zeugnis_text) html += `
Zeugnis-Text:${esc(obj.zeugnis_text)}
`;
const gradeNum = (g) => {
const n = typeof g === 'number' ? g : parseFloat(String(g).replace(',','.'));
return isNaN(n) ? null : n;
};
const gradeColor = (g) => {
const n = gradeNum(g);
if (n != null) return n <= 2 ? '#22c55e' : n <= 3 ? '#eab308' : '#dc2626';
return ({ sehr_gut:'#22c55e', gut:'#22c55e', befriedigend:'#eab308', ausreichend:'#eab308', mangelhaft:'#dc2626', ungenuegend:'#dc2626' }[String(g||'')] || '#8b8a99');
};
if (obj.overall_grade != null && obj.overall_grade !== '') {
const ogColor = gradeColor(obj.overall_grade);
html += `
Gesamt-Note: ${esc(obj.overall_grade)}${obj.grade_label ? ' — ' + esc(obj.grade_label) : ''}
`;
}
// Sub-Noten (verhalten/schlussformel)
if (obj.verhalten_grade != null || obj.schlussformel_grade != null) {
html += '
';
if (obj.verhalten_grade != null) html += `Verhalten: ${esc(obj.verhalten_grade)} `;
if (obj.schlussformel_grade != null) html += ` · Schlussformel: ${esc(obj.schlussformel_grade)}`;
html += '
';
}
// Akzeptiere mehrere Schema-Varianten: decodierung/codes/findings + passage/quote, klartext/bedeutung/meaning, note/grade, code/section
const dec = obj.decodierung || obj.codes || obj.findings || [];
if (dec.length) {
html += '
Code-Decodierung:';
dec.forEach(d => {
const passage = d.passage || d.quote || d.text || '';
const klartext = d.klartext || d.bedeutung || d.meaning || '';
const code = d.code || d.section || '';
const note = d.note || d.grade || '';
const risk = d.risk || '';
const noteColor = note !== '' ? gradeColor(note) : '#8b8a99';
html += `
"${esc(passage)}"
`;
if (code) html += `
${esc(code)}
`;
if (klartext) html += `
↳ ${esc(klartext)}
`;
if (note !== '') html += `
Note: ${esc(note)}${risk ? ` · Risiko: ${esc(risk)}` : ''}
`;
html += '
';
});
html += '
';
}
const redFlags = obj.red_flags || [];
if (redFlags.length) {
html += '
🚩 Red Flags:';
redFlags.forEach(f => { html += `- ${esc(f)}
`; });
html += '
';
}
const missing = obj.missing_elements || [];
if (missing.length) {
html += '
Fehlende Pflicht-Elemente:';
missing.forEach(m => { html += `- ${esc(m)}
`; });
html += '
';
}
const rewrites = obj.rewrite_suggestions || [];
if (rewrites.length) {
html += '
Umschreib-Vorschläge:| Original | Vorschlag | Warum |
';
rewrites.forEach(r => {
html += `| ${esc(r.original||'')} | ${esc(r.rewrite||'')} | ${esc(r.why||'')} |
`;
});
html += '
';
}
const dsources = obj.sources || obj.quellen || [];
if (dsources.length) {
html += '
Quellen: ' + dsources.map(s => `${esc(s)}`).join(' · ') + '
';
}
} else if (obj.type === 'write') {
// Zita — Zeugnis-Schreiber
if (obj.role) html += `
Rolle: ${esc(obj.role)}
`;
if (obj.grade != null) {
const gColor = obj.grade <= 2 ? '#22c55e' : obj.grade <= 3 ? '#eab308' : '#dc2626';
html += `
Note: ${esc(obj.grade)}
`;
}
const zeugnisText = obj.zeugnis || obj.zeugnis_text || obj.markdown || obj.text;
if (zeugnisText) {
html += '
Zeugnis-Entwurf:';
html += `
${esc(zeugnisText)}
`;
html += '
';
}
const notenSignale = obj.noten_signale || [];
if (notenSignale.length) {
html += '
Noten-Signale:| Satz | Codiert |
';
notenSignale.forEach(s => {
html += `| ${esc(s.satz || '')} | ${esc(s.codiert || '')} |
`;
});
html += '
';
}
const paragraphen = obj.verwendete_paragraphen || obj.paragraphen || [];
if (paragraphen.length) {
html += '
Verwendete Paragraphen: ' + paragraphen.map(p => `${esc(p)}`).join(' · ') + '
';
}
const warnings = obj.warnings || obj.warnungen || [];
if (warnings.length) {
html += '
⚠ Warnungen:';
warnings.forEach(w => { html += `- ${esc(w)}
`; });
html += '
';
}
const notes = obj.notes || obj.hinweise || [];
if (notes.length) {
html += '
Hinweise:';
notes.forEach(n => { html += `- ${esc(n)}
`; });
html += '
';
}
const wsources = obj.sources || obj.quellen || [];
if (wsources.length) {
html += '
Quellen: ' + wsources.map(s => `${esc(s)}`).join(' · ') + '
';
}
} else if (obj.type === 'calc') {
// LIBRA — Kalkulations-Rechner
if (obj.formel) html += `
Formel: ${esc(obj.formel)}
`;
if (obj.inputs && typeof obj.inputs === 'object') {
html += '
Eingaben:';
Object.entries(obj.inputs).forEach(([k,v]) => { html += `${esc(k)} | ${esc(v)} |
`; });
html += '
';
}
const steps = obj.schritte || obj.steps || [];
if (steps.length) {
html += '
Rechenweg:';
steps.forEach(s => { html += `- ${esc(typeof s === 'string' ? s : (s.text || JSON.stringify(s)))}
`; });
html += '
';
}
if (obj.ergebnis != null) html += `
Ergebnis: ${esc(obj.ergebnis)}
`;
} else if (obj.type === 'unterweisung') {
// IDA — AdA-Unterweisung (4-Stufen / Lehrgespraech / Leittext)
if (obj.methode) html += `
Methode: ${esc(obj.methode)}
`;
const lz = obj.lernzielanalyse || obj.lernziele || null;
if (lz && typeof lz === 'object') {
html += '
Lernzielanalyse:';
if (lz.richtlernziel) html += `| Richtlernziel | ${esc(lz.richtlernziel)} |
`;
if (lz.groblernziel) html += `| Groblernziel | ${esc(lz.groblernziel)} |
`;
if (lz.feinlernziel) html += `| Feinlernziel | ${esc(lz.feinlernziel)} |
`;
if (lz.bereich) html += `| Bereich | ${esc(lz.bereich)} |
`;
html += '
';
}
const phasen = obj.phasen || obj.stufen || obj.phases || [];
if (phasen.length) {
let totalMin = 0;
phasen.forEach(p => { if (typeof p.minuten === 'number') totalMin += p.minuten; });
html += `
Phasen${totalMin?` (${totalMin} Min gesamt)`:''}:`;
phasen.forEach((p, i) => {
html += `
${i+1}. ${esc(p.name || p.stufe || '')}${p.minuten?` · ${esc(p.minuten)} Min`:''}
`;
if (p.ausbilder_tut) html += `
Ausbilder:in: ${esc(p.ausbilder_tut)}
`;
if (p.azubi_tut) html += `
Azubi: ${esc(p.azubi_tut)}
`;
if (p.feedback_check) html += `
Check: ${esc(p.feedback_check)}
`;
html += '
';
});
html += '
';
}
if (obj.erfolgskontrolle) {
html += `
Erfolgskontrolle: ${esc(obj.erfolgskontrolle)}
`;
}
const alt = obj.handlungsalternativen || obj.alternativen || [];
if (alt.length) {
html += '
Handlungs-Alternativen:';
alt.forEach(a => { html += `- ${esc(a)}
`; });
html += '
';
}
}
const HINT_BY_TYPE = {
audit:'↳ Crosswalk gespeichert. Brauchst du Tech-Doku Anhang IV (VESTIGIA) oder die DPIA-Tiefe (Cora)?',
privacy_check:'↳ Bei Unsicherheit: PDL/EL informieren, Schweigepflicht §203 StGB hat Vorrang vor DSGVO.',
mail_check:'↳ Im Zweifel: nicht klicken, an IT/SOC weiterleiten. Eskalation > Schaden.',
plan:'↳ Tipp: 1. Tag-Erlebnis ist der wichtigste Baustein — ohne Wow-Moment bröckelt die Probezeit.',
validate:'↳ Bei "errors": Rechnung nicht buchen, Lieferanten kontaktieren, Validator-Output anhängen.',
interview:'↳ Story-First: lass die Person erzählen, hör 3 Sekunden nach der Antwort weiter zu.',
decode:'↳ Bei kritischen Codes: Anwält:in für Arbeitsrecht konsultieren, Zeugnis-Anfechtung prüfen.',
write:'↳ Vor Übergabe: 4-Augen-Review durch HR-Lead, Datum prüfen, korrekt unterschreiben.',
calc:'↳ Stichproben mit echten Aufträgen testen — Theorie-Marge ≠ Praxis-Marge.',
unterweisung:'↳ Methode folgt Lernziel: psychomotorisch → 4-Stufen, kognitiv → Lehrgespräch, komplex → Leittext.',
};
const hint = HINT_BY_TYPE[obj.type] || '↳ Nutze den
Quiz-,
Karteikarten- oder
Chat-Tab für die interaktive Version.';
html += `
${hint}
`;
html += '
';
return html;
}
async function requestStructured(prompt) {
const data = await chatAPI(prompt, []);
let raw = _extractJSON(data.reply || '');
try { return JSON.parse(raw); } catch (e1) {
try { return JSON.parse(_repairJSON(raw)); } catch (e2) {
const heal = await chatAPI(prompt + "\n\nHINWEIS: Dein letzter JSON-Output war invalid (Parse-Fehler). Antworte JETZT NUR mit sauberem JSON. Keine Kommentare, kein Markdown-Fence, keine trailing commas, alle String-Werte in doppelten Anf\u00fchrungszeichen.", []);
const healRaw = _extractJSON(heal.reply || '');
try { return JSON.parse(healRaw); } catch (e3) {
try { return JSON.parse(_repairJSON(healRaw)); } catch (e4) {
const err = new Error('Konnte JSON nicht parsen \u2014 Modell liefert kaputtes Format. Versuch es noch mal.');
err.original = e1.message; err.raw = raw.slice(0, 300);
throw err;
}
}
}
}
}
// ==== Quiz ====
const quizState = { set: null, idx: 0, correct: 0, topic: null };
function renderQuizIntro() {
const host = $('#quiz-host');
const topics = CURRICULA.curricula.flatMap(c =>
c.modules.map(m => ({ id: m.id, curr: c.title, title: m.title, color: c.color }))
);
host.innerHTML = `