diff --git a/src/static/css/style.css b/src/static/css/style.css index f717069..2fbe9ec 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -4682,20 +4682,31 @@ a.map-popup-article:hover { font-weight: 600; color: var(--text-primary); } -.chat-header-close { +.chat-header-actions { + display: flex; + align-items: center; + gap: 2px; + margin-left: auto; +} +.chat-header-btn { background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 4px; line-height: 1; - font-size: 18px; border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; } -.chat-header-close:hover { +.chat-header-btn:hover { color: var(--text-primary); background: var(--bg-tertiary); } +.chat-header-close { + font-size: 18px; +} .chat-messages { flex: 1; diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 61b90ee..849d203 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -591,7 +591,12 @@
AegisSight Assistent - +
+ + +
@@ -613,7 +618,7 @@ - + diff --git a/src/static/js/chat.js b/src/static/js/chat.js new file mode 100644 index 0000000..ccc822d --- /dev/null +++ b/src/static/js/chat.js @@ -0,0 +1,197 @@ +/** + * AegisSight Chat-Assistent Widget. + */ +const Chat = { + _conversationId: null, + _isOpen: false, + _isLoading: false, + _hasGreeted: 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()); + + form.addEventListener('submit', (e) => { + e.preventDefault(); + this.send(); + }); + + // Enter sendet, Shift+Enter fuer 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. Ich kann dir sowohl Fragen zur Bedienung des Monitors beantworten als auch Auskunft zu deinen angelegten Lagen und Recherchen geben.\n\nBeispiele:\n\n"Welche Lagen gibt es gerade?"\n"Fass mir die aktuelle Lage zusammen"\n"Wie viele Artikel hat die Lage zum Irankonflikt?"\n"Wie erstelle ich eine neue Recherche?"\n"Welche Quellen werden genutzt?"\n\nWenn du eine Lage ge\u00f6ffnet hast, beziehe ich mich automatisch darauf.'); + } + + // 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'); + btn.classList.remove('active'); + this._isOpen = false; + }, + + reset() { + this._conversationId = null; + this._hasGreeted = false; + this._isLoading = false; + const container = document.getElementById('chat-messages'); + if (container) container.innerHTML = ''; + this._updateResetBtn(); + this.open(); + }, + + _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; + + 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); + } 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, '&') + .replace(//g, '>') + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/\n/g, '
'); + + bubble.innerHTML = '
' + formatted + '
'; + 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 = '
'; + 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(); + }, +};