Interaktives Tutorial-System mit 20 Schritten, Spotlight, Sprechblasen und virtuellen Maus-Demos

Neues Tutorial-System fuer gefuehrten Rundgang durch den Monitor:
- tutorial.js: Tutorial-Engine mit Spotlight-Abdunkelung, Bubble-Navigation,
  virtuellem Cursor fuer Drag/Resize-Demos, Keyboard-Support (Escape/Pfeiltasten)
- 20 Steps: Welcome, Sidebar, Lagen, Kacheln, Layout, Theme, Export, Chat, etc.
- Automatisches Ueberspringen von Steps wenn keine Lage geoeffnet
- Modal-Handling fuer Neue-Lage und Quellenverwaltung Steps
- Chat-Integration: Tutorial-Hinweis beim ersten Oeffnen, Keywords (rundgang/tutorial/tour/fuehrung)
- localStorage-Persistenz (osint_tutorial_seen)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Dev
2026-03-16 14:23:32 +01:00
Ursprung bbd4821011
Commit e5bcfb3d75
4 geänderte Dateien mit 1027 neuen und 5 gelöschten Zeilen

Datei anzeigen

@@ -4943,3 +4943,247 @@ a.map-popup-article:hover {
position: relative; position: relative;
z-index: 100; z-index: 100;
} }
/* ================================================================
Tutorial System
================================================================ */
/* Overlay (Hintergrund-Abdunkelung) */
.tutorial-overlay {
display: none;
position: fixed;
inset: 0;
z-index: 9000;
pointer-events: none;
}
.tutorial-overlay.active {
display: block;
}
/* Spotlight */
.tutorial-spotlight {
position: fixed;
z-index: 9001;
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.65);
border: 2px solid var(--accent);
border-radius: var(--radius-lg);
transition: top 0.4s ease, left 0.4s ease, width 0.4s ease, height 0.4s ease, opacity 0.3s ease;
opacity: 0;
pointer-events: none;
}
/* Target-Element klickbar machen */
.tutorial-overlay.active ~ * [data-tutorial-target] {
position: relative;
z-index: 9002;
}
/* Bubble (Sprechblase) */
.tutorial-bubble {
position: fixed;
z-index: 9003;
width: 340px;
background: var(--bg-card);
border: 1px solid var(--accent);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg), 0 0 20px rgba(150, 121, 26, 0.15);
padding: var(--sp-xl);
pointer-events: auto;
opacity: 0;
transition: opacity 0.3s ease, top 0.4s ease, left 0.4s ease, transform 0.4s ease;
font-family: var(--font-body);
}
.tutorial-bubble.visible {
opacity: 1;
}
/* Bubble-Pfeil */
.tutorial-bubble::before {
content: '';
position: absolute;
width: 12px;
height: 12px;
background: var(--bg-card);
border: 1px solid var(--accent);
transform: rotate(45deg);
}
.tutorial-pos-bottom::before {
top: -7px;
left: 50%;
margin-left: -6px;
border-right: none;
border-bottom: none;
}
.tutorial-pos-top::before {
bottom: -7px;
left: 50%;
margin-left: -6px;
border-left: none;
border-top: none;
}
.tutorial-pos-right::before {
left: -7px;
top: 30px;
border-top: none;
border-right: none;
}
.tutorial-pos-left::before {
right: -7px;
top: 30px;
border-bottom: none;
border-left: none;
}
.tutorial-pos-center::before {
display: none;
}
/* Bubble-Inhalt */
.tutorial-bubble-counter {
font-size: 11px;
color: var(--accent);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: var(--sp-sm);
}
.tutorial-bubble-title {
font-family: var(--font-title);
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--sp-md);
}
.tutorial-bubble-text {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: var(--sp-lg);
}
/* Close-Button */
.tutorial-bubble-close {
position: absolute;
top: var(--sp-md);
right: var(--sp-md);
width: 24px;
height: 24px;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius);
transition: color 0.15s, background 0.15s;
line-height: 1;
}
.tutorial-bubble-close:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
/* Fortschrittspunkte */
.tutorial-bubble-dots {
display: flex;
gap: 5px;
justify-content: center;
margin-bottom: var(--sp-lg);
flex-wrap: wrap;
}
.tutorial-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--border);
transition: background 0.2s;
}
.tutorial-dot.active {
background: var(--accent);
width: 18px;
border-radius: 3px;
}
.tutorial-dot.done {
background: var(--accent-hover);
}
/* Nav-Buttons */
.tutorial-bubble-nav {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--sp-md);
}
.tutorial-btn {
border: none;
border-radius: var(--radius);
padding: var(--sp-md) var(--sp-xl);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s, color 0.15s;
font-family: var(--font-body);
}
.tutorial-btn-back {
background: var(--bg-hover);
color: var(--text-secondary);
}
.tutorial-btn-back:hover {
background: var(--bg-elevated);
color: var(--text-primary);
}
.tutorial-btn-next {
background: var(--accent);
color: #fff;
}
.tutorial-btn-next:hover {
background: var(--accent-hover);
}
/* Virtueller Cursor */
.tutorial-cursor {
position: fixed;
z-index: 9500;
width: 24px;
height: 24px;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.tutorial-cursor.visible {
opacity: 1;
}
.tutorial-cursor-default {
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M5 3l14 8-6 2 4 8-3 1-4-8-5 4z' fill='%23fff' stroke='%23000' stroke-width='1'/%3E%3C/svg%3E") no-repeat center/contain;
}
.tutorial-cursor-grabbing {
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M8 10V8a1 1 0 112 0v2h1V7a1 1 0 112 0v3h1V8a1 1 0 112 0v2h.5a1.5 1.5 0 011.5 1.5V16a5 5 0 01-5 5h-2a5 5 0 01-5-5v-3.5A1.5 1.5 0 017.5 11H8z' fill='%23fff' stroke='%23000' stroke-width='0.8'/%3E%3C/svg%3E") no-repeat center/contain;
}
.tutorial-cursor-resize {
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M22 22H20V20H22V22ZM22 18H18V22H16V16H22V18ZM18 18V14H22V12H16V18H18ZM14 22H12V16H18V14H10V22H14Z' fill='%23fff' stroke='%23000' stroke-width='0.3'/%3E%3C/svg%3E") no-repeat center/contain;
}
/* Chat Tutorial-Hinweis */
.chat-tutorial-hint {
background: var(--bg-card);
border: 1px solid var(--accent);
border-radius: var(--radius);
padding: var(--sp-lg);
margin: var(--sp-md) var(--sp-md) 0;
cursor: pointer;
transition: background 0.15s;
font-size: 13px;
color: var(--text-secondary);
line-height: 1.5;
}
.chat-tutorial-hint:hover {
background: var(--tint-accent-subtle);
}
.chat-tutorial-hint strong {
color: var(--accent);
}

Datei anzeigen

@@ -17,7 +17,7 @@
<link rel="stylesheet" href="/static/vendor/MarkerCluster.css"> <link rel="stylesheet" href="/static/vendor/MarkerCluster.css">
<link rel="stylesheet" href="/static/vendor/MarkerCluster.Default.css"> <link rel="stylesheet" href="/static/vendor/MarkerCluster.Default.css">
<link rel="stylesheet" href="/static/css/network.css?v=20260316a"> <link rel="stylesheet" href="/static/css/network.css?v=20260316a">
<link rel="stylesheet" href="/static/css/style.css?v=20260316b"> <link rel="stylesheet" href="/static/css/style.css?v=20260316c">
</head> </head>
<body> <body>
<a href="#main-content" class="skip-link">Zum Hauptinhalt springen</a> <a href="#main-content" class="skip-link">Zum Hauptinhalt springen</a>
@@ -742,6 +742,13 @@
</div> </div>
</div> </div>
<!-- Tutorial -->
<div class="tutorial-overlay" id="tutorial-overlay">
<div class="tutorial-spotlight" id="tutorial-spotlight"></div>
</div>
<div class="tutorial-bubble" id="tutorial-bubble"></div>
<div class="tutorial-cursor" id="tutorial-cursor"></div>
<!-- Toast Container --> <!-- Toast Container -->
<div class="toast-container" id="toast-container" aria-live="polite" aria-atomic="true"></div> <div class="toast-container" id="toast-container" aria-live="polite" aria-atomic="true"></div>
@@ -757,8 +764,9 @@
<script src="/static/js/api_network.js?v=20260316a"></script> <script src="/static/js/api_network.js?v=20260316a"></script>
<script src="/static/js/network-graph.js?v=20260316a"></script> <script src="/static/js/network-graph.js?v=20260316a"></script>
<script src="/static/js/app_network.js?v=20260316a"></script> <script src="/static/js/app_network.js?v=20260316a"></script>
<script src="/static/js/chat.js?v=20260315b"></script> <script src="/static/js/tutorial.js?v=20260316a"></script>
<script>document.addEventListener("DOMContentLoaded",function(){Chat.init()});</script> <script src="/static/js/chat.js?v=20260316c"></script>
<script>document.addEventListener("DOMContentLoaded",function(){Chat.init();Tutorial.init()});</script>
<!-- Map Fullscreen Overlay --> <!-- Map Fullscreen Overlay -->
<div class="map-fullscreen-overlay" id="map-fullscreen-overlay"> <div class="map-fullscreen-overlay" id="map-fullscreen-overlay">

Datei anzeigen

@@ -30,7 +30,7 @@ const Chat = {
this.send(); this.send();
}); });
// Enter sendet, Shift+Enter fuer Zeilenumbruch // Enter sendet, Shift+Enter für Zeilenumbruch
input.addEventListener('keydown', (e) => { input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
@@ -63,7 +63,11 @@ const Chat = {
if (!this._hasGreeted) { if (!this._hasGreeted) {
this._hasGreeted = true; this._hasGreeted = true;
this.addMessage('assistant', 'Hallo! Ich bin der AegisSight Assistent und helfe dir bei der Bedienung des Monitors.\n\nFrag mich zum Beispiel:\n\n"Wie erstelle ich eine neue Lage?"\n"Was bedeuten die Faktencheck-Status?"\n"Wie funktioniert der automatische Refresh?"\n"Wie nutze ich die Kartenansicht?"\n"Wie exportiere ich einen Lagebericht?"\n\nFuer alle weiteren Anliegen erreichst du den Support unter support@aegis-sight.de.'); this.addMessage('assistant', 'Hallo! Ich bin der AegisSight Assistent und helfe dir bei der Bedienung des Monitors.\n\nFrag mich zum Beispiel:\n\n"Wie erstelle ich eine neue Lage?"\n"Was bedeuten die Faktencheck-Status?"\n"Wie funktioniert der automatische Refresh?"\n"Wie nutze ich die Kartenansicht?"\n"Wie exportiere ich einen Lagebericht?"\n\nFür alle weiteren Anliegen erreichst du den Support unter support@aegis-sight.de.');
// Tutorial-Hinweis beim ersten Oeffnen
if (typeof Tutorial !== "undefined" && !Tutorial._hasSeen()) {
this._showTutorialHint();
}
} }
// Focus auf Input // Focus auf Input
@@ -129,6 +133,16 @@ const Chat = {
this._showTyping(); this._showTyping();
this._isLoading = true; this._isLoading = true;
// Tutorial-Keywords abfangen
var lowerText = text.toLowerCase();
if (lowerText === 'rundgang' || lowerText === 'tutorial' || lowerText === 'tour' || lowerText === 'f\u00fchrung') {
this._hideTyping();
this._isLoading = false;
this.close();
if (typeof Tutorial !== 'undefined') Tutorial.start();
return;
}
try { try {
const body = { const body = {
message: text, message: text,
@@ -274,4 +288,17 @@ const Chat = {
} }
}, },
_showTutorialHint() {
var container = document.getElementById('chat-messages');
if (!container) return;
var hint = document.createElement('div');
hint.className = 'chat-tutorial-hint';
hint.innerHTML = '<strong>Tipp:</strong> Kennen Sie schon den interaktiven Rundgang? Er zeigt Ihnen Schritt für Schritt alle Funktionen des Monitors. Klicken Sie hier, um den Rundgang zu starten.';
hint.addEventListener('click', function() {
Chat.close();
if (typeof Tutorial !== 'undefined') Tutorial.start();
});
container.appendChild(hint);
},
}; };

743
src/static/js/tutorial.js Normale Datei
Datei anzeigen

@@ -0,0 +1,743 @@
/**
* AegisSight Tutorial - Interaktiver Rundgang durch den Monitor.
*/
const Tutorial = {
_steps: [],
_currentStep: -1,
_isActive: false,
_cleanupFns: [],
_resizeTimer: null,
_keyHandler: null,
_resizeHandler: null,
_demoRunning: false,
// DOM-Referenzen
_els: {
overlay: null,
spotlight: null,
bubble: null,
cursor: null,
},
init() {
this._defineSteps();
this._els.overlay = document.getElementById('tutorial-overlay');
this._els.spotlight = document.getElementById('tutorial-spotlight');
this._els.bubble = document.getElementById('tutorial-bubble');
this._els.cursor = document.getElementById('tutorial-cursor');
},
// -----------------------------------------------------------------------
// Step-Definitionen
// -----------------------------------------------------------------------
_defineSteps() {
this._steps = [
// 0 - Welcome
{
id: 'welcome',
target: '#main-content',
title: 'Willkommen im AegisSight Monitor',
text: 'Dieser kurze Rundgang zeigt Ihnen die wichtigsten Funktionen des Monitors. Sie können jederzeit mit Escape abbrechen oder mit den Pfeiltasten navigieren.',
position: 'center',
},
// 1 - Sidebar
{
id: 'sidebar',
target: '.sidebar',
title: 'Seitenleiste: Ihre Lagen',
text: 'Hier finden Sie alle Ihre Lagen, unterteilt in Live-Monitoring, Recherchen und das Archiv. Klicken Sie auf eine Lage, um sie zu öffnen.',
position: 'right',
},
// 2 - Neue Lage Button
{
id: 'new-incident-btn',
target: '#new-incident-btn',
title: 'Neue Lage anlegen',
text: 'Mit diesem Button erstellen Sie eine neue Lage. Sie können zwischen Live-Monitoring und Recherche wählen.',
position: 'right',
onEnter: function() {
setTimeout(function() {
var btn = document.getElementById('new-incident-btn');
if (btn) btn.click();
}, 1500);
},
},
// 3 - Neue Lage Modal
{
id: 'new-incident-modal',
target: '#modal-new .modal',
title: 'Lage konfigurieren',
text: 'Hier geben Sie den Titel, die Beschreibung und den Typ der Lage ein. Bei Live-Monitoring-Lagen können Sie zusätzlich Suchbegriffe und Quellen festlegen.',
position: 'right',
requireModal: '#modal-new',
onEnter: function() {
var overlay = document.getElementById('modal-new');
if (overlay && !overlay.classList.contains('active')) {
overlay.classList.add('active');
}
if (overlay) overlay.style.zIndex = '9002';
},
onExit: function() {
var overlay = document.getElementById('modal-new');
if (overlay) {
overlay.classList.remove('active');
overlay.style.zIndex = '';
}
},
},
// 4 - Sidebar Filter
{
id: 'sidebar-filters',
target: '.sidebar-filter',
title: 'Lagen filtern',
text: 'Wechseln Sie zwischen "Alle" Lagen und "Eigene", um nur Ihre selbst erstellten Lagen anzuzeigen.',
position: 'right',
},
// 5 - Lagebild
{
id: 'lagebild',
target: '[gs-id="lagebild"]',
title: 'Lagebild',
text: 'Das Lagebild fasst die aktuelle Situation zusammen. Quellenverweise sind als Nummern markiert und führen direkt zu den Originalartikeln. Über das Pfeil-Symbol öffnen Sie die Vollansicht.',
position: 'top',
requireIncident: true,
},
// 6 - Faktencheck
{
id: 'faktencheck',
target: '[gs-id="faktencheck"]',
title: 'Faktencheck',
text: 'Hier werden Behauptungen automatisch geprüft und in Kategorien eingeteilt: Bestätigt, Widersprüchlich, Unbelegt und Falsch. Jeder Eintrag zeigt die Quellen der Bewertung.',
position: 'top',
requireIncident: true,
},
// 7 - Quellen
{
id: 'quellen',
target: '[gs-id="quellen"]',
title: 'Quellenübersicht',
text: 'Alle verwendeten Quellen auf einen Blick, gruppiert nach Kategorie. Klicken Sie auf eine Gruppe, um die einzelnen Quellen aufzuklappen.',
position: 'top',
requireIncident: true,
},
// 8 - Timeline
{
id: 'timeline',
target: '[gs-id="timeline"]',
title: 'Ereignis-Timeline',
text: 'Die Timeline zeigt alle Ereignisse chronologisch. Nutzen Sie die Filter, um nach Zeitraum oder Kategorie einzugrenzen.',
position: 'top',
requireIncident: true,
},
// 9 - Karte
{
id: 'karte',
target: '[gs-id="karte"]',
title: 'Geografische Verteilung',
text: 'Die Karte zeigt automatisch erkannte Orte aus den Quellen. Klicken Sie auf Marker für Details oder nutzen Sie das Vollbild-Symbol für eine größere Ansicht.',
position: 'top',
requireIncident: true,
},
// 10 - Layout Toolbar
{
id: 'layout-toolbar',
target: '#layout-toolbar',
title: 'Layout-Steuerung',
text: 'Mit diesen Schaltern blenden Sie einzelne Kacheln ein oder aus. So passen Sie das Dashboard an Ihre Bedürfnisse an.',
position: 'bottom',
requireIncident: true,
},
// 11 - Drag Demo
{
id: 'drag-demo',
target: '[gs-id="lagebild"] .card-header',
title: 'Kacheln verschieben',
text: 'Sie können jede Kachel per Drag-and-Drop an eine andere Position verschieben. Greifen Sie dazu die Kopfzeile der Kachel und ziehen Sie sie an die gewünschte Stelle.',
position: 'top',
requireIncident: true,
disableNav: true,
onEnter: function() {
Tutorial._simulateDrag();
},
},
// 12 - Resize Demo
{
id: 'resize-demo',
target: '[gs-id="faktencheck"]',
title: 'Kacheln anpassen',
text: 'Ziehen Sie am rechten unteren Rand einer Kachel, um ihre Größe anzupassen. So können Sie wichtigen Inhalten mehr Platz einräumen.',
position: 'top',
requireIncident: true,
disableNav: true,
onEnter: function() {
Tutorial._simulateResize();
},
},
// 13 - Theme
{
id: 'theme',
target: '#theme-toggle',
title: 'Design umschalten',
text: 'Wechseln Sie zwischen dem dunklen und hellen Design. Ihre Auswahl wird gespeichert.',
position: 'bottom',
},
// 14 - Refresh
{
id: 'refresh',
target: '#refresh-btn',
title: 'Aktualisieren',
text: 'Starten Sie eine manuelle Aktualisierung der Lage. Bei Live-Monitoring-Lagen erfolgt die Aktualisierung zusätzlich automatisch in regelmaessigen Abstaenden.',
position: 'bottom',
requireIncident: true,
},
// 15 - Quellen-Button
{
id: 'sources-btn',
target: '.sidebar-sources-link button:first-child',
title: 'Quellenverwaltung',
text: 'Hier öffnen Sie die Quellenverwaltung, um Quellen hinzuzufügen, zu bearbeiten oder zu deaktivieren.',
position: 'right',
onEnter: function() {
setTimeout(function() {
if (typeof App !== 'undefined' && App.openSourceManagement) {
App.openSourceManagement();
}
}, 1500);
},
},
// 16 - Quellen Modal
{
id: 'sources-modal',
target: '#modal-sources .modal',
title: 'Quellen verwalten',
text: 'In der Quellenverwaltung sehen Sie alle konfigurierten Quellen. Sie können neue Quellen hinzufuegen, bestehende bearbeiten oder einzelne Quellen für bestimmte Lagen ausschließen.',
position: 'right',
requireModal: '#modal-sources',
onEnter: function() {
var overlay = document.getElementById('modal-sources');
if (overlay && !overlay.classList.contains('active')) {
if (typeof App !== 'undefined' && App.openSourceManagement) {
App.openSourceManagement();
}
}
if (overlay) overlay.style.zIndex = '9002';
},
onExit: function() {
var overlay = document.getElementById('modal-sources');
if (overlay) {
overlay.classList.remove('active');
overlay.style.zIndex = '';
}
},
},
// 17 - Export
{
id: 'export',
target: '#export-dropdown',
title: 'Exportieren',
text: 'Exportieren Sie den Lagebericht als Markdown, JSON oder drucken Sie ihn als PDF. Waehlen Sie zwischen dem kompakten Lagebericht und dem Vollexport mit allen Details.',
position: 'bottom',
requireIncident: true,
},
// 18 - Chat
{
id: 'chat',
target: '#chat-toggle-btn',
title: 'Chat-Assistent',
text: 'Der Chat-Assistent hilft Ihnen bei Fragen zur Bedienung des Monitors. Stellen Sie einfach Ihre Frage und erhalten Sie sofort eine Antwort.',
position: 'left',
},
// 19 - Ende
{
id: 'end',
target: null,
title: 'Rundgang abgeschlossen',
text: 'Sie kennen jetzt die wichtigsten Funktionen des AegisSight Monitors. Bei weiteren Fragen steht Ihnen der Chat-Assistent oder unser Support unter support@aegis-sight.de zur Verfügung.',
position: 'center',
},
];
},
// -----------------------------------------------------------------------
// Lifecycle
// -----------------------------------------------------------------------
start() {
if (this._isActive) return;
this._isActive = true;
this._currentStep = -1;
// Chat schliessen falls offen
if (typeof Chat !== 'undefined' && Chat._isOpen) Chat.close();
// Overlay einblenden
this._els.overlay.classList.add('active');
// Keyboard
this._keyHandler = this._onKey.bind(this);
document.addEventListener('keydown', this._keyHandler);
// Resize
this._resizeHandler = this._onResize.bind(this);
window.addEventListener('resize', this._resizeHandler);
this.next();
},
stop() {
if (!this._isActive) return;
// Aktuellen Step aufraumen
if (this._currentStep >= 0) this._exitStep(this._currentStep);
this._isActive = false;
this._currentStep = -1;
this._demoRunning = false;
// Overlay ausblenden
this._els.overlay.classList.remove('active');
this._els.spotlight.style.opacity = '0';
this._els.bubble.classList.remove('visible');
this._hideCursor();
// Events entfernen
if (this._keyHandler) {
document.removeEventListener('keydown', this._keyHandler);
this._keyHandler = null;
}
if (this._resizeHandler) {
window.removeEventListener('resize', this._resizeHandler);
this._resizeHandler = null;
}
// Cleanup-Callbacks
this._cleanupFns.forEach(function(fn) { try { fn(); } catch(e) {} });
this._cleanupFns = [];
this._markSeen();
},
// -----------------------------------------------------------------------
// Navigation
// -----------------------------------------------------------------------
next() {
if (!this._isActive || this._demoRunning) return;
var nextIdx = this._currentStep + 1;
// Skip Steps deren Voraussetzungen nicht erfuellt sind
while (nextIdx < this._steps.length && this._shouldSkip(nextIdx)) {
nextIdx++;
}
if (nextIdx >= this._steps.length) {
this.stop();
return;
}
this.goToStep(nextIdx);
},
prev() {
if (!this._isActive || this._demoRunning) return;
var prevIdx = this._currentStep - 1;
while (prevIdx >= 0 && this._shouldSkip(prevIdx)) {
prevIdx--;
}
if (prevIdx < 0) return;
this.goToStep(prevIdx);
},
goToStep(index) {
if (index < 0 || index >= this._steps.length) return;
if (this._currentStep >= 0) this._exitStep(this._currentStep);
this._currentStep = index;
this._enterStep(index);
},
_shouldSkip(index) {
var step = this._steps[index];
if (!step) return true;
// Steps die eine offene Lage brauchen
if (step.requireIncident) {
var view = document.getElementById('incident-view');
if (!view || view.style.display === 'none' || !view.offsetParent) return true;
}
// Target muss existieren (ausser center-Steps)
if (step.target && step.position !== 'center') {
var el = document.querySelector(step.target);
if (!el) return true;
}
return false;
},
// -----------------------------------------------------------------------
// Step Enter/Exit
// -----------------------------------------------------------------------
_enterStep(i) {
var step = this._steps[i];
var self = this;
// Spotlight positionieren
if (step.target && step.position !== 'center') {
this._spotlightElement(step.target);
} else {
// Kein Spotlight / zentrierte Bubble
this._els.spotlight.style.opacity = '0';
}
// Bubble konfigurieren und anzeigen
this._showBubble(step, i);
// onEnter-Callback
if (step.onEnter) {
step.onEnter();
}
},
_exitStep(i) {
var step = this._steps[i];
// onExit-Callback
if (step.onExit) {
step.onExit();
}
this._hideCursor();
},
// -----------------------------------------------------------------------
// Spotlight
// -----------------------------------------------------------------------
_spotlightElement(selector) {
var el = document.querySelector(selector);
if (!el) {
this._els.spotlight.style.opacity = '0';
return;
}
var rect = el.getBoundingClientRect();
var pad = 8;
var s = this._els.spotlight.style;
s.top = (rect.top - pad) + 'px';
s.left = (rect.left - pad) + 'px';
s.width = (rect.width + pad * 2) + 'px';
s.height = (rect.height + pad * 2) + 'px';
s.borderRadius = '8px';
s.opacity = '1';
},
// -----------------------------------------------------------------------
// Bubble
// -----------------------------------------------------------------------
_showBubble(step, index) {
var bubble = this._els.bubble;
var total = this._steps.length;
// Inhalt
var counterHtml = '<div class="tutorial-bubble-counter">Schritt ' + (index + 1) + ' von ' + total + '</div>';
var titleHtml = '<div class="tutorial-bubble-title">' + step.title + '</div>';
var textHtml = '<div class="tutorial-bubble-text">' + step.text + '</div>';
// Fortschrittspunkte
var dotsHtml = '<div class="tutorial-bubble-dots">';
for (var d = 0; d < total; d++) {
dotsHtml += '<span class="tutorial-dot' + (d === index ? ' active' : '') + (d < index ? ' done' : '') + '"></span>';
}
dotsHtml += '</div>';
// Navigation
var navHtml = '<div class="tutorial-bubble-nav">';
if (index > 0 && !step.disableNav) {
navHtml += '<button class="tutorial-btn tutorial-btn-back" onclick="Tutorial.prev()">Zurück</button>';
} else {
navHtml += '<span></span>';
}
if (index < total - 1 && !step.disableNav) {
navHtml += '<button class="tutorial-btn tutorial-btn-next" onclick="Tutorial.next()">Weiter</button>';
} else if (index === total - 1) {
navHtml += '<button class="tutorial-btn tutorial-btn-next" onclick="Tutorial.stop()">Fertig</button>';
} else {
navHtml += '<span></span>';
}
navHtml += '</div>';
// Close-Button
var closeHtml = '<button class="tutorial-bubble-close" onclick="Tutorial.stop()" title="Rundgang beenden">&times;</button>';
bubble.innerHTML = closeHtml + counterHtml + titleHtml + textHtml + dotsHtml + navHtml;
// Positionierung
this._positionBubble(step);
// Sichtbar machen
bubble.classList.add('visible');
},
_positionBubble(step) {
var bubble = this._els.bubble;
var bw = 340;
// Zentriert (Welcome / End)
if (step.position === 'center' || !step.target) {
bubble.className = 'tutorial-bubble visible tutorial-pos-center';
bubble.style.top = '50%';
bubble.style.left = '50%';
bubble.style.transform = 'translate(-50%, -50%)';
return;
}
var el = document.querySelector(step.target);
if (!el) {
bubble.className = 'tutorial-bubble visible tutorial-pos-center';
bubble.style.top = '50%';
bubble.style.left = '50%';
bubble.style.transform = 'translate(-50%, -50%)';
return;
}
var rect = el.getBoundingClientRect();
var vw = window.innerWidth;
var vh = window.innerHeight;
var gap = 16;
// Reset transform
bubble.style.transform = '';
// Automatische Positionswahl falls nicht genug Platz
var pos = step.position || 'bottom';
var bubbleHeight = 250; // Schaetzung
if (pos === 'bottom' && (rect.bottom + gap + bubbleHeight > vh)) pos = 'top';
if (pos === 'top' && (rect.top - gap - bubbleHeight < 0)) pos = 'bottom';
if (pos === 'right' && (rect.right + gap + bw > vw)) pos = 'left';
if (pos === 'left' && (rect.left - gap - bw < 0)) pos = 'right';
bubble.className = 'tutorial-bubble visible tutorial-pos-' + pos;
switch (pos) {
case 'bottom':
bubble.style.top = (rect.bottom + gap) + 'px';
bubble.style.left = Math.max(8, Math.min(rect.left + rect.width / 2 - bw / 2, vw - bw - 8)) + 'px';
break;
case 'top':
bubble.style.top = (rect.top - gap) + 'px';
bubble.style.left = Math.max(8, Math.min(rect.left + rect.width / 2 - bw / 2, vw - bw - 8)) + 'px';
bubble.style.transform = 'translateY(-100%)';
break;
case 'right':
bubble.style.top = Math.max(8, rect.top + rect.height / 2 - bubbleHeight / 2) + 'px';
bubble.style.left = (rect.right + gap) + 'px';
break;
case 'left':
bubble.style.top = Math.max(8, rect.top + rect.height / 2 - bubbleHeight / 2) + 'px';
bubble.style.left = (rect.left - gap - bw) + 'px';
break;
}
},
// -----------------------------------------------------------------------
// Cursor (virtuelle Maus)
// -----------------------------------------------------------------------
_showCursor(x, y, type) {
var c = this._els.cursor;
c.className = 'tutorial-cursor';
if (type) c.classList.add('tutorial-cursor-' + type);
c.style.left = x + 'px';
c.style.top = y + 'px';
c.classList.add('visible');
},
_hideCursor() {
this._els.cursor.classList.remove('visible');
},
_animateCursor(fromX, fromY, toX, toY, ms) {
var self = this;
return new Promise(function(resolve) {
var c = self._els.cursor;
var start = null;
function easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
function frame(timestamp) {
if (!self._isActive) { resolve(); return; }
if (!start) start = timestamp;
var elapsed = timestamp - start;
var progress = Math.min(elapsed / ms, 1);
var eased = easeInOutCubic(progress);
var cx = fromX + (toX - fromX) * eased;
var cy = fromY + (toY - fromY) * eased;
c.style.left = cx + 'px';
c.style.top = cy + 'px';
if (progress < 1) {
requestAnimationFrame(frame);
} else {
resolve();
}
}
requestAnimationFrame(frame);
});
},
_wait(ms) {
var self = this;
return new Promise(function(resolve) {
setTimeout(function() { resolve(); }, ms);
});
},
// -----------------------------------------------------------------------
// Drag-Demo (Step 11)
// -----------------------------------------------------------------------
async _simulateDrag() {
this._demoRunning = true;
var el = document.querySelector('[gs-id="lagebild"] .card-header');
if (!el) { this._demoRunning = false; this.next(); return; }
var rect = el.getBoundingClientRect();
var startX = rect.left + rect.width / 2;
var startY = rect.top + rect.height / 2;
var endX = startX + 200;
var endY = startY;
// 1. Cursor erscheint
this._showCursor(startX, startY, 'default');
await this._wait(500);
// 2. Grabbing-State
this._els.cursor.classList.remove('tutorial-cursor-default');
this._els.cursor.classList.add('tutorial-cursor-grabbing');
await this._wait(300);
// 3. Nach rechts gleiten
await this._animateCursor(startX, startY, endX, endY, 1500);
await this._wait(200);
// 4. Release + zurück
this._els.cursor.classList.remove('tutorial-cursor-grabbing');
this._els.cursor.classList.add('tutorial-cursor-default');
await this._animateCursor(endX, endY, startX, startY, 800);
await this._wait(300);
// 5. Cursor verschwindet
this._hideCursor();
this._demoRunning = false;
this._enableNavAfterDemo();
},
// -----------------------------------------------------------------------
// Resize-Demo (Step 12)
// -----------------------------------------------------------------------
async _simulateResize() {
this._demoRunning = true;
var el = document.querySelector('[gs-id="faktencheck"]');
if (!el) { this._demoRunning = false; this.next(); return; }
var rect = el.getBoundingClientRect();
var startX = rect.right - 4;
var startY = rect.bottom - 4;
var endX = startX + 100;
var endY = startY + 60;
// 1. Cursor an unterer rechter Ecke
this._showCursor(startX, startY, 'resize');
await this._wait(500);
// 2. Diagonal ziehen
await this._animateCursor(startX, startY, endX, endY, 1200);
await this._wait(200);
// 3. Zurück
await this._animateCursor(endX, endY, startX, startY, 800);
await this._wait(300);
// 4. Cursor verschwindet
this._hideCursor();
this._demoRunning = false;
this._enableNavAfterDemo();
},
_enableNavAfterDemo() {
// Nav-Buttons nachträglich einblenden
var bubble = this._els.bubble;
var nav = bubble.querySelector('.tutorial-bubble-nav');
if (!nav) return;
var step = this._steps[this._currentStep];
var index = this._currentStep;
var total = this._steps.length;
var navHtml = '';
if (index > 0) {
navHtml += '<button class="tutorial-btn tutorial-btn-back" onclick="Tutorial.prev()">Zurück</button>';
} else {
navHtml += '<span></span>';
}
if (index < total - 1) {
navHtml += '<button class="tutorial-btn tutorial-btn-next" onclick="Tutorial.next()">Weiter</button>';
} else {
navHtml += '<button class="tutorial-btn tutorial-btn-next" onclick="Tutorial.stop()">Fertig</button>';
}
nav.innerHTML = navHtml;
},
// -----------------------------------------------------------------------
// Keyboard
// -----------------------------------------------------------------------
_onKey(e) {
if (!this._isActive) return;
switch (e.key) {
case 'Escape':
e.preventDefault();
this.stop();
break;
case 'ArrowRight':
case 'Enter':
e.preventDefault();
this.next();
break;
case 'ArrowLeft':
e.preventDefault();
this.prev();
break;
}
},
// -----------------------------------------------------------------------
// Resize
// -----------------------------------------------------------------------
_onResize() {
if (!this._isActive) return;
clearTimeout(this._resizeTimer);
var self = this;
this._resizeTimer = setTimeout(function() {
if (!self._isActive || self._currentStep < 0) return;
var step = self._steps[self._currentStep];
if (step.target && step.position !== 'center') {
self._spotlightElement(step.target);
}
self._positionBubble(step);
}, 150);
},
// -----------------------------------------------------------------------
// Persistenz
// -----------------------------------------------------------------------
_markSeen() {
try { localStorage.setItem('osint_tutorial_seen', '1'); } catch(e) {}
},
_hasSeen() {
try { return localStorage.getItem('osint_tutorial_seen') === '1'; } catch(e) { return false; }
},
};