feat: Neuer-Chat-Button im Chat-Header
Reload-Button (kreisförmiger Pfeil) neben dem Schließen-Button. Wird erst sichtbar nach der ersten Nachricht. Setzt Konversation zurück, leert den Verlauf und zeigt die Begrüßung erneut. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -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;
|
||||
|
||||
@@ -591,7 +591,12 @@
|
||||
<div class="chat-window" id="chat-window">
|
||||
<div class="chat-header">
|
||||
<span class="chat-header-title">AegisSight Assistent</span>
|
||||
<button class="chat-header-close" id="chat-close-btn" title="Schliessen" aria-label="Chat schliessen">×</button>
|
||||
<div class="chat-header-actions">
|
||||
<button class="chat-header-btn chat-reset-btn" id="chat-reset-btn" title="Neuer Chat" aria-label="Neuen Chat starten" style="display:none">
|
||||
<svg viewBox="0 0 24 24" width="15" height="15"><path d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" fill="currentColor"/></svg>
|
||||
</button>
|
||||
<button class="chat-header-btn chat-header-close" id="chat-close-btn" title="Schließen" aria-label="Chat schließen">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-messages" id="chat-messages"></div>
|
||||
<form class="chat-input-area" id="chat-form" autocomplete="off">
|
||||
@@ -613,7 +618,7 @@
|
||||
<script src="/static/js/components.js?v=20260304h"></script>
|
||||
<script src="/static/js/layout.js?v=20260304h"></script>
|
||||
<script src="/static/js/app.js?v=20260304h"></script>
|
||||
<script src="/static/js/chat.js?v=20260315a"></script>
|
||||
<script src="/static/js/chat.js?v=20260315b"></script>
|
||||
<script>document.addEventListener("DOMContentLoaded",function(){Chat.init()});</script>
|
||||
|
||||
<!-- Map Fullscreen Overlay -->
|
||||
|
||||
197
src/static/js/chat.js
Normale Datei
197
src/static/js/chat.js
Normale Datei
@@ -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, '>')
|
||||
.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();
|
||||
},
|
||||
};
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren