Dateien
AegisSight-Monitor/src/static/js/chat.js
Claude Dev 5e194d43e0 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>
2026-03-17 22:51:06 +01:00

353 Zeilen
14 KiB
JavaScript

/**
* AegisSight Chat-Assistent Widget.
*/
const Chat = {
_conversationId: null,
_isOpen: false,
_isLoading: false,
_hasGreeted: false,
_tutorialHintDismissed: false,
_isFullscreen: false,
init() {
const btn = document.getElementById('chat-toggle-btn');
const closeBtn = document.getElementById('chat-close-btn');
const form = document.getElementById('chat-form');
const input = document.getElementById('chat-input');
if (!btn || !form) return;
btn.addEventListener('click', () => this.toggle());
closeBtn.addEventListener('click', () => this.close());
const resetBtn = document.getElementById('chat-reset-btn');
if (resetBtn) resetBtn.addEventListener('click', () => this.reset());
const fsBtn = document.getElementById('chat-fullscreen-btn');
if (fsBtn) fsBtn.addEventListener('click', () => this.toggleFullscreen());
form.addEventListener('submit', (e) => {
e.preventDefault();
this.send();
});
// Enter sendet, Shift+Enter für Zeilenumbruch
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.send();
}
});
// Auto-resize textarea
input.addEventListener('input', () => {
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 120) + 'px';
});
},
toggle() {
if (this._isOpen) {
this.close();
} else {
this.open();
}
},
open() {
const win = document.getElementById('chat-window');
const btn = document.getElementById('chat-toggle-btn');
if (!win) return;
win.classList.add('open');
btn.classList.add('active');
this._isOpen = true;
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 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
setTimeout(() => {
const input = document.getElementById('chat-input');
if (input) input.focus();
}, 200);
},
close() {
const win = document.getElementById('chat-window');
const btn = document.getElementById('chat-toggle-btn');
if (!win) return;
win.classList.remove('open');
win.classList.remove('fullscreen');
btn.classList.remove('active');
this._isOpen = false;
this._isFullscreen = false;
const fsBtn = document.getElementById('chat-fullscreen-btn');
if (fsBtn) {
fsBtn.title = 'Vollbild';
fsBtn.innerHTML = '<svg viewBox="0 0 24 24" width="15" height="15"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" fill="currentColor"/></svg>';
}
},
reset() {
this._conversationId = null;
this._hasGreeted = false;
this._isLoading = false;
const container = document.getElementById('chat-messages');
if (container) container.innerHTML = '';
this._updateResetBtn();
this.open();
},
toggleFullscreen() {
const win = document.getElementById('chat-window');
const btn = document.getElementById('chat-fullscreen-btn');
if (!win) return;
this._isFullscreen = !this._isFullscreen;
win.classList.toggle('fullscreen', this._isFullscreen);
if (btn) {
btn.title = this._isFullscreen ? 'Vollbild beenden' : 'Vollbild';
btn.innerHTML = this._isFullscreen
? '<svg viewBox="0 0 24 24" width="15" height="15"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" fill="currentColor"/></svg>'
: '<svg viewBox="0 0 24 24" width="15" height="15"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" fill="currentColor"/></svg>';
}
},
_updateResetBtn() {
const btn = document.getElementById('chat-reset-btn');
if (btn) btn.style.display = this._conversationId ? '' : 'none';
},
async send() {
const input = document.getElementById('chat-input');
const text = (input.value || '').trim();
if (!text || this._isLoading) return;
input.value = '';
input.style.height = 'auto';
this.addMessage('user', text);
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,
conversation_id: this._conversationId,
};
// Aktuelle Lage mitschicken falls geoeffnet
const incidentId = this._getIncidentContext();
if (incidentId) {
body.incident_id = incidentId;
}
const data = await this._request(body);
this._conversationId = data.conversation_id;
this._updateResetBtn();
this._hideTyping();
this.addMessage('assistant', data.reply);
this._highlightUI(data.reply);
} catch (err) {
this._hideTyping();
const msg = err.detail || err.message || 'Etwas ist schiefgelaufen. Bitte versuche es erneut.';
this.addMessage('assistant', msg);
} finally {
this._isLoading = false;
}
},
addMessage(role, text) {
const container = document.getElementById('chat-messages');
if (!container) return;
const bubble = document.createElement('div');
bubble.className = 'chat-message ' + role;
// Einfache Formatierung: Zeilenumbrueche und Fettschrift
const formatted = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\n/g, '<br>');
bubble.innerHTML = '<div class="chat-bubble">' + formatted + '</div>';
container.appendChild(bubble);
// User-Nachrichten: nach unten scrollen. Antworten: zum Anfang der Antwort scrollen.
if (role === 'user') {
container.scrollTop = container.scrollHeight;
} else {
bubble.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
},
_showTyping() {
const container = document.getElementById('chat-messages');
if (!container) return;
const el = document.createElement('div');
el.className = 'chat-message assistant chat-typing-msg';
el.innerHTML = '<div class="chat-bubble chat-typing"><span></span><span></span><span></span></div>';
container.appendChild(el);
container.scrollTop = container.scrollHeight;
},
_hideTyping() {
const el = document.querySelector('.chat-typing-msg');
if (el) el.remove();
},
_getIncidentContext() {
if (typeof App !== 'undefined' && App.currentIncidentId) {
return App.currentIncidentId;
}
return null;
},
async _request(body) {
const token = localStorage.getItem('osint_token');
const resp = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token ? 'Bearer ' + token : '',
},
body: JSON.stringify(body),
});
if (!resp.ok) {
const data = await resp.json().catch(() => ({}));
throw data;
}
return await resp.json();
},
// -----------------------------------------------------------------------
// UI-Highlight: Bedienelemente im Dashboard hervorheben wenn im Chat erwaehnt
// -----------------------------------------------------------------------
_UI_HIGHLIGHTS: [
{ keywords: ['neue lage', 'lage erstellen', 'lage anlegen', 'recherche erstellen', 'neuen fall'], selector: '#new-incident-btn' },
{ keywords: ['theme wechseln', 'theme-umschalter', 'farbschema', 'helles design', 'dunkles design', 'hell- und dunkel', 'hellem und dunklem', 'dark mode', 'light mode'], selector: '#theme-toggle' },
{ keywords: ['barrierefreiheit', 'accessibility', 'hoher kontrast', 'focus-anzeige', 'groessere schrift', 'animationen aus'], selector: '#a11y-btn' },
{ keywords: ['abmelden', 'logout', 'ausloggen', 'abmeldung'], selector: '#logout-btn' },
{ keywords: ['benachrichtigung', 'glocken-symbol', 'abonnieren', 'abonniert'], selector: '#notification-btn' },
{ keywords: ['aktualisieren', 'refresh starten'], selector: '#refresh-btn' },
{ keywords: ['exportieren', 'export-button', 'lagebericht exportieren'], selector: 'button[onclick*="toggleExportDropdown"]' },
{ keywords: ['faktencheck', 'factcheck'], selector: '[gs-id="factcheck"]' },
{ keywords: ['kartenansicht', 'karte angezeigt', 'interaktive karte', 'geoparsing'], selector: '[gs-id="map"]' },
{ keywords: ['quellen verwalten', 'quellenverwaltung', 'quelleneinstellung', 'quellenausschluss', 'quellen-einstellung'], selector: 'button[onclick*="openSourceManagement"]' },
{ keywords: ['sichtbarkeit', 'privat oder oeffentlich', 'lage privat'], selector: '#incident-settings-btn' },
{ keywords: ['eigene lagen', 'nur eigene'], selector: '.sidebar-filter-btn[data-filter="mine"]' },
{ keywords: ['alle lagen anzeigen'], selector: '.sidebar-filter-btn[data-filter="all"]' },
{ keywords: ['feedback senden', 'feedback geben', 'rueckmeldung'], selector: 'button[onclick*="openFeedback"]' },
{ keywords: ['lage loeschen', 'lage entfernen', 'fall loeschen'], selector: '#delete-incident-btn' },
],
_highlightUI(text) {
if (!text) return;
var lower = text.toLowerCase();
var highlighted = new Set();
for (var i = 0; i < this._UI_HIGHLIGHTS.length; i++) {
var entry = this._UI_HIGHLIGHTS[i];
for (var k = 0; k < entry.keywords.length; k++) {
var kw = entry.keywords[k];
if (lower.indexOf(kw) !== -1) {
var selectors = entry.selector.split(',');
for (var s = 0; s < selectors.length; s++) {
var sel = selectors[s].trim();
if (highlighted.has(sel)) continue;
var el = document.querySelector(sel);
if (el) {
highlighted.add(sel);
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
(function(element) {
setTimeout(function() {
element.classList.add('chat-ui-highlight');
}, 400);
setTimeout(function() {
element.classList.remove('chat-ui-highlight');
}, 4400);
})(el);
}
}
break;
}
}
}
},
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.style.cursor = 'pointer';
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ßen';
closeBtn.innerHTML = '&times;';
closeBtn.addEventListener('click', function(e) {
e.stopPropagation();
hint.remove();
Chat._tutorialHintDismissed = true;
});
hint.appendChild(textDiv);
hint.appendChild(closeBtn);
container.appendChild(hint);
},
};