From 1e3703ac4bdd20dd2e9c8f1d579861969d5daff9 Mon Sep 17 00:00:00 2001 From: Qognio Bot Extract Date: Wed, 29 Apr 2026 01:35:47 +0200 Subject: [PATCH] init: extract ki-kennzahlen-coach from qognio-bot-widget-template@d2c816f Source files (src/) and rendered bundle (www/) extracted on 2026-04-29T01:35:47+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-25 --- .dockerignore | 7 + .gitignore | 4 + Dockerfile | 13 + README.md | 67 ++ bot.json | 14 + docker-compose.yml | 20 + nginx.conf | 27 + src/check-badges.js | 22 + src/cockpit-overlay/cockpit.css | 705 +++++++++++++ src/cockpit-overlay/cockpit.js | 1249 ++++++++++++++++++++++ src/cockpit-overlay/index.html | 102 ++ src/config.yaml | 33 + src/curricula.json | 386 +++++++ src/levels-fallback.js | 4 + src/welcome.html | 29 + www/app.js | 1753 +++++++++++++++++++++++++++++++ www/cockpit/cockpit.css | 705 +++++++++++++ www/cockpit/cockpit.js | 1249 ++++++++++++++++++++++ www/cockpit/index.html | 102 ++ www/curricula.json | 386 +++++++ www/index.html | 125 +++ www/styles.css | 1038 ++++++++++++++++++ 22 files changed, 8040 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 bot.json create mode 100644 docker-compose.yml create mode 100644 nginx.conf create mode 100644 src/check-badges.js create mode 100644 src/cockpit-overlay/cockpit.css create mode 100644 src/cockpit-overlay/cockpit.js create mode 100644 src/cockpit-overlay/index.html create mode 100644 src/config.yaml create mode 100644 src/curricula.json create mode 100644 src/levels-fallback.js create mode 100644 src/welcome.html create mode 100644 www/app.js create mode 100644 www/cockpit/cockpit.css create mode 100644 www/cockpit/cockpit.js create mode 100644 www/cockpit/index.html create mode 100644 www/curricula.json create mode 100644 www/index.html create mode 100644 www/styles.css 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..cb2d5ab --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# Kai — KI-Kennzahlen & Methodik-Coach + +Kai — dein KI-Kennzahlen-Coach. Methodik, Modell-Bewertung, Bias/Fairness, AI-Reifegrad-Messung für CIOs, CDOs, AI-Leads. Im deutschen Bunker. + +``` +slug : ki-kennzahlen-coach +version : 2026-04-25 +accent : #0891b2 +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 ki-kennzahlen-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-ki-kennzahlen-coach +cd my-customer-ki-kennzahlen-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/* ki-kennzahlen-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:47+02:00. diff --git a/bot.json b/bot.json new file mode 100644 index 0000000..983b133 --- /dev/null +++ b/bot.json @@ -0,0 +1,14 @@ +{ + "slug": "ki-kennzahlen-coach", + "name": "Kai", + "title": "KI-Kennzahlen & Methodik-Coach", + "tagline": "KI-Kennzahlen", + "description": "Kai — dein KI-Kennzahlen-Coach. Methodik, Modell-Bewertung, Bias/Fairness, AI-Reifegrad-Messung für CIOs, CDOs, AI-Leads. Im deutschen Bunker.", + "version": "2026-04-25", + "accent": "#0891b2", + "extracted_from": "qognio-bot-widget-template", + "parent_core_commit": "d2c816f3edbc9760802a11b29ff4151c7aad4b46", + "extracted_at": "2026-04-29T01:35:47+02:00", + "runtime": "nginx:alpine", + "default_port": 80 +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1a383e2 --- /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-ki-kennzahlen-coach:${TAG:-latest} + container_name: bot-ki-kennzahlen-coach + restart: unless-stopped + networks: + - caddy + labels: + caddy: "ki-kennzahlen-coach.on.qognio.com" + caddy.reverse_proxy: "{{upstreams 80}}" + qognio.bot.slug: "ki-kennzahlen-coach" + qognio.bot.version: "2026-04-25" + +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..f46d9a1 --- /dev/null +++ b/src/check-badges.js @@ -0,0 +1,22 @@ + // Erste Metrik-Analyse — 1 korrekte Antwort im Klassifikations-Modul + if ((state.moduleCorrect && state.moduleCorrect['klassifikation-metriken'] >= 1)) unlockBadge('erste_metrik'); + // Metrik-Master — 10 Klassifikations-Metriken-Antworten korrekt + if ((state.moduleCorrect && state.moduleCorrect['klassifikation-metriken'] >= 10)) unlockBadge('metrik_master'); + // Bias-Hunter — 5 korrekte Antworten im Fairness-Metriken-Modul + if ((state.moduleCorrect && state.moduleCorrect['fairness-metriken'] >= 5)) unlockBadge('bias_hunter'); + // EU-AI-Act-Navigator — 5 korrekte Antworten im Risikoklassen-Modul + if ((state.moduleCorrect && state.moduleCorrect['risikoklassen'] >= 5)) unlockBadge('ai_act_navigator'); + // NIST-Praktiker — 5 korrekte Antworten in den 4 NIST-Kernfunktionen + if ((state.moduleCorrect && state.moduleCorrect['vier-funktionen'] >= 5)) unlockBadge('nist_practitioner'); + // ISO-42001-Expert — 5 korrekte Antworten im AIMS-Modul + if ((state.moduleCorrect && state.moduleCorrect['iso-42001-aims'] >= 5)) unlockBadge('iso_expert'); + // Scorecard-Architekt — Scorecard-Design-Modul Flashcards bestanden + if ((state.modulePassedFlash && state.modulePassedFlash['scorecard-design'])) unlockBadge('scorecard_architect'); + // Reifegrad-Lead — 5 korrekte Antworten im Self-Assessment-Modul + if ((state.moduleCorrect && state.moduleCorrect['self-assessment'] >= 5)) unlockBadge('reifegrad_lead'); + // KPI-Master — 22 von 28 Modulen mit ≥80% Quiz-Score abgeschlossen (~79% Master-Coverage) + if ((state.completedCurricula || []).length >= 22) unlockBadge('kpi_master'); + // Night Owl & Early Bird (beibehalten) + const h = new Date().getHours(); + if (h >= 22) unlockBadge('night_owl'); + if (h < 7) unlockBadge('early_bird'); \ No newline at end of file diff --git a/src/cockpit-overlay/cockpit.css b/src/cockpit-overlay/cockpit.css new file mode 100644 index 0000000..809c224 --- /dev/null +++ b/src/cockpit-overlay/cockpit.css @@ -0,0 +1,705 @@ +/* Kai Cockpit — vanilla CSS, no external fonts/CDN. + Extends Kai widget theme: teal #0891b2/#06b6d4, bg #0a0a0f, text #f1f0f5. */ + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +:root { + --bg: #0a0a0f; + --bg-elev: #12121a; + --bg-elev2: #191924; + --bg-elev3: #22222f; + --line: #2a2a38; + --line-soft: #1f1f2b; + --text: #f1f0f5; + --text-mute: #a0a0b5; + --text-dim: #707088; + --teal: #0891b2; + --teal-b: #06b6d4; + --teal-c: #22d3ee; + --teal-dim: rgba(8,145,178,.15); + --ok: #10b981; + --warn: #f59e0b; + --err: #ef4444; + --violet: #a78bfa; + --dock-w: 300px; + --nav-w: 220px; + --radius: 10px; + --radius-sm: 6px; + --shadow: 0 8px 24px rgba(0,0,0,.45); + --t: 0.2s ease; +} + +body { + background: var(--bg); + color: var(--text); + font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 15px; + line-height: 1.5; + min-height: 100vh; + -webkit-font-smoothing: antialiased; +} + +a { color: var(--teal-c); text-decoration: none; } +a:hover { text-decoration: underline; } +button { font-family: inherit; font-size: inherit; } +code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background: #0d0d14; padding: 1px 5px; border-radius: 4px; border: 1px solid var(--line-soft); font-size: .9em; } + +/* ============ LAYOUT ============ */ +.cockpit { + display: grid; + grid-template-columns: var(--nav-w) minmax(0, 1fr) var(--dock-w); + grid-template-rows: auto 1fr auto; + grid-template-areas: + "top top top" + "nav main dock" + "foot foot foot"; + min-height: 100vh; + background: radial-gradient(ellipse at top, rgba(8,145,178,.08), transparent 60%), var(--bg); +} +.topbar { grid-area: top; } +.nav { grid-area: nav; } +.main { grid-area: main; } +.chat-dock { grid-area: dock; } +.footer { grid-area: foot; } + +/* ============ TOPBAR ============ */ +.topbar { + display: flex; + align-items: center; + gap: 16px; + padding: 14px 22px; + border-bottom: 1px solid var(--line); + background: linear-gradient(180deg, rgba(18,18,26,.92), rgba(10,10,15,.92)); + backdrop-filter: blur(6px); + position: sticky; top: 0; z-index: 50; +} +.brand { display: flex; align-items: center; gap: 12px; } +.brand-icon { + width: 36px; height: 36px; border-radius: 9px; + background: linear-gradient(135deg, var(--teal), var(--teal-b)); + display: flex; align-items: center; justify-content: center; + font-weight: 700; font-size: 18px; color: #001013; + box-shadow: 0 0 0 1px rgba(34,211,238,.25), 0 6px 14px rgba(8,145,178,.35); +} +.brand-text { display: flex; flex-direction: column; line-height: 1.1; } +.brand-title { font-weight: 600; font-size: 16px; letter-spacing: -.2px; } +.brand-title small { font-weight: 400; color: var(--text-mute); font-size: 13px; margin-left: 2px; } +.brand-sub { color: var(--text-dim); font-size: 11px; letter-spacing: .04em; text-transform: uppercase; } +.spacer { flex: 1; } + +.xp-wrap { display: flex; flex-direction: column; gap: 4px; min-width: 170px; } +.xp-row { display: flex; justify-content: space-between; font-size: 12px; color: var(--text-mute); } +.xp-level { color: var(--teal-c); font-weight: 500; } +.xp-score { color: var(--text-mute); } +.xp-bar { height: 5px; background: var(--bg-elev2); border-radius: 99px; overflow: hidden; border: 1px solid var(--line-soft); } +.xp-bar-fill { height: 100%; width: 0%; background: linear-gradient(90deg, var(--teal), var(--teal-c)); transition: width .4s ease; } + +.back-link { + padding: 7px 14px; border: 1px solid var(--line); + border-radius: 8px; font-size: 13px; color: var(--text-mute); + transition: var(--t); +} +.back-link:hover { color: var(--text); border-color: var(--teal); background: var(--teal-dim); text-decoration: none; } + +/* ============ NAV ============ */ +.nav { + border-right: 1px solid var(--line); + padding: 22px 14px; + display: flex; flex-direction: column; gap: 4px; + background: rgba(18,18,26,.5); +} +.nav-item { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 10px; + align-items: center; + background: transparent; + border: 1px solid transparent; + color: var(--text-mute); + padding: 12px 14px; + border-radius: var(--radius); + cursor: pointer; + text-align: left; + transition: var(--t); + font-size: 14px; +} +.nav-item:hover { background: var(--bg-elev); color: var(--text); } +.nav-item[aria-selected="true"] { + background: linear-gradient(90deg, rgba(8,145,178,.2), rgba(8,145,178,.06)); + border-color: rgba(34,211,238,.35); + color: var(--text); + box-shadow: inset 2px 0 0 var(--teal-c); +} +.nav-num { font-variant-numeric: tabular-nums; font-size: 11px; color: var(--text-dim); letter-spacing: .08em; } +.nav-item[aria-selected="true"] .nav-num { color: var(--teal-c); } +.nav-label { font-weight: 500; letter-spacing: -.2px; } +.nav-kbd { font-size: 10px; color: var(--text-dim); background: var(--bg-elev2); padding: 2px 5px; border-radius: 4px; border: 1px solid var(--line-soft); } + +/* ============ MAIN ============ */ +.main { padding: 28px 36px 40px; min-width: 0; } +.module { display: none; max-width: 1100px; margin: 0 auto; } +.module[data-active="true"] { display: block; animation: fadeIn .25s ease; } +@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } } + +.mod-head { margin-bottom: 28px; } +.mod-head h1 { + margin: 0 0 8px; font-size: 28px; font-weight: 600; letter-spacing: -0.5px; + background: linear-gradient(90deg, var(--text), var(--teal-c)); + -webkit-background-clip: text; background-clip: text; color: transparent; +} +.mod-head p { margin: 0; color: var(--text-mute); max-width: 720px; font-size: 14px; } +.mod-badge { + display: inline-flex; align-items: center; gap: 6px; + font-size: 11px; font-weight: 500; letter-spacing: .05em; text-transform: uppercase; + color: var(--teal-c); background: var(--teal-dim); + border: 1px solid rgba(34,211,238,.25); + padding: 4px 10px; border-radius: 99px; margin-bottom: 12px; +} + +/* Cards + surfaces */ +.card { + background: var(--bg-elev); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 20px; + transition: var(--t); +} +.card-hover:hover { border-color: rgba(34,211,238,.4); transform: translateY(-1px); cursor: pointer; } + +.btn { + padding: 9px 16px; border-radius: 8px; + font-weight: 500; cursor: pointer; + border: 1px solid var(--line); + background: var(--bg-elev2); color: var(--text); + transition: var(--t); font-size: 14px; +} +.btn:hover:not(:disabled) { border-color: var(--teal); color: var(--teal-c); } +.btn:disabled { opacity: .4; cursor: not-allowed; } +.btn-primary { + background: linear-gradient(135deg, var(--teal), var(--teal-b)); + border: none; color: #001013; font-weight: 600; +} +.btn-primary:hover:not(:disabled) { filter: brightness(1.15); color: #001013; } +.btn-ghost { background: transparent; border: 1px solid var(--line); } +.btn-sm { padding: 6px 12px; font-size: 13px; } +.btn-ask-kai { + background: transparent; + border: 1px dashed rgba(34,211,238,.4); + color: var(--teal-c); + padding: 8px 14px; font-size: 13px; + border-radius: 8px; cursor: pointer; transition: var(--t); + display: inline-flex; align-items: center; gap: 6px; +} +.btn-ask-kai:hover { background: var(--teal-dim); border-style: solid; } + +/* ============ REIFEGRAD ============ */ +.reifegrad-intro { + display: grid; gap: 10px; margin-bottom: 20px; + padding: 16px 20px; + background: linear-gradient(135deg, rgba(8,145,178,.08), rgba(18,18,26,.6)); + border: 1px solid var(--line); + border-radius: var(--radius); +} +.reifegrad-intro .intro-stats { + display: flex; gap: 16px; flex-wrap: wrap; + font-size: 12px; color: var(--text-mute); +} +.reifegrad-intro .intro-stats span strong { color: var(--teal-c); font-variant-numeric: tabular-nums; margin-right: 4px; } + +.dim-grid { + display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 14px; margin-bottom: 24px; +} +.dim-card { + background: var(--bg-elev); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 16px 18px; + cursor: pointer; transition: var(--t); + position: relative; overflow: hidden; +} +.dim-card:hover { border-color: rgba(34,211,238,.35); transform: translateY(-1px); } +.dim-card.done { border-color: rgba(16,185,129,.45); } +.dim-card.done::after { + content: "✓"; position: absolute; top: 10px; right: 12px; + color: var(--ok); font-size: 14px; font-weight: 700; +} +.dim-card h3 { margin: 0 0 6px; font-size: 15px; font-weight: 600; color: var(--text); } +.dim-card p { margin: 0; color: var(--text-mute); font-size: 12.5px; line-height: 1.4; } +.dim-card .dim-score { + display: flex; align-items: center; gap: 6px; + margin-top: 10px; font-size: 12px; + color: var(--teal-c); font-variant-numeric: tabular-nums; +} +.dim-card .dim-score .mini-bar { + flex: 1; height: 4px; background: var(--bg-elev3); border-radius: 2px; overflow: hidden; +} +.dim-card .dim-score .mini-bar-fill { + height: 100%; background: linear-gradient(90deg, var(--teal), var(--teal-c)); + transition: width .3s ease; +} + +/* Assessment question view */ +.assessment { + background: var(--bg-elev); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 24px 28px; + margin-bottom: 24px; +} +.assessment-nav { + display: flex; justify-content: space-between; align-items: center; + margin-bottom: 20px; font-size: 12px; color: var(--text-mute); +} +.assessment-nav button.linklike { + background: transparent; border: none; color: var(--teal-c); cursor: pointer; font-size: 13px; +} +.assessment-nav button.linklike:hover { text-decoration: underline; } +.assessment-progress { + height: 4px; background: var(--bg-elev3); border-radius: 99px; overflow: hidden; margin-bottom: 22px; +} +.assessment-progress-fill { height: 100%; background: linear-gradient(90deg, var(--teal), var(--teal-c)); transition: width .3s ease; } +.question-block h2 { margin: 0 0 6px; font-size: 20px; font-weight: 600; letter-spacing: -.3px; } +.question-block .question-hint { color: var(--text-mute); font-size: 13px; margin-bottom: 22px; } +.likert { + display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; + margin-bottom: 16px; +} +.likert-btn { + background: var(--bg-elev2); + border: 1px solid var(--line); + color: var(--text); padding: 14px 10px; + border-radius: 8px; cursor: pointer; transition: var(--t); + display: flex; flex-direction: column; gap: 4px; text-align: center; +} +.likert-btn:hover { border-color: var(--teal); } +.likert-btn[aria-pressed="true"] { + background: linear-gradient(135deg, rgba(8,145,178,.3), rgba(8,145,178,.1)); + border-color: var(--teal-c); color: var(--teal-c); +} +.likert-btn .likert-n { font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; } +.likert-btn .likert-t { font-size: 11px; color: var(--text-mute); } +.likert-btn[aria-pressed="true"] .likert-t { color: var(--teal-c); } + +.q-nav { display: flex; justify-content: space-between; margin-top: 10px; gap: 10px; } + +/* Radar + results */ +.radar-wrap { + background: var(--bg-elev); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 24px; + display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 30px; + align-items: center; + margin-bottom: 20px; +} +.radar-legend { display: flex; flex-direction: column; gap: 10px; } +.radar-legend .legend-row { display: flex; align-items: center; gap: 10px; font-size: 13px; } +.radar-legend .legend-swatch { width: 14px; height: 14px; border-radius: 3px; flex-shrink: 0; } +.radar-score-big { font-size: 36px; font-weight: 600; color: var(--teal-c); margin: 4px 0 2px; font-variant-numeric: tabular-nums; } +.radar-score-hint { color: var(--text-mute); font-size: 12px; margin-bottom: 16px; } + +.recos { display: grid; gap: 12px; margin-top: 14px; } +.reco-item { + display: grid; grid-template-columns: auto 1fr; gap: 14px; + background: var(--bg-elev); border: 1px solid var(--line); + border-radius: var(--radius); padding: 16px 18px; align-items: start; +} +.reco-num { width: 30px; height: 30px; border-radius: 50%; background: var(--teal-dim); color: var(--teal-c); display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 14px; } +.reco-item h4 { margin: 0 0 4px; font-size: 14px; font-weight: 600; } +.reco-item p { margin: 0 0 10px; color: var(--text-mute); font-size: 13px; } + +/* ============ AI ACT CLASSIFIER ============ */ +.wizard { + background: var(--bg-elev); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 28px; max-width: 720px; margin: 0 auto; +} +.wizard .progress-dots { + display: flex; gap: 6px; justify-content: center; margin-bottom: 22px; +} +.wizard .progress-dots span { + width: 8px; height: 8px; border-radius: 50%; background: var(--bg-elev3); transition: var(--t); +} +.wizard .progress-dots span.active { background: var(--teal-c); } +.wizard .progress-dots span.done { background: var(--teal); } + +.wizard-q h2 { margin: 0 0 8px; font-size: 22px; font-weight: 600; letter-spacing: -.3px; } +.wizard-q p { margin: 0 0 22px; color: var(--text-mute); font-size: 14px; } + +.yesno-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } +.yesno-btn { + padding: 22px; border-radius: var(--radius); + background: var(--bg-elev2); + border: 1px solid var(--line); + color: var(--text); cursor: pointer; transition: var(--t); + font-size: 16px; font-weight: 500; +} +.yesno-btn:hover { border-color: var(--teal); transform: translateY(-1px); } +.yesno-btn.yes:hover { border-color: var(--err); color: var(--err); } +.yesno-btn.no:hover { border-color: var(--teal-c); color: var(--teal-c); } + +.verdict { + padding: 28px; border-radius: var(--radius); + border: 1px solid var(--line); + background: var(--bg-elev); +} +.verdict .verdict-label { + display: inline-block; padding: 4px 12px; border-radius: 99px; + font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; + margin-bottom: 14px; +} +.verdict.prohibited { border-color: var(--err); background: rgba(239,68,68,.06); } +.verdict.prohibited .verdict-label { background: rgba(239,68,68,.18); color: var(--err); } +.verdict.high { border-color: var(--warn); background: rgba(245,158,11,.06); } +.verdict.high .verdict-label { background: rgba(245,158,11,.18); color: var(--warn); } +.verdict.limited { border-color: var(--teal); background: rgba(8,145,178,.06); } +.verdict.limited .verdict-label { background: rgba(8,145,178,.2); color: var(--teal-c); } +.verdict.minimal { border-color: var(--ok); background: rgba(16,185,129,.06); } +.verdict.minimal .verdict-label { background: rgba(16,185,129,.18); color: var(--ok); } +.verdict h2 { margin: 0 0 10px; font-size: 26px; font-weight: 600; } +.verdict p.verdict-summary { margin: 0 0 18px; color: var(--text-mute); } +.verdict .obligation-list { + list-style: none; padding: 0; margin: 0 0 22px; display: grid; gap: 10px; +} +.verdict .obligation-list li { + padding: 10px 14px; background: rgba(255,255,255,.02); + border-left: 3px solid var(--teal-c); + border-radius: 4px; font-size: 13px; color: var(--text); +} +.verdict .obligation-list li strong { color: var(--teal-c); margin-right: 8px; } +.verdict-run-counter { color: var(--text-dim); font-size: 12px; margin-top: 14px; } + +/* ============ DASHBOARD ============ */ +.dash-group { margin-bottom: 26px; } +.dash-group-title { + font-size: 12px; letter-spacing: .08em; text-transform: uppercase; + color: var(--text-dim); margin-bottom: 10px; + display: flex; align-items: center; gap: 10px; +} +.dash-group-title::after { content: ""; flex: 1; height: 1px; background: var(--line); } +.dash-grid { display: grid; gap: 14px; } +.dash-grid.exec { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); } +.dash-grid.ops { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } +.dash-grid.tech { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } +.tile { + background: var(--bg-elev); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 18px 20px; cursor: pointer; transition: var(--t); + text-align: left; display: flex; flex-direction: column; gap: 6px; + position: relative; overflow: hidden; +} +.tile:hover { border-color: rgba(34,211,238,.4); transform: translateY(-2px); box-shadow: var(--shadow); } +.tile.seen { border-color: rgba(34,211,238,.25); } +.tile.seen::before { + content: ""; position: absolute; top: 10px; right: 12px; + width: 6px; height: 6px; border-radius: 50%; background: var(--teal-c); +} +.tile.exec { padding: 22px 24px; } +.tile-label { font-size: 12px; color: var(--text-mute); letter-spacing: .02em; } +.tile-value { font-size: 26px; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; letter-spacing: -0.5px; } +.tile.exec .tile-value { font-size: 34px; } +.tile-trend { + font-size: 11px; display: flex; align-items: center; gap: 4px; + color: var(--text-mute); font-variant-numeric: tabular-nums; +} +.tile-trend.up { color: var(--ok); } +.tile-trend.down { color: var(--err); } +.tile-trend.neutral { color: var(--text-mute); } + +/* ============ MODAL ============ */ +.modal-root { + position: fixed; inset: 0; z-index: 200; + display: none; align-items: center; justify-content: center; + padding: 20px; background: rgba(0,0,0,.7); backdrop-filter: blur(6px); +} +.modal-root.open { display: flex; animation: fadeIn .2s; } +.modal { + background: var(--bg-elev); + border: 1px solid var(--line); + border-radius: 12px; + padding: 26px 28px; + max-width: 560px; width: 100%; max-height: 90vh; overflow-y: auto; + box-shadow: var(--shadow); + animation: modalIn .2s ease; +} +@keyframes modalIn { from { transform: translateY(8px); opacity: 0; } to { transform: none; opacity: 1; } } +.modal-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 14px; gap: 16px; } +.modal-head h3 { margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -.3px; } +.modal-head .modal-close { background: transparent; border: none; color: var(--text-mute); font-size: 22px; cursor: pointer; line-height: 1; } +.modal-head .modal-close:hover { color: var(--text); } +.modal-value { + font-size: 36px; font-weight: 600; color: var(--teal-c); + margin-bottom: 4px; font-variant-numeric: tabular-nums; +} +.modal-value-sub { color: var(--text-mute); font-size: 13px; margin-bottom: 18px; } +.modal p { margin: 0 0 14px; font-size: 13.5px; color: var(--text-mute); line-height: 1.5; } +.modal p strong { color: var(--text); } +.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; flex-wrap: wrap; } + +/* ============ CHAT DOCK ============ */ +.chat-dock { + border-left: 1px solid var(--line); + background: rgba(18,18,26,.6); + display: flex; flex-direction: column; + position: sticky; top: 64px; height: calc(100vh - 64px - 44px); + overflow: hidden; +} +.dock-head { + display: flex; align-items: baseline; gap: 8px; + padding: 14px 18px; border-bottom: 1px solid var(--line); + background: rgba(8,145,178,.06); +} +.dock-title { font-weight: 600; color: var(--teal-c); } +.dock-sub { font-size: 11px; color: var(--text-dim); flex: 1; } +.dock-reset, .dock-collapse { + background: transparent; border: 1px solid var(--line); + color: var(--text-mute); width: 26px; height: 26px; + border-radius: 6px; cursor: pointer; font-size: 12px; + display: flex; align-items: center; justify-content: center; + transition: var(--t); +} +.dock-reset:hover, .dock-collapse:hover { color: var(--text); border-color: var(--teal); } +.dock-box { + flex: 1; overflow-y: auto; padding: 14px; + display: flex; flex-direction: column; gap: 10px; + scrollbar-width: thin; scrollbar-color: var(--line) transparent; +} +.dock-box::-webkit-scrollbar { width: 6px; } +.dock-box::-webkit-scrollbar-thumb { background: var(--line); border-radius: 3px; } +.dock-msg { + padding: 10px 12px; border-radius: 10px; + font-size: 13px; line-height: 1.5; max-width: 95%; + word-wrap: break-word; overflow-wrap: break-word; +} +.dock-msg.user { + align-self: flex-end; + background: linear-gradient(135deg, var(--teal), var(--teal-b)); + color: #001013; + border-bottom-right-radius: 3px; +} +.dock-msg.bot { + align-self: flex-start; + background: var(--bg-elev2); + border: 1px solid var(--line); + border-bottom-left-radius: 3px; +} +.dock-msg.bot p { margin: 0 0 8px; } +.dock-msg.bot p:last-child { margin-bottom: 0; } +.dock-msg.bot ul, .dock-msg.bot ol { margin: 6px 0; padding-left: 20px; } +.dock-msg.bot li { margin-bottom: 2px; } +.dock-msg.bot code { font-size: .88em; } +.dock-msg.bot pre { background: #0d0d14; border: 1px solid var(--line-soft); padding: 10px; border-radius: 6px; overflow-x: auto; font-size: .85em; margin: 8px 0; } +.dock-msg.err { + background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.3); color: #fca5a5; +} +.dock-msg.sys { + font-size: 12px; color: var(--text-dim); + background: transparent; border: 1px dashed var(--line); align-self: center; text-align: center; +} + +.dots { display: inline-flex; gap: 4px; padding: 4px 0; } +.dots span { + width: 6px; height: 6px; border-radius: 50%; background: var(--teal-c); + animation: bounce 1.2s infinite ease-in-out; +} +.dots span:nth-child(2) { animation-delay: .15s; } +.dots span:nth-child(3) { animation-delay: .3s; } +@keyframes bounce { 0%, 80%, 100% { transform: translateY(0); opacity: .4; } 40% { transform: translateY(-4px); opacity: 1; } } + +.dock-form { + display: grid; grid-template-columns: 1fr auto; + gap: 8px; padding: 12px; border-top: 1px solid var(--line); + background: var(--bg-elev); +} +.dock-form textarea { + resize: none; min-height: 38px; max-height: 140px; + background: var(--bg-elev2); + border: 1px solid var(--line); + border-radius: 8px; color: var(--text); + padding: 9px 12px; font-family: inherit; font-size: 13px; + transition: var(--t); +} +.dock-form textarea:focus { outline: none; border-color: var(--teal-c); box-shadow: 0 0 0 3px rgba(34,211,238,.15); } +.btn-send { + background: linear-gradient(135deg, var(--teal), var(--teal-b)); + border: none; color: #001013; font-weight: 700; + width: 40px; height: 40px; border-radius: 8px; cursor: pointer; + font-size: 18px; transition: var(--t); +} +.btn-send:hover:not(:disabled) { filter: brightness(1.15); } +.btn-send:disabled { opacity: .4; cursor: not-allowed; } +.dock-footer-link { + display: block; padding: 8px 14px; font-size: 11px; color: var(--text-dim); + text-align: center; border-top: 1px solid var(--line); background: var(--bg-elev); +} +.dock-footer-link:hover { color: var(--teal-c); text-decoration: none; } + +/* Floating open button (mobile / collapsed state) */ +.dock-open { + display: none; + position: fixed; bottom: 24px; right: 24px; z-index: 60; + padding: 12px 18px; border-radius: 99px; + background: linear-gradient(135deg, var(--teal), var(--teal-b)); + color: #001013; font-weight: 600; border: none; + box-shadow: 0 8px 24px rgba(8,145,178,.5); + cursor: pointer; font-size: 14px; + align-items: center; gap: 8px; +} +.dock-open-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #001013; animation: pulse 1.4s infinite; } +@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } } +.chat-dock.collapsed { display: none; } +.chat-dock.collapsed + .dock-open { display: inline-flex; } + +/* Full chat module (⌃4) — reuses dock style but takes full width */ +.chat-full-wrap { max-width: 780px; margin: 0 auto; } +.chat-full-wrap p { color: var(--text-mute); font-size: 13px; margin-bottom: 18px; } +.chat-full-wrap .chat-full-empty { + display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 10px; margin: 16px 0 22px; +} +.chat-full-wrap .chip { + background: var(--bg-elev); border: 1px solid var(--line); + color: var(--text-mute); padding: 10px 14px; border-radius: 8px; + cursor: pointer; transition: var(--t); font-size: 13px; text-align: left; +} +.chat-full-wrap .chip:hover { border-color: var(--teal); color: var(--text); } + +/* ============ FOOTER ============ */ +.footer { + grid-area: foot; + padding: 12px 22px; + border-top: 1px solid var(--line); + text-align: center; + font-size: 12px; color: var(--text-dim); + background: var(--bg-elev); +} +.footer a { color: var(--text-mute); } + +/* ============ TOASTS ============ */ +.toast-stack { + position: fixed; top: 16px; right: 16px; z-index: 500; + display: flex; flex-direction: column; gap: 8px; pointer-events: none; +} +.toast { + padding: 10px 16px; border-radius: 8px; + background: var(--bg-elev2); border: 1px solid var(--line); + color: var(--text); font-size: 13px; min-width: 220px; + box-shadow: var(--shadow); animation: slideIn .3s ease; + pointer-events: auto; +} +.toast.success { border-color: var(--ok); color: #86efac; } +.toast.err { border-color: var(--err); color: #fca5a5; } +.toast.info { border-color: var(--teal-c); color: var(--teal-c); } +@keyframes slideIn { from { transform: translateX(20px); opacity: 0; } to { transform: none; opacity: 1; } } + +/* ============ BADGES (completion) ============ */ +.badges-row { + display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; +} +.badge-chip { + display: inline-flex; align-items: center; gap: 8px; + padding: 6px 12px; border-radius: 99px; + background: var(--bg-elev2); border: 1px solid var(--line); + font-size: 12px; color: var(--text-mute); +} +.badge-chip.earned { border-color: var(--teal-c); color: var(--teal-c); background: var(--teal-dim); } +.badge-chip .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-dim); } +.badge-chip.earned .dot { background: var(--teal-c); box-shadow: 0 0 6px var(--teal-c); } + +/* ============ RESPONSIVE ============ */ +@media (max-width: 1160px) { + .cockpit { grid-template-columns: 180px minmax(0, 1fr) 260px; --nav-w: 180px; --dock-w: 260px; } + .main { padding: 22px 24px 36px; } +} +@media (max-width: 960px) { + .cockpit { + grid-template-columns: 1fr; + grid-template-areas: "top" "nav" "main" "foot"; + } + .nav { + flex-direction: row; overflow-x: auto; padding: 12px; + border-right: none; border-bottom: 1px solid var(--line); + position: sticky; top: 70px; z-index: 30; + background: rgba(10,10,15,.95); backdrop-filter: blur(8px); + } + .nav-item { flex: 1 0 auto; grid-template-columns: 1fr; gap: 2px; text-align: center; padding: 8px 12px; } + .nav-num, .nav-kbd { display: none; } + .nav-label { font-size: 13px; } + .chat-dock { + position: fixed; top: 0; right: 0; bottom: 0; + width: 100%; max-width: 360px; + height: 100vh; z-index: 100; + box-shadow: -10px 0 40px rgba(0,0,0,.6); + transform: translateX(100%); transition: transform .3s ease; + } + .chat-dock.open { transform: translateX(0); } + .dock-open { display: inline-flex; } + .radar-wrap { grid-template-columns: 1fr; } + .yesno-row { grid-template-columns: 1fr; } + .topbar { flex-wrap: wrap; padding: 10px 14px; } + .xp-wrap { min-width: 140px; order: 3; } + .back-link { order: 2; } + .main { padding: 18px 14px 30px; } + .mod-head h1 { font-size: 22px; } +} +@media (max-width: 480px) { + .likert { grid-template-columns: repeat(5, 1fr); gap: 4px; } + .likert-btn { padding: 10px 4px; } + .likert-btn .likert-n { font-size: 16px; } + .likert-btn .likert-t { font-size: 9px; } + .brand-sub { display: none; } +} + +/* ============ A11Y ============ */ +.sr-only { + position: absolute; width: 1px; height: 1px; + padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); border: 0; +} +button:focus-visible, .nav-item:focus-visible, textarea:focus-visible, a:focus-visible { + outline: 2px solid var(--teal-c); outline-offset: 2px; +} + +/* SVG */ +.radar-svg { width: 100%; height: auto; display: block; } +.sparkline-svg { width: 100%; height: 80px; display: block; } + +/* GFM table (added 2026-04-24) */ +.md-table { + width: 100%; + border-collapse: collapse; + margin: .6rem 0; + font-size: .9em; + background: rgba(255,255,255,0.02); + border: 1px solid rgba(255,255,255,0.08); + border-radius: 6px; + overflow: hidden; +} +.md-table thead { + background: rgba(8,145,178,0.12); +} +.md-table th, +.md-table td { + padding: .5rem .7rem; + border-bottom: 1px solid rgba(255,255,255,0.06); + text-align: left; + vertical-align: top; +} +.md-table th { + color: #06b6d4; + font-weight: 600; + font-size: .78em; + letter-spacing: .04em; + 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; } +.chat-dock .md-table, +.dock-body .md-table { + display: block; + overflow-x: auto; + font-size: .82em; +} diff --git a/src/cockpit-overlay/cockpit.js b/src/cockpit-overlay/cockpit.js new file mode 100644 index 0000000..ae37090 --- /dev/null +++ b/src/cockpit-overlay/cockpit.js @@ -0,0 +1,1249 @@ +/* Kai Cockpit — vanilla JS, no framework, no build, no CDN. + * Shares XP/Level/Streak state with the Kai widget via localStorage key `kai.state.v1`. + * Cockpit-specific keys: kai.cockpit.reifegrad, kai.cockpit.aiact, kai.cockpit.dashboard, kai.cockpit.chat + */ +(() => { + 'use strict'; + + // ========= CONFIG ========= + const API = 'https://llm.qognio.com/api/bots/ki-kennzahlen-coach/chat'; + const RAW_KEY = window.__KAI_KEY__ || ''; + const KEY = /^qb_[a-zA-Z0-9]{6,}$/.test(RAW_KEY) ? RAW_KEY : ''; + + // Shared with widget + const LS_STATE = 'kai.state.v1'; + // Cockpit-specific + const LS_REIFE = 'kai.cockpit.reifegrad'; + const LS_AIACT = 'kai.cockpit.aiact'; + const LS_DASH = 'kai.cockpit.dashboard'; + const LS_CHAT = 'kai.cockpit.chat'; + + // ========= UTIL ========= + const $ = (s, r = document) => r.querySelector(s); + const $$ = (s, r = document) => Array.from(r.querySelectorAll(s)); + const today = () => { + const d = new Date(); + return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; + }; + const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + + // ========= SHARED STATE (with widget) ========= + function loadState() { + try { + const s = JSON.parse(localStorage.getItem(LS_STATE) || '{}'); + return Object.assign({ + 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: [] + }, s); + } catch (e) { + return { xp: 0, badges: {} }; + } + } + function saveState() { localStorage.setItem(LS_STATE, JSON.stringify(state)); } + let state = loadState(); + + function addXP(n, reason = '') { + state.xp = (state.xp || 0) + n; + saveState(); + toast(`+${n} XP${reason ? ' · ' + reason : ''}`, 'info', 2200); + renderXP(); + } + + const LEVELS = [ + { min: 0, title: 'AI-Trainee' }, + { min: 50, title: 'AI-Analyst:in' }, + { min: 200, title: 'ML-Engineer' }, + { min: 500, title: 'AI-Lead' }, + { min: 1250, title: 'AI-Officer' }, + { min: 2500, title: 'AI-Governance-Lead' }, + { min: 5000, title: 'Chief AI Officer' } + ]; + function levelInfo() { + let cur = LEVELS[0]; + for (const l of LEVELS) if ((state.xp || 0) >= 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 }; + } + function renderXP() { + const li = levelInfo(); + const lvl = $('#xp-level'); const xp = $('#xp-score'); const bar = $('#xp-bar-fill'); + if (lvl) lvl.textContent = `Lvl ${li.levelNum} · ${li.title}`; + if (xp) xp.textContent = `${state.xp || 0} XP`; + if (bar) bar.style.width = li.pct + '%'; + } + + // ========= BADGES ========= + const COCKPIT_BADGES = { + cockpit_reifegrad_done: 'Reifegrad-Profi', + cockpit_ai_act_done: 'AI-Act-Navigator (3 Systeme)', + cockpit_dashboard_explored: 'Dashboard-Flaneur:in', + cockpit_power_user: 'Cockpit-Power-User' + }; + function unlockBadge(id) { + if (state.badges[id]) return false; + state.badges[id] = today(); + saveState(); + const title = COCKPIT_BADGES[id] || id; + toast('🏆 Neues Abzeichen: ' + title, 'success', 4200); + // Power-user check + if (id !== 'cockpit_power_user') checkPowerUser(); + return true; + } + function checkPowerUser() { + if (state.badges.cockpit_reifegrad_done + && state.badges.cockpit_ai_act_done + && state.badges.cockpit_dashboard_explored) { + unlockBadge('cockpit_power_user'); + } + } + + // ========= TOAST ========= + function toast(msg, kind = '', ms = 3000) { + const stack = $('#toast-stack'); if (!stack) return; + const el = document.createElement('div'); + el.className = 'toast ' + kind; + el.textContent = msg; + stack.appendChild(el); + setTimeout(() => { el.style.transition = 'opacity .3s'; el.style.opacity = '0'; setTimeout(() => el.remove(), 300); }, ms); + } + + // ========= MARKDOWN (light + GFM tables) ========= + function renderMD(src) { + let t = esc(src); + // code fences + t = t.replace(/```([\s\S]*?)```/g, (_, c) => `
${c}
`); + // inline code + t = t.replace(/`([^`]+)`/g, '$1'); + // GFM tables — consume before line-based processing + t = t.replace(/(?:^|\n)((?:\|[^\n]*\|[ \t]*\n){2,})/g, (block, content) => { + const ls = content.trim().split('\n'); + if (ls.length < 2) return block; + const sep = ls[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(ls[0]); + const aligns = parseRow(sep).map(s => /^:-+:$/.test(s) ? 'center' : /-+:$/.test(s) ? 'right' : 'left'); + const rows = ls.slice(2).map(parseRow); + let h = '\n'; + header.forEach((hd, i) => { h += ``; }); + h += ''; + rows.forEach(r => { + h += ''; + for (let i = 0; i < Math.max(r.length, header.length); i++) { + h += ``; + } + h += ''; + }); + h += '
${hd}
${r[i] || ''}
\n'; + return h; + }); + // bold + italic + t = t.replace(/\*\*([^\*]+)\*\*/g, '$1'); + t = t.replace(/\*([^\*]+)\*/g, '$1'); + // headings (very lightweight) + t = t.replace(/^###\s+(.*)$/gm, '$1'); + t = t.replace(/^##\s+(.*)$/gm, '$1'); + t = t.replace(/^#\s+(.*)$/gm, '$1'); + // bullets + paragraphs (skip inside tables/pre) + const lines = t.split('\n'); const out = []; let inUL = false; let inSkip = false; + for (const l of lines) { + // Skip line-processing while inside or
 blocks
+      if (/'); inUL = false; }
+        out.push(l);
+        if (/<\/table>/.test(l) || /<\/pre>/.test(l)) inSkip = false;
+        continue;
+      }
+      if (/^[-*]\s+/.test(l)) {
+        if (!inUL) { out.push('
    '); inUL = true; } + out.push('
  • ' + l.replace(/^[-*]\s+/, '') + '
  • '); + } else { + if (inUL) { out.push('
'); inUL = false; } + if (l.trim()) out.push('

' + l + '

'); + } + } + if (inUL) out.push(''); + return out.join(''); + } + + // ===================================================================== + // MODULE 1 — REIFEGRAD ASSESSMENT + // ===================================================================== + const DIMENSIONS = [ + { + id: 'strategy', + title: 'Strategie', + short: 'Wo steht KI in eurer Unternehmens-Strategie?', + questions: [ + 'Hat euer Unternehmen eine dokumentierte KI-Strategie mit Executive-Sponsorship?', + 'Sind AI-Use-Cases nach Business-Value priorisiert und in einem Portfolio gelistet?', + 'Gibt es ein klares Budget-Committment für AI über 12 Monate hinaus?', + 'Ist AI Teil der Digital- oder Geschäftsstrategie (nicht nur IT-Plan)?', + 'Wird der ROI von AI-Initiativen auf Portfolio-Ebene an den Vorstand reported?' + ] + }, + { + id: 'data', + title: 'Daten', + short: 'Wie solide ist euer Daten-Fundament?', + questions: [ + 'Existiert ein zentraler Data-Catalog mit Lineage für relevante Datensätze?', + 'Wird Data-Quality (Completeness, Accuracy, Freshness) systematisch gemessen?', + 'Habt ihr dokumentierte Data-Governance-Rollen (Data-Owner, Steward)?', + 'Ist Master-Data für Kunden/Produkte/Mitarbeitende harmonisiert?', + 'Sind Trainingsdaten versioniert und auf Bias/Verzerrungen gescreent (Art. 10 AI Act)?' + ] + }, + { + id: 'technology', + title: 'Technologie', + short: 'MLOps, Registry, Monitoring — wie tief ist euer Stack?', + questions: [ + 'Habt ihr eine MLOps-Plattform (CI/CD für ML, nicht nur Ad-hoc-Notebooks)?', + 'Gibt es eine zentrale Model-Registry mit Versionierung und Approval-Flow?', + 'Sind Inferenz-Pipelines auf Latenz, Throughput und Error-Rate beobachtet?', + 'Läuft automatisches Drift-Monitoring (Daten- oder Modell-Drift) in Produktion?', + 'Ist eure Inferenz-Infrastruktur skalierbar (Cloud/On-Prem/Hybrid) dokumentiert?' + ] + }, + { + id: 'people', + title: 'Menschen', + short: 'Skills, Rollen, AI-Literacy.', + questions: [ + 'Habt ihr einen AI-/ML-Engineer(in) oder ein Data-Science-Team (intern)?', + 'Gibt es AI-Literacy-Training für Non-Tech-Mitarbeitende (Art. 4 AI Act)?', + 'Existieren klare Rollen wie CAIO / AI-Risk-Officer / Data-Owner?', + 'Wird externe Expertise (Consulting, Fachanwalt IT-Recht) strukturiert eingebunden?', + 'Wird AI-Kompetenz in Job-Profile und Hiring-Prozesse integriert?' + ] + }, + { + id: 'process', + title: 'Prozesse', + short: 'Deployment, Monitoring, Incident-Response.', + questions: [ + 'Habt ihr standardisierte Deployment-Prozesse (Design → Train → Deploy → Retire)?', + 'Existiert ein AI-Incident-Response-Plan inkl. Art. 73 AI-Act-Meldewege?', + 'Gibt es ein jährliches AI-Program-Review mit Vorstand oder Geschäftsführung?', + 'Wurden FRIA (Art. 27) / DSFA (Art. 35 DSGVO) für relevante Systeme durchgeführt?', + 'Ist EU AI Act-Klassifikation für ALLE produktiven Systeme abgeschlossen?' + ] + } + ]; + + // Scale: 0 nicht vorhanden / 1 ad-hoc / 2 geplant / 3 etabliert / 4 optimiert + const LIKERT = [ + { v: 0, t: 'nicht vorhanden' }, + { v: 1, t: 'ad-hoc' }, + { v: 2, t: 'geplant' }, + { v: 3, t: 'etabliert' }, + { v: 4, t: 'optimiert' } + ]; + const BENCHMARK = { strategy: 2.1, data: 1.8, technology: 2.3, people: 1.6, process: 1.9 }; + + function loadReifegrad() { + try { return JSON.parse(localStorage.getItem(LS_REIFE) || '{}'); } catch (e) { return {}; } + } + function saveReifegrad(data) { localStorage.setItem(LS_REIFE, JSON.stringify(data)); } + + const reifeState = { + view: 'overview', // 'overview' | 'assess' | 'result' + currentDim: null, + currentQ: 0, + answers: loadReifegrad() // { dimId: [v,v,v,v,v] } + }; + + function reifeScore(dimId) { + const arr = reifeState.answers[dimId]; + if (!arr || arr.length !== 5 || arr.some(v => v == null)) return null; + return arr.reduce((s, v) => s + v, 0) / arr.length; + } + function reifeDimDone(dimId) { + const s = reifeScore(dimId); + return s != null; + } + function reifeCountDone() { return DIMENSIONS.filter(d => reifeDimDone(d.id)).length; } + function reifeOverallScore() { + const scores = DIMENSIONS.map(d => reifeScore(d.id)).filter(v => v != null); + if (!scores.length) return 0; + return scores.reduce((s, v) => s + v, 0) / scores.length; + } + + function renderReifegradModule() { + const host = $('#reifegrad-host'); + if (reifeState.view === 'overview') return renderReifeOverview(host); + if (reifeState.view === 'assess') return renderReifeAssessment(host); + if (reifeState.view === 'result') return renderReifeResult(host); + } + + function renderReifeOverview(host) { + const done = reifeCountDone(); + const overall = reifeOverallScore(); + host.innerHTML = ` +
+ Modul 01 +

Reifegrad-Assessment

+

5-Dimensionen-Self-Assessment auf Basis von Gartner + MIT CISR + Microsoft RAI-MM. Pro Dimension 5 Fragen, 0–4 Likert. Ergibt einen Radar gegen den DACH-Mittelstand-Durchschnitt (BITKOM/Fraunhofer 2024).

+
+
+
+ ${done}/5 Dimensionen beantwortet + ${overall.toFixed(2)} / 4.00 Overall-Score + ${done === 5 ? 'komplett' : 'in Arbeit'} +
+
+
+ ${DIMENSIONS.map(d => { + const s = reifeScore(d.id); + const pct = s != null ? (s / 4) * 100 : 0; + const done = s != null; + return ` + + `; + }).join('')} +
+
+ + ${done === 5 ? `` : ''} + ${done > 0 ? `` : ''} +
+ ${done === 5 ? renderBadgeRow() : ''} + `; + // Events + $$('.dim-card', host).forEach(el => el.addEventListener('click', () => { + reifeState.currentDim = el.dataset.dim; + reifeState.currentQ = 0; + reifeState.view = 'assess'; + renderReifegradModule(); + })); + const startBtn = $('#reife-start', host); + if (startBtn) startBtn.addEventListener('click', () => { + if (done === 5) { reifeState.view = 'result'; renderReifegradModule(); return; } + // find first un-done dim + const next = DIMENSIONS.find(d => !reifeDimDone(d.id)) || DIMENSIONS[0]; + reifeState.currentDim = next.id; + reifeState.currentQ = 0; + reifeState.view = 'assess'; + renderReifegradModule(); + }); + const resBtn = $('#reife-result', host); + if (resBtn) resBtn.addEventListener('click', () => { reifeState.view = 'result'; renderReifegradModule(); }); + const resetBtn = $('#reife-reset', host); + if (resetBtn) resetBtn.addEventListener('click', () => { + if (confirm('Alle Antworten löschen?')) { + reifeState.answers = {}; + saveReifegrad({}); + renderReifegradModule(); + } + }); + } + + function renderBadgeRow() { + const ids = Object.keys(COCKPIT_BADGES); + return `
+ ${ids.map(id => { + const earned = !!state.badges[id]; + return `${esc(COCKPIT_BADGES[id])}`; + }).join('')} +
`; + } + + function renderReifeAssessment(host) { + const dim = DIMENSIONS.find(d => d.id === reifeState.currentDim); + if (!dim) { reifeState.view = 'overview'; return renderReifegradModule(); } + const qi = reifeState.currentQ; + const q = dim.questions[qi]; + const answers = reifeState.answers[dim.id] || Array(5).fill(null); + const selected = answers[qi]; + const total = dim.questions.length; + const pct = ((qi) / total) * 100; + + host.innerHTML = ` +
+ Dimension · ${esc(dim.title)} +

${esc(dim.title)}

+

${esc(dim.short)}

+
+
+
+ + Frage ${qi + 1} / ${total} +
+
+
+

${esc(q)}

+

Skala: 0 = nicht vorhanden, 4 = optimiert & firmenweit verankert

+
+ ${LIKERT.map(l => ` + + `).join('')} +
+
+ + +
+
+
+
+ +
+ `; + $('#reife-back', host).addEventListener('click', () => { reifeState.view = 'overview'; renderReifegradModule(); }); + $$('.likert-btn', host).forEach(b => b.addEventListener('click', () => { + const v = parseInt(b.dataset.v, 10); + if (!reifeState.answers[dim.id]) reifeState.answers[dim.id] = Array(5).fill(null); + const wasEmpty = reifeState.answers[dim.id][qi] == null; + reifeState.answers[dim.id][qi] = v; + saveReifegrad(reifeState.answers); + if (wasEmpty) addXP(2, 'Antwort'); + renderReifeAssessment(host); + })); + const prevBtn = $('#q-prev', host); + if (prevBtn) prevBtn.addEventListener('click', () => { reifeState.currentQ = Math.max(0, qi - 1); renderReifeAssessment(host); }); + const nextBtn = $('#q-next', host); + if (nextBtn) nextBtn.addEventListener('click', () => { + if (qi < total - 1) { + reifeState.currentQ = qi + 1; + renderReifeAssessment(host); + } else { + // dim completed + const justCompleted = reifeDimDone(dim.id); + const prevDoneCount = DIMENSIONS.filter(d => d.id !== dim.id && reifeDimDone(d.id)).length; + if (justCompleted) { + // +20 per dim (first completion only) handled by checking badges flag per dim + const key = `reife_dim_${dim.id}`; + if (!state.badges[key]) { + state.badges[key] = today(); + saveState(); + addXP(20, `Dimension ${dim.title} komplett`); + } + } + const allDone = reifeCountDone() === 5; + if (allDone && !state.badges.cockpit_reifegrad_done) { + addXP(50, 'Reifegrad-Assessment abgeschlossen'); + unlockBadge('cockpit_reifegrad_done'); + } + reifeState.view = allDone ? 'result' : 'overview'; + renderReifegradModule(); + } + }); + $('#ask-kai-dim', host).addEventListener('click', () => { + openDock(); + const prompt = `Was sind die wichtigsten Hebel, um in der Dimension "${dim.title}" vom DACH-Mittelstand (Durchschnitt ${BENCHMARK[dim.id].toFixed(1)}) auf Stufe 3 (etabliert) zu kommen?`; + fillDockInput(prompt); + }); + } + + function renderReifeResult(host) { + const scores = DIMENSIONS.map(d => ({ dim: d, score: reifeScore(d.id) || 0 })); + const overall = reifeOverallScore(); + // Find 3 weakest for recommendations + const weakest = [...scores].sort((a, b) => a.score - b.score).slice(0, 3); + + host.innerHTML = ` +
+ Ergebnis +

Dein Reifegrad-Profil

+

Radar gegen DACH-Mittelstand (BITKOM 2024: ~Gartner-Stufe 2 Mittelwert). Die 3 schwächsten Achsen sind deine Roadmap.

+
+
+
${renderRadarSVG(scores)}
+
+
${overall.toFixed(2)} / 4.00
+
${overallLabel(overall)}
+
+
Deine Werte
+
DACH-Mittelstand Ø
+
+
+ + +
+
+
+

Top-Empfehlungen

+
+ ${weakest.map((w, i) => { + const reco = weakestReco(w.dim.id, w.score); + return ` +
+
${i + 1}
+
+

${esc(w.dim.title)} · ${w.score.toFixed(1)} / 4.0

+

${esc(reco)}

+ +
+
+ `; + }).join('')} +
+ ${renderBadgeRow()} + `; + $('#reife-overview-btn', host).addEventListener('click', () => { reifeState.view = 'overview'; renderReifegradModule(); }); + $('#ask-kai-overall', host).addEventListener('click', () => { + openDock(); + const txt = `Mein Reifegrad-Assessment ergab: ${scores.map(s => `${s.dim.title} ${s.score.toFixed(1)}`).join(', ')}. Gesamt ${overall.toFixed(2)}/4.0. Erstelle mir eine priorisierte 12-Monats-Roadmap für die schwächsten 3 Dimensionen.`; + fillDockInput(txt); + }); + $$('[data-dim]', host).forEach(b => { + if (b.classList.contains('btn-ask-kai')) { + b.addEventListener('click', () => { + const d = DIMENSIONS.find(dd => dd.id === b.dataset.dim); + if (!d) return; + openDock(); + fillDockInput(`Meine Dimension "${d.title}" liegt bei ${(reifeScore(d.id) || 0).toFixed(1)} / 4.0 (DACH-Ø ${BENCHMARK[d.id]}). Was sind die 3 wichtigsten Maßnahmen in den nächsten 90 Tagen?`); + }); + } + }); + } + + function overallLabel(s) { + if (s < 1) return 'Foundational / Experimenting — Start beim AI-Strategie-Dokument + Sponsorship.'; + if (s < 2) return 'Emerging / Piloting — 3-5 Pilots mit klaren Business-Cases als Fokus.'; + if (s < 2.8) return 'Operational / Scaling — der kritische Sprung. MLOps + Monitoring konsolidieren.'; + if (s < 3.5) return 'Scaled — Platform + AI-Engineering-Team in Produktion.'; + return 'Transformational / Optimizing — AI ist Teil des Geschäftsmodells.'; + } + + function weakestReco(dimId, score) { + const low = { + strategy: 'AI-Charter in 2-3 Seiten schreiben, vom CEO unterschreiben lassen. Quartals-Review einführen. Use-Case-Intake-Template verpflichtend ab Tag 1.', + data: 'Data-Catalog aktivieren (Open-Source-Stack reicht initial). Data-Quality-Scorecard für Top-5-Datensätze. Data-Owner benennen.', + technology: 'MLOps auf 1 Pattern einkochen (z.B. Training + Registry + Serving). Modell-Drift-Monitoring als Muss vor jedem Go-Live.', + people: 'AI-Literacy-Training für alle Mitarbeitenden (Art. 4 AI-Act-Pflicht). AI-Risk-Officer benennen — reicht initial als 20%-Rolle.', + process: 'Model-Lifecycle-Standard auf 1 Seite. EU-AI-Act-Klassifikation für alle Systeme durchführen. Incident-Response-Plan einmal durchspielen.' + }; + const mid = { + strategy: 'Portfolio-Review quartalsweise: Welcher Use-Case wirft ROI? Welcher wird retiret? Business-Case-Templates standardisieren.', + data: 'Data-Lineage end-to-end (nicht nur Katalog-Eintrag). Trainingsdaten auf Bias screenen (Art. 10 AI-Act-Pflicht für High-Risk).', + technology: 'Von Ad-hoc-Monitoring zu Self-Service-Observability. Model-Registry mit Approval-Workflow. Automated Retrain-Pipelines.', + people: 'CAIO oder AI-Risk-Officer in Vollzeit. Hiring-Profil überarbeiten. Externe Fachanwalt-IT-Recht als Retainer.', + process: 'FRIA + DSFA kombiniert durchführen. Jährlicher Program-Review mit Vorstand. ISO 42001 als Rahmen adaptieren.' + }; + return score < 2 ? low[dimId] : mid[dimId]; + } + + // ========= RADAR SVG ========= + function renderRadarSVG(scoresArr) { + const size = 420; const cx = size / 2; const cy = size / 2; + const radius = 150; const rings = 4; // 0-4 scale, 4 rings + const n = scoresArr.length; + const angleFor = (i) => (-Math.PI / 2) + (i * 2 * Math.PI / n); + + const pointAt = (i, val) => { + const a = angleFor(i); + const r = (val / 4) * radius; + return [cx + r * Math.cos(a), cy + r * Math.sin(a)]; + }; + + // Grid + let grid = ''; + for (let r = 1; r <= rings; r++) { + const pts = []; + for (let i = 0; i < n; i++) { + const a = angleFor(i); + const rr = (r / rings) * radius; + pts.push([cx + rr * Math.cos(a), cy + rr * Math.sin(a)]); + } + grid += ``; + } + + // Axes + labels + let axes = ''; let labels = ''; + for (let i = 0; i < n; i++) { + const [x, y] = pointAt(i, 4); + axes += ``; + const a = angleFor(i); + const lx = cx + (radius + 26) * Math.cos(a); + const ly = cy + (radius + 26) * Math.sin(a); + labels += `${esc(scoresArr[i].dim.title)}`; + } + + // Benchmark polygon + const benchPts = scoresArr.map((s, i) => pointAt(i, BENCHMARK[s.dim.id] || 2)); + const bench = ``; + + // User polygon + const userPts = scoresArr.map((s, i) => pointAt(i, s.score)); + const user = ``; + const dots = scoresArr.map((s, i) => { + const [x, y] = pointAt(i, s.score); + return ``; + }).join(''); + + return ` + ${grid} + ${axes} + ${bench} + ${user} + ${dots} + ${labels} + `; + } + + // ===================================================================== + // MODULE 2 — EU AI ACT CLASSIFIER + // ===================================================================== + // Simple decision tree — 7 yes/no questions, returns verdict object + const AIACT_TREE = [ + { + id: 'q1', key: 'prohibited', + q: 'Wird das System für Social-Scoring, Massenüberwachung, unterschwellige Manipulation oder Emotion-Recognition am Arbeitsplatz / in der Schule eingesetzt?', + hint: 'Art. 5 AI Act — „Prohibited Practices" gelten seit Februar 2025.', + yes: { verdict: 'prohibited' }, + no: 'q2' + }, + { + id: 'q2', key: 'safety_component', + q: 'Ist das System Sicherheitskomponente eines regulierten Produkts (Medizin-Device, Maschine, Fahrzeug, kritische Infrastruktur, Spielzeug)?', + hint: 'Art. 6 Abs. 1 — Weg A zu High-Risk via Anhang I.', + yes: { verdict: 'high', reason: 'safety_component' }, + no: 'q3' + }, + { + id: 'q3', key: 'annex_iii', + q: 'Wird das System für HR-Entscheidungen, Recruiting, Kredit-/Versicherungs-Scoring, Strafverfolgung, Bildungsbewertung, Migration oder Biometrie eingesetzt?', + hint: 'Anhang III Kategorien 1, 3, 4, 5, 6, 7. Deutsche Mittelständler unterschätzen HR-AI oft.', + yes: { verdict: 'high', reason: 'annex_iii' }, + no: 'q4' + }, + { + id: 'q4', key: 'narrow_task', + q: 'Erfüllt das System nur eine schmale, vorbereitende Aufgabe (z. B. reine Duplikat-Erkennung, Mustererkennung ohne Profiling)?', + hint: 'Art. 6 Abs. 3 — Opt-out aus High-Risk, muss dokumentiert werden.', + yes: { verdict: 'limited', reason: 'narrow_task_opt_out' }, + no: 'q5' + }, + { + id: 'q5', key: 'chatbot', + q: 'Ist es ein Chatbot, ein Deepfake-/Synthetic-Media-Generator oder interagiert direkt mit Endnutzer:innen als AI?', + hint: 'Art. 50 — Transparenzpflicht: „Du sprichst mit einer KI."', + yes: { verdict: 'limited', reason: 'chatbot' }, + no: 'q6' + }, + { + id: 'q6', key: 'pii', + q: 'Werden personenbezogene Daten verarbeitet oder zum Training verwendet?', + hint: 'DSGVO greift zusätzlich — Art. 10 AI Act + Art. 5 DSGVO synchron behandeln.', + yes: { verdict: 'minimal', reason: 'dsgvo_pflicht' }, + no: 'q7' + }, + { + id: 'q7', key: 'gpai', + q: 'Ist es ein General-Purpose-AI-Modell oder wird ein solches direkt in eurer Kontrolle trainiert (Foundation-Model mit > 10^25 FLOPs)?', + hint: 'Art. 51-56 GPAI-Regeln — für die meisten Deployer nicht relevant.', + yes: { verdict: 'minimal', reason: 'gpai_deployer' }, + no: { verdict: 'minimal', reason: 'default' } + } + ]; + + const aiactState = { + step: 0, + answers: {}, + verdict: null, + runs: (function() { try { return JSON.parse(localStorage.getItem(LS_AIACT) || '{}').runs || []; } catch (e) { return []; } })() + }; + function saveAIActRuns() { + localStorage.setItem(LS_AIACT, JSON.stringify({ runs: aiactState.runs })); + } + + const VERDICTS = { + prohibited: { + title: 'Verboten (Prohibited Practice)', + label: 'Art. 5 AI Act', + cls: 'prohibited', + summary: 'Dein System fällt unter Art. 5 und ist seit dem 2. Februar 2025 in der EU verboten. Weiterbetrieb ist keine Option.', + obligations: [ + 'Art. 5: Unverzüglicher Stopp und Abbau', + 'Art. 99: Strafen bis zu 35 Mio. EUR oder 7 % weltweiter Jahresumsatz (höher)', + 'Legal-Review + Information der Aufsichtsbehörde prüfen', + 'Alternative Implementierung ohne Art-5-Praktik entwerfen' + ] + }, + high: { + title: 'High-Risk', + label: 'Art. 6 + Anhang III', + cls: 'high', + summary: 'Umfangreiche Pflichten. Für Deployer + Provider unterschiedlich. Vollständige Anwendung ab 2. August 2026.', + obligations: [ + 'Art. 9: Risk-Management-System etablieren', + 'Art. 10: Data-Governance — Training/Validation/Test dokumentieren, Bias-Screening', + 'Art. 11 + Anhang IV: Technical Documentation', + 'Art. 13: Transparency — User-Instructions für Deployer', + 'Art. 14: Human Oversight — dokumentierte Eingriffsmöglichkeit', + 'Art. 15: Accuracy, Robustness, Cybersecurity', + 'Art. 26: Deployer-Pflichten (Monitoring, Logs, Information)', + 'Art. 27: Fundamental Rights Impact Assessment (FRIA) — kombinierbar mit DSFA', + 'Art. 43: Conformity Assessment + CE-Kennzeichnung', + 'Art. 49: Registrierung in EU-Datenbank' + ] + }, + limited: { + title: 'Limited Risk', + label: 'Art. 50', + cls: 'limited', + summary: 'Transparenzpflichten. Kein CE-Zwang, aber Hinweispflicht gegenüber Nutzer:innen.', + obligations: [ + 'Art. 50 Abs. 1: Kennzeichnung — "Du interagierst mit einer KI"', + 'Art. 50 Abs. 2: Synthetic-Content maschinenlesbar markieren', + 'Art. 50 Abs. 4: Deepfakes offenlegen', + 'DSGVO Art. 13/14: Informationspflichten, wenn personenbezogene Daten verarbeitet', + 'Art. 4 (AI-Literacy) — gilt für ALLE AI-Nutzungen, unabhängig von Klasse' + ] + }, + minimal: { + title: 'Minimal Risk', + label: 'Freiwillig + Art. 4', + cls: 'minimal', + summary: 'Keine spezifischen AI-Act-Pflichten außer AI-Literacy (Art. 4). DSGVO gilt weiterhin bei personenbezogenen Daten.', + obligations: [ + 'Art. 4: AI-Literacy-Training für Mitarbeitende (seit Feb 2025)', + 'Freiwillige Codes of Conduct empfohlen (Art. 95)', + 'DSGVO Art. 5/6: Rechtmäßigkeitsgrundlage + Grundsätze prüfen', + 'Art. 35 DSGVO: DSFA bei hohem Risiko für Betroffene' + ] + } + }; + + function renderAIActModule() { + const host = $('#aiact-host'); + if (aiactState.verdict) return renderAIActVerdict(host); + renderAIActQuestion(host); + } + + function renderAIActQuestion(host) { + const step = aiactState.step; + const node = AIACT_TREE[step]; + const totalSteps = AIACT_TREE.length; + + host.innerHTML = ` +
+ Modul 02 +

EU AI Act — Risk-Classifier

+

Entscheidungsbaum in einfacher Sprache. Am Ende: Risikoklasse + konkrete Pflichten mit Artikel-Bezug. Keine Rechtsberatung — für die konkrete Umsetzung mit IT-Recht-Fachanwalt sprechen.

+
+
+
+ ${AIACT_TREE.map((_, i) => ``).join('')} +
+
+

${esc(node.q)}

+

${esc(node.hint)}

+
+ + +
+
+ +
+
+
+ ${aiactState.runs.length > 0 ? ` +
+ Bereits ${aiactState.runs.length} Systeme klassifiziert. + ${aiactState.runs.length >= 3 && !state.badges.cockpit_ai_act_done ? '' : ''} +
+ ` : ''} + `; + $$('[data-ans]', host).forEach(btn => btn.addEventListener('click', () => { + const ans = btn.dataset.ans; + aiactState.answers[node.key] = ans; + const branch = node[ans]; + if (branch && typeof branch === 'object' && branch.verdict) { + aiactState.verdict = { kind: branch.verdict, reason: branch.reason || null }; + // record + aiactState.runs.push({ ts: Date.now(), verdict: branch.verdict, answers: { ...aiactState.answers } }); + // cap stored runs at 10 + aiactState.runs = aiactState.runs.slice(-10); + saveAIActRuns(); + addXP(30, 'AI-Act-Classifier Run'); + if (aiactState.runs.length >= 3 && !state.badges.cockpit_ai_act_done) { + unlockBadge('cockpit_ai_act_done'); + } + renderAIActModule(); + } else if (typeof branch === 'string') { + // find next step + const nextIdx = AIACT_TREE.findIndex(n => n.id === branch); + aiactState.step = nextIdx !== -1 ? nextIdx : step + 1; + renderAIActModule(); + } else { + // default — move on + aiactState.step = step + 1; + renderAIActModule(); + } + })); + $('#aiact-restart', host).addEventListener('click', () => { + aiactState.step = 0; + aiactState.answers = {}; + aiactState.verdict = null; + renderAIActModule(); + }); + } + + function renderAIActVerdict(host) { + const v = VERDICTS[aiactState.verdict.kind]; + const reason = aiactState.verdict.reason; + host.innerHTML = ` +
+ Ergebnis +

Klassifikation

+

Basierend auf deinen Antworten. Speichere das Ergebnis, druck's aus, zeig's dem Fachanwalt.

+
+
+ ${esc(v.label)} +

${esc(v.title)}

+

${esc(v.summary)}

+

Konkrete Pflichten & Artikel

+
    + ${v.obligations.map(o => { + const parts = o.split(':'); + if (parts.length > 1) return `
  • ${esc(parts[0])}:${esc(parts.slice(1).join(':'))}
  • `; + return `
  • ${esc(o)}
  • `; + }).join('')} +
+
+ + +
+
Runs insgesamt: ${aiactState.runs.length} / für „AI-Act-Navigator"-Badge 3 verschiedene Systeme
+
+ `; + $('#aiact-another', host).addEventListener('click', () => { + aiactState.step = 0; + aiactState.answers = {}; + aiactState.verdict = null; + renderAIActModule(); + }); + $('#aiact-ask-kai', host).addEventListener('click', () => { + openDock(); + const p = `Mein System wurde als "${v.title}" (${v.label}) klassifiziert. Was sind die 5 häufigsten Umsetzungsfehler in dieser Klasse, die ich vermeiden soll?`; + fillDockInput(p); + }); + } + + // ===================================================================== + // MODULE 3 — KPI DASHBOARD (demo) + // ===================================================================== + const TILES = [ + // Executive + { id: 'adoption', group: 'exec', label: 'Adoption Rate', value: '73%', trend: '+4% vs. Vormonat', trendKind: 'up', + explain: 'Anteil der anspruchsberechtigten User:innen, die das AI-System im letzten Monat aktiv genutzt haben. Proxy für Change-Success.', + series: [54, 58, 62, 64, 67, 70, 73] }, + { id: 'savings', group: 'exec', label: 'Monthly Cost Savings', value: '€ 38.400', trend: '+€3.2k vs. Vormonat', trendKind: 'up', + explain: 'Eingesparte operative Kosten durch AI-Automatisierung (Service-Deflection, Process-Shortening). Teil des Business-KPI-Sets (Modul 02).', + series: [22000, 26500, 29000, 30500, 33000, 35200, 38400] }, + { id: 'trust', group: 'exec', label: 'Trust Score', value: '4.1 / 5', trend: '+0.2 vs. Q-1', trendKind: 'up', + explain: 'User-Survey NPS-ähnlich: „Vertrauen Sie den AI-Antworten?". Zielwert > 4.0 für Skalierung.', + series: [3.5, 3.6, 3.7, 3.8, 3.9, 4.0, 4.1] }, + // Operational + { id: 'latency', group: 'ops', label: 'P95 Latency', value: '182 ms', trend: '-18ms vs. Vorwoche', trendKind: 'up', + explain: 'P95-Response-Time Ende-zu-Ende. Ziel < 300ms für konversationelle UX. Hinweis: nicht der Mittelwert — der P95 zählt.', + series: [210, 205, 200, 195, 190, 185, 182] }, + { id: 'throughput', group: 'ops', label: 'Throughput', value: '840 req/s', trend: '+12 req/s', trendKind: 'up', + explain: 'Peak-Requests pro Sekunde. Kapazitätsplanung + Kostenschätzung pro Inferenz.', + series: [720, 750, 760, 785, 800, 820, 840] }, + { id: 'errors', group: 'ops', label: 'Error Rate', value: '0.8%', trend: '-0.1% vs. Vorwoche', trendKind: 'up', + explain: 'Fehlgeschlagene Requests (5xx + Fachlich-Invalid). Ziel < 1%. Alarm bei > 2%.', + series: [1.2, 1.1, 1.0, 0.95, 0.9, 0.85, 0.8] }, + { id: 'availability', group: 'ops', label: 'Availability', value: '99.92%', trend: 'SLO ≥ 99.9%', trendKind: 'neutral', + explain: 'Uptime des AI-Service. Typisches SLO 99.9% (= ~43min Downtime/Monat). Tracking pro Region + Gesamt.', + series: [99.85, 99.87, 99.9, 99.92, 99.91, 99.92, 99.92] }, + // Technical + { id: 'accuracy', group: 'tech', label: 'Model Accuracy', value: '94.3%', trend: '+0.4% vs. Baseline', trendKind: 'up', + explain: 'Classification-Accuracy auf Holdout-Set. Achtung: Accuracy alleine unzuverlässig bei imbalancierten Klassen — Prüfe zusätzlich Precision/Recall.', + series: [92.5, 93.0, 93.4, 93.7, 94.0, 94.2, 94.3] }, + { id: 'f1', group: 'tech', label: 'F1 Score', value: '0.89', trend: '+0.02 vs. Vormonat', trendKind: 'up', + explain: 'Harmonisches Mittel aus Precision und Recall. Robust gegen Klassenungleichheit.', + series: [0.83, 0.85, 0.86, 0.87, 0.88, 0.885, 0.89] }, + { id: 'drift', group: 'tech', label: 'Drift (PSI)', value: '0.07', trend: 'ok (< 0.1)', trendKind: 'up', + explain: 'Population Stability Index. 0 = keine Drift, > 0.1 = moderate Drift, > 0.25 = signifikant → Retrain.', + series: [0.03, 0.04, 0.05, 0.055, 0.06, 0.065, 0.07] }, + { id: 'bias', group: 'tech', label: 'Bias (EO Gap)', value: '0.03', trend: 'EO-OK (< 0.05)', trendKind: 'up', + explain: 'Equal-Opportunity-Differenz zwischen protected und unprotected group. Hinweis: Bias-Metriken immer auf Fairness-Definition (DP vs. EO vs. EOd) abstimmen.', + series: [0.07, 0.06, 0.055, 0.05, 0.045, 0.04, 0.03] } + ]; + + function loadDash() { + try { return JSON.parse(localStorage.getItem(LS_DASH) || '{"seen": []}'); } catch (e) { return { seen: [] }; } + } + function saveDash(d) { localStorage.setItem(LS_DASH, JSON.stringify(d)); } + const dashState = loadDash(); + if (!Array.isArray(dashState.seen)) dashState.seen = []; + + function renderDashboardModule() { + const host = $('#dashboard-host'); + const groups = [ + { key: 'exec', title: 'Executive Layer — Board-Kennzahlen', cls: 'exec' }, + { key: 'ops', title: 'Operational Layer — Service-Health', cls: 'ops' }, + { key: 'tech', title: 'Technical Layer — Model-Qualität', cls: 'tech' } + ]; + host.innerHTML = ` +
+ Modul 03 +

KPI-Dashboard für KI-Systeme

+

So könnte dein erstes Dashboard aussehen — drei Layer, Executive/Operational/Technical. Klick auf jedes Tile für Sparkline + „Was misst das?". Daten sind Demo-Werte, die Struktur ist das, was zählt.

+
+ ${groups.map(g => ` +
+
${esc(g.title)}
+
+ ${TILES.filter(t => t.group === g.key).map(t => { + const seen = dashState.seen.includes(t.id); + return ` + + `; + }).join('')} +
+
+ `).join('')} +

+ ${dashState.seen.length} / ${TILES.length} Tiles geöffnet — ${dashState.seen.length === TILES.length ? 'komplett!' : `öffne alle für das Dashboard-Flaneur:in-Abzeichen`} +

+ `; + $$('[data-tile]', host).forEach(btn => btn.addEventListener('click', () => openTileModal(btn.dataset.tile))); + } + + function openTileModal(id) { + const tile = TILES.find(t => t.id === id); if (!tile) return; + // mark seen + if (!dashState.seen.includes(id)) { + dashState.seen.push(id); + saveDash(dashState); + // partial XP per new tile + addXP(2, 'Tile geöffnet'); + if (dashState.seen.length === TILES.length && !state.badges.cockpit_dashboard_explored) { + addXP(25, 'Alle Tiles erkundet'); + unlockBadge('cockpit_dashboard_explored'); + } + } + // re-render to reflect seen state + renderDashboardModule(); + // open modal + openModal(` + + + +
${renderSparkline(tile.series)}
+

${esc(tile.explain)}

+ + `); + $('#modal-ask-kai').addEventListener('click', () => { + closeModal(); + openDock(); + fillDockInput(`Erklär mir die Kennzahl "${tile.label}" im Kontext eines AI-Systems in Produktion. Welche Alert-Schwellen sind üblich?`); + }); + } + + function renderSparkline(series) { + if (!series || series.length < 2) return ''; + const w = 420; const h = 80; const pad = 8; + const min = Math.min(...series); const max = Math.max(...series); + const range = (max - min) || 1; + const xStep = (w - pad * 2) / (series.length - 1); + const pts = series.map((v, i) => { + const x = pad + i * xStep; + const y = h - pad - ((v - min) / range) * (h - pad * 2); + return [x, y]; + }); + const path = pts.map((p, i) => (i === 0 ? 'M' : 'L') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' '); + // area fill + const area = path + ` L ${pts[pts.length-1][0].toFixed(1)} ${h - pad} L ${pts[0][0].toFixed(1)} ${h - pad} Z`; + const last = pts[pts.length - 1]; + return ` + + + + + + + + + + `; + } + + // ========= MODAL ========= + function openModal(innerHTML) { + const root = $('#modal-root'); + root.innerHTML = ``; + root.classList.add('open'); + root.setAttribute('aria-hidden', 'false'); + $$('[data-close]', root).forEach(b => b.addEventListener('click', closeModal)); + root.addEventListener('click', backdropClose); + document.addEventListener('keydown', modalEscClose); + // focus first focusable + const first = root.querySelector('button,[href],textarea,input'); + if (first) setTimeout(() => first.focus(), 50); + } + function backdropClose(e) { if (e.target.id === 'modal-root') closeModal(); } + function modalEscClose(e) { if (e.key === 'Escape') closeModal(); } + function closeModal() { + const root = $('#modal-root'); + root.classList.remove('open'); + root.setAttribute('aria-hidden', 'true'); + root.innerHTML = ''; + root.removeEventListener('click', backdropClose); + document.removeEventListener('keydown', modalEscClose); + } + + // ===================================================================== + // MODULE 4 — CHAT DOCK (+ FULL CHAT in module-4) + // ===================================================================== + function loadDockChat() { + try { return JSON.parse(localStorage.getItem(LS_CHAT) || '[]'); } catch (e) { return []; } + } + function saveDockChat() { + const trimmed = dockHistory.slice(-30); + localStorage.setItem(LS_CHAT, JSON.stringify(trimmed)); + } + let dockHistory = loadDockChat(); + let dockBusy = false; + + function renderDockMessages() { + const box = $('#dock-box'); if (!box) return; + if (dockHistory.length === 0) { + box.innerHTML = `
Hi, ich bin Kai. Frag mich zu Metriken, AI Act, Reifegrad — oder klick "Frag Kai" in den Modulen.
`; + return; + } + box.innerHTML = ''; + for (const m of dockHistory) { + const el = document.createElement('div'); + el.className = 'dock-msg ' + (m.role === 'assistant' ? 'bot' : 'user'); + if (m.role === 'assistant') el.innerHTML = renderMD(m.content); + else el.textContent = m.content; + box.appendChild(el); + } + box.scrollTop = box.scrollHeight; + } + + function openDock() { + const dock = $('#chat-dock'); + dock.classList.remove('collapsed'); + dock.classList.add('open'); + // mobile: focus input + setTimeout(() => { const ta = $('#dock-input'); if (ta) ta.focus(); }, 60); + } + function closeDock() { + const dock = $('#chat-dock'); + dock.classList.remove('open'); + if (window.innerWidth > 960) dock.classList.add('collapsed'); + } + function fillDockInput(text) { + const ta = $('#dock-input'); if (!ta) return; + ta.value = text; ta.focus(); + // autoresize + ta.style.height = 'auto'; ta.style.height = Math.min(140, ta.scrollHeight) + 'px'; + } + + async function sendDockChat(text) { + if (!text.trim() || dockBusy) return; + if (!KEY) { + appendDockMsg('err', 'Kein API-Schlüssel konfiguriert. window.__KAI_KEY__ fehlt.'); + return; + } + dockBusy = true; + const sendBtn = $('#dock-send'); if (sendBtn) sendBtn.disabled = true; + + dockHistory.push({ role: 'user', content: text }); + renderDockMessages(); + saveDockChat(); + + const box = $('#dock-box'); + const pending = document.createElement('div'); + pending.className = 'dock-msg bot'; + pending.innerHTML = ''; + box.appendChild(pending); + box.scrollTop = box.scrollHeight; + + try { + const r = await fetch(API, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + KEY }, + body: JSON.stringify({ message: text, history: dockHistory.slice(0, -1).slice(-20) }) + }); + 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)); + pending.remove(); + dockHistory.push({ role: 'assistant', content: data.reply || '(leere Antwort)' }); + saveDockChat(); + renderDockMessages(); + // award small XP for first-ever question of the session (tracked via timestamp) + if (!state._lastDockQ || (Date.now() - state._lastDockQ) > 60000) { + state._lastDockQ = Date.now(); + saveState(); + addXP(5, 'Frage an Kai'); + } + } catch (err) { + pending.remove(); + appendDockMsg('err', 'Fehler: ' + (err.message || String(err))); + } finally { + dockBusy = false; + if (sendBtn) sendBtn.disabled = false; + const ta = $('#dock-input'); if (ta) { ta.value = ''; ta.style.height = 'auto'; ta.focus(); } + } + } + + function appendDockMsg(kind, content) { + const box = $('#dock-box'); if (!box) return; + const el = document.createElement('div'); + el.className = 'dock-msg ' + kind; + el.textContent = content; + box.appendChild(el); + box.scrollTop = box.scrollHeight; + } + + function resetDock() { + if (!confirm('Chat-Verlauf im Dock löschen?')) return; + dockHistory = []; + saveDockChat(); + renderDockMessages(); + toast('Dock-Chat zurückgesetzt', 'info', 1800); + } + + // Full chat module (module 4) — shows a larger empty-state + shares state with dock + function renderChatFullModule() { + const host = $('#chat-full-host'); + host.innerHTML = ` +
+ Modul 04 +

Chat mit Kai

+

Dein ruhiger, systematischer Sparring-Coach. Isolierter Chat-Verlauf vom Widget — hier diskutierst du Cockpit-Themen. Shortcut: Ctrl+K fokussiert das Dock-Input rechts.

+
+
+

Quick-Starter:

+
+ + + + + + +
+

Öffne das Dock (rechts oder unten), um zu chatten — oder klicke einen Starter, um eine Frage vorzubereiten.

+
+ `; + $$('.chip', host).forEach(c => c.addEventListener('click', () => { + openDock(); + fillDockInput(c.dataset.chip); + })); + } + + // ===================================================================== + // NAV + ROUTING + // ===================================================================== + function switchModule(modId) { + $$('.module').forEach(m => m.dataset.active = (m.id === 'mod-' + modId) ? 'true' : 'false'); + $$('.nav-item').forEach(n => n.setAttribute('aria-selected', (n.dataset.module === modId) ? 'true' : 'false')); + // Re-render current module fresh + if (modId === 'reifegrad') renderReifegradModule(); + if (modId === 'aiact') renderAIActModule(); + if (modId === 'dashboard') renderDashboardModule(); + if (modId === 'chat') renderChatFullModule(); + } + + function installNav() { + $$('.nav-item').forEach(n => n.addEventListener('click', () => switchModule(n.dataset.module))); + } + + function installKeyboardShortcuts() { + document.addEventListener('keydown', (e) => { + // Ctrl+1..4 -> switch module + if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && ['1','2','3','4'].includes(e.key)) { + const map = { '1': 'reifegrad', '2': 'aiact', '3': 'dashboard', '4': 'chat' }; + e.preventDefault(); + switchModule(map[e.key]); + return; + } + // Ctrl+K -> focus dock + if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'k') { + e.preventDefault(); + openDock(); + const ta = $('#dock-input'); if (ta) ta.focus(); + } + }); + } + + function installDock() { + renderDockMessages(); + const form = $('#dock-form'); + const input = $('#dock-input'); + // autoresize + input.addEventListener('input', () => { + input.style.height = 'auto'; + input.style.height = Math.min(140, input.scrollHeight) + 'px'; + }); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendDockChat(input.value); + } + }); + form.addEventListener('submit', (e) => { e.preventDefault(); sendDockChat(input.value); }); + $('#dock-reset').addEventListener('click', resetDock); + $('#dock-collapse').addEventListener('click', () => { + const dock = $('#chat-dock'); + if (window.innerWidth <= 960) { dock.classList.remove('open'); } + else { dock.classList.add('collapsed'); } + }); + $('#dock-open').addEventListener('click', openDock); + } + + // ===================================================================== + // BOOT + // ===================================================================== + function boot() { + // Ensure badges object exists + if (!state.badges) { state.badges = {}; saveState(); } + renderXP(); + installNav(); + installKeyboardShortcuts(); + installDock(); + // Initial module + switchModule('reifegrad'); + // Touch activity for streak (shared widget logic would also do this, harmless redundancy) + state.lastActive = today(); saveState(); + // Welcome toast if first visit + if (!state.seenCockpitWelcome) { + state.seenCockpitWelcome = true; saveState(); + setTimeout(() => toast('Willkommen im Kai-Cockpit. Start mit dem Reifegrad-Assessment.', 'info', 3800), 600); + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', boot); + } else { + boot(); + } +})(); diff --git a/src/cockpit-overlay/index.html b/src/cockpit-overlay/index.html new file mode 100644 index 0000000..7b78d36 --- /dev/null +++ b/src/cockpit-overlay/index.html @@ -0,0 +1,102 @@ + + + + + Kai Cockpit · KI-Governance, Reifegrad, EU AI Act + + + + + + + +
+ +
+
+ +
+ Kai Cockpit + KI-Governance · Reifegrad · AI Act +
+
+
+
+
+ Lvl 1 · AI-Trainee + 0 XP +
+
+
+ Widget › +
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + +
+ Sovereign AI · Deutscher Bunker · Qognio · DSGVO-konform · Keine externen CDNs +
+ +
+ + +
+ + + + diff --git a/src/config.yaml b/src/config.yaml new file mode 100644 index 0000000..c26f036 --- /dev/null +++ b/src/config.yaml @@ -0,0 +1,33 @@ +slug: ki-kennzahlen-coach +bot_name: Kai +bot_title: "KI-Kennzahlen & Methodik-Coach" +brand_letter: K +title: "Kai · KI-Kennzahlen & Methodik-Coach" +tagline: "KI-Kennzahlen & Methodik-Coach" +tagline_short: "KI-Kennzahlen" +meta_description: "Kai — dein KI-Kennzahlen-Coach. Methodik, Modell-Bewertung, Bias/Fairness, AI-Reifegrad-Messung für CIOs, CDOs, AI-Leads. Im deutschen Bunker." +bot_key_var: __KAI_KEY__ +bot_key_value: qb_qj0ahcfv6coz +ls_prefix: kai +bot_version: "2026-04-25" + +# Color theme — cyan/teal (Brücke zwischen DSGVO-grün/Cora und AI-Act-violett/KURT) +accent: "#0891b2" +accent_2: "#06b6d4" +accent_dark: "#0e7490" +accent_rgb: "8, 145, 178" +accent_rgb_compact: "8,145,178" +success_color: "#22c55e" +msg_strong_color: "#a5f3fc" + +# UI Labels +tab_flash_label: Karten +tab_curriculum_label: Module +curriculum_long_label: KI-Methodik-Atlas + +# Bot-personality strings +quiz_intro_hint: "Wähle ein Modul — Kai generiert Szenario-Fragen aus dem KI-Projekt-Alltag (Modell-Bewertung, Bias, Reifegrad)." +quiz_verb: erstellt +quiz_noun: "KI-Methodik-Fragen" +flash_intro_hint: "Karteikarten zu Klassifikations-Metriken, Fairness, AI-Reifegrad — Spaced-Repetition." +flash_verb: generiert diff --git a/src/curricula.json b/src/curricula.json new file mode 100644 index 0000000..e1e1726 --- /dev/null +++ b/src/curricula.json @@ -0,0 +1,386 @@ +{ + "version": "2026-04-24", + "updated": "2026-04-24", + "curricula": [ + { + "id": "klassische-ml-metriken", + "title": "Klassische ML-Metriken", + "short": "ML-Metriken", + "icon": "chart", + "color": "#0891b2", + "description": "Accuracy, Precision, Recall, F1, ROC-AUC, Cohen's Kappa, MCC — welche Metrik wann, und warum", + "modules": [ + { + "id": "klassifikation-metriken", + "title": "Klassifikations-Metriken", + "subtopics": [ + {"id": "acc-prec-rec", "title": "Accuracy vs Precision vs Recall", "objectives": ["Formeln erklären", "Wann welche priorisieren", "Class-Imbalance-Fallstricke erkennen"]}, + {"id": "f1-mcc-kappa", "title": "F1, MCC, Cohen's Kappa", "objectives": ["Harmonisches Mittel verstehen", "Wann MCC besser als F1", "Inter-Rater-Reliability"]}, + {"id": "roc-pr-auc", "title": "ROC-AUC vs PR-AUC", "objectives": ["Threshold-unabhängige Bewertung", "Imbalanced Data: warum PR-AUC besser", "Trade-offs visualisieren"]} + ] + }, + { + "id": "regression-metriken", + "title": "Regressions-Metriken", + "subtopics": [ + {"id": "rmse-mae", "title": "RMSE vs MAE vs MAPE", "objectives": ["Skalen-Abhängigkeit", "Outlier-Sensitivität", "Relative Fehler"]}, + {"id": "r2-adjusted", "title": "R² und Adjusted R²", "objectives": ["Erklärte Varianz", "Model-Complexity-Penalty"]} + ] + }, + { + "id": "nlp-metriken", + "title": "NLP & LLM-Metriken", + "subtopics": [ + {"id": "bleu-rouge", "title": "BLEU, ROUGE, METEOR", "objectives": ["n-gram-Matching", "Grenzen automatischer Metriken"]}, + {"id": "perplexity", "title": "Perplexity & Token-Metriken", "objectives": ["Information Theory-Grundlage", "Pro-Token-Verlust"]}, + {"id": "llm-as-judge", "title": "LLM-as-a-Judge", "objectives": ["Evaluation mit Modell-Richter", "Bias in der Evaluation"]} + ] + } + ] + }, + { + "id": "business-kpis", + "title": "Business-KPIs für KI", + "short": "Business-KPIs", + "icon": "briefcase", + "color": "#0891b2", + "description": "ROI, Time-to-Value, Adoption, Kosten-pro-Inference — KI im Unternehmen messbar machen", + "modules": [ + { + "id": "roi-tco", + "title": "ROI, TCO, Time-to-Value", + "subtopics": [ + {"id": "roi-formel", "title": "ROI-Berechnung für KI-Projekte", "objectives": ["FTE-Äquivalente einrechnen", "Indirekte Effekte quantifizieren", "Amortisationsdauer"]}, + {"id": "tco-hidden", "title": "TCO & versteckte Kosten", "objectives": ["Inferenzkosten vs Trainingskosten", "Operational Overhead", "Vendor-Lock-in"]} + ] + }, + { + "id": "adoption-satisfaction", + "title": "Adoption & Zufriedenheit", + "subtopics": [ + {"id": "adoption-rate", "title": "Adoption-Rate & Active-Users", "objectives": ["WAU/MAU-Unterschied", "Benchmarks aus Praxis"]}, + {"id": "csat-nps", "title": "CSAT, NPS für KI-Features", "objectives": ["Feature-spezifische Messung", "Anti-Patterns"]} + ] + } + ] + }, + { + "id": "operational", + "title": "Operational Metrics", + "short": "Operations", + "icon": "activity", + "color": "#0891b2", + "description": "Latency, Throughput, Availability — KI-Systeme im Produktivbetrieb überwachen", + "modules": [ + { + "id": "slo-sla", + "title": "SLO/SLA-Design", + "subtopics": [ + {"id": "latency-percentiles", "title": "P50/P95/P99 Latency", "objectives": ["Warum nicht Average", "Percentile lesen"]}, + {"id": "throughput-tps", "title": "Throughput & Tokens-per-Second", "objectives": ["LLM-Durchsatz messen", "Batching-Effekte"]}, + {"id": "availability", "title": "Availability & Error Budget", "objectives": ["SLO-Definition", "Error-Budget-Policy"]} + ] + }, + { + "id": "monitoring-tooling", + "title": "Monitoring-Stack", + "subtopics": [ + {"id": "mlflow-arize", "title": "MLflow, Arize, WhyLabs, W&B", "objectives": ["Tool-Landschaft verstehen", "Auswahl-Kriterien"]}, + {"id": "drift-alerts", "title": "Drift-Alerts & Incident-Playbook", "objectives": ["Schwellenwerte setzen", "False-Positive-Rate"]} + ] + } + ] + }, + { + "id": "datenqualitaet", + "title": "Datenqualität & Drift", + "short": "Daten", + "icon": "database", + "color": "#0891b2", + "description": "DAMA-Dimensionen, PSI, KL-Divergence — Datenprobleme vor Modell-Problemen erkennen", + "modules": [ + { + "id": "dama-dimensions", + "title": "DAMA-Dimensionen", + "subtopics": [ + {"id": "completeness-accuracy", "title": "Completeness, Accuracy, Validity", "objectives": ["Jede Dimension mit Beispiel", "Messmethoden"]}, + {"id": "timeliness-uniqueness", "title": "Timeliness, Uniqueness, Consistency", "objectives": ["Stale-Data-Risiken", "Dedup-Strategien"]} + ] + }, + { + "id": "drift-detection", + "title": "Drift-Detection", + "subtopics": [ + {"id": "psi-kl", "title": "PSI, KL- und JS-Divergence", "objectives": ["Formel und Interpretation", "Schwellenwerte"]}, + {"id": "ks-test", "title": "Kolmogorov-Smirnov-Test", "objectives": ["Nicht-parametrischer Test", "Feature-vs-Label-Drift"]} + ] + } + ] + }, + { + "id": "bias-fairness", + "title": "Bias & Fairness", + "short": "Fairness", + "icon": "scale", + "color": "#0891b2", + "description": "Demographic Parity, Equal Opportunity, Disparate Impact — Diskriminierungsfreie Systeme", + "modules": [ + { + "id": "fairness-metriken", + "title": "Fairness-Metriken", + "subtopics": [ + {"id": "demographic-parity", "title": "Demographic Parity", "objectives": ["Formel", "Einschränkungen"]}, + {"id": "equal-opportunity", "title": "Equal Opportunity & Equalized Odds", "objectives": ["Unterschied zu Parity", "Praxisbeispiel HR-Tool"]}, + {"id": "disparate-impact", "title": "Disparate Impact & 80%-Regel", "objectives": ["US-EEOC-Standard", "EU-Bezug"]} + ] + }, + { + "id": "fairness-tooling", + "title": "Tooling", + "subtopics": [ + {"id": "aif360-fairlearn", "title": "AIF360 & Fairlearn", "objectives": ["Library-Überblick", "Bias-Audits durchführen"]} + ] + } + ] + }, + { + "id": "explainability", + "title": "Explainability (XAI)", + "short": "XAI", + "icon": "eye", + "color": "#0891b2", + "description": "SHAP, LIME, Counterfactuals — Modell-Entscheidungen erklärbar machen (EU AI Act Art. 13)", + "modules": [ + { + "id": "xai-methoden", + "title": "XAI-Methoden", + "subtopics": [ + {"id": "shap-lime", "title": "SHAP vs LIME", "objectives": ["Shapley-Values verstehen", "Lokale vs globale Erklärung"]}, + {"id": "counterfactuals", "title": "Counterfactual & Anchor Explanations", "objectives": ["Minimal-Änderungs-Prinzip", "Praxisnutzen"]} + ] + }, + { + "id": "xai-qualitaet", + "title": "Qualität von Erklärungen", + "subtopics": [ + {"id": "fidelity-stability", "title": "Fidelity, Stability, Comprehensibility", "objectives": ["Messgrößen für Erklärungsqualität", "Trade-offs"]} + ] + } + ] + }, + { + "id": "robustheit-security", + "title": "Robustheit & Security", + "short": "Robustheit", + "icon": "shield", + "color": "#0891b2", + "description": "Adversarial Robustness, Prompt-Injection, Data-Poisoning — Angriffsvektoren verstehen", + "modules": [ + { + "id": "adversarial", + "title": "Adversarial Robustness", + "subtopics": [ + {"id": "pgd-fgsm", "title": "PGD & FGSM-Attacks", "objectives": ["Perturbations verstehen", "L-infinity-Budget"]}, + {"id": "certified-robustness", "title": "Certified Robustness", "objectives": ["Formale Garantien vs empirische Tests"]} + ] + }, + { + "id": "llm-security", + "title": "LLM-Security", + "subtopics": [ + {"id": "prompt-injection", "title": "Prompt-Injection & Jailbreaks", "objectives": ["OWASP LLM Top 10", "Mitigationen"]}, + {"id": "data-poisoning", "title": "Data-Poisoning-Erkennung", "objectives": ["Training-Set-Forensik", "Monitoring-Metriken"]} + ] + } + ] + }, + { + "id": "governance-reifegrad", + "title": "Governance & Reifegrad", + "short": "Reifegrad", + "icon": "gauge", + "color": "#0891b2", + "description": "Gartner AI Maturity, MIT CISR, Microsoft RAI MM — wo stehst du, wo willst du hin", + "modules": [ + { + "id": "reifegradmodelle", + "title": "AI-Reifegradmodelle im Vergleich", + "subtopics": [ + {"id": "gartner-maturity", "title": "Gartner AI Maturity Model", "objectives": ["5 Stufen", "Self-Assessment"]}, + {"id": "mit-cisr", "title": "MIT CISR & Microsoft RAI MM", "objectives": ["Unterschiede", "DACH-Anwendung"]} + ] + }, + { + "id": "self-assessment", + "title": "Self-Assessment-Dimensionen", + "subtopics": [ + {"id": "strategy-data", "title": "Strategy, Data, Technology", "objectives": ["Fragebogen-Struktur", "Reifegrad-Score berechnen"]}, + {"id": "people-processes", "title": "People, Processes", "objectives": ["Kompetenz-Matrix", "Prozess-Reife"]} + ] + } + ] + }, + { + "id": "eu-ai-act", + "title": "EU AI Act Compliance", + "short": "EU AI Act", + "icon": "flag", + "color": "#0891b2", + "description": "Risikoklassen, CE-Kennzeichnung, Artikel-Pflichten — Was muss ich ab wann erfüllen", + "modules": [ + { + "id": "risikoklassen", + "title": "Risikoklassen", + "subtopics": [ + {"id": "verboten-high-risk", "title": "Verboten vs High-Risk", "objectives": ["Art. 5 vs Annex III", "Grenzfälle erkennen"]}, + {"id": "limited-minimal", "title": "Limited & Minimal Risk", "objectives": ["Transparenz-Pflichten", "Chatbots"]} + ] + }, + { + "id": "high-risk-pflichten", + "title": "High-Risk-Pflichten", + "subtopics": [ + {"id": "risk-management", "title": "Art. 9 Risikomanagement-System", "objectives": ["Dokumentations-Anforderungen"]}, + {"id": "data-governance", "title": "Art. 10 Data Governance", "objectives": ["Trainings-, Validierungs- und Testdaten"]}, + {"id": "transparency-art13", "title": "Art. 13 Transparenz", "objectives": ["User-Information", "Logging"]}, + {"id": "human-oversight", "title": "Art. 14 Menschliche Aufsicht", "objectives": ["Override-Mechanismen"]} + ] + }, + { + "id": "timeline-ce", + "title": "Timeline & CE-Kennzeichnung", + "subtopics": [ + {"id": "phasen-einfuehrung", "title": "Inkrafttreten-Phasen", "objectives": ["2025/2026/2027-Meilensteine"]}, + {"id": "ce-marking", "title": "CE-Kennzeichnung für High-Risk", "objectives": ["Konformitäts-Assessment", "Benannte Stellen"]} + ] + } + ] + }, + { + "id": "nist-ai-rmf", + "title": "NIST AI RMF", + "short": "NIST", + "icon": "book", + "color": "#0891b2", + "description": "Govern, Map, Measure, Manage — US-Framework mit Crosswalk zu EU AI Act und ISO 42001", + "modules": [ + { + "id": "vier-funktionen", + "title": "Die 4 Kernfunktionen", + "subtopics": [ + {"id": "govern", "title": "Govern", "objectives": ["Organisations-Kultur", "Policies"]}, + {"id": "map", "title": "Map", "objectives": ["Kontext & Risiken erfassen"]}, + {"id": "measure", "title": "Measure", "objectives": ["KPIs und Tests"]}, + {"id": "manage", "title": "Manage", "objectives": ["Priorisieren, Response, Recovery"]} + ] + }, + { + "id": "crosswalk", + "title": "Crosswalk zu anderen Frameworks", + "subtopics": [ + {"id": "nist-vs-eu-ai-act", "title": "NIST vs EU AI Act", "objectives": ["Überlappungen nutzen"]}, + {"id": "nist-vs-iso", "title": "NIST vs ISO 42001", "objectives": ["Mapping-Tabellen"]} + ] + } + ] + }, + { + "id": "iso-42001", + "title": "ISO/IEC 42001 & 23894", + "short": "ISO", + "icon": "award", + "color": "#0891b2", + "description": "AI Management System, AI Risk Management — zertifizierbare Standards seit 2023", + "modules": [ + { + "id": "iso-42001-aims", + "title": "ISO 42001 — AIMS", + "subtopics": [ + {"id": "aims-struktur", "title": "Was ist ein AIMS", "objectives": ["Management-System-Struktur", "PDCA für KI"]}, + {"id": "zertifizierungspfad", "title": "Zertifizierungspfad", "objectives": ["Akkreditierte Stellen DACH", "Aufwand schätzen"]} + ] + }, + { + "id": "iso-23894", + "title": "ISO 23894 — AI Risk Management", + "subtopics": [ + {"id": "risk-assessment", "title": "Risk Assessment Prozess", "objectives": ["Risiko identifizieren/analysieren/bewerten"]}, + {"id": "komplement-27001", "title": "Komplementarität zu ISO 27001", "objectives": ["Synergie mit ISMS"]} + ] + } + ] + }, + { + "id": "scorecards", + "title": "Scorecards & Dashboards", + "short": "Scorecards", + "icon": "clipboard", + "color": "#0891b2", + "description": "KPI-Hierarchie Exec/Operational/Technical — vom Dashboard zur Entscheidung", + "modules": [ + { + "id": "scorecard-design", + "title": "Scorecard-Design", + "subtopics": [ + {"id": "kpi-hierarchie", "title": "Exec-, Operational-, Technical-Layer", "objectives": ["Welche KPI auf welcher Ebene", "Aggregations-Logik"]}, + {"id": "vanity-metrics", "title": "Vanity-Metrics erkennen", "objectives": ["Anti-Patterns", "Actionability-Test"]} + ] + }, + { + "id": "dashboarding-tools", + "title": "Dashboarding-Tools", + "subtopics": [ + {"id": "tableau-looker-superset", "title": "Tableau, Looker, Superset, Grafana", "objectives": ["Stärken/Schwächen", "Kosten"]} + ] + } + ] + }, + { + "id": "okr-ki", + "title": "OKRs für KI-Teams", + "short": "OKR", + "icon": "target", + "color": "#0891b2", + "description": "Objectives & Key Results — Balance zwischen Tech-KPIs und Business-Impact", + "modules": [ + { + "id": "okr-grundlagen", + "title": "OKR-Grundlagen für AI", + "subtopics": [ + {"id": "obj-vs-kr", "title": "Objectives vs Key Results", "objectives": ["Qualitativ vs quantitativ"]}, + {"id": "balance-tech-biz", "title": "Balance Tech vs Business", "objectives": ["Anti-Pattern: Over-Tech-Indexed"]} + ] + }, + { + "id": "beispiele-dach", + "title": "Beispiele aus DACH-Mittelstand", + "subtopics": [ + {"id": "mittelstand-cases", "title": "5 Praxisbeispiele", "objectives": ["Formulierungs-Muster"]} + ] + } + ] + } + ], + "badges": [ + {"id": "erste_metrik", "title": "Erste Metrik-Analyse", "icon": "target", "description": "1. Quiz zu klassischen ML-Metriken bestanden"}, + {"id": "metrik_master", "title": "Metrik-Master:in", "icon": "trophy", "description": "10 Klassifikations-Metriken-Fragen korrekt"}, + {"id": "bias_hunter", "title": "Bias-Jäger:in", "icon": "scale", "description": "Bias & Fairness-Modul abgeschlossen"}, + {"id": "ai_act_navigator", "title": "EU-AI-Act-Navigator:in", "icon": "flag", "description": "EU-AI-Act-Modul abgeschlossen"}, + {"id": "nist_practitioner", "title": "NIST-Praktiker:in", "icon": "book", "description": "NIST AI RMF-Modul abgeschlossen"}, + {"id": "iso_expert", "title": "ISO-42001-Expert:in", "icon": "award", "description": "ISO 42001/23894-Modul abgeschlossen"}, + {"id": "scorecard_architect", "title": "Scorecard-Architekt:in", "icon": "clipboard", "description": "Scorecards-Flashcards bestanden"}, + {"id": "governance_lead", "title": "Governance-Lead", "icon": "crown", "description": "Reifegrad-Modul abgeschlossen"}, + {"id": "kpi_master", "title": "KI-Kennzahlen-Master", "icon": "star", "description": "Alle 13 Curricula abgeschlossen"}, + {"id": "night_owl", "title": "Nachteule", "icon": "moon", "description": "Nach 22 Uhr gelernt"}, + {"id": "early_bird", "title": "Frühaufsteher:in", "icon": "sun", "description": "Vor 7 Uhr gelernt"} + ], + "levels": [ + {"min": 0, "title": "Einsteiger:in"}, + {"min": 50, "title": "Analyst:in"}, + {"min": 200, "title": "Data-Scientist:in"}, + {"min": 500, "title": "ML-Engineer:in"}, + {"min": 1250, "title": "AI-Program-Lead"}, + {"min": 2500, "title": "Head of AI"}, + {"min": 5000, "title": "Chief Data/AI-Officer"} + ] +} diff --git a/src/levels-fallback.js b/src/levels-fallback.js new file mode 100644 index 0000000..0f8c302 --- /dev/null +++ b/src/levels-fallback.js @@ -0,0 +1,4 @@ + { min: 0, title: 'AI-Trainee' }, { min: 50, title: 'AI-Analyst' }, + { min: 200, title: 'ML-Engineer' }, { min: 500, title: 'AI-Lead' }, + { min: 1250, title: 'AI-Officer' }, { min: 2500, title: 'AI-Governance-Lead' }, + { min: 5000, title: 'Chief AI Officer' } diff --git a/src/welcome.html b/src/welcome.html new file mode 100644 index 0000000..b54c8c5 --- /dev/null +++ b/src/welcome.html @@ -0,0 +1,29 @@ +

Willkommen bei Kai!

+

Ich bin dein:e Sparringspartner:in für KI-Kennzahlen, Modell-Bewertung, Reifegradmessung und AI-Governance — ruhig, systematisch, zahlenliebend. Warum das Ganze? Messbar steuern, Bias erkennen, ROI zeigen. Läuft im deutschen Bunker, deine Metriken bleiben hier.

+
+ + + + + + + Cockpit ↗ + Reifegrad-Assessment, EU-AI-Act-Risk-Classifier, KPI-Dashboard — Spezial-App neben dem Chat. + +
+

In 3 Sätzen: Chat zum Verstehen → Quiz zum Testen → Flashcards zum Merken. Das Cockpit liefert dir Reifegrad-Score und AI-Act-Klassifizierung. Brauchst du operative AI-Act-Tiefe? → ruf KURT. AI-Act-Audit-Trail? → VESTIGIA.

\ No newline at end of file diff --git a/www/app.js b/www/app.js new file mode 100644 index 0000000..5521dc9 --- /dev/null +++ b/www/app.js @@ -0,0 +1,1753 @@ +/* Kai — KI-Kennzahlen & Methodik-Coach Widget + * Vanilla JS, no build, no framework, keine externen Fonts/Analytics. + * Chat | Quiz | Flashcards | Fortschritt | KI-Methodik-Atlas — localStorage only. + */ +(() => { + 'use strict'; + + // ==== Config ==== + const API = 'https://llm.qognio.com/api/bots/ki-kennzahlen-coach/chat'; + const RAW_KEY = window.__KAI_KEY__ || ''; + const KEY = /^qb_[a-zA-Z0-9]{6,}$/.test(RAW_KEY) ? RAW_KEY : ''; + const LS_KEY = 'kai.state.v1'; + const LS_CHAT = 'kai.chat.v1'; + const LS_FLASH = 'kai.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: 'AI-Trainee' }, { min: 50, title: 'AI-Analyst' }, + { min: 200, title: 'ML-Engineer' }, { min: 500, title: 'AI-Lead' }, + { min: 1250, title: 'AI-Officer' }, { min: 2500, title: 'AI-Governance-Lead' }, + { min: 5000, title: 'Chief AI Officer' } + ]; + 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() { + // Erste Metrik-Analyse — 1 korrekte Antwort im Klassifikations-Modul + if ((state.moduleCorrect && state.moduleCorrect['klassifikation-metriken'] >= 1)) unlockBadge('erste_metrik'); + // Metrik-Master — 10 Klassifikations-Metriken-Antworten korrekt + if ((state.moduleCorrect && state.moduleCorrect['klassifikation-metriken'] >= 10)) unlockBadge('metrik_master'); + // Bias-Hunter — 5 korrekte Antworten im Fairness-Metriken-Modul + if ((state.moduleCorrect && state.moduleCorrect['fairness-metriken'] >= 5)) unlockBadge('bias_hunter'); + // EU-AI-Act-Navigator — 5 korrekte Antworten im Risikoklassen-Modul + if ((state.moduleCorrect && state.moduleCorrect['risikoklassen'] >= 5)) unlockBadge('ai_act_navigator'); + // NIST-Praktiker — 5 korrekte Antworten in den 4 NIST-Kernfunktionen + if ((state.moduleCorrect && state.moduleCorrect['vier-funktionen'] >= 5)) unlockBadge('nist_practitioner'); + // ISO-42001-Expert — 5 korrekte Antworten im AIMS-Modul + if ((state.moduleCorrect && state.moduleCorrect['iso-42001-aims'] >= 5)) unlockBadge('iso_expert'); + // Scorecard-Architekt — Scorecard-Design-Modul Flashcards bestanden + if ((state.modulePassedFlash && state.modulePassedFlash['scorecard-design'])) unlockBadge('scorecard_architect'); + // Reifegrad-Lead — 5 korrekte Antworten im Self-Assessment-Modul + if ((state.moduleCorrect && state.moduleCorrect['self-assessment'] >= 5)) unlockBadge('reifegrad_lead'); + // KPI-Master — 22 von 28 Modulen mit ≥80% Quiz-Score abgeschlossen (~79% Master-Coverage) + if ((state.completedCurricula || []).length >= 22) unlockBadge('kpi_master'); + // Night Owl & Early Bird (beibehalten) + const h = new Date().getHours(); + if (h >= 22) unlockBadge('night_owl'); + if (h < 7) unlockBadge('early_bird'); + } + + // ==== Toast ==== + function toast(msg, kind = '', ms = 3200) { + const stack = $('#toast-stack'); + const t = document.createElement('div'); + t.className = 'toast ' + kind; + t.textContent = msg; + stack.appendChild(t); + setTimeout(() => { + t.style.opacity = '0'; + t.style.transition = 'opacity .25s'; + setTimeout(() => t.remove(), 260); + }, ms); + } + function showXPGain(txt) { + const el = document.createElement('div'); + el.className = 'xp-gain'; + el.textContent = txt; + document.body.appendChild(el); + setTimeout(() => el.remove(), 1600); + } + + // ==== Simple markdown renderer ==== + function renderMD(md) { + if (!md) return ''; + let s = md; + // Escape HTML first + s = s.replace(/&/g, '&').replace(//g, '>'); + // Code fences + s = s.replace(/```(\w+)?\n([\s\S]*?)```/g, (_, lang, code) => + `
${code}
`); + // Inline code + s = s.replace(/`([^`\n]+)`/g, '$1'); + // GFM tables: header / separator / body + s = s.replace(/(?:^|\n)((?:\|[^\n]*\|[ \t]*\n){2,})/g, (block, content) => { + const lines = content.trim().split('\n'); + if (lines.length < 2) return block; + const sep = lines[1]; + if (!/^\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\|?\s*$/.test(sep)) return block; + const parseRow = (ln) => ln.replace(/^\|/, '').replace(/\|\s*$/, '').split('|').map(c => c.trim()); + const header = parseRow(lines[0]); + const aligns = parseRow(sep).map(s => /^:-+:$/.test(s) ? 'center' : /-+:$/.test(s) ? 'right' : 'left'); + const rows = lines.slice(2).map(parseRow); + let html = '\n
'; + header.forEach((h, i) => { html += ``; }); + html += ''; + rows.forEach(r => { + html += ''; + for (let i = 0; i < Math.max(r.length, header.length); i++) { + html += ``; + } + html += ''; + }); + html += '
${h}
${r[i] || ''}
\n'; + return html; + }); + // Bold + s = s.replace(/\*\*([^*\n]+)\*\*/g, '$1'); + s = s.replace(/__([^_\n]+)__/g, '$1'); + // Italic + s = s.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1$2'); + // Headings + s = s.replace(/^### (.+)$/gm, '

$1

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

$1

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

$1

'); + // Links + s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '$1'); + // Unordered lists + s = s.replace(/(?:^|\n)((?:[*\-] .+\n?)+)/g, (m) => { + const items = m.trim().split('\n').map(ln => ln.replace(/^[*\-] /, '')).map(li => `
  • ${li}
  • `).join(''); + return '\n
      ' + 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 — Kai generiert Szenario-Fragen aus dem KI-Projekt-Alltag (Modell-Bewertung, Bias, Reifegrad).

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

    Kai erstellt ${count} KI-Methodik-Fragen zu „${topic.mod.title}" …

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

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

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

    Quiz beendet!

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

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

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

    🃏 Flashcards

    +

    Karteikarten zu Klassifikations-Metriken, Fairness, AI-Reifegrad — Spaced-Repetition.

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

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

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

    ⚠ Fehler: ${e.message}

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

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

    Lege neue Karten an oder komm später wieder.

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

    Review beendet 🎉

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

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

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

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

    Mastery pro Curriculum

    + ${masteryRows.length === 0 + ? '

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

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

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

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

    Daten zurücksetzen

    +

    Lokal gespeichert (kein Server-Tracking).

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

    ${escapeHTML(m.title)}

    +

    Lernziele

    +
      ${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('Kai v2026-04-25 ready. XP:', state.xp, 'Streak:', state.currentStreak); + } + + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); + else boot(); +})(); diff --git a/www/cockpit/cockpit.css b/www/cockpit/cockpit.css new file mode 100644 index 0000000..809c224 --- /dev/null +++ b/www/cockpit/cockpit.css @@ -0,0 +1,705 @@ +/* Kai Cockpit — vanilla CSS, no external fonts/CDN. + Extends Kai widget theme: teal #0891b2/#06b6d4, bg #0a0a0f, text #f1f0f5. */ + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +:root { + --bg: #0a0a0f; + --bg-elev: #12121a; + --bg-elev2: #191924; + --bg-elev3: #22222f; + --line: #2a2a38; + --line-soft: #1f1f2b; + --text: #f1f0f5; + --text-mute: #a0a0b5; + --text-dim: #707088; + --teal: #0891b2; + --teal-b: #06b6d4; + --teal-c: #22d3ee; + --teal-dim: rgba(8,145,178,.15); + --ok: #10b981; + --warn: #f59e0b; + --err: #ef4444; + --violet: #a78bfa; + --dock-w: 300px; + --nav-w: 220px; + --radius: 10px; + --radius-sm: 6px; + --shadow: 0 8px 24px rgba(0,0,0,.45); + --t: 0.2s ease; +} + +body { + background: var(--bg); + color: var(--text); + font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 15px; + line-height: 1.5; + min-height: 100vh; + -webkit-font-smoothing: antialiased; +} + +a { color: var(--teal-c); text-decoration: none; } +a:hover { text-decoration: underline; } +button { font-family: inherit; font-size: inherit; } +code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background: #0d0d14; padding: 1px 5px; border-radius: 4px; border: 1px solid var(--line-soft); font-size: .9em; } + +/* ============ LAYOUT ============ */ +.cockpit { + display: grid; + grid-template-columns: var(--nav-w) minmax(0, 1fr) var(--dock-w); + grid-template-rows: auto 1fr auto; + grid-template-areas: + "top top top" + "nav main dock" + "foot foot foot"; + min-height: 100vh; + background: radial-gradient(ellipse at top, rgba(8,145,178,.08), transparent 60%), var(--bg); +} +.topbar { grid-area: top; } +.nav { grid-area: nav; } +.main { grid-area: main; } +.chat-dock { grid-area: dock; } +.footer { grid-area: foot; } + +/* ============ TOPBAR ============ */ +.topbar { + display: flex; + align-items: center; + gap: 16px; + padding: 14px 22px; + border-bottom: 1px solid var(--line); + background: linear-gradient(180deg, rgba(18,18,26,.92), rgba(10,10,15,.92)); + backdrop-filter: blur(6px); + position: sticky; top: 0; z-index: 50; +} +.brand { display: flex; align-items: center; gap: 12px; } +.brand-icon { + width: 36px; height: 36px; border-radius: 9px; + background: linear-gradient(135deg, var(--teal), var(--teal-b)); + display: flex; align-items: center; justify-content: center; + font-weight: 700; font-size: 18px; color: #001013; + box-shadow: 0 0 0 1px rgba(34,211,238,.25), 0 6px 14px rgba(8,145,178,.35); +} +.brand-text { display: flex; flex-direction: column; line-height: 1.1; } +.brand-title { font-weight: 600; font-size: 16px; letter-spacing: -.2px; } +.brand-title small { font-weight: 400; color: var(--text-mute); font-size: 13px; margin-left: 2px; } +.brand-sub { color: var(--text-dim); font-size: 11px; letter-spacing: .04em; text-transform: uppercase; } +.spacer { flex: 1; } + +.xp-wrap { display: flex; flex-direction: column; gap: 4px; min-width: 170px; } +.xp-row { display: flex; justify-content: space-between; font-size: 12px; color: var(--text-mute); } +.xp-level { color: var(--teal-c); font-weight: 500; } +.xp-score { color: var(--text-mute); } +.xp-bar { height: 5px; background: var(--bg-elev2); border-radius: 99px; overflow: hidden; border: 1px solid var(--line-soft); } +.xp-bar-fill { height: 100%; width: 0%; background: linear-gradient(90deg, var(--teal), var(--teal-c)); transition: width .4s ease; } + +.back-link { + padding: 7px 14px; border: 1px solid var(--line); + border-radius: 8px; font-size: 13px; color: var(--text-mute); + transition: var(--t); +} +.back-link:hover { color: var(--text); border-color: var(--teal); background: var(--teal-dim); text-decoration: none; } + +/* ============ NAV ============ */ +.nav { + border-right: 1px solid var(--line); + padding: 22px 14px; + display: flex; flex-direction: column; gap: 4px; + background: rgba(18,18,26,.5); +} +.nav-item { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 10px; + align-items: center; + background: transparent; + border: 1px solid transparent; + color: var(--text-mute); + padding: 12px 14px; + border-radius: var(--radius); + cursor: pointer; + text-align: left; + transition: var(--t); + font-size: 14px; +} +.nav-item:hover { background: var(--bg-elev); color: var(--text); } +.nav-item[aria-selected="true"] { + background: linear-gradient(90deg, rgba(8,145,178,.2), rgba(8,145,178,.06)); + border-color: rgba(34,211,238,.35); + color: var(--text); + box-shadow: inset 2px 0 0 var(--teal-c); +} +.nav-num { font-variant-numeric: tabular-nums; font-size: 11px; color: var(--text-dim); letter-spacing: .08em; } +.nav-item[aria-selected="true"] .nav-num { color: var(--teal-c); } +.nav-label { font-weight: 500; letter-spacing: -.2px; } +.nav-kbd { font-size: 10px; color: var(--text-dim); background: var(--bg-elev2); padding: 2px 5px; border-radius: 4px; border: 1px solid var(--line-soft); } + +/* ============ MAIN ============ */ +.main { padding: 28px 36px 40px; min-width: 0; } +.module { display: none; max-width: 1100px; margin: 0 auto; } +.module[data-active="true"] { display: block; animation: fadeIn .25s ease; } +@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } } + +.mod-head { margin-bottom: 28px; } +.mod-head h1 { + margin: 0 0 8px; font-size: 28px; font-weight: 600; letter-spacing: -0.5px; + background: linear-gradient(90deg, var(--text), var(--teal-c)); + -webkit-background-clip: text; background-clip: text; color: transparent; +} +.mod-head p { margin: 0; color: var(--text-mute); max-width: 720px; font-size: 14px; } +.mod-badge { + display: inline-flex; align-items: center; gap: 6px; + font-size: 11px; font-weight: 500; letter-spacing: .05em; text-transform: uppercase; + color: var(--teal-c); background: var(--teal-dim); + border: 1px solid rgba(34,211,238,.25); + padding: 4px 10px; border-radius: 99px; margin-bottom: 12px; +} + +/* Cards + surfaces */ +.card { + background: var(--bg-elev); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 20px; + transition: var(--t); +} +.card-hover:hover { border-color: rgba(34,211,238,.4); transform: translateY(-1px); cursor: pointer; } + +.btn { + padding: 9px 16px; border-radius: 8px; + font-weight: 500; cursor: pointer; + border: 1px solid var(--line); + background: var(--bg-elev2); color: var(--text); + transition: var(--t); font-size: 14px; +} +.btn:hover:not(:disabled) { border-color: var(--teal); color: var(--teal-c); } +.btn:disabled { opacity: .4; cursor: not-allowed; } +.btn-primary { + background: linear-gradient(135deg, var(--teal), var(--teal-b)); + border: none; color: #001013; font-weight: 600; +} +.btn-primary:hover:not(:disabled) { filter: brightness(1.15); color: #001013; } +.btn-ghost { background: transparent; border: 1px solid var(--line); } +.btn-sm { padding: 6px 12px; font-size: 13px; } +.btn-ask-kai { + background: transparent; + border: 1px dashed rgba(34,211,238,.4); + color: var(--teal-c); + padding: 8px 14px; font-size: 13px; + border-radius: 8px; cursor: pointer; transition: var(--t); + display: inline-flex; align-items: center; gap: 6px; +} +.btn-ask-kai:hover { background: var(--teal-dim); border-style: solid; } + +/* ============ REIFEGRAD ============ */ +.reifegrad-intro { + display: grid; gap: 10px; margin-bottom: 20px; + padding: 16px 20px; + background: linear-gradient(135deg, rgba(8,145,178,.08), rgba(18,18,26,.6)); + border: 1px solid var(--line); + border-radius: var(--radius); +} +.reifegrad-intro .intro-stats { + display: flex; gap: 16px; flex-wrap: wrap; + font-size: 12px; color: var(--text-mute); +} +.reifegrad-intro .intro-stats span strong { color: var(--teal-c); font-variant-numeric: tabular-nums; margin-right: 4px; } + +.dim-grid { + display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 14px; margin-bottom: 24px; +} +.dim-card { + background: var(--bg-elev); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 16px 18px; + cursor: pointer; transition: var(--t); + position: relative; overflow: hidden; +} +.dim-card:hover { border-color: rgba(34,211,238,.35); transform: translateY(-1px); } +.dim-card.done { border-color: rgba(16,185,129,.45); } +.dim-card.done::after { + content: "✓"; position: absolute; top: 10px; right: 12px; + color: var(--ok); font-size: 14px; font-weight: 700; +} +.dim-card h3 { margin: 0 0 6px; font-size: 15px; font-weight: 600; color: var(--text); } +.dim-card p { margin: 0; color: var(--text-mute); font-size: 12.5px; line-height: 1.4; } +.dim-card .dim-score { + display: flex; align-items: center; gap: 6px; + margin-top: 10px; font-size: 12px; + color: var(--teal-c); font-variant-numeric: tabular-nums; +} +.dim-card .dim-score .mini-bar { + flex: 1; height: 4px; background: var(--bg-elev3); border-radius: 2px; overflow: hidden; +} +.dim-card .dim-score .mini-bar-fill { + height: 100%; background: linear-gradient(90deg, var(--teal), var(--teal-c)); + transition: width .3s ease; +} + +/* Assessment question view */ +.assessment { + background: var(--bg-elev); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 24px 28px; + margin-bottom: 24px; +} +.assessment-nav { + display: flex; justify-content: space-between; align-items: center; + margin-bottom: 20px; font-size: 12px; color: var(--text-mute); +} +.assessment-nav button.linklike { + background: transparent; border: none; color: var(--teal-c); cursor: pointer; font-size: 13px; +} +.assessment-nav button.linklike:hover { text-decoration: underline; } +.assessment-progress { + height: 4px; background: var(--bg-elev3); border-radius: 99px; overflow: hidden; margin-bottom: 22px; +} +.assessment-progress-fill { height: 100%; background: linear-gradient(90deg, var(--teal), var(--teal-c)); transition: width .3s ease; } +.question-block h2 { margin: 0 0 6px; font-size: 20px; font-weight: 600; letter-spacing: -.3px; } +.question-block .question-hint { color: var(--text-mute); font-size: 13px; margin-bottom: 22px; } +.likert { + display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; + margin-bottom: 16px; +} +.likert-btn { + background: var(--bg-elev2); + border: 1px solid var(--line); + color: var(--text); padding: 14px 10px; + border-radius: 8px; cursor: pointer; transition: var(--t); + display: flex; flex-direction: column; gap: 4px; text-align: center; +} +.likert-btn:hover { border-color: var(--teal); } +.likert-btn[aria-pressed="true"] { + background: linear-gradient(135deg, rgba(8,145,178,.3), rgba(8,145,178,.1)); + border-color: var(--teal-c); color: var(--teal-c); +} +.likert-btn .likert-n { font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; } +.likert-btn .likert-t { font-size: 11px; color: var(--text-mute); } +.likert-btn[aria-pressed="true"] .likert-t { color: var(--teal-c); } + +.q-nav { display: flex; justify-content: space-between; margin-top: 10px; gap: 10px; } + +/* Radar + results */ +.radar-wrap { + background: var(--bg-elev); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 24px; + display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 30px; + align-items: center; + margin-bottom: 20px; +} +.radar-legend { display: flex; flex-direction: column; gap: 10px; } +.radar-legend .legend-row { display: flex; align-items: center; gap: 10px; font-size: 13px; } +.radar-legend .legend-swatch { width: 14px; height: 14px; border-radius: 3px; flex-shrink: 0; } +.radar-score-big { font-size: 36px; font-weight: 600; color: var(--teal-c); margin: 4px 0 2px; font-variant-numeric: tabular-nums; } +.radar-score-hint { color: var(--text-mute); font-size: 12px; margin-bottom: 16px; } + +.recos { display: grid; gap: 12px; margin-top: 14px; } +.reco-item { + display: grid; grid-template-columns: auto 1fr; gap: 14px; + background: var(--bg-elev); border: 1px solid var(--line); + border-radius: var(--radius); padding: 16px 18px; align-items: start; +} +.reco-num { width: 30px; height: 30px; border-radius: 50%; background: var(--teal-dim); color: var(--teal-c); display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 14px; } +.reco-item h4 { margin: 0 0 4px; font-size: 14px; font-weight: 600; } +.reco-item p { margin: 0 0 10px; color: var(--text-mute); font-size: 13px; } + +/* ============ AI ACT CLASSIFIER ============ */ +.wizard { + background: var(--bg-elev); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 28px; max-width: 720px; margin: 0 auto; +} +.wizard .progress-dots { + display: flex; gap: 6px; justify-content: center; margin-bottom: 22px; +} +.wizard .progress-dots span { + width: 8px; height: 8px; border-radius: 50%; background: var(--bg-elev3); transition: var(--t); +} +.wizard .progress-dots span.active { background: var(--teal-c); } +.wizard .progress-dots span.done { background: var(--teal); } + +.wizard-q h2 { margin: 0 0 8px; font-size: 22px; font-weight: 600; letter-spacing: -.3px; } +.wizard-q p { margin: 0 0 22px; color: var(--text-mute); font-size: 14px; } + +.yesno-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } +.yesno-btn { + padding: 22px; border-radius: var(--radius); + background: var(--bg-elev2); + border: 1px solid var(--line); + color: var(--text); cursor: pointer; transition: var(--t); + font-size: 16px; font-weight: 500; +} +.yesno-btn:hover { border-color: var(--teal); transform: translateY(-1px); } +.yesno-btn.yes:hover { border-color: var(--err); color: var(--err); } +.yesno-btn.no:hover { border-color: var(--teal-c); color: var(--teal-c); } + +.verdict { + padding: 28px; border-radius: var(--radius); + border: 1px solid var(--line); + background: var(--bg-elev); +} +.verdict .verdict-label { + display: inline-block; padding: 4px 12px; border-radius: 99px; + font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; + margin-bottom: 14px; +} +.verdict.prohibited { border-color: var(--err); background: rgba(239,68,68,.06); } +.verdict.prohibited .verdict-label { background: rgba(239,68,68,.18); color: var(--err); } +.verdict.high { border-color: var(--warn); background: rgba(245,158,11,.06); } +.verdict.high .verdict-label { background: rgba(245,158,11,.18); color: var(--warn); } +.verdict.limited { border-color: var(--teal); background: rgba(8,145,178,.06); } +.verdict.limited .verdict-label { background: rgba(8,145,178,.2); color: var(--teal-c); } +.verdict.minimal { border-color: var(--ok); background: rgba(16,185,129,.06); } +.verdict.minimal .verdict-label { background: rgba(16,185,129,.18); color: var(--ok); } +.verdict h2 { margin: 0 0 10px; font-size: 26px; font-weight: 600; } +.verdict p.verdict-summary { margin: 0 0 18px; color: var(--text-mute); } +.verdict .obligation-list { + list-style: none; padding: 0; margin: 0 0 22px; display: grid; gap: 10px; +} +.verdict .obligation-list li { + padding: 10px 14px; background: rgba(255,255,255,.02); + border-left: 3px solid var(--teal-c); + border-radius: 4px; font-size: 13px; color: var(--text); +} +.verdict .obligation-list li strong { color: var(--teal-c); margin-right: 8px; } +.verdict-run-counter { color: var(--text-dim); font-size: 12px; margin-top: 14px; } + +/* ============ DASHBOARD ============ */ +.dash-group { margin-bottom: 26px; } +.dash-group-title { + font-size: 12px; letter-spacing: .08em; text-transform: uppercase; + color: var(--text-dim); margin-bottom: 10px; + display: flex; align-items: center; gap: 10px; +} +.dash-group-title::after { content: ""; flex: 1; height: 1px; background: var(--line); } +.dash-grid { display: grid; gap: 14px; } +.dash-grid.exec { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); } +.dash-grid.ops { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } +.dash-grid.tech { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } +.tile { + background: var(--bg-elev); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 18px 20px; cursor: pointer; transition: var(--t); + text-align: left; display: flex; flex-direction: column; gap: 6px; + position: relative; overflow: hidden; +} +.tile:hover { border-color: rgba(34,211,238,.4); transform: translateY(-2px); box-shadow: var(--shadow); } +.tile.seen { border-color: rgba(34,211,238,.25); } +.tile.seen::before { + content: ""; position: absolute; top: 10px; right: 12px; + width: 6px; height: 6px; border-radius: 50%; background: var(--teal-c); +} +.tile.exec { padding: 22px 24px; } +.tile-label { font-size: 12px; color: var(--text-mute); letter-spacing: .02em; } +.tile-value { font-size: 26px; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; letter-spacing: -0.5px; } +.tile.exec .tile-value { font-size: 34px; } +.tile-trend { + font-size: 11px; display: flex; align-items: center; gap: 4px; + color: var(--text-mute); font-variant-numeric: tabular-nums; +} +.tile-trend.up { color: var(--ok); } +.tile-trend.down { color: var(--err); } +.tile-trend.neutral { color: var(--text-mute); } + +/* ============ MODAL ============ */ +.modal-root { + position: fixed; inset: 0; z-index: 200; + display: none; align-items: center; justify-content: center; + padding: 20px; background: rgba(0,0,0,.7); backdrop-filter: blur(6px); +} +.modal-root.open { display: flex; animation: fadeIn .2s; } +.modal { + background: var(--bg-elev); + border: 1px solid var(--line); + border-radius: 12px; + padding: 26px 28px; + max-width: 560px; width: 100%; max-height: 90vh; overflow-y: auto; + box-shadow: var(--shadow); + animation: modalIn .2s ease; +} +@keyframes modalIn { from { transform: translateY(8px); opacity: 0; } to { transform: none; opacity: 1; } } +.modal-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 14px; gap: 16px; } +.modal-head h3 { margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -.3px; } +.modal-head .modal-close { background: transparent; border: none; color: var(--text-mute); font-size: 22px; cursor: pointer; line-height: 1; } +.modal-head .modal-close:hover { color: var(--text); } +.modal-value { + font-size: 36px; font-weight: 600; color: var(--teal-c); + margin-bottom: 4px; font-variant-numeric: tabular-nums; +} +.modal-value-sub { color: var(--text-mute); font-size: 13px; margin-bottom: 18px; } +.modal p { margin: 0 0 14px; font-size: 13.5px; color: var(--text-mute); line-height: 1.5; } +.modal p strong { color: var(--text); } +.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; flex-wrap: wrap; } + +/* ============ CHAT DOCK ============ */ +.chat-dock { + border-left: 1px solid var(--line); + background: rgba(18,18,26,.6); + display: flex; flex-direction: column; + position: sticky; top: 64px; height: calc(100vh - 64px - 44px); + overflow: hidden; +} +.dock-head { + display: flex; align-items: baseline; gap: 8px; + padding: 14px 18px; border-bottom: 1px solid var(--line); + background: rgba(8,145,178,.06); +} +.dock-title { font-weight: 600; color: var(--teal-c); } +.dock-sub { font-size: 11px; color: var(--text-dim); flex: 1; } +.dock-reset, .dock-collapse { + background: transparent; border: 1px solid var(--line); + color: var(--text-mute); width: 26px; height: 26px; + border-radius: 6px; cursor: pointer; font-size: 12px; + display: flex; align-items: center; justify-content: center; + transition: var(--t); +} +.dock-reset:hover, .dock-collapse:hover { color: var(--text); border-color: var(--teal); } +.dock-box { + flex: 1; overflow-y: auto; padding: 14px; + display: flex; flex-direction: column; gap: 10px; + scrollbar-width: thin; scrollbar-color: var(--line) transparent; +} +.dock-box::-webkit-scrollbar { width: 6px; } +.dock-box::-webkit-scrollbar-thumb { background: var(--line); border-radius: 3px; } +.dock-msg { + padding: 10px 12px; border-radius: 10px; + font-size: 13px; line-height: 1.5; max-width: 95%; + word-wrap: break-word; overflow-wrap: break-word; +} +.dock-msg.user { + align-self: flex-end; + background: linear-gradient(135deg, var(--teal), var(--teal-b)); + color: #001013; + border-bottom-right-radius: 3px; +} +.dock-msg.bot { + align-self: flex-start; + background: var(--bg-elev2); + border: 1px solid var(--line); + border-bottom-left-radius: 3px; +} +.dock-msg.bot p { margin: 0 0 8px; } +.dock-msg.bot p:last-child { margin-bottom: 0; } +.dock-msg.bot ul, .dock-msg.bot ol { margin: 6px 0; padding-left: 20px; } +.dock-msg.bot li { margin-bottom: 2px; } +.dock-msg.bot code { font-size: .88em; } +.dock-msg.bot pre { background: #0d0d14; border: 1px solid var(--line-soft); padding: 10px; border-radius: 6px; overflow-x: auto; font-size: .85em; margin: 8px 0; } +.dock-msg.err { + background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.3); color: #fca5a5; +} +.dock-msg.sys { + font-size: 12px; color: var(--text-dim); + background: transparent; border: 1px dashed var(--line); align-self: center; text-align: center; +} + +.dots { display: inline-flex; gap: 4px; padding: 4px 0; } +.dots span { + width: 6px; height: 6px; border-radius: 50%; background: var(--teal-c); + animation: bounce 1.2s infinite ease-in-out; +} +.dots span:nth-child(2) { animation-delay: .15s; } +.dots span:nth-child(3) { animation-delay: .3s; } +@keyframes bounce { 0%, 80%, 100% { transform: translateY(0); opacity: .4; } 40% { transform: translateY(-4px); opacity: 1; } } + +.dock-form { + display: grid; grid-template-columns: 1fr auto; + gap: 8px; padding: 12px; border-top: 1px solid var(--line); + background: var(--bg-elev); +} +.dock-form textarea { + resize: none; min-height: 38px; max-height: 140px; + background: var(--bg-elev2); + border: 1px solid var(--line); + border-radius: 8px; color: var(--text); + padding: 9px 12px; font-family: inherit; font-size: 13px; + transition: var(--t); +} +.dock-form textarea:focus { outline: none; border-color: var(--teal-c); box-shadow: 0 0 0 3px rgba(34,211,238,.15); } +.btn-send { + background: linear-gradient(135deg, var(--teal), var(--teal-b)); + border: none; color: #001013; font-weight: 700; + width: 40px; height: 40px; border-radius: 8px; cursor: pointer; + font-size: 18px; transition: var(--t); +} +.btn-send:hover:not(:disabled) { filter: brightness(1.15); } +.btn-send:disabled { opacity: .4; cursor: not-allowed; } +.dock-footer-link { + display: block; padding: 8px 14px; font-size: 11px; color: var(--text-dim); + text-align: center; border-top: 1px solid var(--line); background: var(--bg-elev); +} +.dock-footer-link:hover { color: var(--teal-c); text-decoration: none; } + +/* Floating open button (mobile / collapsed state) */ +.dock-open { + display: none; + position: fixed; bottom: 24px; right: 24px; z-index: 60; + padding: 12px 18px; border-radius: 99px; + background: linear-gradient(135deg, var(--teal), var(--teal-b)); + color: #001013; font-weight: 600; border: none; + box-shadow: 0 8px 24px rgba(8,145,178,.5); + cursor: pointer; font-size: 14px; + align-items: center; gap: 8px; +} +.dock-open-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #001013; animation: pulse 1.4s infinite; } +@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } } +.chat-dock.collapsed { display: none; } +.chat-dock.collapsed + .dock-open { display: inline-flex; } + +/* Full chat module (⌃4) — reuses dock style but takes full width */ +.chat-full-wrap { max-width: 780px; margin: 0 auto; } +.chat-full-wrap p { color: var(--text-mute); font-size: 13px; margin-bottom: 18px; } +.chat-full-wrap .chat-full-empty { + display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 10px; margin: 16px 0 22px; +} +.chat-full-wrap .chip { + background: var(--bg-elev); border: 1px solid var(--line); + color: var(--text-mute); padding: 10px 14px; border-radius: 8px; + cursor: pointer; transition: var(--t); font-size: 13px; text-align: left; +} +.chat-full-wrap .chip:hover { border-color: var(--teal); color: var(--text); } + +/* ============ FOOTER ============ */ +.footer { + grid-area: foot; + padding: 12px 22px; + border-top: 1px solid var(--line); + text-align: center; + font-size: 12px; color: var(--text-dim); + background: var(--bg-elev); +} +.footer a { color: var(--text-mute); } + +/* ============ TOASTS ============ */ +.toast-stack { + position: fixed; top: 16px; right: 16px; z-index: 500; + display: flex; flex-direction: column; gap: 8px; pointer-events: none; +} +.toast { + padding: 10px 16px; border-radius: 8px; + background: var(--bg-elev2); border: 1px solid var(--line); + color: var(--text); font-size: 13px; min-width: 220px; + box-shadow: var(--shadow); animation: slideIn .3s ease; + pointer-events: auto; +} +.toast.success { border-color: var(--ok); color: #86efac; } +.toast.err { border-color: var(--err); color: #fca5a5; } +.toast.info { border-color: var(--teal-c); color: var(--teal-c); } +@keyframes slideIn { from { transform: translateX(20px); opacity: 0; } to { transform: none; opacity: 1; } } + +/* ============ BADGES (completion) ============ */ +.badges-row { + display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; +} +.badge-chip { + display: inline-flex; align-items: center; gap: 8px; + padding: 6px 12px; border-radius: 99px; + background: var(--bg-elev2); border: 1px solid var(--line); + font-size: 12px; color: var(--text-mute); +} +.badge-chip.earned { border-color: var(--teal-c); color: var(--teal-c); background: var(--teal-dim); } +.badge-chip .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-dim); } +.badge-chip.earned .dot { background: var(--teal-c); box-shadow: 0 0 6px var(--teal-c); } + +/* ============ RESPONSIVE ============ */ +@media (max-width: 1160px) { + .cockpit { grid-template-columns: 180px minmax(0, 1fr) 260px; --nav-w: 180px; --dock-w: 260px; } + .main { padding: 22px 24px 36px; } +} +@media (max-width: 960px) { + .cockpit { + grid-template-columns: 1fr; + grid-template-areas: "top" "nav" "main" "foot"; + } + .nav { + flex-direction: row; overflow-x: auto; padding: 12px; + border-right: none; border-bottom: 1px solid var(--line); + position: sticky; top: 70px; z-index: 30; + background: rgba(10,10,15,.95); backdrop-filter: blur(8px); + } + .nav-item { flex: 1 0 auto; grid-template-columns: 1fr; gap: 2px; text-align: center; padding: 8px 12px; } + .nav-num, .nav-kbd { display: none; } + .nav-label { font-size: 13px; } + .chat-dock { + position: fixed; top: 0; right: 0; bottom: 0; + width: 100%; max-width: 360px; + height: 100vh; z-index: 100; + box-shadow: -10px 0 40px rgba(0,0,0,.6); + transform: translateX(100%); transition: transform .3s ease; + } + .chat-dock.open { transform: translateX(0); } + .dock-open { display: inline-flex; } + .radar-wrap { grid-template-columns: 1fr; } + .yesno-row { grid-template-columns: 1fr; } + .topbar { flex-wrap: wrap; padding: 10px 14px; } + .xp-wrap { min-width: 140px; order: 3; } + .back-link { order: 2; } + .main { padding: 18px 14px 30px; } + .mod-head h1 { font-size: 22px; } +} +@media (max-width: 480px) { + .likert { grid-template-columns: repeat(5, 1fr); gap: 4px; } + .likert-btn { padding: 10px 4px; } + .likert-btn .likert-n { font-size: 16px; } + .likert-btn .likert-t { font-size: 9px; } + .brand-sub { display: none; } +} + +/* ============ A11Y ============ */ +.sr-only { + position: absolute; width: 1px; height: 1px; + padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); border: 0; +} +button:focus-visible, .nav-item:focus-visible, textarea:focus-visible, a:focus-visible { + outline: 2px solid var(--teal-c); outline-offset: 2px; +} + +/* SVG */ +.radar-svg { width: 100%; height: auto; display: block; } +.sparkline-svg { width: 100%; height: 80px; display: block; } + +/* GFM table (added 2026-04-24) */ +.md-table { + width: 100%; + border-collapse: collapse; + margin: .6rem 0; + font-size: .9em; + background: rgba(255,255,255,0.02); + border: 1px solid rgba(255,255,255,0.08); + border-radius: 6px; + overflow: hidden; +} +.md-table thead { + background: rgba(8,145,178,0.12); +} +.md-table th, +.md-table td { + padding: .5rem .7rem; + border-bottom: 1px solid rgba(255,255,255,0.06); + text-align: left; + vertical-align: top; +} +.md-table th { + color: #06b6d4; + font-weight: 600; + font-size: .78em; + letter-spacing: .04em; + 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; } +.chat-dock .md-table, +.dock-body .md-table { + display: block; + overflow-x: auto; + font-size: .82em; +} diff --git a/www/cockpit/cockpit.js b/www/cockpit/cockpit.js new file mode 100644 index 0000000..ae37090 --- /dev/null +++ b/www/cockpit/cockpit.js @@ -0,0 +1,1249 @@ +/* Kai Cockpit — vanilla JS, no framework, no build, no CDN. + * Shares XP/Level/Streak state with the Kai widget via localStorage key `kai.state.v1`. + * Cockpit-specific keys: kai.cockpit.reifegrad, kai.cockpit.aiact, kai.cockpit.dashboard, kai.cockpit.chat + */ +(() => { + 'use strict'; + + // ========= CONFIG ========= + const API = 'https://llm.qognio.com/api/bots/ki-kennzahlen-coach/chat'; + const RAW_KEY = window.__KAI_KEY__ || ''; + const KEY = /^qb_[a-zA-Z0-9]{6,}$/.test(RAW_KEY) ? RAW_KEY : ''; + + // Shared with widget + const LS_STATE = 'kai.state.v1'; + // Cockpit-specific + const LS_REIFE = 'kai.cockpit.reifegrad'; + const LS_AIACT = 'kai.cockpit.aiact'; + const LS_DASH = 'kai.cockpit.dashboard'; + const LS_CHAT = 'kai.cockpit.chat'; + + // ========= UTIL ========= + const $ = (s, r = document) => r.querySelector(s); + const $$ = (s, r = document) => Array.from(r.querySelectorAll(s)); + const today = () => { + const d = new Date(); + return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; + }; + const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + + // ========= SHARED STATE (with widget) ========= + function loadState() { + try { + const s = JSON.parse(localStorage.getItem(LS_STATE) || '{}'); + return Object.assign({ + 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: [] + }, s); + } catch (e) { + return { xp: 0, badges: {} }; + } + } + function saveState() { localStorage.setItem(LS_STATE, JSON.stringify(state)); } + let state = loadState(); + + function addXP(n, reason = '') { + state.xp = (state.xp || 0) + n; + saveState(); + toast(`+${n} XP${reason ? ' · ' + reason : ''}`, 'info', 2200); + renderXP(); + } + + const LEVELS = [ + { min: 0, title: 'AI-Trainee' }, + { min: 50, title: 'AI-Analyst:in' }, + { min: 200, title: 'ML-Engineer' }, + { min: 500, title: 'AI-Lead' }, + { min: 1250, title: 'AI-Officer' }, + { min: 2500, title: 'AI-Governance-Lead' }, + { min: 5000, title: 'Chief AI Officer' } + ]; + function levelInfo() { + let cur = LEVELS[0]; + for (const l of LEVELS) if ((state.xp || 0) >= 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 }; + } + function renderXP() { + const li = levelInfo(); + const lvl = $('#xp-level'); const xp = $('#xp-score'); const bar = $('#xp-bar-fill'); + if (lvl) lvl.textContent = `Lvl ${li.levelNum} · ${li.title}`; + if (xp) xp.textContent = `${state.xp || 0} XP`; + if (bar) bar.style.width = li.pct + '%'; + } + + // ========= BADGES ========= + const COCKPIT_BADGES = { + cockpit_reifegrad_done: 'Reifegrad-Profi', + cockpit_ai_act_done: 'AI-Act-Navigator (3 Systeme)', + cockpit_dashboard_explored: 'Dashboard-Flaneur:in', + cockpit_power_user: 'Cockpit-Power-User' + }; + function unlockBadge(id) { + if (state.badges[id]) return false; + state.badges[id] = today(); + saveState(); + const title = COCKPIT_BADGES[id] || id; + toast('🏆 Neues Abzeichen: ' + title, 'success', 4200); + // Power-user check + if (id !== 'cockpit_power_user') checkPowerUser(); + return true; + } + function checkPowerUser() { + if (state.badges.cockpit_reifegrad_done + && state.badges.cockpit_ai_act_done + && state.badges.cockpit_dashboard_explored) { + unlockBadge('cockpit_power_user'); + } + } + + // ========= TOAST ========= + function toast(msg, kind = '', ms = 3000) { + const stack = $('#toast-stack'); if (!stack) return; + const el = document.createElement('div'); + el.className = 'toast ' + kind; + el.textContent = msg; + stack.appendChild(el); + setTimeout(() => { el.style.transition = 'opacity .3s'; el.style.opacity = '0'; setTimeout(() => el.remove(), 300); }, ms); + } + + // ========= MARKDOWN (light + GFM tables) ========= + function renderMD(src) { + let t = esc(src); + // code fences + t = t.replace(/```([\s\S]*?)```/g, (_, c) => `
    ${c}
    `); + // inline code + t = t.replace(/`([^`]+)`/g, '$1'); + // GFM tables — consume before line-based processing + t = t.replace(/(?:^|\n)((?:\|[^\n]*\|[ \t]*\n){2,})/g, (block, content) => { + const ls = content.trim().split('\n'); + if (ls.length < 2) return block; + const sep = ls[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(ls[0]); + const aligns = parseRow(sep).map(s => /^:-+:$/.test(s) ? 'center' : /-+:$/.test(s) ? 'right' : 'left'); + const rows = ls.slice(2).map(parseRow); + let h = '\n'; + header.forEach((hd, i) => { h += ``; }); + h += ''; + rows.forEach(r => { + h += ''; + for (let i = 0; i < Math.max(r.length, header.length); i++) { + h += ``; + } + h += ''; + }); + h += '
    ${hd}
    ${r[i] || ''}
    \n'; + return h; + }); + // bold + italic + t = t.replace(/\*\*([^\*]+)\*\*/g, '$1'); + t = t.replace(/\*([^\*]+)\*/g, '$1'); + // headings (very lightweight) + t = t.replace(/^###\s+(.*)$/gm, '$1'); + t = t.replace(/^##\s+(.*)$/gm, '$1'); + t = t.replace(/^#\s+(.*)$/gm, '$1'); + // bullets + paragraphs (skip inside tables/pre) + const lines = t.split('\n'); const out = []; let inUL = false; let inSkip = false; + for (const l of lines) { + // Skip line-processing while inside or
     blocks
    +      if (/'); inUL = false; }
    +        out.push(l);
    +        if (/<\/table>/.test(l) || /<\/pre>/.test(l)) inSkip = false;
    +        continue;
    +      }
    +      if (/^[-*]\s+/.test(l)) {
    +        if (!inUL) { out.push('
      '); inUL = true; } + out.push('
    • ' + l.replace(/^[-*]\s+/, '') + '
    • '); + } else { + if (inUL) { out.push('
    '); inUL = false; } + if (l.trim()) out.push('

    ' + l + '

    '); + } + } + if (inUL) out.push(''); + return out.join(''); + } + + // ===================================================================== + // MODULE 1 — REIFEGRAD ASSESSMENT + // ===================================================================== + const DIMENSIONS = [ + { + id: 'strategy', + title: 'Strategie', + short: 'Wo steht KI in eurer Unternehmens-Strategie?', + questions: [ + 'Hat euer Unternehmen eine dokumentierte KI-Strategie mit Executive-Sponsorship?', + 'Sind AI-Use-Cases nach Business-Value priorisiert und in einem Portfolio gelistet?', + 'Gibt es ein klares Budget-Committment für AI über 12 Monate hinaus?', + 'Ist AI Teil der Digital- oder Geschäftsstrategie (nicht nur IT-Plan)?', + 'Wird der ROI von AI-Initiativen auf Portfolio-Ebene an den Vorstand reported?' + ] + }, + { + id: 'data', + title: 'Daten', + short: 'Wie solide ist euer Daten-Fundament?', + questions: [ + 'Existiert ein zentraler Data-Catalog mit Lineage für relevante Datensätze?', + 'Wird Data-Quality (Completeness, Accuracy, Freshness) systematisch gemessen?', + 'Habt ihr dokumentierte Data-Governance-Rollen (Data-Owner, Steward)?', + 'Ist Master-Data für Kunden/Produkte/Mitarbeitende harmonisiert?', + 'Sind Trainingsdaten versioniert und auf Bias/Verzerrungen gescreent (Art. 10 AI Act)?' + ] + }, + { + id: 'technology', + title: 'Technologie', + short: 'MLOps, Registry, Monitoring — wie tief ist euer Stack?', + questions: [ + 'Habt ihr eine MLOps-Plattform (CI/CD für ML, nicht nur Ad-hoc-Notebooks)?', + 'Gibt es eine zentrale Model-Registry mit Versionierung und Approval-Flow?', + 'Sind Inferenz-Pipelines auf Latenz, Throughput und Error-Rate beobachtet?', + 'Läuft automatisches Drift-Monitoring (Daten- oder Modell-Drift) in Produktion?', + 'Ist eure Inferenz-Infrastruktur skalierbar (Cloud/On-Prem/Hybrid) dokumentiert?' + ] + }, + { + id: 'people', + title: 'Menschen', + short: 'Skills, Rollen, AI-Literacy.', + questions: [ + 'Habt ihr einen AI-/ML-Engineer(in) oder ein Data-Science-Team (intern)?', + 'Gibt es AI-Literacy-Training für Non-Tech-Mitarbeitende (Art. 4 AI Act)?', + 'Existieren klare Rollen wie CAIO / AI-Risk-Officer / Data-Owner?', + 'Wird externe Expertise (Consulting, Fachanwalt IT-Recht) strukturiert eingebunden?', + 'Wird AI-Kompetenz in Job-Profile und Hiring-Prozesse integriert?' + ] + }, + { + id: 'process', + title: 'Prozesse', + short: 'Deployment, Monitoring, Incident-Response.', + questions: [ + 'Habt ihr standardisierte Deployment-Prozesse (Design → Train → Deploy → Retire)?', + 'Existiert ein AI-Incident-Response-Plan inkl. Art. 73 AI-Act-Meldewege?', + 'Gibt es ein jährliches AI-Program-Review mit Vorstand oder Geschäftsführung?', + 'Wurden FRIA (Art. 27) / DSFA (Art. 35 DSGVO) für relevante Systeme durchgeführt?', + 'Ist EU AI Act-Klassifikation für ALLE produktiven Systeme abgeschlossen?' + ] + } + ]; + + // Scale: 0 nicht vorhanden / 1 ad-hoc / 2 geplant / 3 etabliert / 4 optimiert + const LIKERT = [ + { v: 0, t: 'nicht vorhanden' }, + { v: 1, t: 'ad-hoc' }, + { v: 2, t: 'geplant' }, + { v: 3, t: 'etabliert' }, + { v: 4, t: 'optimiert' } + ]; + const BENCHMARK = { strategy: 2.1, data: 1.8, technology: 2.3, people: 1.6, process: 1.9 }; + + function loadReifegrad() { + try { return JSON.parse(localStorage.getItem(LS_REIFE) || '{}'); } catch (e) { return {}; } + } + function saveReifegrad(data) { localStorage.setItem(LS_REIFE, JSON.stringify(data)); } + + const reifeState = { + view: 'overview', // 'overview' | 'assess' | 'result' + currentDim: null, + currentQ: 0, + answers: loadReifegrad() // { dimId: [v,v,v,v,v] } + }; + + function reifeScore(dimId) { + const arr = reifeState.answers[dimId]; + if (!arr || arr.length !== 5 || arr.some(v => v == null)) return null; + return arr.reduce((s, v) => s + v, 0) / arr.length; + } + function reifeDimDone(dimId) { + const s = reifeScore(dimId); + return s != null; + } + function reifeCountDone() { return DIMENSIONS.filter(d => reifeDimDone(d.id)).length; } + function reifeOverallScore() { + const scores = DIMENSIONS.map(d => reifeScore(d.id)).filter(v => v != null); + if (!scores.length) return 0; + return scores.reduce((s, v) => s + v, 0) / scores.length; + } + + function renderReifegradModule() { + const host = $('#reifegrad-host'); + if (reifeState.view === 'overview') return renderReifeOverview(host); + if (reifeState.view === 'assess') return renderReifeAssessment(host); + if (reifeState.view === 'result') return renderReifeResult(host); + } + + function renderReifeOverview(host) { + const done = reifeCountDone(); + const overall = reifeOverallScore(); + host.innerHTML = ` +
    + Modul 01 +

    Reifegrad-Assessment

    +

    5-Dimensionen-Self-Assessment auf Basis von Gartner + MIT CISR + Microsoft RAI-MM. Pro Dimension 5 Fragen, 0–4 Likert. Ergibt einen Radar gegen den DACH-Mittelstand-Durchschnitt (BITKOM/Fraunhofer 2024).

    +
    +
    +
    + ${done}/5 Dimensionen beantwortet + ${overall.toFixed(2)} / 4.00 Overall-Score + ${done === 5 ? 'komplett' : 'in Arbeit'} +
    +
    +
    + ${DIMENSIONS.map(d => { + const s = reifeScore(d.id); + const pct = s != null ? (s / 4) * 100 : 0; + const done = s != null; + return ` + + `; + }).join('')} +
    +
    + + ${done === 5 ? `` : ''} + ${done > 0 ? `` : ''} +
    + ${done === 5 ? renderBadgeRow() : ''} + `; + // Events + $$('.dim-card', host).forEach(el => el.addEventListener('click', () => { + reifeState.currentDim = el.dataset.dim; + reifeState.currentQ = 0; + reifeState.view = 'assess'; + renderReifegradModule(); + })); + const startBtn = $('#reife-start', host); + if (startBtn) startBtn.addEventListener('click', () => { + if (done === 5) { reifeState.view = 'result'; renderReifegradModule(); return; } + // find first un-done dim + const next = DIMENSIONS.find(d => !reifeDimDone(d.id)) || DIMENSIONS[0]; + reifeState.currentDim = next.id; + reifeState.currentQ = 0; + reifeState.view = 'assess'; + renderReifegradModule(); + }); + const resBtn = $('#reife-result', host); + if (resBtn) resBtn.addEventListener('click', () => { reifeState.view = 'result'; renderReifegradModule(); }); + const resetBtn = $('#reife-reset', host); + if (resetBtn) resetBtn.addEventListener('click', () => { + if (confirm('Alle Antworten löschen?')) { + reifeState.answers = {}; + saveReifegrad({}); + renderReifegradModule(); + } + }); + } + + function renderBadgeRow() { + const ids = Object.keys(COCKPIT_BADGES); + return `
    + ${ids.map(id => { + const earned = !!state.badges[id]; + return `${esc(COCKPIT_BADGES[id])}`; + }).join('')} +
    `; + } + + function renderReifeAssessment(host) { + const dim = DIMENSIONS.find(d => d.id === reifeState.currentDim); + if (!dim) { reifeState.view = 'overview'; return renderReifegradModule(); } + const qi = reifeState.currentQ; + const q = dim.questions[qi]; + const answers = reifeState.answers[dim.id] || Array(5).fill(null); + const selected = answers[qi]; + const total = dim.questions.length; + const pct = ((qi) / total) * 100; + + host.innerHTML = ` +
    + Dimension · ${esc(dim.title)} +

    ${esc(dim.title)}

    +

    ${esc(dim.short)}

    +
    +
    +
    + + Frage ${qi + 1} / ${total} +
    +
    +
    +

    ${esc(q)}

    +

    Skala: 0 = nicht vorhanden, 4 = optimiert & firmenweit verankert

    +
    + ${LIKERT.map(l => ` + + `).join('')} +
    +
    + + +
    +
    +
    +
    + +
    + `; + $('#reife-back', host).addEventListener('click', () => { reifeState.view = 'overview'; renderReifegradModule(); }); + $$('.likert-btn', host).forEach(b => b.addEventListener('click', () => { + const v = parseInt(b.dataset.v, 10); + if (!reifeState.answers[dim.id]) reifeState.answers[dim.id] = Array(5).fill(null); + const wasEmpty = reifeState.answers[dim.id][qi] == null; + reifeState.answers[dim.id][qi] = v; + saveReifegrad(reifeState.answers); + if (wasEmpty) addXP(2, 'Antwort'); + renderReifeAssessment(host); + })); + const prevBtn = $('#q-prev', host); + if (prevBtn) prevBtn.addEventListener('click', () => { reifeState.currentQ = Math.max(0, qi - 1); renderReifeAssessment(host); }); + const nextBtn = $('#q-next', host); + if (nextBtn) nextBtn.addEventListener('click', () => { + if (qi < total - 1) { + reifeState.currentQ = qi + 1; + renderReifeAssessment(host); + } else { + // dim completed + const justCompleted = reifeDimDone(dim.id); + const prevDoneCount = DIMENSIONS.filter(d => d.id !== dim.id && reifeDimDone(d.id)).length; + if (justCompleted) { + // +20 per dim (first completion only) handled by checking badges flag per dim + const key = `reife_dim_${dim.id}`; + if (!state.badges[key]) { + state.badges[key] = today(); + saveState(); + addXP(20, `Dimension ${dim.title} komplett`); + } + } + const allDone = reifeCountDone() === 5; + if (allDone && !state.badges.cockpit_reifegrad_done) { + addXP(50, 'Reifegrad-Assessment abgeschlossen'); + unlockBadge('cockpit_reifegrad_done'); + } + reifeState.view = allDone ? 'result' : 'overview'; + renderReifegradModule(); + } + }); + $('#ask-kai-dim', host).addEventListener('click', () => { + openDock(); + const prompt = `Was sind die wichtigsten Hebel, um in der Dimension "${dim.title}" vom DACH-Mittelstand (Durchschnitt ${BENCHMARK[dim.id].toFixed(1)}) auf Stufe 3 (etabliert) zu kommen?`; + fillDockInput(prompt); + }); + } + + function renderReifeResult(host) { + const scores = DIMENSIONS.map(d => ({ dim: d, score: reifeScore(d.id) || 0 })); + const overall = reifeOverallScore(); + // Find 3 weakest for recommendations + const weakest = [...scores].sort((a, b) => a.score - b.score).slice(0, 3); + + host.innerHTML = ` +
    + Ergebnis +

    Dein Reifegrad-Profil

    +

    Radar gegen DACH-Mittelstand (BITKOM 2024: ~Gartner-Stufe 2 Mittelwert). Die 3 schwächsten Achsen sind deine Roadmap.

    +
    +
    +
    ${renderRadarSVG(scores)}
    +
    +
    ${overall.toFixed(2)} / 4.00
    +
    ${overallLabel(overall)}
    +
    +
    Deine Werte
    +
    DACH-Mittelstand Ø
    +
    +
    + + +
    +
    +
    +

    Top-Empfehlungen

    +
    + ${weakest.map((w, i) => { + const reco = weakestReco(w.dim.id, w.score); + return ` +
    +
    ${i + 1}
    +
    +

    ${esc(w.dim.title)} · ${w.score.toFixed(1)} / 4.0

    +

    ${esc(reco)}

    + +
    +
    + `; + }).join('')} +
    + ${renderBadgeRow()} + `; + $('#reife-overview-btn', host).addEventListener('click', () => { reifeState.view = 'overview'; renderReifegradModule(); }); + $('#ask-kai-overall', host).addEventListener('click', () => { + openDock(); + const txt = `Mein Reifegrad-Assessment ergab: ${scores.map(s => `${s.dim.title} ${s.score.toFixed(1)}`).join(', ')}. Gesamt ${overall.toFixed(2)}/4.0. Erstelle mir eine priorisierte 12-Monats-Roadmap für die schwächsten 3 Dimensionen.`; + fillDockInput(txt); + }); + $$('[data-dim]', host).forEach(b => { + if (b.classList.contains('btn-ask-kai')) { + b.addEventListener('click', () => { + const d = DIMENSIONS.find(dd => dd.id === b.dataset.dim); + if (!d) return; + openDock(); + fillDockInput(`Meine Dimension "${d.title}" liegt bei ${(reifeScore(d.id) || 0).toFixed(1)} / 4.0 (DACH-Ø ${BENCHMARK[d.id]}). Was sind die 3 wichtigsten Maßnahmen in den nächsten 90 Tagen?`); + }); + } + }); + } + + function overallLabel(s) { + if (s < 1) return 'Foundational / Experimenting — Start beim AI-Strategie-Dokument + Sponsorship.'; + if (s < 2) return 'Emerging / Piloting — 3-5 Pilots mit klaren Business-Cases als Fokus.'; + if (s < 2.8) return 'Operational / Scaling — der kritische Sprung. MLOps + Monitoring konsolidieren.'; + if (s < 3.5) return 'Scaled — Platform + AI-Engineering-Team in Produktion.'; + return 'Transformational / Optimizing — AI ist Teil des Geschäftsmodells.'; + } + + function weakestReco(dimId, score) { + const low = { + strategy: 'AI-Charter in 2-3 Seiten schreiben, vom CEO unterschreiben lassen. Quartals-Review einführen. Use-Case-Intake-Template verpflichtend ab Tag 1.', + data: 'Data-Catalog aktivieren (Open-Source-Stack reicht initial). Data-Quality-Scorecard für Top-5-Datensätze. Data-Owner benennen.', + technology: 'MLOps auf 1 Pattern einkochen (z.B. Training + Registry + Serving). Modell-Drift-Monitoring als Muss vor jedem Go-Live.', + people: 'AI-Literacy-Training für alle Mitarbeitenden (Art. 4 AI-Act-Pflicht). AI-Risk-Officer benennen — reicht initial als 20%-Rolle.', + process: 'Model-Lifecycle-Standard auf 1 Seite. EU-AI-Act-Klassifikation für alle Systeme durchführen. Incident-Response-Plan einmal durchspielen.' + }; + const mid = { + strategy: 'Portfolio-Review quartalsweise: Welcher Use-Case wirft ROI? Welcher wird retiret? Business-Case-Templates standardisieren.', + data: 'Data-Lineage end-to-end (nicht nur Katalog-Eintrag). Trainingsdaten auf Bias screenen (Art. 10 AI-Act-Pflicht für High-Risk).', + technology: 'Von Ad-hoc-Monitoring zu Self-Service-Observability. Model-Registry mit Approval-Workflow. Automated Retrain-Pipelines.', + people: 'CAIO oder AI-Risk-Officer in Vollzeit. Hiring-Profil überarbeiten. Externe Fachanwalt-IT-Recht als Retainer.', + process: 'FRIA + DSFA kombiniert durchführen. Jährlicher Program-Review mit Vorstand. ISO 42001 als Rahmen adaptieren.' + }; + return score < 2 ? low[dimId] : mid[dimId]; + } + + // ========= RADAR SVG ========= + function renderRadarSVG(scoresArr) { + const size = 420; const cx = size / 2; const cy = size / 2; + const radius = 150; const rings = 4; // 0-4 scale, 4 rings + const n = scoresArr.length; + const angleFor = (i) => (-Math.PI / 2) + (i * 2 * Math.PI / n); + + const pointAt = (i, val) => { + const a = angleFor(i); + const r = (val / 4) * radius; + return [cx + r * Math.cos(a), cy + r * Math.sin(a)]; + }; + + // Grid + let grid = ''; + for (let r = 1; r <= rings; r++) { + const pts = []; + for (let i = 0; i < n; i++) { + const a = angleFor(i); + const rr = (r / rings) * radius; + pts.push([cx + rr * Math.cos(a), cy + rr * Math.sin(a)]); + } + grid += ``; + } + + // Axes + labels + let axes = ''; let labels = ''; + for (let i = 0; i < n; i++) { + const [x, y] = pointAt(i, 4); + axes += ``; + const a = angleFor(i); + const lx = cx + (radius + 26) * Math.cos(a); + const ly = cy + (radius + 26) * Math.sin(a); + labels += `${esc(scoresArr[i].dim.title)}`; + } + + // Benchmark polygon + const benchPts = scoresArr.map((s, i) => pointAt(i, BENCHMARK[s.dim.id] || 2)); + const bench = ``; + + // User polygon + const userPts = scoresArr.map((s, i) => pointAt(i, s.score)); + const user = ``; + const dots = scoresArr.map((s, i) => { + const [x, y] = pointAt(i, s.score); + return ``; + }).join(''); + + return ` + ${grid} + ${axes} + ${bench} + ${user} + ${dots} + ${labels} + `; + } + + // ===================================================================== + // MODULE 2 — EU AI ACT CLASSIFIER + // ===================================================================== + // Simple decision tree — 7 yes/no questions, returns verdict object + const AIACT_TREE = [ + { + id: 'q1', key: 'prohibited', + q: 'Wird das System für Social-Scoring, Massenüberwachung, unterschwellige Manipulation oder Emotion-Recognition am Arbeitsplatz / in der Schule eingesetzt?', + hint: 'Art. 5 AI Act — „Prohibited Practices" gelten seit Februar 2025.', + yes: { verdict: 'prohibited' }, + no: 'q2' + }, + { + id: 'q2', key: 'safety_component', + q: 'Ist das System Sicherheitskomponente eines regulierten Produkts (Medizin-Device, Maschine, Fahrzeug, kritische Infrastruktur, Spielzeug)?', + hint: 'Art. 6 Abs. 1 — Weg A zu High-Risk via Anhang I.', + yes: { verdict: 'high', reason: 'safety_component' }, + no: 'q3' + }, + { + id: 'q3', key: 'annex_iii', + q: 'Wird das System für HR-Entscheidungen, Recruiting, Kredit-/Versicherungs-Scoring, Strafverfolgung, Bildungsbewertung, Migration oder Biometrie eingesetzt?', + hint: 'Anhang III Kategorien 1, 3, 4, 5, 6, 7. Deutsche Mittelständler unterschätzen HR-AI oft.', + yes: { verdict: 'high', reason: 'annex_iii' }, + no: 'q4' + }, + { + id: 'q4', key: 'narrow_task', + q: 'Erfüllt das System nur eine schmale, vorbereitende Aufgabe (z. B. reine Duplikat-Erkennung, Mustererkennung ohne Profiling)?', + hint: 'Art. 6 Abs. 3 — Opt-out aus High-Risk, muss dokumentiert werden.', + yes: { verdict: 'limited', reason: 'narrow_task_opt_out' }, + no: 'q5' + }, + { + id: 'q5', key: 'chatbot', + q: 'Ist es ein Chatbot, ein Deepfake-/Synthetic-Media-Generator oder interagiert direkt mit Endnutzer:innen als AI?', + hint: 'Art. 50 — Transparenzpflicht: „Du sprichst mit einer KI."', + yes: { verdict: 'limited', reason: 'chatbot' }, + no: 'q6' + }, + { + id: 'q6', key: 'pii', + q: 'Werden personenbezogene Daten verarbeitet oder zum Training verwendet?', + hint: 'DSGVO greift zusätzlich — Art. 10 AI Act + Art. 5 DSGVO synchron behandeln.', + yes: { verdict: 'minimal', reason: 'dsgvo_pflicht' }, + no: 'q7' + }, + { + id: 'q7', key: 'gpai', + q: 'Ist es ein General-Purpose-AI-Modell oder wird ein solches direkt in eurer Kontrolle trainiert (Foundation-Model mit > 10^25 FLOPs)?', + hint: 'Art. 51-56 GPAI-Regeln — für die meisten Deployer nicht relevant.', + yes: { verdict: 'minimal', reason: 'gpai_deployer' }, + no: { verdict: 'minimal', reason: 'default' } + } + ]; + + const aiactState = { + step: 0, + answers: {}, + verdict: null, + runs: (function() { try { return JSON.parse(localStorage.getItem(LS_AIACT) || '{}').runs || []; } catch (e) { return []; } })() + }; + function saveAIActRuns() { + localStorage.setItem(LS_AIACT, JSON.stringify({ runs: aiactState.runs })); + } + + const VERDICTS = { + prohibited: { + title: 'Verboten (Prohibited Practice)', + label: 'Art. 5 AI Act', + cls: 'prohibited', + summary: 'Dein System fällt unter Art. 5 und ist seit dem 2. Februar 2025 in der EU verboten. Weiterbetrieb ist keine Option.', + obligations: [ + 'Art. 5: Unverzüglicher Stopp und Abbau', + 'Art. 99: Strafen bis zu 35 Mio. EUR oder 7 % weltweiter Jahresumsatz (höher)', + 'Legal-Review + Information der Aufsichtsbehörde prüfen', + 'Alternative Implementierung ohne Art-5-Praktik entwerfen' + ] + }, + high: { + title: 'High-Risk', + label: 'Art. 6 + Anhang III', + cls: 'high', + summary: 'Umfangreiche Pflichten. Für Deployer + Provider unterschiedlich. Vollständige Anwendung ab 2. August 2026.', + obligations: [ + 'Art. 9: Risk-Management-System etablieren', + 'Art. 10: Data-Governance — Training/Validation/Test dokumentieren, Bias-Screening', + 'Art. 11 + Anhang IV: Technical Documentation', + 'Art. 13: Transparency — User-Instructions für Deployer', + 'Art. 14: Human Oversight — dokumentierte Eingriffsmöglichkeit', + 'Art. 15: Accuracy, Robustness, Cybersecurity', + 'Art. 26: Deployer-Pflichten (Monitoring, Logs, Information)', + 'Art. 27: Fundamental Rights Impact Assessment (FRIA) — kombinierbar mit DSFA', + 'Art. 43: Conformity Assessment + CE-Kennzeichnung', + 'Art. 49: Registrierung in EU-Datenbank' + ] + }, + limited: { + title: 'Limited Risk', + label: 'Art. 50', + cls: 'limited', + summary: 'Transparenzpflichten. Kein CE-Zwang, aber Hinweispflicht gegenüber Nutzer:innen.', + obligations: [ + 'Art. 50 Abs. 1: Kennzeichnung — "Du interagierst mit einer KI"', + 'Art. 50 Abs. 2: Synthetic-Content maschinenlesbar markieren', + 'Art. 50 Abs. 4: Deepfakes offenlegen', + 'DSGVO Art. 13/14: Informationspflichten, wenn personenbezogene Daten verarbeitet', + 'Art. 4 (AI-Literacy) — gilt für ALLE AI-Nutzungen, unabhängig von Klasse' + ] + }, + minimal: { + title: 'Minimal Risk', + label: 'Freiwillig + Art. 4', + cls: 'minimal', + summary: 'Keine spezifischen AI-Act-Pflichten außer AI-Literacy (Art. 4). DSGVO gilt weiterhin bei personenbezogenen Daten.', + obligations: [ + 'Art. 4: AI-Literacy-Training für Mitarbeitende (seit Feb 2025)', + 'Freiwillige Codes of Conduct empfohlen (Art. 95)', + 'DSGVO Art. 5/6: Rechtmäßigkeitsgrundlage + Grundsätze prüfen', + 'Art. 35 DSGVO: DSFA bei hohem Risiko für Betroffene' + ] + } + }; + + function renderAIActModule() { + const host = $('#aiact-host'); + if (aiactState.verdict) return renderAIActVerdict(host); + renderAIActQuestion(host); + } + + function renderAIActQuestion(host) { + const step = aiactState.step; + const node = AIACT_TREE[step]; + const totalSteps = AIACT_TREE.length; + + host.innerHTML = ` +
    + Modul 02 +

    EU AI Act — Risk-Classifier

    +

    Entscheidungsbaum in einfacher Sprache. Am Ende: Risikoklasse + konkrete Pflichten mit Artikel-Bezug. Keine Rechtsberatung — für die konkrete Umsetzung mit IT-Recht-Fachanwalt sprechen.

    +
    +
    +
    + ${AIACT_TREE.map((_, i) => ``).join('')} +
    +
    +

    ${esc(node.q)}

    +

    ${esc(node.hint)}

    +
    + + +
    +
    + +
    +
    +
    + ${aiactState.runs.length > 0 ? ` +
    + Bereits ${aiactState.runs.length} Systeme klassifiziert. + ${aiactState.runs.length >= 3 && !state.badges.cockpit_ai_act_done ? '' : ''} +
    + ` : ''} + `; + $$('[data-ans]', host).forEach(btn => btn.addEventListener('click', () => { + const ans = btn.dataset.ans; + aiactState.answers[node.key] = ans; + const branch = node[ans]; + if (branch && typeof branch === 'object' && branch.verdict) { + aiactState.verdict = { kind: branch.verdict, reason: branch.reason || null }; + // record + aiactState.runs.push({ ts: Date.now(), verdict: branch.verdict, answers: { ...aiactState.answers } }); + // cap stored runs at 10 + aiactState.runs = aiactState.runs.slice(-10); + saveAIActRuns(); + addXP(30, 'AI-Act-Classifier Run'); + if (aiactState.runs.length >= 3 && !state.badges.cockpit_ai_act_done) { + unlockBadge('cockpit_ai_act_done'); + } + renderAIActModule(); + } else if (typeof branch === 'string') { + // find next step + const nextIdx = AIACT_TREE.findIndex(n => n.id === branch); + aiactState.step = nextIdx !== -1 ? nextIdx : step + 1; + renderAIActModule(); + } else { + // default — move on + aiactState.step = step + 1; + renderAIActModule(); + } + })); + $('#aiact-restart', host).addEventListener('click', () => { + aiactState.step = 0; + aiactState.answers = {}; + aiactState.verdict = null; + renderAIActModule(); + }); + } + + function renderAIActVerdict(host) { + const v = VERDICTS[aiactState.verdict.kind]; + const reason = aiactState.verdict.reason; + host.innerHTML = ` +
    + Ergebnis +

    Klassifikation

    +

    Basierend auf deinen Antworten. Speichere das Ergebnis, druck's aus, zeig's dem Fachanwalt.

    +
    +
    + ${esc(v.label)} +

    ${esc(v.title)}

    +

    ${esc(v.summary)}

    +

    Konkrete Pflichten & Artikel

    +
      + ${v.obligations.map(o => { + const parts = o.split(':'); + if (parts.length > 1) return `
    • ${esc(parts[0])}:${esc(parts.slice(1).join(':'))}
    • `; + return `
    • ${esc(o)}
    • `; + }).join('')} +
    +
    + + +
    +
    Runs insgesamt: ${aiactState.runs.length} / für „AI-Act-Navigator"-Badge 3 verschiedene Systeme
    +
    + `; + $('#aiact-another', host).addEventListener('click', () => { + aiactState.step = 0; + aiactState.answers = {}; + aiactState.verdict = null; + renderAIActModule(); + }); + $('#aiact-ask-kai', host).addEventListener('click', () => { + openDock(); + const p = `Mein System wurde als "${v.title}" (${v.label}) klassifiziert. Was sind die 5 häufigsten Umsetzungsfehler in dieser Klasse, die ich vermeiden soll?`; + fillDockInput(p); + }); + } + + // ===================================================================== + // MODULE 3 — KPI DASHBOARD (demo) + // ===================================================================== + const TILES = [ + // Executive + { id: 'adoption', group: 'exec', label: 'Adoption Rate', value: '73%', trend: '+4% vs. Vormonat', trendKind: 'up', + explain: 'Anteil der anspruchsberechtigten User:innen, die das AI-System im letzten Monat aktiv genutzt haben. Proxy für Change-Success.', + series: [54, 58, 62, 64, 67, 70, 73] }, + { id: 'savings', group: 'exec', label: 'Monthly Cost Savings', value: '€ 38.400', trend: '+€3.2k vs. Vormonat', trendKind: 'up', + explain: 'Eingesparte operative Kosten durch AI-Automatisierung (Service-Deflection, Process-Shortening). Teil des Business-KPI-Sets (Modul 02).', + series: [22000, 26500, 29000, 30500, 33000, 35200, 38400] }, + { id: 'trust', group: 'exec', label: 'Trust Score', value: '4.1 / 5', trend: '+0.2 vs. Q-1', trendKind: 'up', + explain: 'User-Survey NPS-ähnlich: „Vertrauen Sie den AI-Antworten?". Zielwert > 4.0 für Skalierung.', + series: [3.5, 3.6, 3.7, 3.8, 3.9, 4.0, 4.1] }, + // Operational + { id: 'latency', group: 'ops', label: 'P95 Latency', value: '182 ms', trend: '-18ms vs. Vorwoche', trendKind: 'up', + explain: 'P95-Response-Time Ende-zu-Ende. Ziel < 300ms für konversationelle UX. Hinweis: nicht der Mittelwert — der P95 zählt.', + series: [210, 205, 200, 195, 190, 185, 182] }, + { id: 'throughput', group: 'ops', label: 'Throughput', value: '840 req/s', trend: '+12 req/s', trendKind: 'up', + explain: 'Peak-Requests pro Sekunde. Kapazitätsplanung + Kostenschätzung pro Inferenz.', + series: [720, 750, 760, 785, 800, 820, 840] }, + { id: 'errors', group: 'ops', label: 'Error Rate', value: '0.8%', trend: '-0.1% vs. Vorwoche', trendKind: 'up', + explain: 'Fehlgeschlagene Requests (5xx + Fachlich-Invalid). Ziel < 1%. Alarm bei > 2%.', + series: [1.2, 1.1, 1.0, 0.95, 0.9, 0.85, 0.8] }, + { id: 'availability', group: 'ops', label: 'Availability', value: '99.92%', trend: 'SLO ≥ 99.9%', trendKind: 'neutral', + explain: 'Uptime des AI-Service. Typisches SLO 99.9% (= ~43min Downtime/Monat). Tracking pro Region + Gesamt.', + series: [99.85, 99.87, 99.9, 99.92, 99.91, 99.92, 99.92] }, + // Technical + { id: 'accuracy', group: 'tech', label: 'Model Accuracy', value: '94.3%', trend: '+0.4% vs. Baseline', trendKind: 'up', + explain: 'Classification-Accuracy auf Holdout-Set. Achtung: Accuracy alleine unzuverlässig bei imbalancierten Klassen — Prüfe zusätzlich Precision/Recall.', + series: [92.5, 93.0, 93.4, 93.7, 94.0, 94.2, 94.3] }, + { id: 'f1', group: 'tech', label: 'F1 Score', value: '0.89', trend: '+0.02 vs. Vormonat', trendKind: 'up', + explain: 'Harmonisches Mittel aus Precision und Recall. Robust gegen Klassenungleichheit.', + series: [0.83, 0.85, 0.86, 0.87, 0.88, 0.885, 0.89] }, + { id: 'drift', group: 'tech', label: 'Drift (PSI)', value: '0.07', trend: 'ok (< 0.1)', trendKind: 'up', + explain: 'Population Stability Index. 0 = keine Drift, > 0.1 = moderate Drift, > 0.25 = signifikant → Retrain.', + series: [0.03, 0.04, 0.05, 0.055, 0.06, 0.065, 0.07] }, + { id: 'bias', group: 'tech', label: 'Bias (EO Gap)', value: '0.03', trend: 'EO-OK (< 0.05)', trendKind: 'up', + explain: 'Equal-Opportunity-Differenz zwischen protected und unprotected group. Hinweis: Bias-Metriken immer auf Fairness-Definition (DP vs. EO vs. EOd) abstimmen.', + series: [0.07, 0.06, 0.055, 0.05, 0.045, 0.04, 0.03] } + ]; + + function loadDash() { + try { return JSON.parse(localStorage.getItem(LS_DASH) || '{"seen": []}'); } catch (e) { return { seen: [] }; } + } + function saveDash(d) { localStorage.setItem(LS_DASH, JSON.stringify(d)); } + const dashState = loadDash(); + if (!Array.isArray(dashState.seen)) dashState.seen = []; + + function renderDashboardModule() { + const host = $('#dashboard-host'); + const groups = [ + { key: 'exec', title: 'Executive Layer — Board-Kennzahlen', cls: 'exec' }, + { key: 'ops', title: 'Operational Layer — Service-Health', cls: 'ops' }, + { key: 'tech', title: 'Technical Layer — Model-Qualität', cls: 'tech' } + ]; + host.innerHTML = ` +
    + Modul 03 +

    KPI-Dashboard für KI-Systeme

    +

    So könnte dein erstes Dashboard aussehen — drei Layer, Executive/Operational/Technical. Klick auf jedes Tile für Sparkline + „Was misst das?". Daten sind Demo-Werte, die Struktur ist das, was zählt.

    +
    + ${groups.map(g => ` +
    +
    ${esc(g.title)}
    +
    + ${TILES.filter(t => t.group === g.key).map(t => { + const seen = dashState.seen.includes(t.id); + return ` + + `; + }).join('')} +
    +
    + `).join('')} +

    + ${dashState.seen.length} / ${TILES.length} Tiles geöffnet — ${dashState.seen.length === TILES.length ? 'komplett!' : `öffne alle für das Dashboard-Flaneur:in-Abzeichen`} +

    + `; + $$('[data-tile]', host).forEach(btn => btn.addEventListener('click', () => openTileModal(btn.dataset.tile))); + } + + function openTileModal(id) { + const tile = TILES.find(t => t.id === id); if (!tile) return; + // mark seen + if (!dashState.seen.includes(id)) { + dashState.seen.push(id); + saveDash(dashState); + // partial XP per new tile + addXP(2, 'Tile geöffnet'); + if (dashState.seen.length === TILES.length && !state.badges.cockpit_dashboard_explored) { + addXP(25, 'Alle Tiles erkundet'); + unlockBadge('cockpit_dashboard_explored'); + } + } + // re-render to reflect seen state + renderDashboardModule(); + // open modal + openModal(` + + + +
    ${renderSparkline(tile.series)}
    +

    ${esc(tile.explain)}

    + + `); + $('#modal-ask-kai').addEventListener('click', () => { + closeModal(); + openDock(); + fillDockInput(`Erklär mir die Kennzahl "${tile.label}" im Kontext eines AI-Systems in Produktion. Welche Alert-Schwellen sind üblich?`); + }); + } + + function renderSparkline(series) { + if (!series || series.length < 2) return ''; + const w = 420; const h = 80; const pad = 8; + const min = Math.min(...series); const max = Math.max(...series); + const range = (max - min) || 1; + const xStep = (w - pad * 2) / (series.length - 1); + const pts = series.map((v, i) => { + const x = pad + i * xStep; + const y = h - pad - ((v - min) / range) * (h - pad * 2); + return [x, y]; + }); + const path = pts.map((p, i) => (i === 0 ? 'M' : 'L') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' '); + // area fill + const area = path + ` L ${pts[pts.length-1][0].toFixed(1)} ${h - pad} L ${pts[0][0].toFixed(1)} ${h - pad} Z`; + const last = pts[pts.length - 1]; + return ` + + + + + + + + + + `; + } + + // ========= MODAL ========= + function openModal(innerHTML) { + const root = $('#modal-root'); + root.innerHTML = ``; + root.classList.add('open'); + root.setAttribute('aria-hidden', 'false'); + $$('[data-close]', root).forEach(b => b.addEventListener('click', closeModal)); + root.addEventListener('click', backdropClose); + document.addEventListener('keydown', modalEscClose); + // focus first focusable + const first = root.querySelector('button,[href],textarea,input'); + if (first) setTimeout(() => first.focus(), 50); + } + function backdropClose(e) { if (e.target.id === 'modal-root') closeModal(); } + function modalEscClose(e) { if (e.key === 'Escape') closeModal(); } + function closeModal() { + const root = $('#modal-root'); + root.classList.remove('open'); + root.setAttribute('aria-hidden', 'true'); + root.innerHTML = ''; + root.removeEventListener('click', backdropClose); + document.removeEventListener('keydown', modalEscClose); + } + + // ===================================================================== + // MODULE 4 — CHAT DOCK (+ FULL CHAT in module-4) + // ===================================================================== + function loadDockChat() { + try { return JSON.parse(localStorage.getItem(LS_CHAT) || '[]'); } catch (e) { return []; } + } + function saveDockChat() { + const trimmed = dockHistory.slice(-30); + localStorage.setItem(LS_CHAT, JSON.stringify(trimmed)); + } + let dockHistory = loadDockChat(); + let dockBusy = false; + + function renderDockMessages() { + const box = $('#dock-box'); if (!box) return; + if (dockHistory.length === 0) { + box.innerHTML = `
    Hi, ich bin Kai. Frag mich zu Metriken, AI Act, Reifegrad — oder klick "Frag Kai" in den Modulen.
    `; + return; + } + box.innerHTML = ''; + for (const m of dockHistory) { + const el = document.createElement('div'); + el.className = 'dock-msg ' + (m.role === 'assistant' ? 'bot' : 'user'); + if (m.role === 'assistant') el.innerHTML = renderMD(m.content); + else el.textContent = m.content; + box.appendChild(el); + } + box.scrollTop = box.scrollHeight; + } + + function openDock() { + const dock = $('#chat-dock'); + dock.classList.remove('collapsed'); + dock.classList.add('open'); + // mobile: focus input + setTimeout(() => { const ta = $('#dock-input'); if (ta) ta.focus(); }, 60); + } + function closeDock() { + const dock = $('#chat-dock'); + dock.classList.remove('open'); + if (window.innerWidth > 960) dock.classList.add('collapsed'); + } + function fillDockInput(text) { + const ta = $('#dock-input'); if (!ta) return; + ta.value = text; ta.focus(); + // autoresize + ta.style.height = 'auto'; ta.style.height = Math.min(140, ta.scrollHeight) + 'px'; + } + + async function sendDockChat(text) { + if (!text.trim() || dockBusy) return; + if (!KEY) { + appendDockMsg('err', 'Kein API-Schlüssel konfiguriert. window.__KAI_KEY__ fehlt.'); + return; + } + dockBusy = true; + const sendBtn = $('#dock-send'); if (sendBtn) sendBtn.disabled = true; + + dockHistory.push({ role: 'user', content: text }); + renderDockMessages(); + saveDockChat(); + + const box = $('#dock-box'); + const pending = document.createElement('div'); + pending.className = 'dock-msg bot'; + pending.innerHTML = ''; + box.appendChild(pending); + box.scrollTop = box.scrollHeight; + + try { + const r = await fetch(API, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + KEY }, + body: JSON.stringify({ message: text, history: dockHistory.slice(0, -1).slice(-20) }) + }); + 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)); + pending.remove(); + dockHistory.push({ role: 'assistant', content: data.reply || '(leere Antwort)' }); + saveDockChat(); + renderDockMessages(); + // award small XP for first-ever question of the session (tracked via timestamp) + if (!state._lastDockQ || (Date.now() - state._lastDockQ) > 60000) { + state._lastDockQ = Date.now(); + saveState(); + addXP(5, 'Frage an Kai'); + } + } catch (err) { + pending.remove(); + appendDockMsg('err', 'Fehler: ' + (err.message || String(err))); + } finally { + dockBusy = false; + if (sendBtn) sendBtn.disabled = false; + const ta = $('#dock-input'); if (ta) { ta.value = ''; ta.style.height = 'auto'; ta.focus(); } + } + } + + function appendDockMsg(kind, content) { + const box = $('#dock-box'); if (!box) return; + const el = document.createElement('div'); + el.className = 'dock-msg ' + kind; + el.textContent = content; + box.appendChild(el); + box.scrollTop = box.scrollHeight; + } + + function resetDock() { + if (!confirm('Chat-Verlauf im Dock löschen?')) return; + dockHistory = []; + saveDockChat(); + renderDockMessages(); + toast('Dock-Chat zurückgesetzt', 'info', 1800); + } + + // Full chat module (module 4) — shows a larger empty-state + shares state with dock + function renderChatFullModule() { + const host = $('#chat-full-host'); + host.innerHTML = ` +
    + Modul 04 +

    Chat mit Kai

    +

    Dein ruhiger, systematischer Sparring-Coach. Isolierter Chat-Verlauf vom Widget — hier diskutierst du Cockpit-Themen. Shortcut: Ctrl+K fokussiert das Dock-Input rechts.

    +
    +
    +

    Quick-Starter:

    +
    + + + + + + +
    +

    Öffne das Dock (rechts oder unten), um zu chatten — oder klicke einen Starter, um eine Frage vorzubereiten.

    +
    + `; + $$('.chip', host).forEach(c => c.addEventListener('click', () => { + openDock(); + fillDockInput(c.dataset.chip); + })); + } + + // ===================================================================== + // NAV + ROUTING + // ===================================================================== + function switchModule(modId) { + $$('.module').forEach(m => m.dataset.active = (m.id === 'mod-' + modId) ? 'true' : 'false'); + $$('.nav-item').forEach(n => n.setAttribute('aria-selected', (n.dataset.module === modId) ? 'true' : 'false')); + // Re-render current module fresh + if (modId === 'reifegrad') renderReifegradModule(); + if (modId === 'aiact') renderAIActModule(); + if (modId === 'dashboard') renderDashboardModule(); + if (modId === 'chat') renderChatFullModule(); + } + + function installNav() { + $$('.nav-item').forEach(n => n.addEventListener('click', () => switchModule(n.dataset.module))); + } + + function installKeyboardShortcuts() { + document.addEventListener('keydown', (e) => { + // Ctrl+1..4 -> switch module + if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && ['1','2','3','4'].includes(e.key)) { + const map = { '1': 'reifegrad', '2': 'aiact', '3': 'dashboard', '4': 'chat' }; + e.preventDefault(); + switchModule(map[e.key]); + return; + } + // Ctrl+K -> focus dock + if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'k') { + e.preventDefault(); + openDock(); + const ta = $('#dock-input'); if (ta) ta.focus(); + } + }); + } + + function installDock() { + renderDockMessages(); + const form = $('#dock-form'); + const input = $('#dock-input'); + // autoresize + input.addEventListener('input', () => { + input.style.height = 'auto'; + input.style.height = Math.min(140, input.scrollHeight) + 'px'; + }); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendDockChat(input.value); + } + }); + form.addEventListener('submit', (e) => { e.preventDefault(); sendDockChat(input.value); }); + $('#dock-reset').addEventListener('click', resetDock); + $('#dock-collapse').addEventListener('click', () => { + const dock = $('#chat-dock'); + if (window.innerWidth <= 960) { dock.classList.remove('open'); } + else { dock.classList.add('collapsed'); } + }); + $('#dock-open').addEventListener('click', openDock); + } + + // ===================================================================== + // BOOT + // ===================================================================== + function boot() { + // Ensure badges object exists + if (!state.badges) { state.badges = {}; saveState(); } + renderXP(); + installNav(); + installKeyboardShortcuts(); + installDock(); + // Initial module + switchModule('reifegrad'); + // Touch activity for streak (shared widget logic would also do this, harmless redundancy) + state.lastActive = today(); saveState(); + // Welcome toast if first visit + if (!state.seenCockpitWelcome) { + state.seenCockpitWelcome = true; saveState(); + setTimeout(() => toast('Willkommen im Kai-Cockpit. Start mit dem Reifegrad-Assessment.', 'info', 3800), 600); + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', boot); + } else { + boot(); + } +})(); diff --git a/www/cockpit/index.html b/www/cockpit/index.html new file mode 100644 index 0000000..7b78d36 --- /dev/null +++ b/www/cockpit/index.html @@ -0,0 +1,102 @@ + + + + + Kai Cockpit · KI-Governance, Reifegrad, EU AI Act + + + + + + + +
    + +
    +
    + +
    + Kai Cockpit + KI-Governance · Reifegrad · AI Act +
    +
    +
    +
    +
    + Lvl 1 · AI-Trainee + 0 XP +
    +
    +
    + Widget › +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + + +
    + Sovereign AI · Deutscher Bunker · Qognio · DSGVO-konform · Keine externen CDNs +
    + +
    + + +
    + + + + diff --git a/www/curricula.json b/www/curricula.json new file mode 100644 index 0000000..e1e1726 --- /dev/null +++ b/www/curricula.json @@ -0,0 +1,386 @@ +{ + "version": "2026-04-24", + "updated": "2026-04-24", + "curricula": [ + { + "id": "klassische-ml-metriken", + "title": "Klassische ML-Metriken", + "short": "ML-Metriken", + "icon": "chart", + "color": "#0891b2", + "description": "Accuracy, Precision, Recall, F1, ROC-AUC, Cohen's Kappa, MCC — welche Metrik wann, und warum", + "modules": [ + { + "id": "klassifikation-metriken", + "title": "Klassifikations-Metriken", + "subtopics": [ + {"id": "acc-prec-rec", "title": "Accuracy vs Precision vs Recall", "objectives": ["Formeln erklären", "Wann welche priorisieren", "Class-Imbalance-Fallstricke erkennen"]}, + {"id": "f1-mcc-kappa", "title": "F1, MCC, Cohen's Kappa", "objectives": ["Harmonisches Mittel verstehen", "Wann MCC besser als F1", "Inter-Rater-Reliability"]}, + {"id": "roc-pr-auc", "title": "ROC-AUC vs PR-AUC", "objectives": ["Threshold-unabhängige Bewertung", "Imbalanced Data: warum PR-AUC besser", "Trade-offs visualisieren"]} + ] + }, + { + "id": "regression-metriken", + "title": "Regressions-Metriken", + "subtopics": [ + {"id": "rmse-mae", "title": "RMSE vs MAE vs MAPE", "objectives": ["Skalen-Abhängigkeit", "Outlier-Sensitivität", "Relative Fehler"]}, + {"id": "r2-adjusted", "title": "R² und Adjusted R²", "objectives": ["Erklärte Varianz", "Model-Complexity-Penalty"]} + ] + }, + { + "id": "nlp-metriken", + "title": "NLP & LLM-Metriken", + "subtopics": [ + {"id": "bleu-rouge", "title": "BLEU, ROUGE, METEOR", "objectives": ["n-gram-Matching", "Grenzen automatischer Metriken"]}, + {"id": "perplexity", "title": "Perplexity & Token-Metriken", "objectives": ["Information Theory-Grundlage", "Pro-Token-Verlust"]}, + {"id": "llm-as-judge", "title": "LLM-as-a-Judge", "objectives": ["Evaluation mit Modell-Richter", "Bias in der Evaluation"]} + ] + } + ] + }, + { + "id": "business-kpis", + "title": "Business-KPIs für KI", + "short": "Business-KPIs", + "icon": "briefcase", + "color": "#0891b2", + "description": "ROI, Time-to-Value, Adoption, Kosten-pro-Inference — KI im Unternehmen messbar machen", + "modules": [ + { + "id": "roi-tco", + "title": "ROI, TCO, Time-to-Value", + "subtopics": [ + {"id": "roi-formel", "title": "ROI-Berechnung für KI-Projekte", "objectives": ["FTE-Äquivalente einrechnen", "Indirekte Effekte quantifizieren", "Amortisationsdauer"]}, + {"id": "tco-hidden", "title": "TCO & versteckte Kosten", "objectives": ["Inferenzkosten vs Trainingskosten", "Operational Overhead", "Vendor-Lock-in"]} + ] + }, + { + "id": "adoption-satisfaction", + "title": "Adoption & Zufriedenheit", + "subtopics": [ + {"id": "adoption-rate", "title": "Adoption-Rate & Active-Users", "objectives": ["WAU/MAU-Unterschied", "Benchmarks aus Praxis"]}, + {"id": "csat-nps", "title": "CSAT, NPS für KI-Features", "objectives": ["Feature-spezifische Messung", "Anti-Patterns"]} + ] + } + ] + }, + { + "id": "operational", + "title": "Operational Metrics", + "short": "Operations", + "icon": "activity", + "color": "#0891b2", + "description": "Latency, Throughput, Availability — KI-Systeme im Produktivbetrieb überwachen", + "modules": [ + { + "id": "slo-sla", + "title": "SLO/SLA-Design", + "subtopics": [ + {"id": "latency-percentiles", "title": "P50/P95/P99 Latency", "objectives": ["Warum nicht Average", "Percentile lesen"]}, + {"id": "throughput-tps", "title": "Throughput & Tokens-per-Second", "objectives": ["LLM-Durchsatz messen", "Batching-Effekte"]}, + {"id": "availability", "title": "Availability & Error Budget", "objectives": ["SLO-Definition", "Error-Budget-Policy"]} + ] + }, + { + "id": "monitoring-tooling", + "title": "Monitoring-Stack", + "subtopics": [ + {"id": "mlflow-arize", "title": "MLflow, Arize, WhyLabs, W&B", "objectives": ["Tool-Landschaft verstehen", "Auswahl-Kriterien"]}, + {"id": "drift-alerts", "title": "Drift-Alerts & Incident-Playbook", "objectives": ["Schwellenwerte setzen", "False-Positive-Rate"]} + ] + } + ] + }, + { + "id": "datenqualitaet", + "title": "Datenqualität & Drift", + "short": "Daten", + "icon": "database", + "color": "#0891b2", + "description": "DAMA-Dimensionen, PSI, KL-Divergence — Datenprobleme vor Modell-Problemen erkennen", + "modules": [ + { + "id": "dama-dimensions", + "title": "DAMA-Dimensionen", + "subtopics": [ + {"id": "completeness-accuracy", "title": "Completeness, Accuracy, Validity", "objectives": ["Jede Dimension mit Beispiel", "Messmethoden"]}, + {"id": "timeliness-uniqueness", "title": "Timeliness, Uniqueness, Consistency", "objectives": ["Stale-Data-Risiken", "Dedup-Strategien"]} + ] + }, + { + "id": "drift-detection", + "title": "Drift-Detection", + "subtopics": [ + {"id": "psi-kl", "title": "PSI, KL- und JS-Divergence", "objectives": ["Formel und Interpretation", "Schwellenwerte"]}, + {"id": "ks-test", "title": "Kolmogorov-Smirnov-Test", "objectives": ["Nicht-parametrischer Test", "Feature-vs-Label-Drift"]} + ] + } + ] + }, + { + "id": "bias-fairness", + "title": "Bias & Fairness", + "short": "Fairness", + "icon": "scale", + "color": "#0891b2", + "description": "Demographic Parity, Equal Opportunity, Disparate Impact — Diskriminierungsfreie Systeme", + "modules": [ + { + "id": "fairness-metriken", + "title": "Fairness-Metriken", + "subtopics": [ + {"id": "demographic-parity", "title": "Demographic Parity", "objectives": ["Formel", "Einschränkungen"]}, + {"id": "equal-opportunity", "title": "Equal Opportunity & Equalized Odds", "objectives": ["Unterschied zu Parity", "Praxisbeispiel HR-Tool"]}, + {"id": "disparate-impact", "title": "Disparate Impact & 80%-Regel", "objectives": ["US-EEOC-Standard", "EU-Bezug"]} + ] + }, + { + "id": "fairness-tooling", + "title": "Tooling", + "subtopics": [ + {"id": "aif360-fairlearn", "title": "AIF360 & Fairlearn", "objectives": ["Library-Überblick", "Bias-Audits durchführen"]} + ] + } + ] + }, + { + "id": "explainability", + "title": "Explainability (XAI)", + "short": "XAI", + "icon": "eye", + "color": "#0891b2", + "description": "SHAP, LIME, Counterfactuals — Modell-Entscheidungen erklärbar machen (EU AI Act Art. 13)", + "modules": [ + { + "id": "xai-methoden", + "title": "XAI-Methoden", + "subtopics": [ + {"id": "shap-lime", "title": "SHAP vs LIME", "objectives": ["Shapley-Values verstehen", "Lokale vs globale Erklärung"]}, + {"id": "counterfactuals", "title": "Counterfactual & Anchor Explanations", "objectives": ["Minimal-Änderungs-Prinzip", "Praxisnutzen"]} + ] + }, + { + "id": "xai-qualitaet", + "title": "Qualität von Erklärungen", + "subtopics": [ + {"id": "fidelity-stability", "title": "Fidelity, Stability, Comprehensibility", "objectives": ["Messgrößen für Erklärungsqualität", "Trade-offs"]} + ] + } + ] + }, + { + "id": "robustheit-security", + "title": "Robustheit & Security", + "short": "Robustheit", + "icon": "shield", + "color": "#0891b2", + "description": "Adversarial Robustness, Prompt-Injection, Data-Poisoning — Angriffsvektoren verstehen", + "modules": [ + { + "id": "adversarial", + "title": "Adversarial Robustness", + "subtopics": [ + {"id": "pgd-fgsm", "title": "PGD & FGSM-Attacks", "objectives": ["Perturbations verstehen", "L-infinity-Budget"]}, + {"id": "certified-robustness", "title": "Certified Robustness", "objectives": ["Formale Garantien vs empirische Tests"]} + ] + }, + { + "id": "llm-security", + "title": "LLM-Security", + "subtopics": [ + {"id": "prompt-injection", "title": "Prompt-Injection & Jailbreaks", "objectives": ["OWASP LLM Top 10", "Mitigationen"]}, + {"id": "data-poisoning", "title": "Data-Poisoning-Erkennung", "objectives": ["Training-Set-Forensik", "Monitoring-Metriken"]} + ] + } + ] + }, + { + "id": "governance-reifegrad", + "title": "Governance & Reifegrad", + "short": "Reifegrad", + "icon": "gauge", + "color": "#0891b2", + "description": "Gartner AI Maturity, MIT CISR, Microsoft RAI MM — wo stehst du, wo willst du hin", + "modules": [ + { + "id": "reifegradmodelle", + "title": "AI-Reifegradmodelle im Vergleich", + "subtopics": [ + {"id": "gartner-maturity", "title": "Gartner AI Maturity Model", "objectives": ["5 Stufen", "Self-Assessment"]}, + {"id": "mit-cisr", "title": "MIT CISR & Microsoft RAI MM", "objectives": ["Unterschiede", "DACH-Anwendung"]} + ] + }, + { + "id": "self-assessment", + "title": "Self-Assessment-Dimensionen", + "subtopics": [ + {"id": "strategy-data", "title": "Strategy, Data, Technology", "objectives": ["Fragebogen-Struktur", "Reifegrad-Score berechnen"]}, + {"id": "people-processes", "title": "People, Processes", "objectives": ["Kompetenz-Matrix", "Prozess-Reife"]} + ] + } + ] + }, + { + "id": "eu-ai-act", + "title": "EU AI Act Compliance", + "short": "EU AI Act", + "icon": "flag", + "color": "#0891b2", + "description": "Risikoklassen, CE-Kennzeichnung, Artikel-Pflichten — Was muss ich ab wann erfüllen", + "modules": [ + { + "id": "risikoklassen", + "title": "Risikoklassen", + "subtopics": [ + {"id": "verboten-high-risk", "title": "Verboten vs High-Risk", "objectives": ["Art. 5 vs Annex III", "Grenzfälle erkennen"]}, + {"id": "limited-minimal", "title": "Limited & Minimal Risk", "objectives": ["Transparenz-Pflichten", "Chatbots"]} + ] + }, + { + "id": "high-risk-pflichten", + "title": "High-Risk-Pflichten", + "subtopics": [ + {"id": "risk-management", "title": "Art. 9 Risikomanagement-System", "objectives": ["Dokumentations-Anforderungen"]}, + {"id": "data-governance", "title": "Art. 10 Data Governance", "objectives": ["Trainings-, Validierungs- und Testdaten"]}, + {"id": "transparency-art13", "title": "Art. 13 Transparenz", "objectives": ["User-Information", "Logging"]}, + {"id": "human-oversight", "title": "Art. 14 Menschliche Aufsicht", "objectives": ["Override-Mechanismen"]} + ] + }, + { + "id": "timeline-ce", + "title": "Timeline & CE-Kennzeichnung", + "subtopics": [ + {"id": "phasen-einfuehrung", "title": "Inkrafttreten-Phasen", "objectives": ["2025/2026/2027-Meilensteine"]}, + {"id": "ce-marking", "title": "CE-Kennzeichnung für High-Risk", "objectives": ["Konformitäts-Assessment", "Benannte Stellen"]} + ] + } + ] + }, + { + "id": "nist-ai-rmf", + "title": "NIST AI RMF", + "short": "NIST", + "icon": "book", + "color": "#0891b2", + "description": "Govern, Map, Measure, Manage — US-Framework mit Crosswalk zu EU AI Act und ISO 42001", + "modules": [ + { + "id": "vier-funktionen", + "title": "Die 4 Kernfunktionen", + "subtopics": [ + {"id": "govern", "title": "Govern", "objectives": ["Organisations-Kultur", "Policies"]}, + {"id": "map", "title": "Map", "objectives": ["Kontext & Risiken erfassen"]}, + {"id": "measure", "title": "Measure", "objectives": ["KPIs und Tests"]}, + {"id": "manage", "title": "Manage", "objectives": ["Priorisieren, Response, Recovery"]} + ] + }, + { + "id": "crosswalk", + "title": "Crosswalk zu anderen Frameworks", + "subtopics": [ + {"id": "nist-vs-eu-ai-act", "title": "NIST vs EU AI Act", "objectives": ["Überlappungen nutzen"]}, + {"id": "nist-vs-iso", "title": "NIST vs ISO 42001", "objectives": ["Mapping-Tabellen"]} + ] + } + ] + }, + { + "id": "iso-42001", + "title": "ISO/IEC 42001 & 23894", + "short": "ISO", + "icon": "award", + "color": "#0891b2", + "description": "AI Management System, AI Risk Management — zertifizierbare Standards seit 2023", + "modules": [ + { + "id": "iso-42001-aims", + "title": "ISO 42001 — AIMS", + "subtopics": [ + {"id": "aims-struktur", "title": "Was ist ein AIMS", "objectives": ["Management-System-Struktur", "PDCA für KI"]}, + {"id": "zertifizierungspfad", "title": "Zertifizierungspfad", "objectives": ["Akkreditierte Stellen DACH", "Aufwand schätzen"]} + ] + }, + { + "id": "iso-23894", + "title": "ISO 23894 — AI Risk Management", + "subtopics": [ + {"id": "risk-assessment", "title": "Risk Assessment Prozess", "objectives": ["Risiko identifizieren/analysieren/bewerten"]}, + {"id": "komplement-27001", "title": "Komplementarität zu ISO 27001", "objectives": ["Synergie mit ISMS"]} + ] + } + ] + }, + { + "id": "scorecards", + "title": "Scorecards & Dashboards", + "short": "Scorecards", + "icon": "clipboard", + "color": "#0891b2", + "description": "KPI-Hierarchie Exec/Operational/Technical — vom Dashboard zur Entscheidung", + "modules": [ + { + "id": "scorecard-design", + "title": "Scorecard-Design", + "subtopics": [ + {"id": "kpi-hierarchie", "title": "Exec-, Operational-, Technical-Layer", "objectives": ["Welche KPI auf welcher Ebene", "Aggregations-Logik"]}, + {"id": "vanity-metrics", "title": "Vanity-Metrics erkennen", "objectives": ["Anti-Patterns", "Actionability-Test"]} + ] + }, + { + "id": "dashboarding-tools", + "title": "Dashboarding-Tools", + "subtopics": [ + {"id": "tableau-looker-superset", "title": "Tableau, Looker, Superset, Grafana", "objectives": ["Stärken/Schwächen", "Kosten"]} + ] + } + ] + }, + { + "id": "okr-ki", + "title": "OKRs für KI-Teams", + "short": "OKR", + "icon": "target", + "color": "#0891b2", + "description": "Objectives & Key Results — Balance zwischen Tech-KPIs und Business-Impact", + "modules": [ + { + "id": "okr-grundlagen", + "title": "OKR-Grundlagen für AI", + "subtopics": [ + {"id": "obj-vs-kr", "title": "Objectives vs Key Results", "objectives": ["Qualitativ vs quantitativ"]}, + {"id": "balance-tech-biz", "title": "Balance Tech vs Business", "objectives": ["Anti-Pattern: Over-Tech-Indexed"]} + ] + }, + { + "id": "beispiele-dach", + "title": "Beispiele aus DACH-Mittelstand", + "subtopics": [ + {"id": "mittelstand-cases", "title": "5 Praxisbeispiele", "objectives": ["Formulierungs-Muster"]} + ] + } + ] + } + ], + "badges": [ + {"id": "erste_metrik", "title": "Erste Metrik-Analyse", "icon": "target", "description": "1. Quiz zu klassischen ML-Metriken bestanden"}, + {"id": "metrik_master", "title": "Metrik-Master:in", "icon": "trophy", "description": "10 Klassifikations-Metriken-Fragen korrekt"}, + {"id": "bias_hunter", "title": "Bias-Jäger:in", "icon": "scale", "description": "Bias & Fairness-Modul abgeschlossen"}, + {"id": "ai_act_navigator", "title": "EU-AI-Act-Navigator:in", "icon": "flag", "description": "EU-AI-Act-Modul abgeschlossen"}, + {"id": "nist_practitioner", "title": "NIST-Praktiker:in", "icon": "book", "description": "NIST AI RMF-Modul abgeschlossen"}, + {"id": "iso_expert", "title": "ISO-42001-Expert:in", "icon": "award", "description": "ISO 42001/23894-Modul abgeschlossen"}, + {"id": "scorecard_architect", "title": "Scorecard-Architekt:in", "icon": "clipboard", "description": "Scorecards-Flashcards bestanden"}, + {"id": "governance_lead", "title": "Governance-Lead", "icon": "crown", "description": "Reifegrad-Modul abgeschlossen"}, + {"id": "kpi_master", "title": "KI-Kennzahlen-Master", "icon": "star", "description": "Alle 13 Curricula abgeschlossen"}, + {"id": "night_owl", "title": "Nachteule", "icon": "moon", "description": "Nach 22 Uhr gelernt"}, + {"id": "early_bird", "title": "Frühaufsteher:in", "icon": "sun", "description": "Vor 7 Uhr gelernt"} + ], + "levels": [ + {"min": 0, "title": "Einsteiger:in"}, + {"min": 50, "title": "Analyst:in"}, + {"min": 200, "title": "Data-Scientist:in"}, + {"min": 500, "title": "ML-Engineer:in"}, + {"min": 1250, "title": "AI-Program-Lead"}, + {"min": 2500, "title": "Head of AI"}, + {"min": 5000, "title": "Chief Data/AI-Officer"} + ] +} diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..798b510 --- /dev/null +++ b/www/index.html @@ -0,0 +1,125 @@ + + + + + Kai · KI-Kennzahlen & Methodik-Coach + + + + + + + +
    + +
    +
    + + Kai KI-Kennzahlen +
    +
    + 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..39c9707 --- /dev/null +++ b/www/styles.css @@ -0,0 +1,1038 @@ +/* Kai — KI-Kennzahlen & Methodik-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: #0891b2; + --accent-2: #06b6d4; + --accent-dim: rgba(8, 145, 178, 0.15); + --accent-strong: rgba(8, 145, 178, 0.45); + --success: #22c55e; + --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, #0891b2 0%, #0e7490 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: #a5f3fc; 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: #0e7490; } +.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, #0891b2, #06b6d4); + -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, #0891b2, #06b6d4); + -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%, #06b6d4 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, #0891b2, #06b6d4); + 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(8,145,178,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:#0891b2; 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); }