commit ab94f4a7e9034c0250ed96860b2b89e5ee48d5de Author: Qognio Bot Extract Date: Wed Apr 29 01:35:46 2026 +0200 init: extract bar-coach from qognio-bot-widget-template@d2c816f Source files (src/) and rendered bundle (www/) extracted on 2026-04-29T01:35:46+02:00. Adds nginx:alpine Dockerfile + docker-compose.yml (Caddy-labels) so the bot runs stand-alone or as a per-customer template clone. Parent monorepo commit: d2c816f3edbc9760802a11b29ff4151c7aad4b46 Bot version: 2026-04-21 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9e02047 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitignore +README.md +bot.json +src/ +docker-compose.yml +*.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6758e09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +*.log +*.tmp +node_modules/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1f9c43c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +# Static-bundle bot — nginx:alpine serves www/ on port 80. +FROM nginx:1.27-alpine + +# nginx config: gzip + cache headers + index.html no-store +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Static bundle +COPY www/ /usr/share/nginx/html/ + +# Run as non-root via nginx's built-in unprivileged image features +EXPOSE 80 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -q --spider http://127.0.0.1/index.html || exit 1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a7e3d68 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# Max — Bar & Bühnen-Coach + +Max — dein Bar & Bühnen-Coach. Crash-Kurs für Bar-Kräfte und Event-Crews — rechtssicher, praxisnah, Deutschland-legal. DSGVO-konform im Bunker. + +``` +slug : bar-coach +version : 2026-04-21 +accent : #f59e0b +runtime : nginx:alpine (static bundle) +template : qognio-bot-template-core (former qognio-bot-widget-template) +``` + +## Layout + +``` +. +├── src/ source — config.yaml, welcome.html, curricula.json, etc. +├── www/ rendered, directly servable static bundle +├── Dockerfile nginx:alpine + www/ → port 80 +├── docker-compose.yml bot-host pattern (Caddy-labels, restart unless-stopped) +├── nginx.conf gzip + cache + SPA fallback +└── bot.json metadata + parent_core_commit +``` + +## Run locally + +```bash +docker compose up --build +# → http://localhost (you'll need to tweak ports for local-only use) +``` + +## Re-render after upstream core changes + +This repo only stores src + rendered output; the rendering engine lives in +`qognio-bot-template-core`. To pull in core changes: + +```bash +cd /path/to/qognio-bot-template-core +./scripts/render.sh bar-coach --bot-repo /path/to/this/repo +git -C /path/to/this/repo commit -am "render: refresh from core@" +``` + +## Per-customer copy (template usage) + +This repo is a **template**. To clone for a customer: + +```bash +git clone my-customer-bar-coach +cd my-customer-bar-coach +# tweak src/config.yaml (slug, bot_key_value, accent), src/welcome.html, src/curricula.json +docker compose -f docker-compose.yml up --build +``` + +## Deploy to qognio bot-host (.42 LXC pattern — legacy) + +The bot-manager spawns LXC containers named after the slug. Push www/ via: + +```bash +ssh fmh@46.243.203.42 +sudo lxc file push /tmp/www/* bar-coach/var/www/html/ +``` + +(Or run the docker-compose pattern on a Docker host — same network as Caddy.) + +--- + +Generated by `qognio-bot-template-core/scripts/extract-to-repo.sh` on 2026-04-29T01:35:46+02:00. diff --git a/bot.json b/bot.json new file mode 100644 index 0000000..24dcb6a --- /dev/null +++ b/bot.json @@ -0,0 +1,14 @@ +{ + "slug": "bar-coach", + "name": "Max", + "title": "Bar & Bühnen-Coach", + "tagline": "Bar & Bühnen-Coach", + "description": "Max — dein Bar & Bühnen-Coach. Crash-Kurs für Bar-Kräfte und Event-Crews — rechtssicher, praxisnah, Deutschland-legal. DSGVO-konform im Bunker.", + "version": "2026-04-21", + "accent": "#f59e0b", + "extracted_from": "qognio-bot-widget-template", + "parent_core_commit": "d2c816f3edbc9760802a11b29ff4151c7aad4b46", + "extracted_at": "2026-04-29T01:35:46+02:00", + "runtime": "nginx:alpine", + "default_port": 80 +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ce1b60a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +# Stand-alone bot container. +# Designed for the "caddy" external network on the bot host (qognio pattern). +# Override the hostname via SLUG env var if you reuse this template per customer. +services: + bot: + build: . + image: qognio/bot-bar-coach:${TAG:-latest} + container_name: bot-bar-coach + restart: unless-stopped + networks: + - caddy + labels: + caddy: "bar-coach.on.qognio.com" + caddy.reverse_proxy: "{{upstreams 80}}" + qognio.bot.slug: "bar-coach" + qognio.bot.version: "2026-04-21" + +networks: + caddy: + external: true diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..30cda98 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,27 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # gzip + gzip on; + gzip_vary on; + gzip_types text/css application/javascript application/json image/svg+xml text/plain; + gzip_min_length 512; + + # index.html: never cache (so welcome screen / wiring updates land instantly) + location = /index.html { + add_header Cache-Control "no-store, must-revalidate" always; + } + + # static assets: cache 1h + location ~* \.(?:css|js|json|svg|png|jpe?g|webp|gif|ico|woff2?)$ { + add_header Cache-Control "public, max-age=3600" always; + try_files $uri =404; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/src/check-badges.js b/src/check-badges.js new file mode 100644 index 0000000..78128e1 --- /dev/null +++ b/src/check-badges.js @@ -0,0 +1,23 @@ + // Feuer frei — 1. Quiz bestanden + if ((state.completedQuizzes || 0) >= 1) unlockBadge('erste_schicht'); + // Cocktail-Ninja — alle Mixology-Submodule in completedCurricula + const mixMods = ['technik','klassiker','glas-garnitur','masse']; + if (mixMods.every(m => (state.completedCurricula || []).includes(m))) unlockBadge('cocktail_ninja'); + // Jugendschutz-Watchdog — alle Jugendschutz-Fragen korrekt, mind. 5 beantwortet + if (state.moduleTotal && state.moduleTotal['juschg'] >= 5 && + state.moduleCorrect['juschg'] === state.moduleTotal['juschg']) unlockBadge('jugendschutz_watchdog'); + // DGUV-Profi — Event-Tech-Modul komplett + const dguvMods = ['dguv','vstaettvo','rigging','pyro-strom']; + if (dguvMods.every(m => (state.completedCurricula || []).includes(m))) unlockBadge('dguv_profi'); + // Gast-Flüsterer — Service-Etikette-Modul komplett + const serviceMods = ['sequence','reklamation','zahlung','gast-konflikt']; + if (serviceMods.every(m => (state.completedCurricula || []).includes(m))) unlockBadge('gast_whisperer'); + // Kassen-Profi — Kassen-Modul komplett + const kasseMods = ['tse','bon','kassenfuehrung','nachschau']; + if (kasseMods.every(m => (state.completedCurricula || []).includes(m))) unlockBadge('kassen_pro'); + // Thekengeneral — alle 8 Curricula (alle Module aller Curricula) + if (CURRICULA && (CURRICULA.curricula || []).every(c => + c.modules.every(m => (state.completedCurricula || []).includes(m.id)))) unlockBadge('all_rounder'); + // Night Owl + const h = new Date().getHours(); + if (h >= 22) unlockBadge('night_owl'); \ No newline at end of file diff --git a/src/config.yaml b/src/config.yaml new file mode 100644 index 0000000..e26aed1 --- /dev/null +++ b/src/config.yaml @@ -0,0 +1,32 @@ +slug: bar-coach +bot_name: Max +bot_title: "Bar & Bühnen-Coach" +brand_letter: M +title: "Max · Bar & Bühnen-Coach" +tagline: "Bar und Bühnen-Coach" +tagline_short: "Bar & Bühnen-Coach" +meta_description: "Max — dein Bar & Bühnen-Coach. Crash-Kurs für Bar-Kräfte und Event-Crews — rechtssicher, praxisnah, Deutschland-legal. DSGVO-konform im Bunker." +bot_key_var: __MAX_KEY__ +bot_key_value: qb_vdxwoq9mh6hy +ls_prefix: max +bot_version: "2026-04-21" + +# Color theme (Max — amber/gold) +accent: "#f59e0b" +accent_2: "#fbbf24" +accent_dark: "#d97706" +accent_rgb: "245, 158, 11" +success_color: "#10b981" +msg_strong_color: "#fde68a" + +# UI Labels +tab_flash_label: Karten +tab_curriculum_label: Know-How +curriculum_long_label: Know-How + +# Bot personality +quiz_intro_hint: "Wähle ein Modul — Max baut Szenario-Fragen aus dem Bar-/Event-Alltag." +quiz_verb: baut +quiz_noun: "Szenario-Fragen" +flash_intro_hint: "Max baut Karteikarten zu einem Thema. Bewerte dein Erinnerungsvermögen — das System wiederholt schwere Karten öfter (SM-2)." +flash_verb: baut diff --git a/src/curricula.json b/src/curricula.json new file mode 100644 index 0000000..5b311aa --- /dev/null +++ b/src/curricula.json @@ -0,0 +1,497 @@ +{ + "version": "2026-04-24", + "updated": "2026-04-24", + "curricula": [ + { + "id": "bar-basics", + "title": "1 · Bar-Basics Recht", + "short": "JuSchG, § 43 IfSG, LMHV, HACCP", + "icon": "shield", + "color": "#f59e0b", + "description": "Die rechtlichen Mindestpflichten hinter der Theke: Jugendschutz, Belehrung nach § 43 IfSG, Lebensmittelhygiene, HACCP-Grundsätze.", + "source_md": "bar-basics.md", + "modules": [ + { + "id": "juschg", + "title": "Jugendschutz (§ 9 / § 10 JuSchG)", + "objectives": [ + "Altersgrenzen für Bier/Wein/Spirituosen sicher benennen", + "Ausnahme § 9 Abs. 2 (Personensorge) erklären", + "Testkauf-Situationen sauber handhaben" + ], + "topics": ["§ 9 JuSchG", "§ 10 JuSchG", "Alkopops", "Testkauf", "Ausweiskontrolle"], + "difficulty": "einfach", + "source_heading": "Jugendschutz" + }, + { + "id": "ifsg43", + "title": "Belehrung nach § 43 IfSG", + "objectives": [ + "Erst- und Folgebelehrung unterscheiden", + "Meldepflichten bei Infektionen kennen", + "Frist-Grenzen im Anstellungsfall" + ], + "topics": ["§ 43 IfSG", "Gesundheitszeugnis", "Erstbelehrung", "Folgebelehrung"], + "difficulty": "einfach", + "source_heading": "Infektionsschutzgesetz § 43" + }, + { + "id": "lmhv-haccp", + "title": "LMHV + VO 852/2004 + HACCP", + "objectives": [ + "Die 7 HACCP-Grundsätze aufzählen", + "Kühltemperatur-Grenzen (+4 °C, −18 °C)", + "Dokumentationspflicht erklären" + ], + "topics": ["VO 852/2004", "LMHV § 4", "HACCP", "Kühlkette", "Reinigungsplan"], + "difficulty": "mittel", + "source_heading": "LMHV + HACCP" + }, + { + "id": "konzession", + "title": "Konzessionen & Gestattung", + "objectives": [ + "Gaststättenerlaubnis vs. Gestattung", + "§ 12 GastG bei Events", + "Pflichten der Schankkraft ohne Konzession" + ], + "topics": ["GastG", "§ 12 GastG Gestattung", "Konzessionspflicht"], + "difficulty": "mittel", + "source_heading": "Gastgewerbeverordnung" + } + ] + }, + { + "id": "mixology", + "title": "2 · Mixology Basics", + "short": "Klassiker, Technik, Glaswahl", + "icon": "medal", + "color": "#ea580c", + "description": "IBA-Klassiker und das Handwerk der Bar — rühren, shaken, bauen, muddeln. Von Old Fashioned bis Aperol Spritz.", + "source_md": "mixology-grundlagen.md", + "modules": [ + { + "id": "technik", + "title": "Grundtechniken (Rühren/Shaken/Build/Muddle)", + "objectives": [ + "Die 5 Techniken den richtigen Drinks zuordnen", + "Gain-Verlust durch zu langes Shaken verstehen", + "Dry Shake bei Eiweißdrinks einsetzen" + ], + "topics": ["Stirring", "Shaking", "Building", "Muddling", "Dry Shake"], + "difficulty": "einfach", + "source_heading": "Techniken" + }, + { + "id": "klassiker", + "title": "Die 10 Unforgettables", + "objectives": [ + "Rezepte für Old Fashioned, Negroni, Daiquiri sicher", + "Mojito, Margarita, Martini, Manhattan", + "Whisky Sour, Aperol Spritz, Gin Tonic" + ], + "topics": ["Old Fashioned", "Negroni", "Daiquiri", "Mojito", "Margarita", "Martini"], + "difficulty": "mittel", + "source_heading": "Die Klassiker" + }, + { + "id": "glas-garnitur", + "title": "Glaswahl & Garnituren", + "objectives": [ + "Tumbler/Highball/Coupette richtig einsetzen", + "Zeste-Technik (Öl freisetzen)", + "Salzrand & Garnitur-Basics" + ], + "topics": ["Tumbler", "Highball", "Coupette", "Zeste", "Salzrand"], + "difficulty": "einfach", + "source_heading": "Glaswahl" + }, + { + "id": "masse", + "title": "Maß-Systeme (cl / ml / oz)", + "objectives": [ + "cl-ml-oz schnell umrechnen", + "Jigger sicher anwenden", + "Freihand-Schütten vermeiden" + ], + "topics": ["cl", "ml", "oz", "Jigger", "1 oz = 3 cl"], + "difficulty": "einfach", + "source_heading": "Maß-Systeme" + } + ] + }, + { + "id": "service", + "title": "3 · Service & Etikette", + "short": "Sequence, Reklamation, Zahlung", + "icon": "handshake", + "color": "#d97706", + "description": "Gastgeber sein: Sequence of Service, Bestellaufnahme, Reklamationshandling (LAST), Zahlung & Trinkgeld.", + "source_md": "service-etikette.md", + "modules": [ + { + "id": "sequence", + "title": "Sequence of Service", + "objectives": [ + "Die 10 Schritte vom Begrüßen bis zum Farewell", + "Check-Back-Timing (2-3 Min nach Speiseabgabe)", + "Gastrhythmus erkennen" + ], + "topics": ["Greet", "Take Order", "Check-Back", "Farewell"], + "difficulty": "einfach", + "source_heading": "Sequence of Service" + }, + { + "id": "reklamation", + "title": "Reklamation (LAST-Regel)", + "objectives": [ + "Listen-Acknowledge-Solve-Thank anwenden", + "Entschädigung angemessen wählen", + "Eskalation zur Schichtleitung" + ], + "topics": ["LAST", "Entschädigung", "Eskalation"], + "difficulty": "mittel", + "source_heading": "Reklamationsmanagement" + }, + { + "id": "zahlung", + "title": "Zahlung, Trinkgeld, Bonpflicht", + "objectives": [ + "Bonpflicht § 146a AO sicher umsetzen", + "Trinkgeld-Etikette (DE)", + "Split-Rechnung effizient" + ], + "topics": ["§ 146a AO", "Trinkgeld", "Split-Rechnung", "Kartenzahlung"], + "difficulty": "einfach", + "source_heading": "Zahlungsabwicklung" + }, + { + "id": "gast-konflikt", + "title": "Heikle Situationen", + "objectives": [ + "Angetrunkenen Gast deeskalieren", + "Zechpreller richtig handhaben", + "Security/110 richtig einsetzen" + ], + "topics": ["Deeskalation", "Zechprellerei § 263a", "Hausrecht"], + "difficulty": "mittel", + "source_heading": "Heikle Situationen" + } + ] + }, + { + "id": "event-tech", + "title": "4 · Veranstaltungstechnik", + "short": "DGUV V17, VStättVO, Rigging", + "icon": "shield", + "color": "#b45309", + "description": "Sicherheit im Event-Betrieb: DGUV V17/V18, Versammlungsstättenverordnung, Rigging-Basics, elektrische Sicherheit, Pyrotechnik.", + "source_md": "veranstaltungstechnik-basics.md", + "modules": [ + { + "id": "dguv", + "title": "DGUV V17/V18 + Regel 115-002", + "objectives": [ + "Anwendungsbereich V17 vs. V18", + "Pflicht-Unterweisung + Prüfung (jährlich)", + "Verantwortliche Person nach VStättVO" + ], + "topics": ["DGUV V17", "DGUV V18", "Regel 115-002", "Verantwortliche Person"], + "difficulty": "mittel", + "source_heading": "DGUV V17 & V18" + }, + { + "id": "vstaettvo", + "title": "VStättVO + Brandschutz", + "objectives": [ + "Besucherzahl-Grenzen (> 200 indoor / > 1000 outdoor)", + "Fluchtweg-Regeln (max. 30 m)", + "Brandschutz auf Bühne (offenes Feuer grundsätzlich verboten)" + ], + "topics": ["MVStättVO", "Fluchtweg", "Brandschau", "B1-Brandverhalten"], + "difficulty": "schwer", + "source_heading": "Versammlungsstättenverordnung" + }, + { + "id": "rigging", + "title": "Rigging & Lastberechnung", + "objectives": [ + "WLL vs. MBL unterscheiden", + "Sicherheitsfaktoren (5:1 / 7:1 / 10:1)", + "Secondary (Safety) Pflicht" + ], + "topics": ["WLL", "MBL", "Secondary", "Kettenzug", "Traverse"], + "difficulty": "schwer", + "source_heading": "Rigging" + }, + { + "id": "pyro-strom", + "title": "Pyrotechnik + Elektrik", + "objectives": [ + "T1 vs. T2 Pyrotechnik (§ 20 / § 27 SprengG)", + "DGUV V3 Prüfung (Elektrik)", + "FI/RCD Pflicht im Bühnenumfeld" + ], + "topics": ["SprengG", "Kat. T1/T2", "DGUV V3", "FI-Schutzschalter"], + "difficulty": "schwer", + "source_heading": "Pyrotechnik & Elektrische Sicherheit" + } + ] + }, + { + "id": "licht-ton", + "title": "5 · Licht & Ton", + "short": "DMX, Mischpult, 99 dB", + "icon": "medal", + "color": "#92400e", + "description": "Grundlagen der Licht- und Tontechnik: DMX-512, Moving Lights, Mischpult-Signal, FOH vs. Monitor, DIN 15905-5 Lärmschutz.", + "source_md": "licht-ton-basics.md", + "modules": [ + { + "id": "dmx", + "title": "DMX-512 Grundlagen", + "objectives": [ + "512 Kanäle, Startadresse, Daisy Chain", + "Terminator + XLR 110 Ω", + "Art-Net / sACN Überblick" + ], + "topics": ["DMX-512", "Startadresse", "Universum", "Terminator", "sACN"], + "difficulty": "mittel", + "source_heading": "DMX-512" + }, + { + "id": "scheinwerfer", + "title": "Scheinwerfer-Typen + 4-Licht-Konzept", + "objectives": [ + "Spot / Wash / Beam unterscheiden", + "Key/Fill/Back/Kicker anwenden", + "LED vs. Entladungslampe" + ], + "topics": ["Spot", "Wash", "Beam", "Key-Licht", "Fill"], + "difficulty": "mittel", + "source_heading": "Scheinwerfer-Typen" + }, + { + "id": "mischpult", + "title": "Mischpult-Signal + Gain-Staging", + "objectives": [ + "Gain-Stufe zuerst einstellen", + "EQ: schnitzen statt boosten", + "FOH vs. Monitor-Mix" + ], + "topics": ["Gain", "EQ", "Aux-Send", "FOH", "Monitor"], + "difficulty": "mittel", + "source_heading": "Mischpult-Struktur" + }, + { + "id": "din15905", + "title": "DIN 15905-5 Schallschutz", + "objectives": [ + "99 dB(A) LAeq 30 min Grenze", + "135 dB(C) Peak-Grenze", + "Messpflicht ab 85 dB(A)" + ], + "topics": ["DIN 15905-5", "LAeq", "LCpk", "Gehörschutz"], + "difficulty": "schwer", + "source_heading": "DIN 15905-5" + } + ] + }, + { + "id": "kasse-recht", + "title": "6 · Kassensystem-Recht", + "short": "KassenSichV, TSE, § 146a AO", + "icon": "award", + "color": "#fbbf24", + "description": "Das Kassensystem als rechtliche Infrastruktur: TSE-Pflicht, Bonausgabepflicht, DSFinV-K, Kassen-Nachschau.", + "source_md": "kassensystem-recht.md", + "modules": [ + { + "id": "tse", + "title": "TSE-Pflicht + § 146a AO", + "objectives": [ + "Was die TSE technisch leistet", + "Kassen-Meldepflicht (ELSTER seit 2025)", + "Unveränderbare Aufzeichnung" + ], + "topics": ["TSE", "BSI TR-03153", "§ 146a AO", "ELSTER"], + "difficulty": "mittel", + "source_heading": "KassenSichV" + }, + { + "id": "bon", + "title": "Bonpflicht + Pflichtinhalte", + "objectives": [ + "Pflichtinhalte eines Bons", + "Digitaler Bon vs. Papier", + "Befreiungs-Ausnahme § 148 AO" + ], + "topics": ["Belegausgabepflicht", "§ 6 KassenSichV", "QR-Code"], + "difficulty": "einfach", + "source_heading": "Bonpflicht" + }, + { + "id": "kassenfuehrung", + "title": "Kassenführung im Tagesgeschäft", + "objectives": [ + "Kassensturz + Z-Bon", + "Storno-Regeln dokumentieren", + "Trinkgeld separat" + ], + "topics": ["Z-Bon", "Kassensturz", "Storno", "Trinkgeld"], + "difficulty": "mittel", + "source_heading": "Kassenführung" + }, + { + "id": "nachschau", + "title": "Kassen-Nachschau (§ 146b AO)", + "objectives": [ + "Was das Finanzamt darf", + "DSFinV-K-Export bereitstellen", + "Ruhig bleiben, Steuerberater rufen" + ], + "topics": ["§ 146b AO", "DSFinV-K", "Prüfung", "Zuschätzung"], + "difficulty": "schwer", + "source_heading": "Kassen-Nachschau" + } + ] + }, + { + "id": "arbeitsschutz", + "title": "7 · Arbeitsschutz", + "short": "ArbZG, Unfälle, 1. Hilfe", + "icon": "clock", + "color": "#f97316", + "description": "Arbeitszeitgesetz, Mindestlohn, typische Unfallrisiken, Alkohol am Arbeitsplatz, Stress und die Erste-Hilfe-Kette.", + "source_md": "arbeitsschutz-gastro.md", + "modules": [ + { + "id": "arbzg", + "title": "ArbZG in der Gastro", + "objectives": [ + "Ruhezeit (11h, Gastro-Ausnahme 10h)", + "Nachtarbeit-Zuschlag 25 %", + "Pflicht-Arbeitszeiterfassung (BAG 2022)" + ], + "topics": ["§ 3-5 ArbZG", "Ruhezeit", "Nachtarbeit", "Zeiterfassung"], + "difficulty": "mittel", + "source_heading": "Arbeitszeitgesetz" + }, + { + "id": "mindestlohn", + "title": "Mindestlohn 2026 + Jugend/Mutter", + "objectives": [ + "13,90 € / h 2026", + "Jugendarbeitsschutz < 18", + "Mutterschutz-Arbeitsverbote" + ], + "topics": ["MiLoG", "JArbSchG", "MuSchG"], + "difficulty": "einfach", + "source_heading": "Mindestlohn 2026" + }, + { + "id": "unfaelle", + "title": "Typische Unfälle + Prävention", + "objectives": [ + "Top-5 Unfallarten in Gastro/Event", + "S2-Schuhe, Schnittschutz, Hitzeschutz", + "BG-Meldung > 3 Tage AU" + ], + "topics": ["Stürze", "Schnittverletzung", "Verbrennung", "§ 193 SGB VII"], + "difficulty": "einfach", + "source_heading": "Unfallrisiken" + }, + { + "id": "erste-hilfe", + "title": "Erste Hilfe + Notfallplan", + "objectives": [ + "Notruf 112 mit W-Fragen", + "CPR 30:2 Rhythmus", + "Verbandbuch + Ersthelfer-Quote" + ], + "topics": ["112", "CPR", "Verbandbuch", "DGUV V1 § 26"], + "difficulty": "einfach", + "source_heading": "Erste-Hilfe-Kette" + } + ] + }, + { + "id": "faq", + "title": "8 · FAQ Alltag", + "short": "20 typische Situationen", + "icon": "star", + "color": "#eab308", + "description": "Aus dem echten Bar-/Event-Alltag: 20 konkrete Situationen mit pragmatischer Handlungsanweisung.", + "source_md": "faq-frequent-situations.md", + "modules": [ + { + "id": "faq-alkohol", + "title": "FAQ: Alkohol & Jugendschutz", + "objectives": [ + "Minderjährige an der Theke", + "Angetrunkene Gäste", + "Fahrtüchtigkeit" + ], + "topics": ["§ 9 JuSchG", "§ 323c StGB", "Hausrecht"], + "difficulty": "einfach", + "source_heading": "Alkohol & Jugendschutz" + }, + { + "id": "faq-zahlung", + "title": "FAQ: Zahlung & Zechpreller", + "objectives": [ + "Bei Zahlungsweigerung ruhig bleiben", + "Zechprellerei § 263a StGB", + "Kartenzahlung defekt → Bar" + ], + "topics": ["§ 263a StGB", "Zechprellerei", "Rechnung"], + "difficulty": "mittel", + "source_heading": "Zahlung & Abrechnung" + }, + { + "id": "faq-notfall", + "title": "FAQ: Notfall, Brand, Schlägerei", + "objectives": [ + "Fettbrand: niemals Wasser", + "Schlägerei: 110, nicht dazwischen", + "Stromausfall: Notbeleuchtung" + ], + "topics": ["CO₂-Löscher", "110", "112", "Notbeleuchtung"], + "difficulty": "mittel", + "source_heading": "Notfälle" + }, + { + "id": "faq-event", + "title": "FAQ: Event-Technik-Pannen", + "objectives": [ + "Pult-Crash kurz vor Türöffnung", + "Musiker will über 99 dB", + "Rigging-Unfall" + ], + "topics": ["Reboot", "DIN 15905-5", "BG-Meldung"], + "difficulty": "schwer", + "source_heading": "Event-Technik" + } + ] + } + ], + "badges": [ + {"id": "erste_schicht", "title": "Feuer frei", "icon": "flame", "description": "1. Quiz bestanden — Willkommen hinter der Theke."}, + {"id": "cocktail_ninja", "title": "Cocktail-Ninja", "icon": "star", "description": "Mixology-Modul komplett (≥ 80 % in allen 4 Teilen)."}, + {"id": "jugendschutz_watchdog", "title": "Jugendschutz-Wachhund", "icon": "shield", "description": "Alle Jugendschutz-Quizfragen korrekt."}, + {"id": "dguv_profi", "title": "DGUV-Profi", "icon": "medal", "description": "Veranstaltungstechnik-Modul komplett."}, + {"id": "gast_whisperer", "title": "Gast-Flüsterer", "icon": "handshake", "description": "Service-Etikette-Modul komplett."}, + {"id": "kassen_pro", "title": "Kassen-Profi", "icon": "award", "description": "KassenSichV-Modul komplett."}, + {"id": "all_rounder", "title": "Thekengeneral", "icon": "crown", "description": "Alle 8 Module erfolgreich abgeschlossen."}, + {"id": "night_owl", "title": "Nachteule", "icon": "moon", "description": "Nach 22 Uhr gelernt — wie ein echter Barkeeper."} + ], + "levels": [ + {"min": 0, "title": "Barhilfe"}, + {"min": 50, "title": "Azubi"}, + {"min": 200, "title": "Shiftleader"}, + {"min": 500, "title": "Barchef:in"}, + {"min": 1250, "title": "Location-Manager:in"}, + {"min": 2500, "title": "Betriebsleiter:in"}, + {"min": 5000, "title": "Gastro-Unternehmer:in"} + ] +} diff --git a/src/levels-fallback.js b/src/levels-fallback.js new file mode 100644 index 0000000..132b610 --- /dev/null +++ b/src/levels-fallback.js @@ -0,0 +1,4 @@ + { min: 0, title: 'Barhilfe' }, { min: 50, title: 'Azubi' }, + { min: 200, title: 'Shiftleader' }, { min: 500, title: 'Barchef:in' }, + { min: 1250, title: 'Location-Manager:in' }, { min: 2500, title: 'Betriebsleiter:in' }, + { min: 5000, title: 'Gastro-Unternehmer:in' } diff --git a/src/welcome.html b/src/welcome.html new file mode 100644 index 0000000..9df5b9d --- /dev/null +++ b/src/welcome.html @@ -0,0 +1,25 @@ +

Hi, ich bin Max.

+

Ich bring dich durch die wichtigsten Basics für Bar-Crew + Bühne + Veranstaltungstechnik — rechtssicher, praxisnah, mit Quiz und Karten. Von Mixology über Kassenrecht (TSE/§ 146 AO) bis Licht-/Ton-Rigging und Veranstaltungs-Sicherheits-Konzept (VStättVO, §38 BImSchG). Sag mir, was bei dir ansteht — neuer Azubi am Tresen, Festival-Vorbereitung, FOH-Crew-Briefing.

+
+ + + + + +
+

Tipp: Chat zum Verstehen → Quiz zum Testen → Karten zum Merken. Fortschritt zeigt dir, wo du stehst.

\ No newline at end of file diff --git a/www/app.js b/www/app.js new file mode 100644 index 0000000..f6b5e2f --- /dev/null +++ b/www/app.js @@ -0,0 +1,1754 @@ +/* Max — Bar & Bühnen-Coach Widget + * Vanilla JS, no build, no framework, keine externen Fonts/Analytics. + * Chat | Quiz | Flashcards | Fortschritt | Know-How — localStorage only. + */ +(() => { + 'use strict'; + + // ==== Config ==== + const API = 'https://llm.qognio.com/api/bots/bar-coach/chat'; + const RAW_KEY = window.__MAX_KEY__ || ''; + const KEY = /^qb_[a-zA-Z0-9]{6,}$/.test(RAW_KEY) ? RAW_KEY : ''; + const LS_KEY = 'max.state.v1'; + const LS_CHAT = 'max.chat.v1'; + const LS_FLASH = 'max.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: 'Barhilfe' }, { min: 50, title: 'Azubi' }, + { min: 200, title: 'Shiftleader' }, { min: 500, title: 'Barchef:in' }, + { min: 1250, title: 'Location-Manager:in' }, { min: 2500, title: 'Betriebsleiter:in' }, + { min: 5000, title: 'Gastro-Unternehmer: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() { + // Feuer frei — 1. Quiz bestanden + if ((state.completedQuizzes || 0) >= 1) unlockBadge('erste_schicht'); + // Cocktail-Ninja — alle Mixology-Submodule in completedCurricula + const mixMods = ['technik','klassiker','glas-garnitur','masse']; + if (mixMods.every(m => (state.completedCurricula || []).includes(m))) unlockBadge('cocktail_ninja'); + // Jugendschutz-Watchdog — alle Jugendschutz-Fragen korrekt, mind. 5 beantwortet + if (state.moduleTotal && state.moduleTotal['juschg'] >= 5 && + state.moduleCorrect['juschg'] === state.moduleTotal['juschg']) unlockBadge('jugendschutz_watchdog'); + // DGUV-Profi — Event-Tech-Modul komplett + const dguvMods = ['dguv','vstaettvo','rigging','pyro-strom']; + if (dguvMods.every(m => (state.completedCurricula || []).includes(m))) unlockBadge('dguv_profi'); + // Gast-Flüsterer — Service-Etikette-Modul komplett + const serviceMods = ['sequence','reklamation','zahlung','gast-konflikt']; + if (serviceMods.every(m => (state.completedCurricula || []).includes(m))) unlockBadge('gast_whisperer'); + // Kassen-Profi — Kassen-Modul komplett + const kasseMods = ['tse','bon','kassenfuehrung','nachschau']; + if (kasseMods.every(m => (state.completedCurricula || []).includes(m))) unlockBadge('kassen_pro'); + // Thekengeneral — alle 8 Curricula (alle Module aller Curricula) + if (CURRICULA && (CURRICULA.curricula || []).every(c => + c.modules.every(m => (state.completedCurricula || []).includes(m.id)))) unlockBadge('all_rounder'); + // Night Owl + 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) => + `
${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
      ' + items + '
    '; + }); + // 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 += '
      '; + 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:'; + 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:
      '; + 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 += `
    1. ${esc(a)}
    2. `; }); + 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 += `
    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 += '
      '; + 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:'; + 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:
      '; + 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:'; + 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:
      '; + 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 += ``; }); + 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:
      '; + 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 = ` +
    +
    +

    🎯 Quiz-Thema wählen

    +

    Wähle ein Modul — Max baut Szenario-Fragen aus dem Bar-/Event-Alltag.

    +
    +
    + + +
    + +
    +
    +
    + `; + 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 = `

    Max baut ${count} Szenario-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

    +

    Max baut Karteikarten zu einem Thema. Bewerte dein Erinnerungsvermögen — das System wiederholt schwere Karten öfter (SM-2).

    +
    +
    + + +
    + + +
    +
    +
    +
    + `; + 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 = `

    Max baut 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

    +
      ${m.objectives.map(o => `
    • ${escapeHTML(o)}
    • `).join('')}
    +

    Kernthemen

    +
      ${(m.topics || []).map(t => `
    • ${escapeHTML(t)}
    • `).join('')}
    + ${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('Max v2026-04-21 ready. XP:', state.xp, 'Streak:', state.currentStreak); + } + + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); + else boot(); +})(); diff --git a/www/curricula.json b/www/curricula.json new file mode 100644 index 0000000..5b311aa --- /dev/null +++ b/www/curricula.json @@ -0,0 +1,497 @@ +{ + "version": "2026-04-24", + "updated": "2026-04-24", + "curricula": [ + { + "id": "bar-basics", + "title": "1 · Bar-Basics Recht", + "short": "JuSchG, § 43 IfSG, LMHV, HACCP", + "icon": "shield", + "color": "#f59e0b", + "description": "Die rechtlichen Mindestpflichten hinter der Theke: Jugendschutz, Belehrung nach § 43 IfSG, Lebensmittelhygiene, HACCP-Grundsätze.", + "source_md": "bar-basics.md", + "modules": [ + { + "id": "juschg", + "title": "Jugendschutz (§ 9 / § 10 JuSchG)", + "objectives": [ + "Altersgrenzen für Bier/Wein/Spirituosen sicher benennen", + "Ausnahme § 9 Abs. 2 (Personensorge) erklären", + "Testkauf-Situationen sauber handhaben" + ], + "topics": ["§ 9 JuSchG", "§ 10 JuSchG", "Alkopops", "Testkauf", "Ausweiskontrolle"], + "difficulty": "einfach", + "source_heading": "Jugendschutz" + }, + { + "id": "ifsg43", + "title": "Belehrung nach § 43 IfSG", + "objectives": [ + "Erst- und Folgebelehrung unterscheiden", + "Meldepflichten bei Infektionen kennen", + "Frist-Grenzen im Anstellungsfall" + ], + "topics": ["§ 43 IfSG", "Gesundheitszeugnis", "Erstbelehrung", "Folgebelehrung"], + "difficulty": "einfach", + "source_heading": "Infektionsschutzgesetz § 43" + }, + { + "id": "lmhv-haccp", + "title": "LMHV + VO 852/2004 + HACCP", + "objectives": [ + "Die 7 HACCP-Grundsätze aufzählen", + "Kühltemperatur-Grenzen (+4 °C, −18 °C)", + "Dokumentationspflicht erklären" + ], + "topics": ["VO 852/2004", "LMHV § 4", "HACCP", "Kühlkette", "Reinigungsplan"], + "difficulty": "mittel", + "source_heading": "LMHV + HACCP" + }, + { + "id": "konzession", + "title": "Konzessionen & Gestattung", + "objectives": [ + "Gaststättenerlaubnis vs. Gestattung", + "§ 12 GastG bei Events", + "Pflichten der Schankkraft ohne Konzession" + ], + "topics": ["GastG", "§ 12 GastG Gestattung", "Konzessionspflicht"], + "difficulty": "mittel", + "source_heading": "Gastgewerbeverordnung" + } + ] + }, + { + "id": "mixology", + "title": "2 · Mixology Basics", + "short": "Klassiker, Technik, Glaswahl", + "icon": "medal", + "color": "#ea580c", + "description": "IBA-Klassiker und das Handwerk der Bar — rühren, shaken, bauen, muddeln. Von Old Fashioned bis Aperol Spritz.", + "source_md": "mixology-grundlagen.md", + "modules": [ + { + "id": "technik", + "title": "Grundtechniken (Rühren/Shaken/Build/Muddle)", + "objectives": [ + "Die 5 Techniken den richtigen Drinks zuordnen", + "Gain-Verlust durch zu langes Shaken verstehen", + "Dry Shake bei Eiweißdrinks einsetzen" + ], + "topics": ["Stirring", "Shaking", "Building", "Muddling", "Dry Shake"], + "difficulty": "einfach", + "source_heading": "Techniken" + }, + { + "id": "klassiker", + "title": "Die 10 Unforgettables", + "objectives": [ + "Rezepte für Old Fashioned, Negroni, Daiquiri sicher", + "Mojito, Margarita, Martini, Manhattan", + "Whisky Sour, Aperol Spritz, Gin Tonic" + ], + "topics": ["Old Fashioned", "Negroni", "Daiquiri", "Mojito", "Margarita", "Martini"], + "difficulty": "mittel", + "source_heading": "Die Klassiker" + }, + { + "id": "glas-garnitur", + "title": "Glaswahl & Garnituren", + "objectives": [ + "Tumbler/Highball/Coupette richtig einsetzen", + "Zeste-Technik (Öl freisetzen)", + "Salzrand & Garnitur-Basics" + ], + "topics": ["Tumbler", "Highball", "Coupette", "Zeste", "Salzrand"], + "difficulty": "einfach", + "source_heading": "Glaswahl" + }, + { + "id": "masse", + "title": "Maß-Systeme (cl / ml / oz)", + "objectives": [ + "cl-ml-oz schnell umrechnen", + "Jigger sicher anwenden", + "Freihand-Schütten vermeiden" + ], + "topics": ["cl", "ml", "oz", "Jigger", "1 oz = 3 cl"], + "difficulty": "einfach", + "source_heading": "Maß-Systeme" + } + ] + }, + { + "id": "service", + "title": "3 · Service & Etikette", + "short": "Sequence, Reklamation, Zahlung", + "icon": "handshake", + "color": "#d97706", + "description": "Gastgeber sein: Sequence of Service, Bestellaufnahme, Reklamationshandling (LAST), Zahlung & Trinkgeld.", + "source_md": "service-etikette.md", + "modules": [ + { + "id": "sequence", + "title": "Sequence of Service", + "objectives": [ + "Die 10 Schritte vom Begrüßen bis zum Farewell", + "Check-Back-Timing (2-3 Min nach Speiseabgabe)", + "Gastrhythmus erkennen" + ], + "topics": ["Greet", "Take Order", "Check-Back", "Farewell"], + "difficulty": "einfach", + "source_heading": "Sequence of Service" + }, + { + "id": "reklamation", + "title": "Reklamation (LAST-Regel)", + "objectives": [ + "Listen-Acknowledge-Solve-Thank anwenden", + "Entschädigung angemessen wählen", + "Eskalation zur Schichtleitung" + ], + "topics": ["LAST", "Entschädigung", "Eskalation"], + "difficulty": "mittel", + "source_heading": "Reklamationsmanagement" + }, + { + "id": "zahlung", + "title": "Zahlung, Trinkgeld, Bonpflicht", + "objectives": [ + "Bonpflicht § 146a AO sicher umsetzen", + "Trinkgeld-Etikette (DE)", + "Split-Rechnung effizient" + ], + "topics": ["§ 146a AO", "Trinkgeld", "Split-Rechnung", "Kartenzahlung"], + "difficulty": "einfach", + "source_heading": "Zahlungsabwicklung" + }, + { + "id": "gast-konflikt", + "title": "Heikle Situationen", + "objectives": [ + "Angetrunkenen Gast deeskalieren", + "Zechpreller richtig handhaben", + "Security/110 richtig einsetzen" + ], + "topics": ["Deeskalation", "Zechprellerei § 263a", "Hausrecht"], + "difficulty": "mittel", + "source_heading": "Heikle Situationen" + } + ] + }, + { + "id": "event-tech", + "title": "4 · Veranstaltungstechnik", + "short": "DGUV V17, VStättVO, Rigging", + "icon": "shield", + "color": "#b45309", + "description": "Sicherheit im Event-Betrieb: DGUV V17/V18, Versammlungsstättenverordnung, Rigging-Basics, elektrische Sicherheit, Pyrotechnik.", + "source_md": "veranstaltungstechnik-basics.md", + "modules": [ + { + "id": "dguv", + "title": "DGUV V17/V18 + Regel 115-002", + "objectives": [ + "Anwendungsbereich V17 vs. V18", + "Pflicht-Unterweisung + Prüfung (jährlich)", + "Verantwortliche Person nach VStättVO" + ], + "topics": ["DGUV V17", "DGUV V18", "Regel 115-002", "Verantwortliche Person"], + "difficulty": "mittel", + "source_heading": "DGUV V17 & V18" + }, + { + "id": "vstaettvo", + "title": "VStättVO + Brandschutz", + "objectives": [ + "Besucherzahl-Grenzen (> 200 indoor / > 1000 outdoor)", + "Fluchtweg-Regeln (max. 30 m)", + "Brandschutz auf Bühne (offenes Feuer grundsätzlich verboten)" + ], + "topics": ["MVStättVO", "Fluchtweg", "Brandschau", "B1-Brandverhalten"], + "difficulty": "schwer", + "source_heading": "Versammlungsstättenverordnung" + }, + { + "id": "rigging", + "title": "Rigging & Lastberechnung", + "objectives": [ + "WLL vs. MBL unterscheiden", + "Sicherheitsfaktoren (5:1 / 7:1 / 10:1)", + "Secondary (Safety) Pflicht" + ], + "topics": ["WLL", "MBL", "Secondary", "Kettenzug", "Traverse"], + "difficulty": "schwer", + "source_heading": "Rigging" + }, + { + "id": "pyro-strom", + "title": "Pyrotechnik + Elektrik", + "objectives": [ + "T1 vs. T2 Pyrotechnik (§ 20 / § 27 SprengG)", + "DGUV V3 Prüfung (Elektrik)", + "FI/RCD Pflicht im Bühnenumfeld" + ], + "topics": ["SprengG", "Kat. T1/T2", "DGUV V3", "FI-Schutzschalter"], + "difficulty": "schwer", + "source_heading": "Pyrotechnik & Elektrische Sicherheit" + } + ] + }, + { + "id": "licht-ton", + "title": "5 · Licht & Ton", + "short": "DMX, Mischpult, 99 dB", + "icon": "medal", + "color": "#92400e", + "description": "Grundlagen der Licht- und Tontechnik: DMX-512, Moving Lights, Mischpult-Signal, FOH vs. Monitor, DIN 15905-5 Lärmschutz.", + "source_md": "licht-ton-basics.md", + "modules": [ + { + "id": "dmx", + "title": "DMX-512 Grundlagen", + "objectives": [ + "512 Kanäle, Startadresse, Daisy Chain", + "Terminator + XLR 110 Ω", + "Art-Net / sACN Überblick" + ], + "topics": ["DMX-512", "Startadresse", "Universum", "Terminator", "sACN"], + "difficulty": "mittel", + "source_heading": "DMX-512" + }, + { + "id": "scheinwerfer", + "title": "Scheinwerfer-Typen + 4-Licht-Konzept", + "objectives": [ + "Spot / Wash / Beam unterscheiden", + "Key/Fill/Back/Kicker anwenden", + "LED vs. Entladungslampe" + ], + "topics": ["Spot", "Wash", "Beam", "Key-Licht", "Fill"], + "difficulty": "mittel", + "source_heading": "Scheinwerfer-Typen" + }, + { + "id": "mischpult", + "title": "Mischpult-Signal + Gain-Staging", + "objectives": [ + "Gain-Stufe zuerst einstellen", + "EQ: schnitzen statt boosten", + "FOH vs. Monitor-Mix" + ], + "topics": ["Gain", "EQ", "Aux-Send", "FOH", "Monitor"], + "difficulty": "mittel", + "source_heading": "Mischpult-Struktur" + }, + { + "id": "din15905", + "title": "DIN 15905-5 Schallschutz", + "objectives": [ + "99 dB(A) LAeq 30 min Grenze", + "135 dB(C) Peak-Grenze", + "Messpflicht ab 85 dB(A)" + ], + "topics": ["DIN 15905-5", "LAeq", "LCpk", "Gehörschutz"], + "difficulty": "schwer", + "source_heading": "DIN 15905-5" + } + ] + }, + { + "id": "kasse-recht", + "title": "6 · Kassensystem-Recht", + "short": "KassenSichV, TSE, § 146a AO", + "icon": "award", + "color": "#fbbf24", + "description": "Das Kassensystem als rechtliche Infrastruktur: TSE-Pflicht, Bonausgabepflicht, DSFinV-K, Kassen-Nachschau.", + "source_md": "kassensystem-recht.md", + "modules": [ + { + "id": "tse", + "title": "TSE-Pflicht + § 146a AO", + "objectives": [ + "Was die TSE technisch leistet", + "Kassen-Meldepflicht (ELSTER seit 2025)", + "Unveränderbare Aufzeichnung" + ], + "topics": ["TSE", "BSI TR-03153", "§ 146a AO", "ELSTER"], + "difficulty": "mittel", + "source_heading": "KassenSichV" + }, + { + "id": "bon", + "title": "Bonpflicht + Pflichtinhalte", + "objectives": [ + "Pflichtinhalte eines Bons", + "Digitaler Bon vs. Papier", + "Befreiungs-Ausnahme § 148 AO" + ], + "topics": ["Belegausgabepflicht", "§ 6 KassenSichV", "QR-Code"], + "difficulty": "einfach", + "source_heading": "Bonpflicht" + }, + { + "id": "kassenfuehrung", + "title": "Kassenführung im Tagesgeschäft", + "objectives": [ + "Kassensturz + Z-Bon", + "Storno-Regeln dokumentieren", + "Trinkgeld separat" + ], + "topics": ["Z-Bon", "Kassensturz", "Storno", "Trinkgeld"], + "difficulty": "mittel", + "source_heading": "Kassenführung" + }, + { + "id": "nachschau", + "title": "Kassen-Nachschau (§ 146b AO)", + "objectives": [ + "Was das Finanzamt darf", + "DSFinV-K-Export bereitstellen", + "Ruhig bleiben, Steuerberater rufen" + ], + "topics": ["§ 146b AO", "DSFinV-K", "Prüfung", "Zuschätzung"], + "difficulty": "schwer", + "source_heading": "Kassen-Nachschau" + } + ] + }, + { + "id": "arbeitsschutz", + "title": "7 · Arbeitsschutz", + "short": "ArbZG, Unfälle, 1. Hilfe", + "icon": "clock", + "color": "#f97316", + "description": "Arbeitszeitgesetz, Mindestlohn, typische Unfallrisiken, Alkohol am Arbeitsplatz, Stress und die Erste-Hilfe-Kette.", + "source_md": "arbeitsschutz-gastro.md", + "modules": [ + { + "id": "arbzg", + "title": "ArbZG in der Gastro", + "objectives": [ + "Ruhezeit (11h, Gastro-Ausnahme 10h)", + "Nachtarbeit-Zuschlag 25 %", + "Pflicht-Arbeitszeiterfassung (BAG 2022)" + ], + "topics": ["§ 3-5 ArbZG", "Ruhezeit", "Nachtarbeit", "Zeiterfassung"], + "difficulty": "mittel", + "source_heading": "Arbeitszeitgesetz" + }, + { + "id": "mindestlohn", + "title": "Mindestlohn 2026 + Jugend/Mutter", + "objectives": [ + "13,90 € / h 2026", + "Jugendarbeitsschutz < 18", + "Mutterschutz-Arbeitsverbote" + ], + "topics": ["MiLoG", "JArbSchG", "MuSchG"], + "difficulty": "einfach", + "source_heading": "Mindestlohn 2026" + }, + { + "id": "unfaelle", + "title": "Typische Unfälle + Prävention", + "objectives": [ + "Top-5 Unfallarten in Gastro/Event", + "S2-Schuhe, Schnittschutz, Hitzeschutz", + "BG-Meldung > 3 Tage AU" + ], + "topics": ["Stürze", "Schnittverletzung", "Verbrennung", "§ 193 SGB VII"], + "difficulty": "einfach", + "source_heading": "Unfallrisiken" + }, + { + "id": "erste-hilfe", + "title": "Erste Hilfe + Notfallplan", + "objectives": [ + "Notruf 112 mit W-Fragen", + "CPR 30:2 Rhythmus", + "Verbandbuch + Ersthelfer-Quote" + ], + "topics": ["112", "CPR", "Verbandbuch", "DGUV V1 § 26"], + "difficulty": "einfach", + "source_heading": "Erste-Hilfe-Kette" + } + ] + }, + { + "id": "faq", + "title": "8 · FAQ Alltag", + "short": "20 typische Situationen", + "icon": "star", + "color": "#eab308", + "description": "Aus dem echten Bar-/Event-Alltag: 20 konkrete Situationen mit pragmatischer Handlungsanweisung.", + "source_md": "faq-frequent-situations.md", + "modules": [ + { + "id": "faq-alkohol", + "title": "FAQ: Alkohol & Jugendschutz", + "objectives": [ + "Minderjährige an der Theke", + "Angetrunkene Gäste", + "Fahrtüchtigkeit" + ], + "topics": ["§ 9 JuSchG", "§ 323c StGB", "Hausrecht"], + "difficulty": "einfach", + "source_heading": "Alkohol & Jugendschutz" + }, + { + "id": "faq-zahlung", + "title": "FAQ: Zahlung & Zechpreller", + "objectives": [ + "Bei Zahlungsweigerung ruhig bleiben", + "Zechprellerei § 263a StGB", + "Kartenzahlung defekt → Bar" + ], + "topics": ["§ 263a StGB", "Zechprellerei", "Rechnung"], + "difficulty": "mittel", + "source_heading": "Zahlung & Abrechnung" + }, + { + "id": "faq-notfall", + "title": "FAQ: Notfall, Brand, Schlägerei", + "objectives": [ + "Fettbrand: niemals Wasser", + "Schlägerei: 110, nicht dazwischen", + "Stromausfall: Notbeleuchtung" + ], + "topics": ["CO₂-Löscher", "110", "112", "Notbeleuchtung"], + "difficulty": "mittel", + "source_heading": "Notfälle" + }, + { + "id": "faq-event", + "title": "FAQ: Event-Technik-Pannen", + "objectives": [ + "Pult-Crash kurz vor Türöffnung", + "Musiker will über 99 dB", + "Rigging-Unfall" + ], + "topics": ["Reboot", "DIN 15905-5", "BG-Meldung"], + "difficulty": "schwer", + "source_heading": "Event-Technik" + } + ] + } + ], + "badges": [ + {"id": "erste_schicht", "title": "Feuer frei", "icon": "flame", "description": "1. Quiz bestanden — Willkommen hinter der Theke."}, + {"id": "cocktail_ninja", "title": "Cocktail-Ninja", "icon": "star", "description": "Mixology-Modul komplett (≥ 80 % in allen 4 Teilen)."}, + {"id": "jugendschutz_watchdog", "title": "Jugendschutz-Wachhund", "icon": "shield", "description": "Alle Jugendschutz-Quizfragen korrekt."}, + {"id": "dguv_profi", "title": "DGUV-Profi", "icon": "medal", "description": "Veranstaltungstechnik-Modul komplett."}, + {"id": "gast_whisperer", "title": "Gast-Flüsterer", "icon": "handshake", "description": "Service-Etikette-Modul komplett."}, + {"id": "kassen_pro", "title": "Kassen-Profi", "icon": "award", "description": "KassenSichV-Modul komplett."}, + {"id": "all_rounder", "title": "Thekengeneral", "icon": "crown", "description": "Alle 8 Module erfolgreich abgeschlossen."}, + {"id": "night_owl", "title": "Nachteule", "icon": "moon", "description": "Nach 22 Uhr gelernt — wie ein echter Barkeeper."} + ], + "levels": [ + {"min": 0, "title": "Barhilfe"}, + {"min": 50, "title": "Azubi"}, + {"min": 200, "title": "Shiftleader"}, + {"min": 500, "title": "Barchef:in"}, + {"min": 1250, "title": "Location-Manager:in"}, + {"min": 2500, "title": "Betriebsleiter:in"}, + {"min": 5000, "title": "Gastro-Unternehmer:in"} + ] +} diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..0b3264a --- /dev/null +++ b/www/index.html @@ -0,0 +1,121 @@ + + + + + Max · Bar & Bühnen-Coach + + + + + + + +
    + +
    +
    + + Max Bar & Bühnen-Coach +
    +
    + Online +
    + + + +
    + +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    +
    + +
    +
    +
    + + + + +
    +
    + +
    + Sovereign AI · Deutscher Bunker · Qognio  ·  DSGVO-konform · Keine externen Fonts · Keine Cookies +
    +
    + +
    + + + + diff --git a/www/styles.css b/www/styles.css new file mode 100644 index 0000000..f196787 --- /dev/null +++ b/www/styles.css @@ -0,0 +1,1038 @@ +/* Max — Bar & Bühnen-Coach Widget */ +:root { + --bg: #0a0a0f; + --bg-elev: #13131a; + --bg-elev-2: #1a1a24; + --border: rgba(255, 255, 255, 0.08); + --border-strong: rgba(255, 255, 255, 0.16); + --text: #f1f0f5; + --text-dim: #a9a8b6; + --text-mute: #6b6a78; + --accent: #f59e0b; + --accent-2: #fbbf24; + --accent-dim: rgba(245, 158, 11, 0.15); + --accent-strong: rgba(245, 158, 11, 0.45); + --success: #10b981; + --warn: #f59e0b; + --danger: #ef4444; + --radius: 14px; + --radius-sm: 8px; + --shadow: 0 6px 24px rgba(0, 0, 0, 0.35); +} + +* { box-sizing: border-box; margin: 0; padding: 0; } +html, body { height: 100%; } +body { + font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + background: var(--bg); + color: var(--text); + -webkit-font-smoothing: antialiased; + overflow: hidden; +} + +/* Layout */ +.app { + display: grid; + grid-template-rows: auto auto 1fr auto; + height: 100vh; + max-width: 980px; + margin: 0 auto; +} + +/* Header */ +.topbar { + padding: 0.9rem 1.25rem; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + gap: 0.75rem; + background: var(--bg); + z-index: 10; +} +.brand { + display: flex; + align-items: center; + gap: 0.55rem; + font-weight: 800; + letter-spacing: -0.02em; + font-size: 1.05rem; +} +.brand-icon { + width: 30px; height: 30px; + display: grid; place-items: center; + background: linear-gradient(140deg, #f59e0b 0%, #d97706 100%); + border-radius: 9px; + font-size: 1rem; +} +.brand small { font-weight: 500; color: var(--text-dim); font-size: 0.72rem; margin-left: .25rem; } +.spacer { flex: 1; } +.status { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.25rem 0.6rem; + border-radius: 999px; + background: rgba(16, 185, 129, 0.12); + color: var(--success); + font-size: 0.7rem; + font-weight: 500; + border: 1px solid rgba(16, 185, 129, 0.28); +} +.status::before { + content: ""; + width: 6px; height: 6px; + border-radius: 50%; + background: var(--success); + animation: pulse 2s infinite; +} +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.35; } +} + +/* Tab bar */ +.tabbar { + display: grid; + grid-template-columns: repeat(5, 1fr); + border-bottom: 1px solid var(--border); + background: var(--bg); + position: sticky; + top: 0; + z-index: 5; +} +.tab { + padding: 0.75rem 0.5rem; + text-align: center; + font-size: 0.85rem; + font-weight: 500; + color: var(--text-dim); + background: none; + border: none; + border-bottom: 2px solid transparent; + cursor: pointer; + font-family: inherit; + transition: all 0.18s ease; + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} +.tab:hover { color: var(--text); background: rgba(255, 255, 255, 0.02); } +.tab[aria-selected="true"] { + color: var(--accent); + border-bottom-color: var(--accent); + background: var(--accent-dim); +} +.tab-kbd { + font-size: 0.6rem; + color: var(--text-mute); + font-weight: 400; +} + +/* Main + views */ +.main { + overflow: hidden; + position: relative; +} +.view { + position: absolute; + inset: 0; + padding: 1rem 1.25rem; + overflow-y: auto; + opacity: 0; + transform: translateY(8px); + pointer-events: none; + transition: opacity 0.22s ease, transform 0.22s ease; +} +.view[data-active="true"] { + opacity: 1; + transform: translateY(0); + pointer-events: auto; +} + +/* Chat */ +.chat-box { + display: flex; + flex-direction: column; + gap: 0.6rem; + padding: 0.25rem 0; +} +.msg { + max-width: 88%; + padding: 0.7rem 0.95rem; + border-radius: 16px; + line-height: 1.55; + font-size: 0.94rem; + word-wrap: break-word; +} +.msg.user { + background: var(--accent-dim); + border: 1px solid var(--accent-strong); + align-self: flex-end; + border-bottom-right-radius: 4px; +} +.msg.bot { + background: var(--bg-elev); + border: 1px solid var(--border); + align-self: flex-start; + border-bottom-left-radius: 4px; +} +.msg.sys { + align-self: center; + color: var(--text-mute); + font-size: 0.78rem; + font-style: italic; + padding: 0.15rem 0; + background: none; + border: none; + max-width: 100%; + text-align: center; +} +.msg h1, .msg h2, .msg h3 { margin: 0.45rem 0 0.3rem; font-size: 1.02rem; } +.msg p { margin: 0.3rem 0; } +.msg ul, .msg ol { margin: 0.25rem 0 0.3rem 1.2rem; } +.msg li { margin: 0.1rem 0; } +.msg strong { color: #fde68a; font-weight: 600; } +.msg em { color: #d6d3de; } +.msg code { + font-family: ui-monospace, Menlo, monospace; + background: rgba(255, 255, 255, 0.08); + padding: 0.05rem 0.35rem; + border-radius: 3px; + font-size: 0.85em; +} +.msg pre { + background: rgba(0, 0, 0, 0.4); + padding: 0.7rem 0.85rem; + border-radius: 8px; + overflow-x: auto; + margin: 0.35rem 0; + border: 1px solid var(--border); +} +.msg pre code { + background: none; + padding: 0; + font-size: 0.82em; +} +.msg a { color: var(--accent); text-decoration: underline; } + +.dots { display: inline-flex; gap: 0.28rem; padding: 0.3rem 0; } +.dots span { + width: 7px; height: 7px; + background: var(--text-dim); + border-radius: 50%; + animation: bounce 1.3s ease-in-out infinite; +} +.dots span:nth-child(2) { animation-delay: 0.15s; } +.dots span:nth-child(3) { animation-delay: 0.3s; } +@keyframes bounce { + 0%, 80%, 100% { opacity: 0.3; transform: scale(0.75); } + 40% { opacity: 1; transform: scale(1); } +} + +/* Composer */ +.composer { + padding: 0.75rem 1.25rem 1rem; + border-top: 1px solid var(--border); + background: var(--bg); + display: flex; + flex-direction: column; + gap: 0.5rem; +} +.composer.dragover { + background: color-mix(in srgb, var(--accent) 10%, var(--bg)); + outline: 2px dashed var(--accent); + outline-offset: -8px; +} +.composer-row { + display: flex; + gap: 0.5rem; + align-items: flex-end; +} +.composer textarea { + flex: 1; + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: inherit; + font: inherit; + padding: 0.7rem 0.95rem; + resize: none; + min-height: 46px; + max-height: 140px; + line-height: 1.5; +} +.composer textarea:focus { + outline: 2px solid var(--accent-strong); + outline-offset: -1px; +} +.btn-attach { + min-height: 46px; + width: 46px; + border: 1px solid var(--border); + background: var(--bg-elev); + color: var(--text); + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 1.2rem; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: background .12s, border-color .12s; +} +.btn-attach:hover { background: var(--accent); color: #fff; border-color: var(--accent); } +.btn-attach:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; } + +.attach-strip { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} +.attach-strip:empty { display: none; } +.attach-chip { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.3rem 0.55rem; + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: 999px; + font-size: 0.78rem; + max-width: 280px; +} +.attach-chip.is-error { border-color: var(--danger, #b91c1c); color: var(--danger, #b91c1c); } +.attach-chip-name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 180px; +} +.attach-chip-size { color: var(--text-mute); font-variant-numeric: tabular-nums; } +.attach-chip-remove { + background: transparent; + border: 0; + color: inherit; + cursor: pointer; + font-size: 1rem; + line-height: 1; + padding: 0 0.1rem; + opacity: 0.6; +} +.attach-chip-remove:hover { opacity: 1; } +.msg .msg-attachments { + margin-top: 0.4rem; + display: flex; + flex-wrap: wrap; + gap: 0.3rem; +} +.msg .msg-attachments .att-name { + font-size: 0.75rem; + padding: 0.15rem 0.5rem; + background: rgba(255,255,255,0.18); + border-radius: 4px; +} +.msg.user .msg-attachments .att-name { background: rgba(255,255,255,0.3); } +.attachment-notice { + margin-top: 0.4rem; + font-size: 0.75rem; + color: var(--text-mute); + font-style: italic; +} +.btn-primary { + background: var(--accent); + color: #fff; + border: none; + padding: 0 1.25rem; + min-height: 46px; + border-radius: var(--radius-sm); + font-weight: 600; + cursor: pointer; + font-family: inherit; + font-size: 0.9rem; + transition: background 0.15s; +} +.btn-primary:hover:not(:disabled) { background: #d97706; } +.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; } +.btn-sec { + background: var(--bg-elev); + color: var(--text); + border: 1px solid var(--border-strong); + padding: 0.55rem 0.9rem; + border-radius: var(--radius-sm); + font-weight: 500; + cursor: pointer; + font-family: inherit; + font-size: 0.85rem; + transition: background 0.15s; +} +.btn-sec:hover { background: var(--bg-elev-2); } +.btn-ghost { + background: none; + color: var(--text-dim); + border: 1px solid transparent; + padding: 0.35rem 0.6rem; + border-radius: 6px; + font-family: inherit; + cursor: pointer; + font-size: 0.82rem; +} +.btn-ghost:hover { color: var(--text); background: rgba(255, 255, 255, 0.04); } + +/* Welcome screen */ +.welcome { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 80%; + padding: 1.5rem; + text-align: center; +} +.welcome h2 { + font-size: 1.5rem; + letter-spacing: -0.02em; + margin-bottom: 0.5rem; + background: linear-gradient(140deg, #f59e0b, #fbbf24); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} +.welcome p { + color: var(--text-dim); + margin-bottom: 1.25rem; + max-width: 38rem; + line-height: 1.55; +} +.mode-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 0.75rem; + width: 100%; + max-width: 42rem; + margin-bottom: 1rem; +} +.mode-card { + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.9rem; + text-align: left; + cursor: pointer; + transition: all 0.18s; + font-family: inherit; + color: inherit; +} +.mode-card:hover { + border-color: var(--accent-strong); + background: var(--bg-elev-2); + transform: translateY(-2px); +} +.mode-card strong { display: block; font-size: 0.95rem; margin-bottom: 0.2rem; } +.mode-card span { color: var(--text-dim); font-size: 0.8rem; } + +/* Quiz */ +.quiz-intro { + display: flex; + flex-direction: column; + gap: 0.75rem; + max-width: 640px; + margin: 0 auto; +} +.topic-select { + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem; +} +.topic-select h3 { margin-bottom: 0.5rem; font-size: 1rem; } +.topic-btn-row { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + margin-top: 0.5rem; +} +.topic-pill { + background: var(--bg-elev-2); + border: 1px solid var(--border); + color: var(--text); + padding: 0.4rem 0.8rem; + border-radius: 999px; + font-family: inherit; + font-size: 0.8rem; + cursor: pointer; + transition: all 0.15s; +} +.topic-pill:hover { + border-color: var(--accent-strong); + background: var(--accent-dim); +} +.topic-pill[aria-selected="true"] { + background: var(--accent-dim); + border-color: var(--accent); + color: var(--accent); +} +.quiz-card { + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem; + max-width: 640px; + margin: 0 auto; +} +.quiz-progress { + display: flex; + justify-content: space-between; + color: var(--text-dim); + font-size: 0.8rem; + margin-bottom: 0.75rem; +} +.quiz-q { + font-size: 1.05rem; + line-height: 1.55; + margin-bottom: 1rem; +} +.quiz-options { display: flex; flex-direction: column; gap: 0.5rem; } +.quiz-option { + background: var(--bg-elev-2); + border: 1px solid var(--border); + color: var(--text); + padding: 0.75rem 1rem; + border-radius: var(--radius-sm); + text-align: left; + font-family: inherit; + font-size: 0.92rem; + cursor: pointer; + transition: all 0.15s; + display: flex; + align-items: center; + gap: 0.75rem; +} +.quiz-option:hover:not(:disabled) { + border-color: var(--accent-strong); + background: var(--accent-dim); +} +.quiz-option .opt-letter { + width: 24px; height: 24px; + display: inline-grid; + place-items: center; + background: var(--bg); + border-radius: 6px; + font-size: 0.75rem; + font-weight: 600; + flex-shrink: 0; +} +.quiz-option.correct { + background: rgba(16, 185, 129, 0.12); + border-color: rgba(16, 185, 129, 0.5); + color: var(--success); +} +.quiz-option.correct .opt-letter { background: var(--success); color: #000; } +.quiz-option.wrong { + background: rgba(239, 68, 68, 0.1); + border-color: rgba(239, 68, 68, 0.5); + color: var(--danger); +} +.quiz-option.wrong .opt-letter { background: var(--danger); color: #fff; } +.quiz-option:disabled { cursor: default; } +.quiz-explain { + margin-top: 1rem; + padding: 0.75rem; + background: var(--bg); + border-left: 3px solid var(--accent); + border-radius: 4px; + font-size: 0.88rem; + line-height: 1.55; + color: var(--text-dim); +} +.quiz-explain strong { color: var(--text); } +.quiz-next { + margin-top: 1rem; + display: flex; + justify-content: flex-end; +} +.quiz-done { + text-align: center; + padding: 1.5rem; +} +.quiz-done h3 { font-size: 1.5rem; margin-bottom: 0.5rem; } +.quiz-done .score { + font-size: 3rem; + font-weight: 700; + background: linear-gradient(140deg, #f59e0b, #fbbf24); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + margin: 1rem 0; +} +.quiz-done .actions { + display: flex; + gap: 0.5rem; + justify-content: center; + margin-top: 1rem; +} + +/* Flashcards */ +.flash-intro { max-width: 640px; margin: 0 auto; } +.flashcard { + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 2rem 1.5rem; + max-width: 640px; + margin: 0 auto; + min-height: 280px; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; + cursor: pointer; + transition: transform 0.3s ease, opacity 0.2s; + user-select: none; +} +.flashcard:hover { border-color: var(--accent-strong); } +.flashcard.flipping { opacity: 0.6; transform: scale(0.98); } +.flashcard-front { + font-size: 1.15rem; + font-weight: 500; + line-height: 1.5; +} +.flashcard-back { + font-size: 0.98rem; + line-height: 1.6; + color: var(--text-dim); +} +.flashcard-back strong { color: var(--text); display: block; margin-bottom: 0.5rem; } +.flashcard-hint { + margin-top: 1rem; + font-size: 0.8rem; + color: var(--text-mute); + font-style: italic; +} +.flash-controls { + max-width: 640px; + margin: 1rem auto 0; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.4rem; +} +.flash-btn { + padding: 0.7rem 0.4rem; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + background: var(--bg-elev); + color: var(--text); + font-family: inherit; + font-size: 0.82rem; + cursor: pointer; + font-weight: 500; + transition: all 0.15s; +} +.flash-btn:hover { background: var(--bg-elev-2); } +.flash-btn .label { display: block; font-size: 0.72rem; color: var(--text-mute); margin-top: 2px; } +.flash-btn[data-rating="0"]:hover { border-color: var(--danger); color: var(--danger); } +.flash-btn[data-rating="1"]:hover { border-color: var(--warn); color: var(--warn); } +.flash-btn[data-rating="2"]:hover { border-color: var(--accent); color: var(--accent); } +.flash-btn[data-rating="3"]:hover { border-color: var(--success); color: var(--success); } + +.flash-meta { + max-width: 640px; + margin: 0.5rem auto 0; + display: flex; + justify-content: space-between; + color: var(--text-mute); + font-size: 0.78rem; +} + +/* Progress */ +.progress-grid { + display: grid; + gap: 1rem; + max-width: 640px; + margin: 0 auto; + padding-bottom: 1rem; +} +.stat-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 0.75rem; +} +.stat-card { + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.9rem; + text-align: center; +} +.stat-card .val { + font-size: 1.8rem; + font-weight: 700; + letter-spacing: -0.02em; +} +.stat-card .lbl { + color: var(--text-dim); + font-size: 0.78rem; + margin-top: 0.2rem; +} +.stat-card.accent .val { color: var(--accent); } +.stat-card.streak .val { color: var(--warn); } +.stat-card.level .val { color: var(--success); font-size: 1.2rem; } + +.section-card { + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem; +} +.section-card h3 { + font-size: 0.95rem; + margin-bottom: 0.75rem; + letter-spacing: -0.01em; +} +.mastery-row { + display: flex; + flex-direction: column; + gap: 0.55rem; +} +.mastery-bar { + display: flex; + flex-direction: column; + gap: 0.25rem; +} +.mastery-head { + display: flex; + justify-content: space-between; + font-size: 0.8rem; +} +.mastery-head .pct { color: var(--accent); font-weight: 600; } +.bar-bg { + height: 6px; + background: var(--bg-elev-2); + border-radius: 3px; + overflow: hidden; +} +.bar-fg { + height: 100%; + background: linear-gradient(90deg, var(--accent) 0%, #fbbf24 100%); + border-radius: 3px; + transition: width 0.4s ease; +} + +.badge-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); + gap: 0.6rem; +} +.badge { + background: var(--bg-elev-2); + border: 1px solid var(--border); + border-radius: 10px; + padding: 0.75rem; + text-align: center; + transition: all 0.2s; +} +.badge.earned { + border-color: var(--accent-strong); + background: var(--accent-dim); +} +.badge.locked { opacity: 0.4; } +.badge .icon { + font-size: 1.6rem; + margin-bottom: 0.35rem; + display: block; +} +.badge .title { font-size: 0.78rem; font-weight: 600; } +.badge .desc { font-size: 0.7rem; color: var(--text-mute); margin-top: 0.15rem; line-height: 1.3; } + +/* Curriculum tree */ +.curr-tree { max-width: 720px; margin: 0 auto; } +.curr-root { + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: 0.6rem; + overflow: hidden; +} +.curr-root-head { + width: 100%; + background: none; + border: none; + color: var(--text); + padding: 0.85rem 1rem; + display: flex; + align-items: center; + gap: 0.75rem; + cursor: pointer; + font-family: inherit; + font-size: 0.95rem; + text-align: left; +} +.curr-root-head:hover { background: rgba(255, 255, 255, 0.02); } +.curr-root-head .ic { + width: 34px; height: 34px; + border-radius: 8px; + display: grid; + place-items: center; + font-size: 1.1rem; + color: #fff; + font-weight: 700; + flex-shrink: 0; +} +.curr-root-head .txt { flex: 1; } +.curr-root-head .txt strong { display: block; } +.curr-root-head .txt small { color: var(--text-dim); font-size: 0.78rem; } +.curr-root-head .chev { + transition: transform 0.2s; + color: var(--text-dim); +} +.curr-root[open] .curr-root-head .chev { transform: rotate(180deg); } +.curr-mods { + padding: 0.2rem 0 0.6rem; + border-top: 1px solid var(--border); +} +.curr-mod { + padding: 0.55rem 1rem 0.55rem 3.75rem; + border-left: 2px solid transparent; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; + transition: all 0.15s; +} +.curr-mod:hover { + background: rgba(5, 150, 105, 0.05); + border-left-color: var(--accent); +} +.curr-mod .m-title { + font-size: 0.88rem; + line-height: 1.3; +} +.curr-mod .m-arrow { + color: var(--text-mute); + font-size: 0.9rem; +} +.mod-detail { + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem; + max-width: 720px; + margin: 0 auto; +} +.mod-detail h3 { margin-bottom: 0.5rem; letter-spacing: -0.01em; } +.mod-detail .breadcrumb { + font-size: 0.8rem; + color: var(--text-dim); + margin-bottom: 0.75rem; +} +.mod-detail .breadcrumb button { + background: none; + border: none; + color: var(--accent); + cursor: pointer; + font-family: inherit; + padding: 0; + font-size: inherit; +} +.mod-detail h4 { margin: 1rem 0 0.35rem; font-size: 0.9rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; } +.mod-detail ul { margin-left: 1.2rem; color: var(--text); } +.mod-detail li { margin: 0.25rem 0; line-height: 1.5; } +.mod-actions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + margin-top: 1.25rem; +} + +/* Footer */ +.footer { + padding: 0.55rem 1.25rem; + text-align: center; + color: var(--text-mute); + font-size: 0.7rem; + border-top: 1px solid var(--border); +} +.footer a { color: var(--accent); text-decoration: none; } + +/* Toast */ +.toast-stack { + position: fixed; + top: 1rem; + right: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + z-index: 100; + pointer-events: none; +} +.toast { + background: var(--bg-elev); + border: 1px solid var(--border); + border-left: 3px solid var(--accent); + border-radius: var(--radius-sm); + padding: 0.7rem 0.9rem; + font-size: 0.85rem; + max-width: 300px; + box-shadow: var(--shadow); + animation: toast-in 0.25s ease; + pointer-events: auto; +} +.toast.error { border-left-color: var(--danger); } +.toast.success { border-left-color: var(--success); } +.toast.warn { border-left-color: var(--warn); } +@keyframes toast-in { + from { opacity: 0; transform: translateX(20px); } + to { opacity: 1; transform: translateX(0); } +} + +/* XP gain animation */ +.xp-gain { + position: fixed; + bottom: 5rem; + left: 50%; + transform: translateX(-50%); + background: linear-gradient(140deg, #f59e0b, #fbbf24); + color: #fff; + padding: 0.5rem 1rem; + border-radius: 999px; + font-weight: 600; + font-size: 0.9rem; + pointer-events: none; + animation: xp-fly 1.5s ease-out forwards; + z-index: 50; +} +@keyframes xp-fly { + 0% { opacity: 0; transform: translate(-50%, 20px) scale(0.8); } + 20% { opacity: 1; transform: translate(-50%, 0) scale(1); } + 80% { opacity: 1; transform: translate(-50%, -40px) scale(1); } + 100% { opacity: 0; transform: translate(-50%, -60px) scale(0.9); } +} + +/* Responsive */ +@media (max-width: 640px) { + .topbar { padding: 0.7rem 0.9rem; } + .brand { font-size: 0.95rem; } + .brand small { display: none; } + .tabbar { grid-template-columns: repeat(5, 1fr); } + .tab { font-size: 0.75rem; padding: 0.6rem 0.2rem; } + .tab-kbd { display: none; } + .view { padding: 0.75rem 0.9rem; } + .composer { padding: 0.6rem 0.9rem 0.75rem; } + .msg { max-width: 92%; font-size: 0.9rem; } + .quiz-card, .flashcard, .mod-detail { padding: 1rem; } + .quiz-q { font-size: 0.98rem; } + .flash-controls { grid-template-columns: repeat(2, 1fr); } + .stat-card .val { font-size: 1.45rem; } + .welcome h2 { font-size: 1.25rem; } + .welcome p { font-size: 0.88rem; } +} + +/* Focus visible */ +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +/* Utility */ +.hidden { display: none !important; } +.flex-row { display: flex; align-items: center; gap: 0.5rem; } +.mono { font-family: ui-monospace, Menlo, monospace; } + +/* GFM table (added 2026-04-24) */ +.md-table { width:100%; border-collapse:collapse; margin:.7rem 0; font-size:.92em; background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.08); border-radius:8px; overflow:hidden; } +.md-table thead { background:rgba(245,158,11,0.12); } +.md-table th, .md-table td { padding:.55rem .8rem; border-bottom:1px solid rgba(255,255,255,0.06); text-align:left; vertical-align:top; } +.md-table th { color:#f59e0b; font-weight:600; font-size:.85em; letter-spacing:.02em; text-transform:uppercase; } +.md-table tbody tr:last-child td { border-bottom:none; } +.md-table tbody tr:hover { background:rgba(255,255,255,0.03); } +.md-table code { font-size:.92em; padding:1px 5px; } +.msg.bot .md-table, .chat-message.bot .md-table { display:block; overflow-x:auto; max-width:100%; } + + +/* Deep-Dive bar (flashcards + quiz) — 2026-04-25 */ +.deepdive-bar { + display: flex; + flex-wrap: wrap; + gap: .4rem; + margin-top: .8rem; + padding-top: .6rem; + border-top: 1px dashed rgba(255,255,255,0.08); +} +.deepdive-btn { + font-size: .78em; + padding: .4rem .65rem; + background: rgba(255,255,255,0.04); + border: 1px solid rgba(255,255,255,0.1); + color: var(--text, #f1f0f5); + border-radius: 6px; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; +} +.deepdive-btn:hover { + background: rgba(255,255,255,0.08); + border-color: rgba(255,255,255,0.2); +} +.deepdive-panel { + margin-top: .7rem; + padding: .9rem 1rem; + background: rgba(255,255,255,0.04); + border: 1px solid rgba(255,255,255,0.12); + border-radius: 8px; + font-size: .92em; + line-height: 1.55; +} +.deepdive-panel.hidden { display: none; } +.deepdive-panel .dd-loading { display: flex; align-items: center; gap: .6rem; color: var(--text-mute, #888); font-style: italic; } +.deepdive-panel .dd-body { margin-bottom: .7rem; } +.deepdive-panel .dd-body p:first-child { margin-top: 0; } +.deepdive-panel .dd-body p:last-child { margin-bottom: 0; } +.deepdive-panel .dd-body ul, .deepdive-panel .dd-body ol { margin: .3rem 0 .4rem 1.2rem; } +.deepdive-panel .dd-body li { margin-bottom: .15rem; } +.deepdive-panel .dd-body code { background: rgba(255,255,255,0.08); padding: 1px 5px; border-radius: 3px; } +.deepdive-panel .close-dd { font-size: .78em; padding: .3rem .6rem; margin-top: .4rem; } +@media (max-width: 640px) { .deepdive-bar { gap: .3rem; } .deepdive-btn { font-size: .72em; padding: .35rem .5rem; } } + + +/* Structured-reply fallback in chat (2026-04-25) */ +.structured-chat { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: .9rem 1rem; font-size: .92em; } +.struct-badge { display: inline-block; font-size: .75em; padding: .2rem .55rem; background: rgba(255,255,255,0.08); border-radius: 4px; margin-bottom: .6rem; letter-spacing: .03em; } +.struct-topic { font-weight: 600; font-size: 1.02em; margin-bottom: .5rem; } +.struct-scenario { background: rgba(255,255,255,0.04); padding: .6rem .8rem; border-radius: 6px; margin-bottom: .7rem; line-height: 1.55; } +.struct-question { margin: .7rem 0; padding-left: .4rem; border-left: 2px solid rgba(255,255,255,0.15); } +.struct-options { list-style: none; padding-left: 0; margin: .35rem 0 .5rem 0; } +.struct-options li { padding: .2rem 0; color: var(--text-mute, #bbb); } +.struct-options li.correct { color: var(--success, #10b981); font-weight: 500; } +.struct-explain { font-size: .9em; margin-top: .3rem; padding: .4rem .6rem; background: rgba(255,255,255,0.03); border-radius: 4px; line-height: 1.5; } +.struct-lessons { margin-top: .6rem; font-size: .9em; } +.struct-lessons ul { margin: .25rem 0 .3rem 1.2rem; } +.struct-norms { margin-top: .4rem; font-size: .85em; color: var(--text-mute, #aaa); } +.struct-norms code { background: rgba(255,255,255,0.08); padding: 1px 6px; border-radius: 3px; margin-right: .2rem; } +.struct-flashcard { margin: .5rem 0; padding: .5rem .8rem; background: rgba(255,255,255,0.03); border-radius: 6px; } +.fc-front { font-weight: 500; } +.fc-back { margin-top: .25rem; color: var(--text, #f1f0f5); } +.fc-hint { margin-top: .25rem; font-size: .85em; color: var(--text-mute, #aaa); } +.struct-objectives { margin-bottom: .5rem; font-size: .9em; } +.struct-slide { margin: .5rem 0; padding: .5rem .8rem; border-left: 2px solid rgba(255,255,255,0.15); } +.slide-content { margin-top: .3rem; font-size: .95em; line-height: 1.55; } +.slide-content p:first-child { margin-top: 0; } +.slide-key { margin-top: .35rem; font-size: .88em; color: var(--accent, #f59e0b); } +.struct-hint { margin-top: .7rem; font-size: .78em; color: var(--text-mute, #888); font-style: italic; } + +/* Bot-spezifische Card-Erweiterungen (audit/privacy_check/mail_check/plan/validate/interview/decode/write/calc — 2026-04-25) */ +.struct-row { margin: .35rem 0; line-height: 1.5; } +.struct-row code { background: rgba(255,255,255,0.08); padding: 1px 6px; border-radius: 3px; font-size: .9em; } +.struct-section { margin-top: .65rem; padding-top: .55rem; border-top: 1px solid rgba(255,255,255,0.08); } +.struct-section ul, .struct-section ol { margin: .3rem 0 .3rem 1.2rem; line-height: 1.5; } +.struct-section li { padding: .1rem 0; } +.struct-table { width: 100%; border-collapse: collapse; margin-top: .35rem; font-size: .88em; } +.struct-table th, .struct-table td { padding: .35rem .55rem; border-bottom: 1px solid rgba(255,255,255,0.06); text-align: left; vertical-align: top; } +.struct-table th { color: var(--text-mute, #bbb); font-weight: 500; font-size: .85em; text-transform: uppercase; letter-spacing: .04em; } +.struct-table code { background: rgba(255,255,255,0.06); padding: 1px 5px; border-radius: 3px; } +.struct-step { margin: .4rem 0; padding: .4rem .65rem; background: rgba(255,255,255,0.03); border-left: 2px solid var(--accent, #7c3aed); border-radius: 4px; } +.struct-step .step-head { font-weight: 500; margin-bottom: .25rem; } +.struct-step .step-success { font-size: .85em; color: var(--success, #22c55e); margin-top: .25rem; } +.struct-step ul { margin: .25rem 0 .25rem 1.1rem; font-size: .9em; } +.struct-doc { font-family: Georgia, 'Iowan Old Style', serif; line-height: 1.6; } +.struct-doc blockquote { margin: .4rem 0; padding-left: .75rem; border-left: 3px solid var(--accent, #7c3aed); color: var(--text, #cfcedb); }