/* LIMEN — Wissenstransfer-Begleiter Widget * Vanilla JS, no build, no framework, keine externen Fonts/Analytics. * Chat | Quiz | Flashcards | Fortschritt | Wissens-Achsen — localStorage only. */ (() => { 'use strict'; // ==== Config ==== const API = 'https://llm.qognio.com/api/bots/limen-wissenstransfer/chat'; const RAW_KEY = window.__LIMEN_KEY__ || ''; const KEY = /^qb_[a-zA-Z0-9]{6,}$/.test(RAW_KEY) ? RAW_KEY : ''; const LS_KEY = 'limen.state.v1'; const LS_CHAT = 'limen.chat.v1'; const LS_FLASH = 'limen.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: 'Übergabe-Anfänger:in' }, { min: 50, title: 'Übergabe-Begleiter:in' }, { min: 200, title: 'Wissens-Hüter:in' }, { min: 500, title: 'Übergabe-Architekt:in' }, { min: 1250, title: 'Wissens-Diplomat:in' }, { min: 2500, title: 'Wissens-Schatzmeister:in' }, { min: 5000, title: 'Übergabe-Großmeister: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() { // Erstes Interview — 1 Quiz im Tagesroutinen-Modul if ((state.moduleCorrect && state.moduleCorrect['tagesroutinen'] >= 1)) unlockBadge('erstes_interview'); // Quirks-Sammler:in — 5 Quiz korrekt im Ausnahmen-Regeln-Modul (Quirks) if ((state.moduleCorrect && state.moduleCorrect['ausnahmen-regeln'] >= 5)) unlockBadge('quirks_sammler'); // Lieferanten-Kenner:in — 3 Quiz korrekt im Kontakte-mappen-Modul if ((state.moduleCorrect && state.moduleCorrect['kontakte-mappen'] >= 3)) unlockBadge('lieferanten_kenner'); // Story-Hüter:in — 3 Quiz korrekt im Krisen-Erinnerungen-Modul if ((state.moduleCorrect && state.moduleCorrect['krisen-erinnerungen'] >= 3)) unlockBadge('story_hueter'); // Ritual-Wächter:in — Co-Working-Phase-Modul Flashcards bestanden if ((state.modulePassedFlash && state.modulePassedFlash['co-working-phase'])) unlockBadge('ritual_waechter'); // LIMEN-Meister:in — 9 von 12 Modulen mit ≥80% Quiz-Score abgeschlossen (75% Master-Coverage) if ((state.completedCurricula || []).length >= 9) unlockBadge('limen_meister'); // Streak 7 — 7 Tage in Folge aktiv (entspricht 1 Interview-Sprint) if (state.maxStreak >= 7) unlockBadge('streak_7'); // Night Owl & Early Bird (beibehalten) const h = new Date().getHours(); if (h >= 22) unlockBadge('night_owl'); if (h < 7) unlockBadge('early_bird'); } // ==== 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) => `
${code}
`); // Inline code s = s.replace(/`([^`\n]+)`/g, '$1'); // GFM tables: header / separator / body s = s.replace(/(?:^|\n)((?:\|[^\n]*\|[ \t]*\n){2,})/g, (block, content) => { const lines = content.trim().split('\n'); if (lines.length < 2) return block; const sep = lines[1]; if (!/^\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\|?\s*$/.test(sep)) return block; const parseRow = (ln) => ln.replace(/^\|/, '').replace(/\|\s*$/, '').split('|').map(c => c.trim()); const header = parseRow(lines[0]); const aligns = parseRow(sep).map(s => /^:-+:$/.test(s) ? 'center' : /-+:$/.test(s) ? 'right' : 'left'); const rows = lines.slice(2).map(parseRow); let html = '\n'; header.forEach((h, i) => { html += ``; }); html += ''; rows.forEach(r => { html += ''; for (let i = 0; i < Math.max(r.length, header.length); i++) { html += ``; } html += ''; }); html += '
${h}
${r[i] || ''}
\n'; return html; }); // Bold s = s.replace(/\*\*([^*\n]+)\*\*/g, '$1'); s = s.replace(/__([^_\n]+)__/g, '$1'); // Italic s = s.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1$2'); // Headings s = s.replace(/^### (.+)$/gm, '

$1

'); s = s.replace(/^## (.+)$/gm, '

$1

'); s = s.replace(/^# (.+)$/gm, '

$1

'); // Links s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '$1'); // Unordered lists s = s.replace(/(?:^|\n)((?:[*\-] .+\n?)+)/g, (m) => { const items = m.trim().split('\n').map(ln => ln.replace(/^[*\-] /, '')).map(li => `
  • ${li}
  • `).join(''); return '\n'; }); // Ordered lists s = s.replace(/(?:^|\n)((?:\d+\. .+\n?)+)/g, (m) => { const items = m.trim().split('\n').map(ln => ln.replace(/^\d+\. /, '')).map(li => `
  • ${li}
  • `).join(''); return '\n
      ' + items + '
    '; }); // Paragraphs: split on blank lines s = s.split(/\n{2,}/).map(block => { if (/^<(h\d|ul|ol|pre|blockquote|table)/.test(block.trim())) return block; return '

    ' + block.replace(/\n/g, '
    ') + '

    '; }).join('\n'); return s; } // ==== Chat API ==== async function chatAPI(message, history, attachments) { const body = { message, history }; if (Array.isArray(attachments) && attachments.length > 0) body.attachments = attachments; const r = await fetch(API, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + KEY }, body: JSON.stringify(body) }); let data; try { data = await r.json(); } catch (e) { throw new Error('Server-Antwort nicht lesbar'); } if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status)); return data; } // ==== Attachments (file upload) ==== const ATTACH_MAX_COUNT = 5; const ATTACH_MAX_BYTES = 8 * 1024 * 1024; const ATTACH_ACCEPTED_RE = /\.(pdf|txt|md|csv|json|xml|yaml|yml|log|png|jpg|jpeg|webp|gif)$/i; let pendingAttachments = []; // [{ name, mimeType, dataUrl, size }] function fmtSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / 1024 / 1024).toFixed(1) + ' MB'; } function fileToDataUrl(file) { return new Promise((resolve, reject) => { const fr = new FileReader(); fr.onload = () => resolve(fr.result); fr.onerror = () => reject(new Error('Datei konnte nicht gelesen werden')); fr.readAsDataURL(file); }); } function renderAttachStrip() { const strip = $('#attach-strip'); if (!strip) return; strip.innerHTML = ''; pendingAttachments.forEach((a, idx) => { const chip = document.createElement('span'); chip.className = 'attach-chip'; const ico = a.mimeType.startsWith('image/') ? '🖼' : (a.mimeType === 'application/pdf' || /\.pdf$/i.test(a.name)) ? '📄' : '📝'; chip.innerHTML = `${ico}${a.name.replace(/[<>"']/g, '')}${fmtSize(a.size)}`; const rm = document.createElement('button'); rm.type = 'button'; rm.className = 'attach-chip-remove'; rm.setAttribute('aria-label', 'Anhang entfernen'); rm.textContent = '×'; rm.addEventListener('click', () => { pendingAttachments.splice(idx, 1); renderAttachStrip(); }); chip.appendChild(rm); strip.appendChild(chip); }); } async function addFiles(fileList) { const files = Array.from(fileList || []); for (const f of files) { if (pendingAttachments.length >= ATTACH_MAX_COUNT) { toast(`Max ${ATTACH_MAX_COUNT} Anhänge — weitere ignoriert`, 'warn'); break; } if (!ATTACH_ACCEPTED_RE.test(f.name) && !f.type.startsWith('image/') && !/text|json|xml|pdf/i.test(f.type)) { toast(`${f.name}: Format nicht unterstützt`, 'error'); continue; } if (f.size > ATTACH_MAX_BYTES) { toast(`${f.name}: ${fmtSize(f.size)} > Limit ${fmtSize(ATTACH_MAX_BYTES)}`, 'error'); continue; } try { const dataUrl = await fileToDataUrl(f); pendingAttachments.push({ name: f.name, mimeType: f.type || 'application/octet-stream', dataUrl, size: f.size, }); } catch (e) { toast(`${f.name}: ${e.message}`, 'error'); } } renderAttachStrip(); } // ==== Chat UI ==== function addMsg(role, content, { markdown = false, pending = false, attachments = null } = {}) { const box = $('#chat-box'); const el = document.createElement('div'); el.className = 'msg ' + role; if (pending) { el.innerHTML = ''; } else if (markdown) { el.innerHTML = renderMD(content); } else { el.textContent = content; } if (Array.isArray(attachments) && attachments.length > 0) { const wrap = document.createElement('div'); wrap.className = 'msg-attachments'; attachments.forEach(a => { const ico = a.mimeType && a.mimeType.startsWith('image/') ? '🖼' : (a.mimeType === 'application/pdf' || /\.pdf$/i.test(a.name)) ? '📄' : '📝'; const span = document.createElement('span'); span.className = 'att-name'; span.textContent = `${ico} ${a.name} (${fmtSize(a.size)})`; wrap.appendChild(span); }); el.appendChild(wrap); } box.appendChild(el); box.scrollTop = box.scrollHeight; setTimeout(() => { $('.main').scrollTop = $('.main').scrollHeight; }, 20); return el; } function clearChatUI() { $('#chat-box').innerHTML = ''; } function renderWelcome() { if (state.xp === 0 && chatHistory.length === 0 && !state.seenWelcome) { $('#welcome-screen').classList.remove('hidden'); $('#welcome-screen').setAttribute('aria-hidden', 'false'); } else { $('#welcome-screen').classList.add('hidden'); $('#welcome-screen').setAttribute('aria-hidden', 'true'); } } function restoreChat() { clearChatUI(); for (const m of chatHistory) { addMsg(m.role === 'assistant' ? 'bot' : 'user', m.content, { markdown: m.role === 'assistant' }); } renderWelcome(); } async function sendChat(text) { const attaches = pendingAttachments.slice(); if (!text.trim() && attaches.length === 0) return; $('#welcome-screen').classList.add('hidden'); state.seenWelcome = true; addMsg('user', text || '(nur Anhang)', { attachments: attaches }); chatHistory.push({ role: 'user', content: text + (attaches.length ? '\n[Anhänge: ' + attaches.map(a => a.name).join(', ') + ']' : ''), }); pendingAttachments = []; renderAttachStrip(); const pend = addMsg('bot', '', { pending: true }); $('#composer-send').disabled = true; try { const hist = chatHistory.slice(-20, -1); const data = await chatAPI(text, hist, attaches.length ? attaches.map(a => ({ name: a.name, mimeType: a.mimeType, dataUrl: a.dataUrl })) : null); pend.classList.remove('pending'); const structured = _tryParseStructuredReply(data.reply || ''); if (structured) { pend.innerHTML = _renderStructuredInChat(structured); } else { pend.innerHTML = renderMD(data.reply || ''); } if (Array.isArray(data.attachment_notes) && data.attachment_notes.length) { const notice = document.createElement('div'); notice.className = 'attachment-notice'; notice.textContent = '📎 ' + data.attachment_notes.join(' · '); pend.appendChild(notice); } chatHistory.push({ role: 'assistant', content: data.reply }); saveChatHistory(); touchActivity(); // Soft-Gate: postMessage to parent window after N user→assistant // exchanges (configurable via ?showcase=1 query param). The outer // qognio.com/showcase page listens for this event and triggers the // lead-form modal. Sent only when running in an iframe. try { const isShowcase = new URLSearchParams(window.location.search).get('showcase') === '1'; if (isShowcase && window.parent && window.parent !== window) { const userMessages = chatHistory.filter((m) => m.role === 'user').length; const SOFT_GATE_AT = 3; if (userMessages === SOFT_GATE_AT) { window.parent.postMessage( { type: 'qognio:soft-gate', afterMessages: userMessages, botSlug: window.location.hostname.split('.')[0] }, '*' ); } } } catch (sgErr) { // non-fatal — swallow } } catch (e) { pend.className = 'msg sys'; pend.textContent = '⚠ Fehler: ' + (e.message || 'unbekannt'); toast('Verbindung fehlgeschlagen', 'error'); } finally { $('#composer-send').disabled = false; $('#composer').focus(); } } // ==== Structured request helper ==== function _extractJSON(reply) { let s = (reply || '').trim(); const fence = s.match(/```(?:json)?\s*([\s\S]*?)\s*```/); if (fence) s = fence[1].trim(); const b = s.indexOf('{'); const e = s.lastIndexOf('}'); if (b >= 0 && e > b) s = s.slice(b, e + 1); return s; } function _repairJSON(s) { s = s.replace(/,(\s*[}\]])/g, '$1'); s = s.replace(/,\s*,/g, ','); s = s.replace(/(\})\s*\n\s*(\{)/g, '$1,\n$2'); s = s.replace(/(")(\s*\n\s*)(")/g, '$1,$2$3'); return s; } // --- Structured-Reply-Fallback fuer Chat (2026-04-25) --- // Wenn der Bot versehentlich QUIZ/FLASHCARD/CASE/EXAM-JSON liefert statt Markdown, // parse + render hier lesbar statt Raw-JSON im Chat-Bubble anzuzeigen. function _tryParseStructuredReply(reply) { let s = (reply || '').trim(); const fence = s.match(/```(?:json)?\s*([\s\S]*?)\s*```/); if (fence) s = fence[1].trim(); if (!s.startsWith('{')) return null; const b = s.indexOf('{'), e = s.lastIndexOf('}'); if (b < 0 || e <= b) return null; const raw = s.slice(b, e + 1); let obj; try { obj = JSON.parse(raw); } catch { try { obj = JSON.parse(_repairJSON(raw)); } catch { return null; } } if (!obj || typeof obj !== 'object') return null; const KNOWN = [ 'case','quiz','flashcards','exam','lesson','presentation', // Bot-spezifische Strukturen (KURT/VESTIGIA/PAUL/Pia/Otto/Eli/LIMEN/Zita/LIBRA/IDA) 'audit','privacy_check','mail_check','plan','validate','interview','decode','write','calc','unterweisung' ]; if (!KNOWN.includes(obj.type)) return null; return obj; } function _renderStructuredInChat(obj) { const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const label = ['A','B','C','D','E','F','G','H']; const TYPE_BADGE = { case:'📖 Fallbeispiel', quiz:'🧠 Quiz-Fragen', flashcards:'📇 Karteikarten', exam:'📝 Prüfung', lesson:'🎓 Lektion', presentation:'🖼 Präsentation', audit:'🛡 AI-Act Audit-Trail', privacy_check:'🩺 Pflege-Datenschutz-Check', mail_check:'📧 Mail-Check', plan:'🗓 Plan', validate:'✓ Validierung', interview:'🎙 Wissens-Interview', decode:'🔍 Zeugnis-Decoder', write:'✍ Zeugnis-Entwurf', calc:'🧮 Kalkulation', unterweisung:'🛠 AdA-Unterweisung', }; let html = `
    ${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 += ''; } const ex = f.explain || f.explanation; if (ex) html += `
    Erklärung: ${esc(ex)}
    `; html += '
    '; }); const lessons = obj.lessons || []; if (lessons.length) { html += '
    Lessons:
    '; } 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 += ''; } 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:
    '; } (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:'; arts.forEach(a => { const sColor = { required:'#dc2626', optional:'#eab308', 'not-required':'#22c55e' }[a.status] || '#8b8a99'; html += ``; }); html += '
    ArtefaktStatusBasis
    ${esc(a.name||'')}${esc(a.status||'')}${esc(a.based_on||'')}
    '; } const cw = obj.crosswalk_savings || []; if (cw.length) { html += '
    Crosswalk-Einsparung:
    '; } const dl = obj.deadlines || []; if (dl.length) { html += '
    Fristen:
    '; } const ws = obj.warnings || obj.warnung || []; if (ws.length) { html += '
    ⚠ Warnungen:
    '; } } 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 += `
    1. ${esc(a)}
    2. `; }); html += '
    '; } const wns = obj.warnung || obj.warnungen || []; if (wns.length) { html += `
    ⚠ Achtung:
    '; } } 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:
    '; } const recs = obj.empfohlene_aktion || obj.aktionen || []; if (recs.length) { html += '
    Empfohlene Aktion:
      '; recs.forEach(a => { html += `
    1. ${esc(a)}
    2. `; }); 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 += ''; } 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:'; issues.forEach(i => { const sev = i.severity || 'info'; const sevColor = { error:'#dc2626', warning:'#eab308', info:'#06b6d4' }[sev] || '#8b8a99'; html += ``; }); html += '
    SeverityMeldungFix
    ${esc(sev)}${esc(i.message||i.msg||'')}${esc(i.fix||'')}
    '; } } 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:
    '; } const missing = obj.missing_elements || []; if (missing.length) { html += '
    Fehlende Pflicht-Elemente:
    '; } const rewrites = obj.rewrite_suggestions || []; if (rewrites.length) { html += '
    Umschreib-Vorschläge:'; rewrites.forEach(r => { html += ``; }); html += '
    OriginalVorschlagWarum
    ${esc(r.original||'')}${esc(r.rewrite||'')}${esc(r.why||'')}
    '; } 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:'; notenSignale.forEach(s => { html += ``; }); html += '
    SatzCodiert
    ${esc(s.satz || '')}${esc(s.codiert || '')}
    '; } 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:
    '; } const notes = obj.notes || obj.hinweise || []; if (notes.length) { html += '
    Hinweise:
    '; } 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 += ``; }); html += '
    ${esc(k)}${esc(v)}
    '; } const steps = obj.schritte || obj.steps || []; if (steps.length) { html += '
    Rechenweg:
      '; steps.forEach(s => { html += `
    1. ${esc(typeof s === 'string' ? s : (s.text || JSON.stringify(s)))}
    2. `; }); 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 += ``; if (lz.groblernziel) html += ``; if (lz.feinlernziel) html += ``; if (lz.bereich) html += ``; html += '
    Richtlernziel${esc(lz.richtlernziel)}
    Groblernziel${esc(lz.groblernziel)}
    Feinlernziel${esc(lz.feinlernziel)}
    Bereich${esc(lz.bereich)}
    '; } 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:
    '; } } 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 = `

    🎯 Quiz-Thema wählen

    Wähle eine Wissens-Achse — LIMEN führt dich durch Interview-Fragen für die ausscheidende Person.

    `; const pillHost = $('#quiz-topic-pills'); CURRICULA.curricula.forEach(c => { c.modules.forEach(m => { const btn = document.createElement('button'); btn.className = 'topic-pill'; btn.dataset.curr = c.id; btn.dataset.mod = m.id; btn.textContent = m.title; btn.title = c.title; btn.addEventListener('click', () => { $$('#quiz-topic-pills .topic-pill').forEach(p => p.setAttribute('aria-selected', 'false')); btn.setAttribute('aria-selected', 'true'); quizState.topic = { curr: c, mod: m }; }); pillHost.appendChild(btn); }); }); $('#quiz-start').addEventListener('click', () => { if (!quizState.topic) { toast('Bitte ein Thema wählen.', 'warn'); return; } const count = parseInt($('#quiz-count').value, 10); startQuiz(quizState.topic, count); }); } async function startQuiz(topic, count) { const host = $('#quiz-host'); host.innerHTML = `

    LIMEN stellt ${count} Interview-Fragen zu „${topic.mod.title}" …

    `; try { const topicText = `${topic.curr.title} — ${topic.mod.title}`; const data = await requestStructured(`QUIZ_REQUEST topic="${topicText}" count=${count}`); if (!data.questions || !Array.isArray(data.questions) || data.questions.length === 0) { throw new Error('Keine Fragen erhalten'); } quizState.set = data.questions; quizState.idx = 0; quizState.correct = 0; renderQuizQuestion(); } catch (e) { host.innerHTML = `

    ⚠ Konnte Quiz nicht erstellen: ${e.message}

    `; toast('Quiz-Generierung fehlgeschlagen', 'error'); } } function renderQuizQuestion() { const host = $('#quiz-host'); const q = quizState.set[quizState.idx]; if (!q) return renderQuizDone(); const letters = ['A', 'B', 'C', 'D', 'E', 'F']; host.innerHTML = `
    Frage ${quizState.idx + 1} / ${quizState.set.length} ✓ ${quizState.correct}
    ${escapeHTML(q.q)}
    ${q.options.map((opt, i) => ` `).join('')}
    `; $$('#quiz-opts .quiz-option').forEach(btn => { btn.addEventListener('click', () => handleQuizAnswer(btn, q)); }); } function handleQuizAnswer(btn, q) { const chosen = parseInt(btn.dataset.idx, 10); const correct = q.correct; state.totalAnswers += 1; const curId = quizState.topic.curr.id; const modId = quizState.topic.mod.id; if (!state.mastery[curId]) state.mastery[curId] = { correct: 0, total: 0 }; state.mastery[curId].total += 1; state.moduleTotal[modId] = (state.moduleTotal[modId] || 0) + 1; $$('#quiz-opts .quiz-option').forEach((b, i) => { b.disabled = true; if (i === correct) b.classList.add('correct'); else if (i === chosen) b.classList.add('wrong'); }); const ex = $('#quiz-explain'); ex.classList.remove('hidden'); if (chosen === correct) { quizState.correct += 1; state.correctAnswers += 1; state.quizStreak += 1; if (state.quizStreak > state.maxQuizStreak) state.maxQuizStreak = state.quizStreak; state.mastery[curId].correct += 1; state.moduleCorrect[modId] = (state.moduleCorrect[modId] || 0) + 1; addXP(10, 'Richtig!'); ex.innerHTML = `✓ Richtig!
    ${escapeHTML(q.explain || '')}
    `; $$('#quiz-explain .deepdive-btn').forEach(b => b.addEventListener('click', () => quizDeepdive(b.dataset.kind, q, chosen, correct))); } else { state.quizStreak = 0; ex.innerHTML = `✗ Falsch. Richtig wäre ${['A','B','C','D','E','F'][correct]}. ${escapeHTML(q.explain || '')}
    `; $$('#quiz-explain .deepdive-btn').forEach(b => b.addEventListener('click', () => quizDeepdive(b.dataset.kind, q, chosen, correct))); } saveState(); touchActivity(); checkBadges(); $('#quiz-next').classList.remove('hidden'); $('#quiz-next-btn').addEventListener('click', () => { quizState.idx += 1; if (quizState.idx >= quizState.set.length) renderQuizDone(); else renderQuizQuestion(); }); } function renderQuizDone() { const host = $('#quiz-host'); const pct = Math.round(quizState.correct / quizState.set.length * 100); state.completedQuizzes += 1; // Module complete (>=80% correct in this quiz) — bonus XP + track for DSGVO-Master if (pct >= 80 && quizState.topic) { const modId = quizState.topic.mod.id; if (!state.completedCurricula.includes(modId)) { state.completedCurricula.push(modId); addXP(20, 'Modul abgeschlossen: ' + quizState.topic.mod.title); } // Lernreise: Quiz mit ≥80% beendet → Step kann auto-completed werden. // Portal-Listener entscheidet, ob er das übernimmt. sendJourneyBeacon('quiz_passed', { module: modId, score_pct: pct }); } saveState(); checkBadges(); host.innerHTML = `

    Quiz beendet!

    ${quizState.correct} / ${quizState.set.length}

    ${pct}% richtig — ${pct >= 80 ? 'Ausgezeichnet!' : pct >= 60 ? 'Solide!' : 'Probier es noch mal.'}

    `; $('#quiz-again').addEventListener('click', () => startQuiz(quizState.topic, quizState.set.length)); $('#quiz-new').addEventListener('click', () => renderQuizIntro()); } // ==== Flashcards (SM-2 Spaced Repetition) ==== const flashState = { topic: null, deck: null, cur: null, showBack: false }; function renderFlashIntro() { const host = $('#flash-host'); host.innerHTML = `

    🃏 Flashcards

    Karteikarten als Gedächtnisstütze für den Wissenstransfer (Was-Wer-Wie-Achsen).

    `; const pillHost = $('#flash-topic-pills'); CURRICULA.curricula.forEach(c => { c.modules.forEach(m => { const btn = document.createElement('button'); btn.className = 'topic-pill'; btn.textContent = m.title; btn.title = c.title; btn.addEventListener('click', () => { $$('#flash-topic-pills .topic-pill').forEach(p => p.setAttribute('aria-selected', 'false')); btn.setAttribute('aria-selected', 'true'); flashState.topic = { curr: c, mod: m }; updateFlashStats(); }); pillHost.appendChild(btn); }); }); $('#flash-start').addEventListener('click', () => { if (!flashState.topic) { toast('Bitte ein Thema wählen.', 'warn'); return; } loadNewFlashCards(flashState.topic, parseInt($('#flash-count').value, 10)); }); $('#flash-review').addEventListener('click', () => { if (!flashState.topic) { toast('Bitte ein Thema wählen.', 'warn'); return; } startReview(flashState.topic); }); updateFlashStats(); } function updateFlashStats() { const el = $('#flash-stats'); if (!el) return; if (!flashState.topic) { el.textContent = ''; return; } const tid = flashState.topic.mod.id; const cards = flashCards[tid] || []; const due = cards.filter(c => !c.due || c.due <= now()).length; el.textContent = cards.length === 0 ? 'Noch keine Karten in diesem Thema.' : `${cards.length} Karten gespeichert · ${due} fällig heute`; } async function loadNewFlashCards(topic, count) { const host = $('#flash-host'); host.innerHTML = `

    LIMEN generiert Karten zu „${topic.mod.title}" …

    `; try { const topicText = `${topic.curr.title} — ${topic.mod.title}`; const data = await requestStructured(`FLASHCARD_REQUEST topic="${topicText}" count=${count}`); if (!data.cards || !data.cards.length) throw new Error('Keine Karten erhalten'); const tid = topic.mod.id; if (!flashCards[tid]) flashCards[tid] = []; for (const c of data.cards) { flashCards[tid].push({ front: c.front, back: c.back, hint: c.hint || '', ef: 2.5, interval: 0, reps: 0, due: now() }); } saveFlashCards(); startReview(topic); } catch (e) { host.innerHTML = `

    ⚠ Fehler: ${e.message}

    `; toast('Karten-Generierung fehlgeschlagen', 'error'); } } function startReview(topic) { flashState.topic = topic; const tid = topic.mod.id; flashState.deck = (flashCards[tid] || []).filter(c => !c.due || c.due <= now()); if (flashState.deck.length === 0) { const host = $('#flash-host'); host.innerHTML = `

    Keine fälligen Karten in „${escapeHTML(topic.mod.title)}"

    Lege neue Karten an oder komm später wieder.

    `; $('#back-to-flash').addEventListener('click', renderFlashIntro); return; } flashState.cur = 0; flashState.showBack = false; renderFlashCard(); } function renderFlashCard() { const host = $('#flash-host'); const card = flashState.deck[flashState.cur]; if (!card) { host.innerHTML = `

    Review beendet 🎉

    Alle fälligen Karten in „${escapeHTML(flashState.topic.mod.title)}" durch.

    `; $('#back-to-flash').addEventListener('click', renderFlashIntro); return; } host.innerHTML = `
    Karte ${flashState.cur + 1} / ${flashState.deck.length} ${escapeHTML(flashState.topic.mod.title)}
    ${flashState.showBack ? `
    ${escapeHTML(card.front)}${escapeHTML(card.back)}
    ` : `
    ${escapeHTML(card.front)}
    ${card.hint ? `
    Hinweis: ${escapeHTML(card.hint)}
    ` : ''}
    Klicken oder Leertaste drücken zum Umdrehen
    `}
    ${flashState.showBack ? `
    ` : ''} `; $('#flash-card').addEventListener('click', flipCard); $('#flash-card').addEventListener('keydown', (ev) => { if (ev.key === ' ' || ev.key === 'Enter') { ev.preventDefault(); flipCard(); } }); $('#flash-card').focus(); $$('.flash-btn').forEach(btn => btn.addEventListener('click', () => rateCard(parseInt(btn.dataset.rating, 10)))); $$('.deepdive-btn').forEach(btn => btn.addEventListener('click', () => cardDeepdive(btn.dataset.kind, card))); } async function cardDeepdive(kind, card) { const prompts = { more: `Zu dieser Lernkarte — gib mir 2-3 vertiefende Informationen, die über die Karte hinausgehen. Keine Wiederholung.\n\nFrage: ${card.front}\nAntwort: ${card.back}${card.hint ? '\nHinweis: ' + card.hint : ''}\n\nMarkdown erlaubt. Kurz halten (≤180 Wörter).`, sources: `Für diese Lernkarte — wo kann der Lerner das vertiefen? Nenne 3-5 konkrete öffentliche Quellen (Gesetzestext + Paragraph, Norm-Nummer, Fachbuch mit Autor, Paper, offizielle Webseite, IHK-Leitfaden). Je Quelle EINE Zeile. Keine Allgemeinplätze wie „Fachliteratur".\n\nFrage: ${card.front}\nAntwort: ${card.back}\n\nFormat: Markdown-Liste.`, example: `Praxis-Beispiel zu dieser Lernkarte — ein konkretes Szenario aus dem Berufsalltag in 3-5 Sätzen, kein Theorie-Kram.\n\nFrage: ${card.front}\nAntwort: ${card.back}`, ask: null, }; const host = $('#flash-deepdive'); host.classList.remove('hidden'); if (kind === 'ask') { const q = window.prompt('Deine Frage zu dieser Karte:', ''); if (!q || !q.trim()) { host.classList.add('hidden'); return; } host.innerHTML = `
    denkt …
    `; try { const data = await chatAPI(`Zu dieser Lernkarte:\nFrage: ${card.front}\nAntwort: ${card.back}\n\nMeine Zusatzfrage: ${q.trim()}\n\nBeantworte präzise, Markdown erlaubt, ≤200 Wörter.`, []); host.innerHTML = '
    ' + renderMD(data.reply || '') + '
    '; } catch (e) { host.innerHTML = `
    Fehler: ${e.message || 'unbekannt'}
    `; } } else { host.innerHTML = `
    ${kind === 'sources' ? 'sucht Quellen' : kind === 'example' ? 'baut Beispiel' : 'vertieft'} …
    `; try { const data = await chatAPI(prompts[kind], []); host.innerHTML = '
    ' + renderMD(data.reply || '') + '
    '; } catch (e) { host.innerHTML = `
    Fehler: ${e.message || 'unbekannt'}
    `; } } const cb = $('#flash-deepdive .close-dd'); if (cb) cb.addEventListener('click', () => host.classList.add('hidden')); } async function quizDeepdive(kind, q, chosenIdx, correctIdx) { const host = $('#quiz-deepdive'); host.classList.remove('hidden'); const chosenTxt = q.options[chosenIdx], correctTxt = q.options[correctIdx]; const prompts = { more: `Gib mir zu dieser Quiz-Frage 2-3 weiterführende Infos, die über die knappe Erklärung hinausgehen. Kein Wiederholen.\n\nFrage: ${q.q}\nRichtig: ${correctTxt}\nErklärung: ${q.explain || '(—)'}\n\nMarkdown erlaubt, ≤180 Wörter.`, sources: `Für diese Quiz-Frage — wo kann der Lerner das vertiefen? 3-5 konkrete öffentliche Quellen (Gesetzestext+Paragraph, Norm-Nummer, Fachbuch mit Autor, Paper, offizielle Webseite). Je Quelle EINE Zeile, keine Allgemeinplätze.\n\nFrage: ${q.q}\nRichtig: ${correctTxt}\n\nMarkdown-Liste.`, why: chosenIdx === correctIdx ? `Ich hab richtig geantwortet — erkläre mir NOCH tiefer warum das die richtige Wahl ist. Was hätte falsch gelegen bei den anderen Optionen? Je Option 1 Satz.\n\nFrage: ${q.q}\nOptionen: ${q.options.map((o,i)=>(i+1)+'. '+o).join(' | ')}\nRichtig: ${correctTxt}` : `Ich hab „${chosenTxt}" gewählt, richtig wäre „${correctTxt}". Erkläre mir präzise wo mein Denkfehler lag und wie ich das beim nächsten Mal anders angehe. Sokratisch wenn möglich.\n\nFrage: ${q.q}\nMeine Antwort: ${chosenTxt}\nRichtig: ${correctTxt}`, ask: null, }; if (kind === 'ask') { const userQ = window.prompt('Deine Frage zu dieser Quiz-Frage:', ''); if (!userQ || !userQ.trim()) { host.classList.add('hidden'); return; } host.innerHTML = `
    denkt …
    `; try { const data = await chatAPI(`Zur Quiz-Frage: "${q.q}"\nRichtige Antwort: ${correctTxt}\nMeine Antwort: ${chosenTxt}\n\nZusatzfrage: ${userQ.trim()}\n\nBeantworte präzise, Markdown, ≤200 Wörter.`, []); host.innerHTML = '
    ' + renderMD(data.reply || '') + '
    '; } catch (e) { host.innerHTML = `
    Fehler: ${e.message || 'unbekannt'}
    `; } } else { host.innerHTML = `
    ${kind === 'sources' ? 'sucht Quellen' : kind === 'why' ? 'analysiert' : 'vertieft'} …
    `; try { const data = await chatAPI(prompts[kind], []); host.innerHTML = '
    ' + renderMD(data.reply || '') + '
    '; } catch (e) { host.innerHTML = `
    Fehler: ${e.message || 'unbekannt'}
    `; } } const cb = $('#quiz-deepdive .close-dd'); if (cb) cb.addEventListener('click', () => host.classList.add('hidden')); } function flipCard() { if (flashState.showBack) return; const el = $('#flash-card'); if (el) { el.classList.add('flipping'); setTimeout(() => { flashState.showBack = true; renderFlashCard(); }, 120); } } function rateCard(rating) { const card = flashState.deck[flashState.cur]; // SM-2 algorithm const q = rating; // 0..3 (we map: 0→ again, 1→hard, 2→good, 3→easy) const qMap = [0, 3, 4, 5]; // SM-2 uses 0..5, we map our 0..3 to subset const sm2q = qMap[q]; if (sm2q < 3) { card.reps = 0; card.interval = 0; card.due = now() + 60 * 1000; // 1 min } else { card.reps += 1; if (card.reps === 1) card.interval = 1; else if (card.reps === 2) card.interval = 3; else card.interval = Math.round(card.interval * card.ef); card.due = now() + card.interval * 86400 * 1000; } card.ef = Math.max(1.3, card.ef + (0.1 - (5 - sm2q) * (0.08 + (5 - sm2q) * 0.02))); saveFlashCards(); // XP state.flashCardsRated += 1; state.totalAnswers += 1; if (q === 2) { addXP(5, 'Flashcard Gut'); state.correctAnswers += 1; } else if (q === 3) { addXP(2, 'Flashcard Leicht'); state.correctAnswers += 1; } // Track per-run ratings for modulePassedFlash (all cards in deck rated >= Gut) flashState.runRatings = flashState.runRatings || []; flashState.runRatings.push(q); saveState(); touchActivity(); checkBadges(); flashState.showBack = false; flashState.cur += 1; // If end of deck and topic set, check if all cards rated Gut/Leicht if (flashState.cur >= flashState.deck.length && flashState.topic) { const allGood = flashState.runRatings.length >= 3 && flashState.runRatings.every(r => r >= 2); if (allGood) { const modId = flashState.topic.mod.id; state.modulePassedFlash[modId] = true; saveState(); checkBadges(); } flashState.runRatings = []; } renderFlashCard(); } // ==== Progress ==== function renderProgress() { const host = $('#progress-host'); const li = levelInfo(); const masteryRows = Object.entries(state.mastery) .map(([cid, m]) => { const c = CURRICULA.curricula.find(x => x.id === cid); if (!c || m.total === 0) return null; const pct = Math.round(m.correct / m.total * 100); return { title: c.title, color: c.color, pct, correct: m.correct, total: m.total }; }) .filter(Boolean) .sort((a, b) => b.pct - a.pct); const badges = (CURRICULA.badges || []); host.innerHTML = `
    ${state.xp}
    XP gesamt
    Lvl ${li.levelNum} · ${li.title}
    ${li.next ? li.next.min - state.xp + ' XP bis ' + li.next.title : 'Top-Level erreicht'}
    🔥 ${state.currentStreak}
    Tage-Streak (max. ${state.maxStreak})
    ${state.totalAnswers}
    Antworten gesamt
    ${state.totalAnswers === 0 ? '0%' : Math.round(state.correctAnswers / state.totalAnswers * 100) + '%'}
    Trefferquote
    ${state.completedQuizzes}
    Quizze
    ${li.next ? `

    Fortschritt zu „${li.next.title}"

    ${Math.round(li.pct)}% · ${state.xp} / ${li.next.min} XP
    ` : ''}

    Mastery pro Curriculum

    ${masteryRows.length === 0 ? '

    Noch keine Daten. Mach ein Quiz, um Mastery aufzubauen.

    ' : '
    ' + masteryRows.map(r => `
    ${escapeHTML(r.title)} ${r.pct}% (${r.correct}/${r.total})
    `).join('') + '
    ' }

    Abzeichen (${Object.keys(state.badges).length}/${badges.length})

    ${badges.map(b => { const earned = !!state.badges[b.id]; const icons = { award:'🏆', flame:'🔥', star:'⭐', calendar:'🗓', crown:'👑', moon:'🌙', sun:'☀️', shield:'🛡️', detective:'🕵️', clock:'⏱️', handshake:'🤝', medal:'🎖️', diamond:'💎' }; return `
    ${icons[b.icon] || '🎖'}
    ${escapeHTML(b.title)}
    ${escapeHTML(b.description)}
    `; }).join('')}

    Daten zurücksetzen

    Lokal gespeichert (kein Server-Tracking).

    `; $('#reset-data').addEventListener('click', () => { if (confirm('Alle lokalen Daten (XP, Streaks, Flashcards, Chat) wirklich löschen?')) { localStorage.removeItem(LS_KEY); localStorage.removeItem(LS_CHAT); localStorage.removeItem(LS_FLASH); state = loadState(); chatHistory = []; flashCards = {}; restoreChat(); renderProgress(); toast('Daten zurückgesetzt.', 'success'); } }); } // ==== Curriculum tree ==== function renderCurriculum() { const host = $('#curr-host'); host.innerHTML = '
    '; const root = $('#curr-tree-root'); CURRICULA.curricula.forEach(c => { const rootEl = document.createElement('details'); rootEl.className = 'curr-root'; rootEl.innerHTML = ` ${c.title.charAt(0)} ${escapeHTML(c.title)} ${escapeHTML(c.short)} · ${c.modules.length} Module
    ${c.modules.map(m => `
    ${escapeHTML(m.title)}
    `).join('')}
    `; root.appendChild(rootEl); }); $$('.curr-mod').forEach(el => el.addEventListener('click', () => { const c = CURRICULA.curricula.find(x => x.id === el.dataset.curr); const m = c.modules.find(x => x.id === el.dataset.mod); renderModuleDetail(c, m); })); } function renderModuleDetail(c, m) { const host = $('#curr-host'); host.innerHTML = `

    ${escapeHTML(m.title)}

    Lernziele

    Kernthemen

    ${m.hours ? `

    Umfang: ~${m.hours} h

    ` : ''}
    `; $('#breadcrumb-back').addEventListener('click', renderCurriculum); $$('.mod-actions button').forEach(b => b.addEventListener('click', () => { const act = b.dataset.action; if (act === 'quiz') { quizState.topic = { curr: c, mod: m }; switchMode('quiz'); setTimeout(() => startQuiz({ curr: c, mod: m }, 10), 50); } else if (act === 'flash') { flashState.topic = { curr: c, mod: m }; switchMode('flash'); setTimeout(() => { const tid = m.id; if (flashCards[tid] && flashCards[tid].length) startReview({ curr: c, mod: m }); else loadNewFlashCards({ curr: c, mod: m }, 10); }, 50); } else if (act === 'ask') { switchMode('chat'); $('#composer').value = `Erklär mir kurz und klar: ${m.title} (aus ${c.title}). Fokus auf ${m.topics.slice(0,3).join(', ')}.`; $('#composer').focus(); } })); } // ==== Mode switching ==== function switchMode(mode) { $$('.tab').forEach(t => { const isActive = t.dataset.mode === mode; t.setAttribute('aria-selected', isActive ? 'true' : 'false'); }); $$('.view').forEach(v => { v.dataset.active = (v.id === 'view-' + mode) ? 'true' : 'false'; }); // The composer is shown only in chat mode $('#composer-form').classList.toggle('hidden', mode !== 'chat'); if (mode === 'quiz') renderQuizIntro(); if (mode === 'flash') renderFlashIntro(); if (mode === 'progress') renderProgress(); if (mode === 'curriculum') renderCurriculum(); if (mode === 'chat') setTimeout(() => $('#composer').focus(), 50); } // ==== Escape HTML ==== function escapeHTML(s) { return String(s).replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch])); } // ==== Boot ==== // Lernreise-State (URL-Params), einmal beim Boot ausgelesen. // Wird von renderJourneyBanner() für die UI und von sendJourneyBeacon() // für die postMessage-Kommunikation zurück zum Portal-Tab benutzt. const JOURNEY = (() => { try { const p = new URLSearchParams(window.location.search); const slug = p.get('journey'); if (!slug) return null; const step = parseInt(p.get('step') || '0', 10); const total = parseInt(p.get('total') || '0', 10); let returnOrigin = ''; try { if (p.get('return')) returnOrigin = new URL(p.get('return')).origin; } catch {} return { slug, step, total, returnOrigin }; } catch { return null; } })(); // Beacon zurück an den Portal-Tab. Funktioniert nur wenn das Widget über // `target="_blank" rel="opener"` aus der Lernreise-Seite geöffnet wurde // (window.opener erreichbar). Bei direktem Aufruf der Bot-URL no-op. function sendJourneyBeacon(event, extra = {}) { if (!JOURNEY || !window.opener) return; if (!JOURNEY.returnOrigin) return; try { window.opener.postMessage({ type: 'qognio:journey-step-event', journey: JOURNEY.slug, step: JOURNEY.step, event, // 'quiz_passed' | 'curriculum_completed' | 'manual' ...extra, }, JOURNEY.returnOrigin); } catch (e) { console.warn('journey beacon failed', e); } } // Lernreise-Banner: gerendert wenn der Bot via Portal-Lernreise geöffnet wurde // (`?journey=&step=&total=&return=`). Nicht-invasiv, weglinkbar. function renderJourneyBanner() { try { const params = new URLSearchParams(window.location.search); const journey = params.get('journey'); const step = parseInt(params.get('step') || '0', 10); const total = parseInt(params.get('total') || '0', 10); const ret = params.get('return') || ''; if (!journey || step <= 0) return; // Dismissed for this journey/step combo? (LS sticky) const dismissKey = 'qognio.journey-banner.dismiss.v1'; const dismissed = JSON.parse(localStorage.getItem(dismissKey) || '{}'); const dKey = journey + ':' + step; if (dismissed[dKey]) return; const bar = document.createElement('div'); bar.id = 'qognio-journey-banner'; bar.setAttribute('role', 'note'); bar.style.cssText = [ 'position:sticky', 'top:0', 'z-index:100', 'display:flex', 'align-items:center', 'gap:.75rem', 'padding:.55rem .9rem', 'background:linear-gradient(90deg, rgba(124,58,237,.18), rgba(34,211,238,.10))', 'border-bottom:1px solid rgba(124,58,237,.35)', 'color:var(--text, #e5e7eb)', 'font-size:.875rem', 'box-shadow:0 1px 0 rgba(0,0,0,.05)', ].join(';'); const human = journey.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); bar.innerHTML = [ '', 'Lernreise: ' + human + '', 'Schritt ' + step + (total > 0 ? ' von ' + total : '') + '', '', ret ? '↩ Zurück zur Reise' : '', '', ].join(''); document.body.prepend(bar); const btn = bar.querySelector('#qognio-journey-dismiss'); if (btn) btn.addEventListener('click', () => { dismissed[dKey] = Date.now(); localStorage.setItem(dismissKey, JSON.stringify(dismissed)); bar.remove(); }); } catch (e) { console.warn('journey banner failed', e); } } async function boot() { renderJourneyBanner(); try { const r = await fetch('curricula.json?v=2026-04-21'); if (!r.ok) throw new Error('curricula.json HTTP ' + r.status); CURRICULA = await r.json(); } catch (e) { toast('Curriculum konnte nicht geladen werden.', 'error', 6000); console.error(e); return; } restoreChat(); touchActivity(); // Tabs $$('.tab').forEach(tab => tab.addEventListener('click', () => switchMode(tab.dataset.mode))); // Welcome-card shortcuts $$('[data-goto]').forEach(b => b.addEventListener('click', () => { state.seenWelcome = true; saveState(); switchMode(b.dataset.goto); })); // Welcome-card prompt-fillers (special *_REQUEST modes) $$('[data-prompt]').forEach(b => b.addEventListener('click', () => { state.seenWelcome = true; saveState(); switchMode('chat'); const composer = $('#composer'); if (composer) { composer.value = b.dataset.prompt; autogrow(composer); setTimeout(() => composer.focus(), 50); } })); // Composer const form = $('#composer-form'); const ta = $('#composer'); form.addEventListener('submit', (ev) => { ev.preventDefault(); const text = ta.value.trim(); if (!text && pendingAttachments.length === 0) return; ta.value = ''; autogrow(ta); sendChat(text); }); ta.addEventListener('keydown', (ev) => { if (ev.key === 'Enter' && !ev.shiftKey) { ev.preventDefault(); form.requestSubmit(); } }); // Attachments const fileInput = $('#composer-file'); const attachBtn = $('#composer-attach'); if (attachBtn && fileInput) { attachBtn.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', async (ev) => { await addFiles(ev.target.files); ev.target.value = ''; }); } // Drag & Drop on composer ['dragenter', 'dragover'].forEach(evt => form.addEventListener(evt, (ev) => { if (ev.dataTransfer && Array.from(ev.dataTransfer.types || []).includes('Files')) { ev.preventDefault(); form.classList.add('dragover'); } })); ['dragleave', 'drop'].forEach(evt => form.addEventListener(evt, (ev) => { if (evt === 'dragleave' && ev.target !== form) return; form.classList.remove('dragover'); })); form.addEventListener('drop', async (ev) => { if (ev.dataTransfer && ev.dataTransfer.files && ev.dataTransfer.files.length) { ev.preventDefault(); await addFiles(ev.dataTransfer.files); } }); // Paste image / file ta.addEventListener('paste', async (ev) => { const items = ev.clipboardData && ev.clipboardData.files; if (items && items.length) { ev.preventDefault(); await addFiles(items); } }); ta.addEventListener('input', () => autogrow(ta)); function autogrow(el) { el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 140) + 'px'; } // Keyboard shortcuts Ctrl+1..5 document.addEventListener('keydown', (ev) => { if (!(ev.ctrlKey || ev.metaKey)) return; const map = { '1': 'chat', '2': 'quiz', '3': 'flash', '4': 'progress', '5': 'curriculum' }; if (map[ev.key]) { ev.preventDefault(); switchMode(map[ev.key]); } }); // Default focus ta.focus(); // Update status once per min (visual cue) setInterval(() => { /* placeholder for future heartbeat */ }, 60000); console.log('LIMEN v2026-04-25 ready. XP:', state.xp, 'Streak:', state.currentStreak); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot(); })();