diff --git a/src/static/css/style.css b/src/static/css/style.css
index 85e7861..bed0256 100644
--- a/src/static/css/style.css
+++ b/src/static/css/style.css
@@ -4943,3 +4943,247 @@ a.map-popup-article:hover {
position: relative;
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);
+}
diff --git a/src/static/dashboard.html b/src/static/dashboard.html
index 19ecc3c..433e6ca 100644
--- a/src/static/dashboard.html
+++ b/src/static/dashboard.html
@@ -17,7 +17,7 @@
-
+
diff --git a/src/static/js/chat.js b/src/static/js/chat.js
index 81172cc..041075b 100644
--- a/src/static/js/chat.js
+++ b/src/static/js/chat.js
@@ -30,7 +30,7 @@ const Chat = {
this.send();
});
- // Enter sendet, Shift+Enter fuer Zeilenumbruch
+ // Enter sendet, Shift+Enter für Zeilenumbruch
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
@@ -63,7 +63,11 @@ const Chat = {
if (!this._hasGreeted) {
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
@@ -129,6 +133,16 @@ const Chat = {
this._showTyping();
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 {
const body = {
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 = '
Tipp: 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);
+ },
+
};
diff --git a/src/static/js/tutorial.js b/src/static/js/tutorial.js
new file mode 100644
index 0000000..0e0751f
--- /dev/null
+++ b/src/static/js/tutorial.js
@@ -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 = '
Schritt ' + (index + 1) + ' von ' + total + '
';
+ var titleHtml = '
' + step.title + '
';
+ var textHtml = '
' + step.text + '
';
+
+ // Fortschrittspunkte
+ var dotsHtml = '
';
+ for (var d = 0; d < total; d++) {
+ dotsHtml += '';
+ }
+ dotsHtml += '
';
+
+ // Navigation
+ var navHtml = '
';
+ if (index > 0 && !step.disableNav) {
+ navHtml += '';
+ } else {
+ navHtml += '';
+ }
+ if (index < total - 1 && !step.disableNav) {
+ navHtml += '';
+ } else if (index === total - 1) {
+ navHtml += '';
+ } else {
+ navHtml += '';
+ }
+ navHtml += '
';
+
+ // Close-Button
+ var closeHtml = '
';
+
+ 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 += '
';
+ } else {
+ navHtml += '
';
+ }
+ if (index < total - 1) {
+ navHtml += '
';
+ } else {
+ navHtml += '
';
+ }
+ 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; }
+ },
+};