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).
343 lines
12 KiB
HTML
343 lines
12 KiB
HTML
{# ─────────────────────────────────────────────────────────────────────
|
||
Geführte Tour durch das Antrag-Detail (#185).
|
||
|
||
Was diese Komponente liefert:
|
||
- "Tour"-Button rendert in der userrow (siehe antrag_detail.html)
|
||
- Vier Stationen: Score-Hero, Matrix, Programm-Treue, Stimmverhalten
|
||
- Spotlight-Overlay (Klick außerhalb → Tour-Ende)
|
||
- Erklärblase mit Vor / Zurück / Schließen
|
||
- Audio: Web Speech API (Browser-eingebaute Stimme), de-DE,
|
||
stoppbar bei Schritt-Wechsel und Schließen.
|
||
|
||
Stand: Phase 1 — Browser-TTS. Phase 2 (ElevenLabs) kommt separat
|
||
und tauscht nur das Audio-Backend; das Tour-Skript bleibt gleich.
|
||
───────────────────────────────────────────────────────────────────── #}
|
||
<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; }
|
||
.gwoe-tour-bubble .v3-action-btn.primary {
|
||
background: var(--ecg-teal);
|
||
color: #fff;
|
||
border-color: var(--ecg-teal);
|
||
}
|
||
|
||
@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 () {
|
||
// Tour-Skript: 4 Stationen. Selectoren werden zur Laufzeit aufgelöst —
|
||
// wenn ein Element fehlt (z.B. keine Plenum-Votes), überspringt die
|
||
// Tour den Schritt automatisch.
|
||
const STEPS = [
|
||
{
|
||
selector: '.v3-bewertung',
|
||
title: 'Die Gemeinwohl-Note',
|
||
text: 'Diese große Zahl ist die Gemeinwohl-Note. Null ist destruktiv, Zehn vorbildlich. Sie zeigt, wie sehr der Antrag dem Allgemeinwohl dient. Daneben steht die Empfehlung — Unterstützen, Überarbeiten oder Ablehnen.',
|
||
},
|
||
{
|
||
selector: '.v3-matrix-grid, .v3-matrix, [class*="matrix"]',
|
||
title: 'Die Gemeinwohl-Matrix',
|
||
text: 'Dieses Raster prüft fünf Werte: Würde, Solidarität, Nachhaltigkeit, Gerechtigkeit und Demokratie. Pro Wert wird geschaut, wie der Antrag fünf Berührungsgruppen betrifft — von Lieferanten bis zur Gesellschaft als Ganzes. Grün heißt: der Antrag fördert diesen Wert. Rot: er widerspricht ihm.',
|
||
},
|
||
{
|
||
selector: '.v3-fraktionen',
|
||
title: 'Programm-Treue pro Fraktion',
|
||
text: 'Hier sehen Sie, wie gut der Antrag zum Wahl- und Parteiprogramm jeder Fraktion passt. Die Zahl rechts ist der Programm-Score von Null bis Zehn. Ein Klick darauf zeigt die Begründung mit Zitaten aus dem Programm.',
|
||
},
|
||
{
|
||
selector: '.v3-vote-section',
|
||
title: 'Stimmverhalten und Marker',
|
||
text: 'Hier sehen Sie, wie die Fraktionen tatsächlich abgestimmt haben. Das Warnschild neben einer Fraktion bedeutet Heuchelei: Sie stimmt mit Nein, obwohl der Antrag exakt zu ihrem eigenen Wahlprogramm passt. Das Ausrufezeichen markiert Opportunismus: Ja-Stimme bei einem Antrag, der dem eigenen Programm widerspricht.',
|
||
},
|
||
];
|
||
|
||
let _tourIdx = 0;
|
||
let _resolvedSteps = []; // STEPS gefiltert auf vorhandene Elemente
|
||
let _tourMuted = false;
|
||
let _tourUtter = null;
|
||
|
||
function $(id) { return document.getElementById(id); }
|
||
|
||
function speak(text) {
|
||
if (_tourMuted) return;
|
||
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;
|
||
// Versuche eine deutsche, möglichst weibliche Stimme zu wählen.
|
||
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 */ }
|
||
}
|
||
|
||
function stopSpeak() {
|
||
if ('speechSynthesis' in window) {
|
||
try { window.speechSynthesis.cancel(); } catch (_) {}
|
||
}
|
||
_tourUtter = null;
|
||
}
|
||
|
||
function resolveSteps() {
|
||
_resolvedSteps = [];
|
||
for (const s of STEPS) {
|
||
// 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 () {
|
||
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>
|