feat: Tutorial-Fortschritt serverseitig persistieren (Resume/Restart)
- Neuer Router /api/tutorial mit GET/PUT/DELETE für Fortschritt pro User - DB-Migration: tutorial_step + tutorial_completed in users-Tabelle - Resume-Dialog bei abgebrochenem Tutorial (Fortsetzen/Neu starten) - Chat-Hinweis passt sich dem Tutorial-Status dynamisch an - API-Methoden: getTutorialState, saveTutorialState, resetTutorialState Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -215,6 +215,19 @@ const API = {
|
||||
},
|
||||
|
||||
// Export
|
||||
|
||||
// Tutorial-Fortschritt
|
||||
getTutorialState() {
|
||||
return this._request('GET', '/tutorial/state');
|
||||
},
|
||||
|
||||
saveTutorialState(data) {
|
||||
return this._request('PUT', '/tutorial/state', data);
|
||||
},
|
||||
|
||||
resetTutorialState() {
|
||||
return this._request('DELETE', '/tutorial/state');
|
||||
},
|
||||
exportIncident(id, format, scope) {
|
||||
const token = localStorage.getItem('osint_token');
|
||||
return fetch(`${this.baseUrl}/incidents/${id}/export?format=${format}&scope=${scope}`, {
|
||||
|
||||
@@ -6,6 +6,7 @@ const Chat = {
|
||||
_isOpen: false,
|
||||
_isLoading: false,
|
||||
_hasGreeted: false,
|
||||
_tutorialHintDismissed: false,
|
||||
_isFullscreen: false,
|
||||
|
||||
init() {
|
||||
@@ -64,10 +65,13 @@ const Chat = {
|
||||
if (!this._hasGreeted) {
|
||||
this._hasGreeted = true;
|
||||
this.addMessage('assistant', 'Hallo! Ich bin der AegisSight Assistent. Stell mir gerne jede Frage rund um die Bedienung des Monitors, ich helfe dir weiter.');
|
||||
// Tutorial-Hinweis beim ersten Chat-Oeffnen der Session
|
||||
if (typeof Tutorial !== 'undefined' && !sessionStorage.getItem('osint_tutorial_hint_dismissed')) {
|
||||
this._showTutorialHint();
|
||||
}
|
||||
}
|
||||
|
||||
// Tutorial-Hinweis bei jedem Oeffnen aktualisieren (wenn nicht dismissed)
|
||||
if (typeof Tutorial !== 'undefined' && !this._tutorialHintDismissed) {
|
||||
var oldHint = document.getElementById('chat-tutorial-hint');
|
||||
if (oldHint) oldHint.remove();
|
||||
this._showTutorialHint();
|
||||
}
|
||||
|
||||
// Focus auf Input
|
||||
@@ -288,29 +292,57 @@ const Chat = {
|
||||
}
|
||||
},
|
||||
|
||||
_showTutorialHint() {
|
||||
async _showTutorialHint() {
|
||||
var container = document.getElementById('chat-messages');
|
||||
if (!container) return;
|
||||
|
||||
// API-State laden (Fallback: Standard-Hint)
|
||||
var state = null;
|
||||
try { state = await API.getTutorialState(); } catch(e) {}
|
||||
|
||||
var hint = document.createElement('div');
|
||||
hint.className = 'chat-tutorial-hint';
|
||||
hint.id = 'chat-tutorial-hint';
|
||||
var textDiv = document.createElement('div');
|
||||
textDiv.className = 'chat-tutorial-hint-text';
|
||||
textDiv.innerHTML = '<strong>Tipp:</strong> Kennen Sie schon den interaktiven Rundgang? Er zeigt Ihnen Schritt f\u00fcr Schritt alle Funktionen des Monitors. Klicken Sie hier, um ihn zu starten.';
|
||||
textDiv.style.cursor = 'pointer';
|
||||
textDiv.addEventListener('click', function() {
|
||||
Chat.close();
|
||||
sessionStorage.setItem('osint_tutorial_hint_dismissed', '1');
|
||||
if (typeof Tutorial !== 'undefined') Tutorial.start();
|
||||
});
|
||||
|
||||
if (state && !state.completed && state.current_step !== null && state.current_step > 0) {
|
||||
// Mittendrin abgebrochen
|
||||
var totalSteps = (typeof Tutorial !== 'undefined') ? Tutorial._steps.length : 32;
|
||||
textDiv.innerHTML = '<strong>Tipp:</strong> Sie haben den Rundgang bei Schritt ' + (state.current_step + 1) + '/' + totalSteps + ' unterbrochen. Klicken Sie hier, um fortzusetzen.';
|
||||
textDiv.addEventListener('click', function() {
|
||||
Chat.close();
|
||||
Chat._tutorialHintDismissed = true;
|
||||
if (typeof Tutorial !== 'undefined') Tutorial.start();
|
||||
});
|
||||
} else if (state && state.completed) {
|
||||
// Bereits abgeschlossen
|
||||
textDiv.innerHTML = '<strong>Tipp:</strong> Sie haben den Rundgang bereits abgeschlossen. <span style="text-decoration:underline;">Erneut starten?</span>';
|
||||
textDiv.addEventListener('click', async function() {
|
||||
Chat.close();
|
||||
Chat._tutorialHintDismissed = true;
|
||||
try { await API.resetTutorialState(); } catch(e) {}
|
||||
if (typeof Tutorial !== 'undefined') Tutorial.start(true);
|
||||
});
|
||||
} else {
|
||||
// Nie gestartet
|
||||
textDiv.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 ihn zu starten.';
|
||||
textDiv.addEventListener('click', function() {
|
||||
Chat.close();
|
||||
Chat._tutorialHintDismissed = true;
|
||||
if (typeof Tutorial !== 'undefined') Tutorial.start();
|
||||
});
|
||||
}
|
||||
|
||||
var closeBtn = document.createElement('button');
|
||||
closeBtn.className = 'chat-tutorial-hint-close';
|
||||
closeBtn.title = 'Schlie\u00dfen';
|
||||
closeBtn.title = 'Schließen';
|
||||
closeBtn.innerHTML = '×';
|
||||
closeBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
hint.remove();
|
||||
sessionStorage.setItem('osint_tutorial_hint_dismissed', '1');
|
||||
Chat._tutorialHintDismissed = true;
|
||||
});
|
||||
hint.appendChild(textDiv);
|
||||
hint.appendChild(closeBtn);
|
||||
|
||||
@@ -12,6 +12,7 @@ const Tutorial = {
|
||||
_resizeHandler: null,
|
||||
_demoRunning: false,
|
||||
_lastExitedStep: -1,
|
||||
_highestStep: -1,
|
||||
_stepTimers: [], // setTimeout-IDs fuer den aktuellen Step
|
||||
_savedState: null, // Dashboard-Zustand vor dem Tutorial
|
||||
|
||||
@@ -1152,14 +1153,52 @@ const Tutorial = {
|
||||
// -----------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// -----------------------------------------------------------------------
|
||||
start() {
|
||||
async start(forceRestart) {
|
||||
if (this._isActive) return;
|
||||
this._isActive = true;
|
||||
this._currentStep = -1;
|
||||
|
||||
// Chat schließen falls offen
|
||||
// Chat schliessen falls offen
|
||||
if (typeof Chat !== 'undefined' && Chat._isOpen) Chat.close();
|
||||
|
||||
// Server-State laden (Fallback: direkt starten)
|
||||
var state = null;
|
||||
try { state = await API.getTutorialState(); } catch(e) {}
|
||||
|
||||
// Resume-Dialog wenn mittendrin abgebrochen
|
||||
if (!forceRestart && state && !state.completed && state.current_step !== null && state.current_step > 0) {
|
||||
this._showResumeDialog(state.current_step);
|
||||
return;
|
||||
}
|
||||
|
||||
this._startInternal(forceRestart ? 0 : null);
|
||||
},
|
||||
|
||||
_showResumeDialog(step) {
|
||||
var self = this;
|
||||
var overlay = document.createElement('div');
|
||||
overlay.className = 'tutorial-resume-overlay';
|
||||
overlay.innerHTML = '<div class=tutorial-resume-dialog>'
|
||||
+ '<p>Sie haben den Rundgang bei <strong>Schritt ' + (step + 1) + '/' + this._steps.length + '</strong> unterbrochen.</p>'
|
||||
+ '<div class=tutorial-resume-actions>'
|
||||
+ '<button class=tutorial-btn tutorial-btn-next id=tutorial-resume-btn>Fortsetzen</button>'
|
||||
+ '<button class=tutorial-btn tutorial-btn-secondary id=tutorial-restart-btn>Neu starten</button>'
|
||||
+ '</div></div>';
|
||||
document.body.appendChild(overlay);
|
||||
document.getElementById('tutorial-resume-btn').addEventListener('click', function() {
|
||||
overlay.remove();
|
||||
self._startInternal(step);
|
||||
});
|
||||
document.getElementById('tutorial-restart-btn').addEventListener('click', async function() {
|
||||
overlay.remove();
|
||||
try { await API.resetTutorialState(); } catch(e) {}
|
||||
self._startInternal(0);
|
||||
});
|
||||
},
|
||||
|
||||
_startInternal(resumeStep) {
|
||||
this._isActive = true;
|
||||
this._highestStep = -1;
|
||||
this._currentStep = -1;
|
||||
|
||||
// Overlay einblenden + Klicks blockieren
|
||||
this._els.overlay.classList.add('active');
|
||||
document.body.classList.add('tutorial-active');
|
||||
@@ -1172,7 +1211,11 @@ const Tutorial = {
|
||||
this._resizeHandler = this._onResize.bind(this);
|
||||
window.addEventListener('resize', this._resizeHandler);
|
||||
|
||||
this.next();
|
||||
if (resumeStep && resumeStep > 0) {
|
||||
this.goToStep(resumeStep);
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
},
|
||||
|
||||
stop() {
|
||||
@@ -1230,7 +1273,14 @@ const Tutorial = {
|
||||
this._resizeHandler = null;
|
||||
}
|
||||
|
||||
this._markSeen();
|
||||
// Fortschritt serverseitig speichern
|
||||
if (this._lastExitedStep >= 0 && this._lastExitedStep < this._steps.length - 1) {
|
||||
// Mittendrin abgebrochen — Schritt speichern
|
||||
API.saveTutorialState({ current_step: this._lastExitedStep }).catch(function() {});
|
||||
} else {
|
||||
// Komplett durchlaufen oder letzter Schritt
|
||||
this._markSeen();
|
||||
}
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -1258,6 +1308,7 @@ const Tutorial = {
|
||||
|
||||
if (this._currentStep >= 0) this._exitStep(this._currentStep);
|
||||
this._currentStep = index;
|
||||
if (index > this._highestStep) this._highestStep = index;
|
||||
this._enterStep(index);
|
||||
},
|
||||
|
||||
@@ -2231,6 +2282,7 @@ const Tutorial = {
|
||||
// -----------------------------------------------------------------------
|
||||
_markSeen() {
|
||||
try { localStorage.setItem('osint_tutorial_seen', '1'); } catch(e) {}
|
||||
API.saveTutorialState({ completed: true, current_step: null }).catch(function() {});
|
||||
},
|
||||
|
||||
_hasSeen() {
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren