From e5bcfb3d75c9e89fa7a14ad63223eb7a49667c77 Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Mon, 16 Mar 2026 14:23:32 +0100 Subject: [PATCH] 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) --- src/static/css/style.css | 244 +++++++++++++ src/static/dashboard.html | 14 +- src/static/js/chat.js | 31 +- src/static/js/tutorial.js | 743 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 1027 insertions(+), 5 deletions(-) create mode 100644 src/static/js/tutorial.js 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 @@ - + @@ -742,6 +742,13 @@ + +
+
+
+
+
+
@@ -757,8 +764,9 @@ - - + + +
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; } + }, +};