feat(antrag-detail): geführte Tour mit Sprachausgabe (#185 Phase 1)
Schaltfläche „🧭 Tour" in der userrow neben „Merken". Klick öffnet ein
Spotlight-Overlay mit vier Stationen, ermächtigend statt vereinfachend
formuliert:
1. Die Gemeinwohl-Note (was die Zahl 0–10 sagt, was die Empfehlung ist)
2. Die GWÖ-Matrix (5 Werte × 5 Berührungsgruppen, Farbcodierung)
3. Programm-Treue pro Fraktion (Score + Belege)
4. Stimmverhalten + Marker (Heuchelei ⚠ und Opportunismus !)
Audio: Web Speech API (Browser-eingebaute Stimme), de-DE, möglichst
weibliche Stimme. „Stimme an / aus"-Toggle in der Bubble. Bei
ESC oder Klick auf Overlay-Hintergrund: Tour-Ende, Audio stoppt.
Phase 2 (separate Iteration) wird das Audio-Backend gegen ElevenLabs
tauschen — Tour-Skript + UI bleiben gleich, nur ``speak()`` ruft dann
einen Server-Endpoint, der eine vorgenerierte und gecachte MP3 liefert.
Komponente in ``app/templates/v3/components/tour.html``, included am
Ende von antrag_detail.html. CSS inline in der Komponente (1 ``<style>``-
Block, keine ``style=""``-Attribute — Anti-Regression-Wache aus #184
respektiert).
2026-05-09 02:39:01 +02:00
{# ─────────────────────────────────────────────────────────────────────
2026-05-09 02:47:04 +02:00
Geführte Tour-Engine (#185).
feat(antrag-detail): geführte Tour mit Sprachausgabe (#185 Phase 1)
Schaltfläche „🧭 Tour" in der userrow neben „Merken". Klick öffnet ein
Spotlight-Overlay mit vier Stationen, ermächtigend statt vereinfachend
formuliert:
1. Die Gemeinwohl-Note (was die Zahl 0–10 sagt, was die Empfehlung ist)
2. Die GWÖ-Matrix (5 Werte × 5 Berührungsgruppen, Farbcodierung)
3. Programm-Treue pro Fraktion (Score + Belege)
4. Stimmverhalten + Marker (Heuchelei ⚠ und Opportunismus !)
Audio: Web Speech API (Browser-eingebaute Stimme), de-DE, möglichst
weibliche Stimme. „Stimme an / aus"-Toggle in der Bubble. Bei
ESC oder Klick auf Overlay-Hintergrund: Tour-Ende, Audio stoppt.
Phase 2 (separate Iteration) wird das Audio-Backend gegen ElevenLabs
tauschen — Tour-Skript + UI bleiben gleich, nur ``speak()`` ruft dann
einen Server-Endpoint, der eine vorgenerierte und gecachte MP3 liefert.
Komponente in ``app/templates/v3/components/tour.html``, included am
Ende von antrag_detail.html. CSS inline in der Komponente (1 ``<style>``-
Block, keine ``style=""``-Attribute — Anti-Regression-Wache aus #184
respektiert).
2026-05-09 02:39:01 +02:00
Was diese Komponente liefert:
2026-05-09 02:47:04 +02:00
- Spotlight-Overlay + Erklärblase + Vor/Zurück/Schließen + Mute
- Audio via Web Speech API (de-DE, möglichst weiblich)
- Engine: liest pro Page hinterlegte ``window.GWOE_TOUR_STEPS``
(Liste mit ``{selector, title, text}``), zeigt nur Stationen
deren Element wirklich da ist.
feat(antrag-detail): geführte Tour mit Sprachausgabe (#185 Phase 1)
Schaltfläche „🧭 Tour" in der userrow neben „Merken". Klick öffnet ein
Spotlight-Overlay mit vier Stationen, ermächtigend statt vereinfachend
formuliert:
1. Die Gemeinwohl-Note (was die Zahl 0–10 sagt, was die Empfehlung ist)
2. Die GWÖ-Matrix (5 Werte × 5 Berührungsgruppen, Farbcodierung)
3. Programm-Treue pro Fraktion (Score + Belege)
4. Stimmverhalten + Marker (Heuchelei ⚠ und Opportunismus !)
Audio: Web Speech API (Browser-eingebaute Stimme), de-DE, möglichst
weibliche Stimme. „Stimme an / aus"-Toggle in der Bubble. Bei
ESC oder Klick auf Overlay-Hintergrund: Tour-Ende, Audio stoppt.
Phase 2 (separate Iteration) wird das Audio-Backend gegen ElevenLabs
tauschen — Tour-Skript + UI bleiben gleich, nur ``speak()`` ruft dann
einen Server-Endpoint, der eine vorgenerierte und gecachte MP3 liefert.
Komponente in ``app/templates/v3/components/tour.html``, included am
Ende von antrag_detail.html. CSS inline in der Komponente (1 ``<style>``-
Block, keine ``style=""``-Attribute — Anti-Regression-Wache aus #184
respektiert).
2026-05-09 02:39:01 +02:00
2026-05-09 02:47:04 +02:00
Pro Page:
< script > window . GWOE _TOUR _STEPS = [ { selector : '...' , title : '...' , text : '...' } ] ; < / script >
{% include "v3/components/tour.html" %}
< button onclick = "gwoeTourStart()" > Tour starten< / button >
Phase 1: Browser-TTS. Phase 2 (ElevenLabs) tauscht nur das
Audio-Backend; das Tour-Skript bleibt gleich.
feat(antrag-detail): geführte Tour mit Sprachausgabe (#185 Phase 1)
Schaltfläche „🧭 Tour" in der userrow neben „Merken". Klick öffnet ein
Spotlight-Overlay mit vier Stationen, ermächtigend statt vereinfachend
formuliert:
1. Die Gemeinwohl-Note (was die Zahl 0–10 sagt, was die Empfehlung ist)
2. Die GWÖ-Matrix (5 Werte × 5 Berührungsgruppen, Farbcodierung)
3. Programm-Treue pro Fraktion (Score + Belege)
4. Stimmverhalten + Marker (Heuchelei ⚠ und Opportunismus !)
Audio: Web Speech API (Browser-eingebaute Stimme), de-DE, möglichst
weibliche Stimme. „Stimme an / aus"-Toggle in der Bubble. Bei
ESC oder Klick auf Overlay-Hintergrund: Tour-Ende, Audio stoppt.
Phase 2 (separate Iteration) wird das Audio-Backend gegen ElevenLabs
tauschen — Tour-Skript + UI bleiben gleich, nur ``speak()`` ruft dann
einen Server-Endpoint, der eine vorgenerierte und gecachte MP3 liefert.
Komponente in ``app/templates/v3/components/tour.html``, included am
Ende von antrag_detail.html. CSS inline in der Komponente (1 ``<style>``-
Block, keine ``style=""``-Attribute — Anti-Regression-Wache aus #184
respektiert).
2026-05-09 02:39:01 +02:00
───────────────────────────────────────────────────────────────────── #}
< div id = "gwoe-tour-overlay" class = "gwoe-tour-overlay" hidden aria-hidden = "true" >
< div id = "gwoe-tour-spotlight" class = "gwoe-tour-spotlight" > < / div >
< div id = "gwoe-tour-bubble" class = "gwoe-tour-bubble" role = "dialog"
aria-labelledby="gwoe-tour-title" aria-describedby="gwoe-tour-text">
< div class = "gwoe-tour-head" >
< span id = "gwoe-tour-step" > Schritt 1 / 4< / span >
< button type = "button" onclick = "gwoeTourEnd()" class = "gwoe-tour-close"
title="Tour beenden" aria-label="Tour beenden">× < / button >
< / div >
< h3 id = "gwoe-tour-title" class = "gwoe-tour-title" > Titel< / h3 >
< p id = "gwoe-tour-text" class = "gwoe-tour-text" > Erklär-Text< / p >
< div class = "gwoe-tour-actions" >
< button type = "button" id = "gwoe-tour-mute"
onclick="gwoeTourToggleMute()"
class="v3-action-btn gwoe-tour-mute"
title="Sprachausgabe an / aus">
< span id = "gwoe-tour-mute-icon" > 🔊< / span >
< span id = "gwoe-tour-mute-label" > Stimme an< / span >
< / button >
< span class = "gwoe-tour-spacer" > < / span >
< button type = "button" id = "gwoe-tour-prev"
onclick="gwoeTourPrev()" class="v3-action-btn">← Zurück< / button >
< button type = "button" id = "gwoe-tour-next"
onclick="gwoeTourNext()" class="v3-action-btn primary">Weiter →< / button >
< / div >
< / div >
< / div >
< style >
.gwoe-tour-overlay {
position: fixed;
inset: 0;
z-index: 99998;
background: rgba(0, 0, 0, 0.55);
pointer-events: auto;
}
.gwoe-tour-overlay[hidden] { display: none; }
.gwoe-tour-spotlight {
position: absolute;
border-radius: 6px;
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55);
pointer-events: none;
transition: top 0.3s ease, left 0.3s ease, width 0.3s ease, height 0.3s ease;
outline: 2px solid var(--ecg-teal);
outline-offset: 4px;
}
.gwoe-tour-bubble {
position: absolute;
z-index: 99999;
background: var(--ecg-card-bg, #fff);
color: var(--ecg-dark, #1a1a1a);
border: 1px solid var(--hairline, rgba(0,0,0,0.12));
border-radius: 6px;
padding: 14px 16px 12px;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
max-width: 380px;
font-family: var(--font-sans);
transition: top 0.3s ease, left 0.3s ease;
}
.gwoe-tour-head {
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.06em;
text-transform: uppercase;
opacity: 0.7;
margin-bottom: 4px;
}
.gwoe-tour-close {
background: none;
border: none;
font-size: 20px;
line-height: 1;
cursor: pointer;
color: var(--ecg-dark);
opacity: 0.7;
padding: 0 4px;
}
.gwoe-tour-close:hover { opacity: 1; }
.gwoe-tour-title {
font-family: var(--font-display);
font-size: 16px;
margin: 4px 0 8px;
color: var(--ecg-teal);
}
.gwoe-tour-text {
font-size: 14px;
line-height: 1.5;
margin: 0 0 12px;
}
.gwoe-tour-actions {
display: flex;
gap: 6px;
align-items: center;
border-top: 1px dashed var(--hairline, rgba(0,0,0,0.1));
padding-top: 10px;
}
.gwoe-tour-spacer { flex: 1 1 auto; }
.gwoe-tour-mute { font-size: 12px; }
fix(tour, nav): Audio-Auto-Play entsperren + Daten-Nav für alle sichtbar
Drei zusammenhängende UI-Bugs:
1) Audio kam nicht — Browser-Auto-Play-Block. ``new Audio(blobUrl).play()``
nach einem await zählt nicht mehr als User-Gesture; Safari/Chrome
kassieren mit NotAllowedError. Fix: persistentes <audio>-Element wird
einmal beim Tour-Start (im Click-Handler, synchron) mit einer
1×1-silent-WAV entsperrt. Folgende src-Updates spielen ohne Block.
2) Tour-Bubble „Weiter"-Button sah 90er aus — der lokale CSS-Override
``.gwoe-tour-bubble .v3-action-btn.primary`` hat den modernen
pill-shaped Style ausgehebelt. Override entfernt; nutzt jetzt das
globale ``.v3-action-btn.primary`` (teal-solid, runde Ecken,
weicher Drop-Shadow).
3) Tour erzählt anonymen User:innen über „Auswertungen" und
„Stimmverhalten", die in der linken Nav für Anonyme nicht sichtbar
waren. Aggregierte Daten sind öffentlich — Daten-Nav-Gruppe jetzt für
alle sichtbar (Auswertungen, Stimmverhalten, Aktuelle Themen,
Export-API, Atom-Feed). Persönliche Items (Merkliste, Abos, Neuer
Antrag, Batch) bleiben eingeloggt. Cluster + Landtag-Suche bleiben
eingeloggt/admin (Backend-Routen sind ohnehin require_auth).
2026-05-09 08:07:27 +02:00
/* primary-Override entfernt — verwendet jetzt das globale .v3-action-btn.primary
Styling (pill-shaped, teal-solid, weicher Drop-Shadow). */
feat(antrag-detail): geführte Tour mit Sprachausgabe (#185 Phase 1)
Schaltfläche „🧭 Tour" in der userrow neben „Merken". Klick öffnet ein
Spotlight-Overlay mit vier Stationen, ermächtigend statt vereinfachend
formuliert:
1. Die Gemeinwohl-Note (was die Zahl 0–10 sagt, was die Empfehlung ist)
2. Die GWÖ-Matrix (5 Werte × 5 Berührungsgruppen, Farbcodierung)
3. Programm-Treue pro Fraktion (Score + Belege)
4. Stimmverhalten + Marker (Heuchelei ⚠ und Opportunismus !)
Audio: Web Speech API (Browser-eingebaute Stimme), de-DE, möglichst
weibliche Stimme. „Stimme an / aus"-Toggle in der Bubble. Bei
ESC oder Klick auf Overlay-Hintergrund: Tour-Ende, Audio stoppt.
Phase 2 (separate Iteration) wird das Audio-Backend gegen ElevenLabs
tauschen — Tour-Skript + UI bleiben gleich, nur ``speak()`` ruft dann
einen Server-Endpoint, der eine vorgenerierte und gecachte MP3 liefert.
Komponente in ``app/templates/v3/components/tour.html``, included am
Ende von antrag_detail.html. CSS inline in der Komponente (1 ``<style>``-
Block, keine ``style=""``-Attribute — Anti-Regression-Wache aus #184
respektiert).
2026-05-09 02:39:01 +02:00
@media (max-width: 700px) {
.gwoe-tour-bubble {
/* auf Mobile zentriert unten andocken statt gefloated, sonst kollidiert
mit dem Spotlight */
left: 12px !important;
right: 12px !important;
max-width: none;
bottom: 12px;
top: auto !important;
}
}
< / style >
< script >
(function () {
2026-05-09 02:47:04 +02:00
// Tour-Stationen kommen pro Page via window.GWOE_TOUR_STEPS.
// Jeder Eintrag: {selector, title, text}. Selectoren werden zur Laufzeit
// aufgelöst — fehlt das Element (z.B. keine Plenum-Votes auf einer
// bestimmten Drucksache), überspringt die Tour den Schritt automatisch.
2026-05-09 08:43:35 +02:00
//
// Fallback: pages ohne eigene Stationen bekommen eine kurze
// Orientierungs-Tour, damit die Tour-Schaltfläche überall einen
// sinnvollen Inhalt zeigt.
const FALLBACK_STEPS = [
{ selector: '.v2-brand-link, .v2-brand',
title: 'Willkommen beim GWÖ-Antragsprüfer',
text: 'Diese Seite bewertet Anträge aus deutschen Parlamenten nach der Gemeinwohl-Matrix. Was sie auszeichnet: Sie schaut nicht nur, ob ein Antrag „gut klingt", sondern wie sehr er fünf Werten dient — Würde, Solidarität, Nachhaltigkeit, Gerechtigkeit und Demokratie. Klick auf das Logo bringt dich jederzeit zur Übersicht zurück.' },
{ selector: '.v2-topbar',
title: 'Topbar',
text: 'Oben findest du Methodik (wie wir bewerten), Quellen (alle Wahl- und Grundsatzprogramme, semantisch durchsuchbar) und den Bundesland-Filter. Rechts ist der Theme-Toggle und — falls eingeloggt — dein Profil.' },
{ selector: '#v2-sidebar nav',
title: 'Navigation links',
text: 'Links findest du die Hauptbereiche. Die Liste passt sich an: angemeldete Nutzer:innen sehen außerdem Auswertungen, Stimmverhalten und persönliche Funktionen wie eine Merkliste.' },
];
2026-05-09 02:47:04 +02:00
function getSteps() {
2026-05-09 08:43:35 +02:00
if (Array.isArray(window.GWOE_TOUR_STEPS) & & window.GWOE_TOUR_STEPS.length > 0) {
return window.GWOE_TOUR_STEPS;
}
return FALLBACK_STEPS;
2026-05-09 02:47:04 +02:00
}
feat(antrag-detail): geführte Tour mit Sprachausgabe (#185 Phase 1)
Schaltfläche „🧭 Tour" in der userrow neben „Merken". Klick öffnet ein
Spotlight-Overlay mit vier Stationen, ermächtigend statt vereinfachend
formuliert:
1. Die Gemeinwohl-Note (was die Zahl 0–10 sagt, was die Empfehlung ist)
2. Die GWÖ-Matrix (5 Werte × 5 Berührungsgruppen, Farbcodierung)
3. Programm-Treue pro Fraktion (Score + Belege)
4. Stimmverhalten + Marker (Heuchelei ⚠ und Opportunismus !)
Audio: Web Speech API (Browser-eingebaute Stimme), de-DE, möglichst
weibliche Stimme. „Stimme an / aus"-Toggle in der Bubble. Bei
ESC oder Klick auf Overlay-Hintergrund: Tour-Ende, Audio stoppt.
Phase 2 (separate Iteration) wird das Audio-Backend gegen ElevenLabs
tauschen — Tour-Skript + UI bleiben gleich, nur ``speak()`` ruft dann
einen Server-Endpoint, der eine vorgenerierte und gecachte MP3 liefert.
Komponente in ``app/templates/v3/components/tour.html``, included am
Ende von antrag_detail.html. CSS inline in der Komponente (1 ``<style>``-
Block, keine ``style=""``-Attribute — Anti-Regression-Wache aus #184
respektiert).
2026-05-09 02:39:01 +02:00
let _tourIdx = 0;
let _resolvedSteps = []; // STEPS gefiltert auf vorhandene Elemente
let _tourMuted = false;
let _tourUtter = null;
fix(tour, nav): Audio-Auto-Play entsperren + Daten-Nav für alle sichtbar
Drei zusammenhängende UI-Bugs:
1) Audio kam nicht — Browser-Auto-Play-Block. ``new Audio(blobUrl).play()``
nach einem await zählt nicht mehr als User-Gesture; Safari/Chrome
kassieren mit NotAllowedError. Fix: persistentes <audio>-Element wird
einmal beim Tour-Start (im Click-Handler, synchron) mit einer
1×1-silent-WAV entsperrt. Folgende src-Updates spielen ohne Block.
2) Tour-Bubble „Weiter"-Button sah 90er aus — der lokale CSS-Override
``.gwoe-tour-bubble .v3-action-btn.primary`` hat den modernen
pill-shaped Style ausgehebelt. Override entfernt; nutzt jetzt das
globale ``.v3-action-btn.primary`` (teal-solid, runde Ecken,
weicher Drop-Shadow).
3) Tour erzählt anonymen User:innen über „Auswertungen" und
„Stimmverhalten", die in der linken Nav für Anonyme nicht sichtbar
waren. Aggregierte Daten sind öffentlich — Daten-Nav-Gruppe jetzt für
alle sichtbar (Auswertungen, Stimmverhalten, Aktuelle Themen,
Export-API, Atom-Feed). Persönliche Items (Merkliste, Abos, Neuer
Antrag, Batch) bleiben eingeloggt. Cluster + Landtag-Suche bleiben
eingeloggt/admin (Backend-Routen sind ohnehin require_auth).
2026-05-09 08:07:27 +02:00
// Persistentes < audio > -Element. Wir entsperren es einmal beim
// Tour-Start (User-Gesture vom Click) mit einer leeren src + play(),
// damit spätere src-Updates ohne Auto-Play-Block laufen — sonst
// kassieren Safari/Chrome jedes ``new Audio(blobUrl).play()`` nach
// einem await mit "NotAllowedError".
let _tourAudio = null;
let _tourAudioUnlocked = false;
2026-05-09 03:17:06 +02:00
// Cache nur in dieser Session: vermeidet doppelte API-Roundtrips bei
// Vor/Zurück-Navigation. Server-seitig sind die MP3s ohnehin gecacht.
const _audioBlobCache = {};
feat(antrag-detail): geführte Tour mit Sprachausgabe (#185 Phase 1)
Schaltfläche „🧭 Tour" in der userrow neben „Merken". Klick öffnet ein
Spotlight-Overlay mit vier Stationen, ermächtigend statt vereinfachend
formuliert:
1. Die Gemeinwohl-Note (was die Zahl 0–10 sagt, was die Empfehlung ist)
2. Die GWÖ-Matrix (5 Werte × 5 Berührungsgruppen, Farbcodierung)
3. Programm-Treue pro Fraktion (Score + Belege)
4. Stimmverhalten + Marker (Heuchelei ⚠ und Opportunismus !)
Audio: Web Speech API (Browser-eingebaute Stimme), de-DE, möglichst
weibliche Stimme. „Stimme an / aus"-Toggle in der Bubble. Bei
ESC oder Klick auf Overlay-Hintergrund: Tour-Ende, Audio stoppt.
Phase 2 (separate Iteration) wird das Audio-Backend gegen ElevenLabs
tauschen — Tour-Skript + UI bleiben gleich, nur ``speak()`` ruft dann
einen Server-Endpoint, der eine vorgenerierte und gecachte MP3 liefert.
Komponente in ``app/templates/v3/components/tour.html``, included am
Ende von antrag_detail.html. CSS inline in der Komponente (1 ``<style>``-
Block, keine ``style=""``-Attribute — Anti-Regression-Wache aus #184
respektiert).
2026-05-09 02:39:01 +02:00
fix(tour, nav): Audio-Auto-Play entsperren + Daten-Nav für alle sichtbar
Drei zusammenhängende UI-Bugs:
1) Audio kam nicht — Browser-Auto-Play-Block. ``new Audio(blobUrl).play()``
nach einem await zählt nicht mehr als User-Gesture; Safari/Chrome
kassieren mit NotAllowedError. Fix: persistentes <audio>-Element wird
einmal beim Tour-Start (im Click-Handler, synchron) mit einer
1×1-silent-WAV entsperrt. Folgende src-Updates spielen ohne Block.
2) Tour-Bubble „Weiter"-Button sah 90er aus — der lokale CSS-Override
``.gwoe-tour-bubble .v3-action-btn.primary`` hat den modernen
pill-shaped Style ausgehebelt. Override entfernt; nutzt jetzt das
globale ``.v3-action-btn.primary`` (teal-solid, runde Ecken,
weicher Drop-Shadow).
3) Tour erzählt anonymen User:innen über „Auswertungen" und
„Stimmverhalten", die in der linken Nav für Anonyme nicht sichtbar
waren. Aggregierte Daten sind öffentlich — Daten-Nav-Gruppe jetzt für
alle sichtbar (Auswertungen, Stimmverhalten, Aktuelle Themen,
Export-API, Atom-Feed). Persönliche Items (Merkliste, Abos, Neuer
Antrag, Batch) bleiben eingeloggt. Cluster + Landtag-Suche bleiben
eingeloggt/admin (Backend-Routen sind ohnehin require_auth).
2026-05-09 08:07:27 +02:00
function ensureAudioElement() {
if (!_tourAudio) {
_tourAudio = new Audio();
_tourAudio.preload = 'auto';
}
return _tourAudio;
}
function unlockAudioOnGesture() {
// Muss SYNCHRON im Click-Handler aufgerufen werden, sonst zählt es
// nicht als User-Gesture. Wir starten ein silent-play und pausieren
// sofort — das markiert das Element als "vom User initiiert".
if (_tourAudioUnlocked) return;
const a = ensureAudioElement();
try {
// 1× 1 silent WAV als Data-URL (winzig, garantiert valid)
a.src = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAESsAACJWAAACABAAZGF0YQAAAAA=';
const p = a.play();
if (p & & typeof p.then === 'function') {
p.then(() => {
a.pause();
a.currentTime = 0;
_tourAudioUnlocked = true;
}).catch(() => { /* manche Browser blocken auch das — ignorieren */ });
} else {
a.pause();
_tourAudioUnlocked = true;
}
} catch (_) { /* ignore */ }
}
feat(antrag-detail): geführte Tour mit Sprachausgabe (#185 Phase 1)
Schaltfläche „🧭 Tour" in der userrow neben „Merken". Klick öffnet ein
Spotlight-Overlay mit vier Stationen, ermächtigend statt vereinfachend
formuliert:
1. Die Gemeinwohl-Note (was die Zahl 0–10 sagt, was die Empfehlung ist)
2. Die GWÖ-Matrix (5 Werte × 5 Berührungsgruppen, Farbcodierung)
3. Programm-Treue pro Fraktion (Score + Belege)
4. Stimmverhalten + Marker (Heuchelei ⚠ und Opportunismus !)
Audio: Web Speech API (Browser-eingebaute Stimme), de-DE, möglichst
weibliche Stimme. „Stimme an / aus"-Toggle in der Bubble. Bei
ESC oder Klick auf Overlay-Hintergrund: Tour-Ende, Audio stoppt.
Phase 2 (separate Iteration) wird das Audio-Backend gegen ElevenLabs
tauschen — Tour-Skript + UI bleiben gleich, nur ``speak()`` ruft dann
einen Server-Endpoint, der eine vorgenerierte und gecachte MP3 liefert.
Komponente in ``app/templates/v3/components/tour.html``, included am
Ende von antrag_detail.html. CSS inline in der Komponente (1 ``<style>``-
Block, keine ``style=""``-Attribute — Anti-Regression-Wache aus #184
respektiert).
2026-05-09 02:39:01 +02:00
function $(id) { return document.getElementById(id); }
2026-05-09 03:17:06 +02:00
function speakWebSpeech(text) {
feat(antrag-detail): geführte Tour mit Sprachausgabe (#185 Phase 1)
Schaltfläche „🧭 Tour" in der userrow neben „Merken". Klick öffnet ein
Spotlight-Overlay mit vier Stationen, ermächtigend statt vereinfachend
formuliert:
1. Die Gemeinwohl-Note (was die Zahl 0–10 sagt, was die Empfehlung ist)
2. Die GWÖ-Matrix (5 Werte × 5 Berührungsgruppen, Farbcodierung)
3. Programm-Treue pro Fraktion (Score + Belege)
4. Stimmverhalten + Marker (Heuchelei ⚠ und Opportunismus !)
Audio: Web Speech API (Browser-eingebaute Stimme), de-DE, möglichst
weibliche Stimme. „Stimme an / aus"-Toggle in der Bubble. Bei
ESC oder Klick auf Overlay-Hintergrund: Tour-Ende, Audio stoppt.
Phase 2 (separate Iteration) wird das Audio-Backend gegen ElevenLabs
tauschen — Tour-Skript + UI bleiben gleich, nur ``speak()`` ruft dann
einen Server-Endpoint, der eine vorgenerierte und gecachte MP3 liefert.
Komponente in ``app/templates/v3/components/tour.html``, included am
Ende von antrag_detail.html. CSS inline in der Komponente (1 ``<style>``-
Block, keine ``style=""``-Attribute — Anti-Regression-Wache aus #184
respektiert).
2026-05-09 02:39:01 +02:00
if (!('speechSynthesis' in window)) return;
try {
window.speechSynthesis.cancel();
const u = new SpeechSynthesisUtterance(text);
u.lang = 'de-DE';
u.rate = 1.0;
u.pitch = 1.0;
const voices = window.speechSynthesis.getVoices();
const de = voices.filter(v => /de(-DE|_DE)?/i.test(v.lang));
const female = de.find(v => /female|frau|anna|petra|katja|helga|marlene|vicki/i.test(v.name));
u.voice = female || de[0] || null;
_tourUtter = u;
window.speechSynthesis.speak(u);
} catch (_) { /* TTS optional */ }
}
2026-05-09 03:17:06 +02:00
async function speakElevenLabs(text) {
// Server-Endpoint ruft ElevenLabs + cacht; bei 503 (kein API-Key)
// fallen wir auf Web Speech zurück.
if (_audioBlobCache[text]) {
playAudio(_audioBlobCache[text]);
return true;
}
try {
const url = '/api/tour/voice?text=' + encodeURIComponent(text);
const resp = await fetch(url);
if (resp.status === 503) return false; // ElevenLabs nicht konfiguriert
if (!resp.ok) {
console.warn('Tour-Audio ' + resp.status + ': fallback auf Web Speech');
return false;
}
const blob = await resp.blob();
const blobUrl = URL.createObjectURL(blob);
_audioBlobCache[text] = blobUrl;
playAudio(blobUrl);
return true;
} catch (e) {
console.warn('Tour-Audio-Fehler, fallback Web Speech:', e);
return false;
}
}
function playAudio(blobUrl) {
stopSpeak();
fix(tour, nav): Audio-Auto-Play entsperren + Daten-Nav für alle sichtbar
Drei zusammenhängende UI-Bugs:
1) Audio kam nicht — Browser-Auto-Play-Block. ``new Audio(blobUrl).play()``
nach einem await zählt nicht mehr als User-Gesture; Safari/Chrome
kassieren mit NotAllowedError. Fix: persistentes <audio>-Element wird
einmal beim Tour-Start (im Click-Handler, synchron) mit einer
1×1-silent-WAV entsperrt. Folgende src-Updates spielen ohne Block.
2) Tour-Bubble „Weiter"-Button sah 90er aus — der lokale CSS-Override
``.gwoe-tour-bubble .v3-action-btn.primary`` hat den modernen
pill-shaped Style ausgehebelt. Override entfernt; nutzt jetzt das
globale ``.v3-action-btn.primary`` (teal-solid, runde Ecken,
weicher Drop-Shadow).
3) Tour erzählt anonymen User:innen über „Auswertungen" und
„Stimmverhalten", die in der linken Nav für Anonyme nicht sichtbar
waren. Aggregierte Daten sind öffentlich — Daten-Nav-Gruppe jetzt für
alle sichtbar (Auswertungen, Stimmverhalten, Aktuelle Themen,
Export-API, Atom-Feed). Persönliche Items (Merkliste, Abos, Neuer
Antrag, Batch) bleiben eingeloggt. Cluster + Landtag-Suche bleiben
eingeloggt/admin (Backend-Routen sind ohnehin require_auth).
2026-05-09 08:07:27 +02:00
const a = ensureAudioElement();
a.src = blobUrl;
a.play().catch((err) => {
// Wenn der Browser doch blockiert (z.B. Page reload ohne Gesture):
// wir loggen, der User kann den Mute-Toggle erneut drücken.
console.warn('Tour-Audio play blocked:', err & & err.name);
});
2026-05-09 03:17:06 +02:00
}
async function speak(text) {
if (_tourMuted) return;
// Bevorzugt ElevenLabs (Server). Bei nicht-konfiguriert auf Web Speech.
const ok = await speakElevenLabs(text);
if (!ok) speakWebSpeech(text);
}
feat(antrag-detail): geführte Tour mit Sprachausgabe (#185 Phase 1)
Schaltfläche „🧭 Tour" in der userrow neben „Merken". Klick öffnet ein
Spotlight-Overlay mit vier Stationen, ermächtigend statt vereinfachend
formuliert:
1. Die Gemeinwohl-Note (was die Zahl 0–10 sagt, was die Empfehlung ist)
2. Die GWÖ-Matrix (5 Werte × 5 Berührungsgruppen, Farbcodierung)
3. Programm-Treue pro Fraktion (Score + Belege)
4. Stimmverhalten + Marker (Heuchelei ⚠ und Opportunismus !)
Audio: Web Speech API (Browser-eingebaute Stimme), de-DE, möglichst
weibliche Stimme. „Stimme an / aus"-Toggle in der Bubble. Bei
ESC oder Klick auf Overlay-Hintergrund: Tour-Ende, Audio stoppt.
Phase 2 (separate Iteration) wird das Audio-Backend gegen ElevenLabs
tauschen — Tour-Skript + UI bleiben gleich, nur ``speak()`` ruft dann
einen Server-Endpoint, der eine vorgenerierte und gecachte MP3 liefert.
Komponente in ``app/templates/v3/components/tour.html``, included am
Ende von antrag_detail.html. CSS inline in der Komponente (1 ``<style>``-
Block, keine ``style=""``-Attribute — Anti-Regression-Wache aus #184
respektiert).
2026-05-09 02:39:01 +02:00
function stopSpeak() {
if ('speechSynthesis' in window) {
try { window.speechSynthesis.cancel(); } catch (_) {}
}
_tourUtter = null;
fix(tour, nav): Audio-Auto-Play entsperren + Daten-Nav für alle sichtbar
Drei zusammenhängende UI-Bugs:
1) Audio kam nicht — Browser-Auto-Play-Block. ``new Audio(blobUrl).play()``
nach einem await zählt nicht mehr als User-Gesture; Safari/Chrome
kassieren mit NotAllowedError. Fix: persistentes <audio>-Element wird
einmal beim Tour-Start (im Click-Handler, synchron) mit einer
1×1-silent-WAV entsperrt. Folgende src-Updates spielen ohne Block.
2) Tour-Bubble „Weiter"-Button sah 90er aus — der lokale CSS-Override
``.gwoe-tour-bubble .v3-action-btn.primary`` hat den modernen
pill-shaped Style ausgehebelt. Override entfernt; nutzt jetzt das
globale ``.v3-action-btn.primary`` (teal-solid, runde Ecken,
weicher Drop-Shadow).
3) Tour erzählt anonymen User:innen über „Auswertungen" und
„Stimmverhalten", die in der linken Nav für Anonyme nicht sichtbar
waren. Aggregierte Daten sind öffentlich — Daten-Nav-Gruppe jetzt für
alle sichtbar (Auswertungen, Stimmverhalten, Aktuelle Themen,
Export-API, Atom-Feed). Persönliche Items (Merkliste, Abos, Neuer
Antrag, Batch) bleiben eingeloggt. Cluster + Landtag-Suche bleiben
eingeloggt/admin (Backend-Routen sind ohnehin require_auth).
2026-05-09 08:07:27 +02:00
// Persistentes _tourAudio nicht zerstören (sonst geht der Unlock
// verloren), nur stoppen.
2026-05-09 03:17:06 +02:00
if (_tourAudio) {
try { _tourAudio.pause(); _tourAudio.currentTime = 0; } catch (_) {}
}
feat(antrag-detail): geführte Tour mit Sprachausgabe (#185 Phase 1)
Schaltfläche „🧭 Tour" in der userrow neben „Merken". Klick öffnet ein
Spotlight-Overlay mit vier Stationen, ermächtigend statt vereinfachend
formuliert:
1. Die Gemeinwohl-Note (was die Zahl 0–10 sagt, was die Empfehlung ist)
2. Die GWÖ-Matrix (5 Werte × 5 Berührungsgruppen, Farbcodierung)
3. Programm-Treue pro Fraktion (Score + Belege)
4. Stimmverhalten + Marker (Heuchelei ⚠ und Opportunismus !)
Audio: Web Speech API (Browser-eingebaute Stimme), de-DE, möglichst
weibliche Stimme. „Stimme an / aus"-Toggle in der Bubble. Bei
ESC oder Klick auf Overlay-Hintergrund: Tour-Ende, Audio stoppt.
Phase 2 (separate Iteration) wird das Audio-Backend gegen ElevenLabs
tauschen — Tour-Skript + UI bleiben gleich, nur ``speak()`` ruft dann
einen Server-Endpoint, der eine vorgenerierte und gecachte MP3 liefert.
Komponente in ``app/templates/v3/components/tour.html``, included am
Ende von antrag_detail.html. CSS inline in der Komponente (1 ``<style>``-
Block, keine ``style=""``-Attribute — Anti-Regression-Wache aus #184
respektiert).
2026-05-09 02:39:01 +02:00
}
function resolveSteps() {
_resolvedSteps = [];
2026-05-09 02:47:04 +02:00
for (const s of getSteps()) {
feat(antrag-detail): geführte Tour mit Sprachausgabe (#185 Phase 1)
Schaltfläche „🧭 Tour" in der userrow neben „Merken". Klick öffnet ein
Spotlight-Overlay mit vier Stationen, ermächtigend statt vereinfachend
formuliert:
1. Die Gemeinwohl-Note (was die Zahl 0–10 sagt, was die Empfehlung ist)
2. Die GWÖ-Matrix (5 Werte × 5 Berührungsgruppen, Farbcodierung)
3. Programm-Treue pro Fraktion (Score + Belege)
4. Stimmverhalten + Marker (Heuchelei ⚠ und Opportunismus !)
Audio: Web Speech API (Browser-eingebaute Stimme), de-DE, möglichst
weibliche Stimme. „Stimme an / aus"-Toggle in der Bubble. Bei
ESC oder Klick auf Overlay-Hintergrund: Tour-Ende, Audio stoppt.
Phase 2 (separate Iteration) wird das Audio-Backend gegen ElevenLabs
tauschen — Tour-Skript + UI bleiben gleich, nur ``speak()`` ruft dann
einen Server-Endpoint, der eine vorgenerierte und gecachte MP3 liefert.
Komponente in ``app/templates/v3/components/tour.html``, included am
Ende von antrag_detail.html. CSS inline in der Komponente (1 ``<style>``-
Block, keine ``style=""``-Attribute — Anti-Regression-Wache aus #184
respektiert).
2026-05-09 02:39:01 +02:00
// Erstes Match aus comma-separated Selector-Liste.
const parts = s.selector.split(',').map(x => x.trim());
let el = null;
for (const p of parts) {
el = document.querySelector(p);
if (el) break;
}
if (el) _resolvedSteps.push({ ...s, el });
}
}
function positionBubble(el) {
const rect = el.getBoundingClientRect();
const bubble = $('gwoe-tour-bubble');
const spotlight = $('gwoe-tour-spotlight');
// Spotlight um das Element legen.
spotlight.style.top = (rect.top - 4) + 'px';
spotlight.style.left = (rect.left - 4) + 'px';
spotlight.style.width = (rect.width + 8) + 'px';
spotlight.style.height = (rect.height + 8) + 'px';
// Bubble unter oder über das Element legen, wo Platz ist.
const bubbleH = 240; // grobe Annahme
const spaceBelow = window.innerHeight - rect.bottom;
const spaceAbove = rect.top;
let top, left;
if (spaceBelow > bubbleH + 20) {
top = rect.bottom + 14;
} else if (spaceAbove > bubbleH + 20) {
top = rect.top - bubbleH - 14;
} else {
// Nichts passt vertikal — Bubble nach rechts neben das Element.
top = Math.max(20, rect.top);
}
left = Math.max(20, Math.min(window.innerWidth - 400, rect.left));
bubble.style.top = top + 'px';
bubble.style.left = left + 'px';
}
function showStep(i) {
if (i < 0 | | i > = _resolvedSteps.length) {
gwoeTourEnd();
return;
}
_tourIdx = i;
const step = _resolvedSteps[i];
const overlay = $('gwoe-tour-overlay');
overlay.hidden = false;
overlay.setAttribute('aria-hidden', 'false');
$('gwoe-tour-step').textContent = `Schritt ${i + 1} / ${_resolvedSteps.length}`;
$('gwoe-tour-title').textContent = step.title;
$('gwoe-tour-text').textContent = step.text;
$('gwoe-tour-prev').disabled = (i === 0);
$('gwoe-tour-next').textContent = (i === _resolvedSteps.length - 1)
? 'Fertig ✓' : 'Weiter →';
// Element ins Sichtfeld scrollen, dann positionieren.
step.el.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => positionBubble(step.el), 350);
// TTS sprechen lassen
speak(step.title + '. ' + step.text);
}
// Globale Schnittstelle
window.gwoeTourStart = function () {
fix(tour, nav): Audio-Auto-Play entsperren + Daten-Nav für alle sichtbar
Drei zusammenhängende UI-Bugs:
1) Audio kam nicht — Browser-Auto-Play-Block. ``new Audio(blobUrl).play()``
nach einem await zählt nicht mehr als User-Gesture; Safari/Chrome
kassieren mit NotAllowedError. Fix: persistentes <audio>-Element wird
einmal beim Tour-Start (im Click-Handler, synchron) mit einer
1×1-silent-WAV entsperrt. Folgende src-Updates spielen ohne Block.
2) Tour-Bubble „Weiter"-Button sah 90er aus — der lokale CSS-Override
``.gwoe-tour-bubble .v3-action-btn.primary`` hat den modernen
pill-shaped Style ausgehebelt. Override entfernt; nutzt jetzt das
globale ``.v3-action-btn.primary`` (teal-solid, runde Ecken,
weicher Drop-Shadow).
3) Tour erzählt anonymen User:innen über „Auswertungen" und
„Stimmverhalten", die in der linken Nav für Anonyme nicht sichtbar
waren. Aggregierte Daten sind öffentlich — Daten-Nav-Gruppe jetzt für
alle sichtbar (Auswertungen, Stimmverhalten, Aktuelle Themen,
Export-API, Atom-Feed). Persönliche Items (Merkliste, Abos, Neuer
Antrag, Batch) bleiben eingeloggt. Cluster + Landtag-Suche bleiben
eingeloggt/admin (Backend-Routen sind ohnehin require_auth).
2026-05-09 08:07:27 +02:00
// Audio-Element JETZT entsperren — synchron im User-Gesture-Frame.
// showStep() macht später async fetches; das play() danach würde
// sonst ohne diesen Trick vom Auto-Play-Block kassiert.
unlockAudioOnGesture();
feat(antrag-detail): geführte Tour mit Sprachausgabe (#185 Phase 1)
Schaltfläche „🧭 Tour" in der userrow neben „Merken". Klick öffnet ein
Spotlight-Overlay mit vier Stationen, ermächtigend statt vereinfachend
formuliert:
1. Die Gemeinwohl-Note (was die Zahl 0–10 sagt, was die Empfehlung ist)
2. Die GWÖ-Matrix (5 Werte × 5 Berührungsgruppen, Farbcodierung)
3. Programm-Treue pro Fraktion (Score + Belege)
4. Stimmverhalten + Marker (Heuchelei ⚠ und Opportunismus !)
Audio: Web Speech API (Browser-eingebaute Stimme), de-DE, möglichst
weibliche Stimme. „Stimme an / aus"-Toggle in der Bubble. Bei
ESC oder Klick auf Overlay-Hintergrund: Tour-Ende, Audio stoppt.
Phase 2 (separate Iteration) wird das Audio-Backend gegen ElevenLabs
tauschen — Tour-Skript + UI bleiben gleich, nur ``speak()`` ruft dann
einen Server-Endpoint, der eine vorgenerierte und gecachte MP3 liefert.
Komponente in ``app/templates/v3/components/tour.html``, included am
Ende von antrag_detail.html. CSS inline in der Komponente (1 ``<style>``-
Block, keine ``style=""``-Attribute — Anti-Regression-Wache aus #184
respektiert).
2026-05-09 02:39:01 +02:00
resolveSteps();
if (_resolvedSteps.length === 0) {
console.warn('Tour: keine sichtbaren Stationen.');
return;
}
_tourIdx = 0;
showStep(0);
document.body.style.overflow = 'hidden'; // Page-Scroll blockieren
};
window.gwoeTourEnd = function () {
const overlay = $('gwoe-tour-overlay');
overlay.hidden = true;
overlay.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
stopSpeak();
};
window.gwoeTourNext = function () {
if (_tourIdx >= _resolvedSteps.length - 1) {
gwoeTourEnd();
return;
}
showStep(_tourIdx + 1);
};
window.gwoeTourPrev = function () {
if (_tourIdx < = 0) return;
showStep(_tourIdx - 1);
};
window.gwoeTourToggleMute = function () {
_tourMuted = !_tourMuted;
$('gwoe-tour-mute-icon').textContent = _tourMuted ? '🔇' : '🔊';
$('gwoe-tour-mute-label').textContent = _tourMuted ? 'Stimme aus' : 'Stimme an';
if (_tourMuted) stopSpeak();
else if (_resolvedSteps[_tourIdx]) {
const s = _resolvedSteps[_tourIdx];
speak(s.title + '. ' + s.text);
}
};
// ESC schließt; Klick auf Overlay (außerhalb der Bubble) auch.
document.addEventListener('keydown', (e) => {
const overlay = $('gwoe-tour-overlay');
if (!overlay || overlay.hidden) return;
if (e.key === 'Escape') gwoeTourEnd();
else if (e.key === 'ArrowRight') gwoeTourNext();
else if (e.key === 'ArrowLeft') gwoeTourPrev();
});
document.addEventListener('click', (e) => {
const overlay = $('gwoe-tour-overlay');
if (!overlay || overlay.hidden) return;
// Overlay-Klick hinter dem Spotlight oder Bubble → Tour-Ende
if (e.target === overlay) gwoeTourEnd();
});
// Bei Resize: aktive Bubble + Spotlight neu positionieren
window.addEventListener('resize', () => {
if (!$('gwoe-tour-overlay').hidden & & _resolvedSteps[_tourIdx]) {
positionBubble(_resolvedSteps[_tourIdx].el);
}
});
// Beim Verlassen der Page: Sprachausgabe stoppen.
window.addEventListener('pagehide', stopSpeak);
// Voices laden — manche Browser füllen sie asynchron.
if ('speechSynthesis' in window) {
window.speechSynthesis.onvoiceschanged = () => { /* trigger preload */ };
window.speechSynthesis.getVoices();
}
})();
< / script >