Chat: Usage-Badge-Overlay mit Key-Switch-Modal
- Usage-Badge oben zentriert im Chat (nur Admin) - Zeigt Sitzung/Woche-Balken des aktiven API-Keys - Blendet bei Scroll-Up aus, bei Scroll-Down ein - Klick auf Badge öffnet Key-Switch-Modal (via _createModal) - Ganze Row klickbar für Key-Wechsel - Refresh-Button neben Badge für manuellen Key-Check - Badge aktualisiert sich automatisch bei neuen Nachrichten (pollHistory) - Cleanup in destroy()
This commit is contained in:
1 parent
d33c1c370c
commit
3ec9ebba45
4 files changed
+274
No files matched your search
@@ -482,6 +482,7 @@ async function pollHistory(force = false) {
|
||||
if (newMessages.length > 0) {
|
||||
renderMessages(newMessages, true);
|
||||
scrollDown();
|
||||
if (typeof _refreshChatUsageBadge === 'function') _refreshChatUsageBadge();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('History poll fehlgeschlagen:', e);
|
||||
|
||||
@@ -4,6 +4,183 @@ let _streamingPollInterval = null;
|
||||
let _recoveryChainId = null;
|
||||
let _emojiPickerEl = null;
|
||||
let _emojiPickerCloseHandler = null;
|
||||
let _chatUsageKeyId = null;
|
||||
let _chatUsageScrollHandler = null;
|
||||
|
||||
// ── Chat Usage Badge ──
|
||||
|
||||
function _createChatUsageBadge(usage5h, usage7d) {
|
||||
if (usage5h == null && usage7d == null) return null;
|
||||
var badge = document.createElement('div');
|
||||
badge.className = 'usage-badge';
|
||||
|
||||
function addRow(label, pct) {
|
||||
if (pct == null) return;
|
||||
var row = document.createElement('div');
|
||||
row.className = 'usage-row';
|
||||
var lbl = document.createElement('span');
|
||||
lbl.className = 'usage-label';
|
||||
lbl.textContent = label;
|
||||
var track = document.createElement('div');
|
||||
track.className = 'usage-track';
|
||||
var fill = document.createElement('div');
|
||||
fill.className = 'usage-fill';
|
||||
if (pct >= 90) fill.className += ' danger';
|
||||
else if (pct >= 70) fill.className += ' warn';
|
||||
fill.style.width = Math.min(pct, 100) + '%';
|
||||
track.appendChild(fill);
|
||||
var val = document.createElement('span');
|
||||
val.className = 'usage-pct';
|
||||
val.textContent = pct + '%';
|
||||
row.appendChild(lbl);
|
||||
row.appendChild(track);
|
||||
row.appendChild(val);
|
||||
badge.appendChild(row);
|
||||
}
|
||||
|
||||
addRow('Sitzung', usage5h);
|
||||
addRow('Woche', usage7d);
|
||||
return badge;
|
||||
}
|
||||
|
||||
async function _initChatUsageBadge() {
|
||||
if (!currentUser || !currentUser.isAdmin) return;
|
||||
var container = document.getElementById('chatUsageBadge');
|
||||
if (!container) return;
|
||||
|
||||
try {
|
||||
var keys = await apiGetApiKeys();
|
||||
var activeKey = keys.find(function(k) { return k.isActive; });
|
||||
if (!activeKey) { container.hidden = true; return; }
|
||||
|
||||
_chatUsageKeyId = activeKey.keyId;
|
||||
_renderChatUsageBadge(container, activeKey);
|
||||
container.hidden = false;
|
||||
|
||||
// Scroll-Listener: Badge ausblenden wenn User nach oben scrollt
|
||||
var chatEl = document.getElementById('chat');
|
||||
if (chatEl) {
|
||||
_chatUsageScrollHandler = function() {
|
||||
var threshold = 60;
|
||||
var atBottom = chatEl.scrollHeight - chatEl.scrollTop - chatEl.clientHeight <= threshold;
|
||||
container.classList.toggle('hidden', !atBottom);
|
||||
};
|
||||
chatEl.addEventListener('scroll', _chatUsageScrollHandler);
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('Usage badge load failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function _refreshChatUsageBadge() {
|
||||
if (!currentUser || !currentUser.isAdmin) return;
|
||||
var container = document.getElementById('chatUsageBadge');
|
||||
if (!container || !_chatUsageKeyId) return;
|
||||
try {
|
||||
var keys = await apiGetApiKeys();
|
||||
var activeKey = keys.find(function(k) { return k.isActive; });
|
||||
if (activeKey) _renderChatUsageBadge(container, activeKey);
|
||||
} catch(e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function _renderChatUsageBadge(container, key) {
|
||||
container.innerHTML = '';
|
||||
|
||||
var badge = _createChatUsageBadge(key.usage5h, key.usage7d);
|
||||
if (badge) {
|
||||
badge.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
_showKeySwitchModal();
|
||||
});
|
||||
badge.title = (key.name || 'API Key') + ' · ' + (key.plan || '?');
|
||||
container.appendChild(badge);
|
||||
}
|
||||
|
||||
var checkBtn = document.createElement('button');
|
||||
checkBtn.className = 'chat-usage-check icon-btn';
|
||||
checkBtn.innerHTML = '<i class="fa fa-refresh"></i>';
|
||||
checkBtn.title = 'Key prüfen';
|
||||
checkBtn.addEventListener('click', async function(e) {
|
||||
e.stopPropagation();
|
||||
checkBtn.disabled = true;
|
||||
checkBtn.querySelector('i').className = 'fa fa-spinner fa-spin';
|
||||
try {
|
||||
var result = await apiCheckApiKey(_chatUsageKeyId);
|
||||
if (result) {
|
||||
var keys = await apiGetApiKeys();
|
||||
var activeKey = keys.find(function(k) { return k.isActive; });
|
||||
if (activeKey) _renderChatUsageBadge(container, activeKey);
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('Key check failed:', e);
|
||||
}
|
||||
checkBtn.disabled = false;
|
||||
checkBtn.querySelector('i').className = 'fa fa-refresh';
|
||||
});
|
||||
container.appendChild(checkBtn);
|
||||
}
|
||||
|
||||
async function _showKeySwitchModal() {
|
||||
var keys = [];
|
||||
try { keys = await apiGetApiKeys(); } catch(e) { return; }
|
||||
|
||||
var { body, close } = _createModal('API Key wechseln', { modal: 'key-switch-modal' });
|
||||
|
||||
var list = document.createElement('div');
|
||||
list.className = 'key-switch-list';
|
||||
|
||||
keys.forEach(function(key) {
|
||||
var row = document.createElement('div');
|
||||
row.className = 'key-switch-row';
|
||||
if (key.isActive) row.classList.add('active');
|
||||
|
||||
var info = document.createElement('div');
|
||||
info.className = 'key-switch-info';
|
||||
|
||||
var name = document.createElement('div');
|
||||
name.className = 'key-switch-name';
|
||||
name.textContent = key.name || key.keyId;
|
||||
info.appendChild(name);
|
||||
|
||||
var meta = document.createElement('div');
|
||||
meta.className = 'key-switch-meta';
|
||||
var parts = [];
|
||||
if (key.plan) parts.push(key.plan);
|
||||
if (key.status) parts.push(key.status === 'valid' ? '✅' : '⚠️');
|
||||
meta.textContent = parts.join(' · ');
|
||||
info.appendChild(meta);
|
||||
|
||||
row.appendChild(info);
|
||||
|
||||
var badge = _createChatUsageBadge(key.usage5h, key.usage7d);
|
||||
if (badge) {
|
||||
row.appendChild(badge);
|
||||
}
|
||||
|
||||
if (!key.isActive) {
|
||||
row.style.cursor = 'pointer';
|
||||
row.addEventListener('click', async function() {
|
||||
row.style.opacity = '0.5';
|
||||
row.style.pointerEvents = 'none';
|
||||
try {
|
||||
var activeKey = keys.find(function(k) { return k.isActive; });
|
||||
if (activeKey) await apiDeactivateApiKey(activeKey.keyId);
|
||||
await apiActivateApiKey(key.keyId);
|
||||
close();
|
||||
await _initChatUsageBadge();
|
||||
} catch(e) {
|
||||
console.error('Key switch failed:', e);
|
||||
row.style.opacity = '';
|
||||
row.style.pointerEvents = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
list.appendChild(row);
|
||||
});
|
||||
|
||||
body.appendChild(list);
|
||||
}
|
||||
|
||||
function _buildEmojiPicker() {
|
||||
const picker = document.createElement('div');
|
||||
@@ -374,6 +551,9 @@ registerPage('chat', {
|
||||
|
||||
await loadHistory();
|
||||
|
||||
// Usage Badge laden (Admin only)
|
||||
_initChatUsageBadge();
|
||||
|
||||
// Nach Font-Loading nochmal scrollen (Schriftart kann Höhe verändern)
|
||||
document.fonts.ready.then(() => {
|
||||
requestAnimationFrame(() => scrollDown());
|
||||
@@ -402,6 +582,13 @@ registerPage('chat', {
|
||||
stopTypingIndicator();
|
||||
setCurrentAgentName(null);
|
||||
closeEmojiPicker();
|
||||
// Usage Badge cleanup
|
||||
if (_chatUsageScrollHandler) {
|
||||
var chatEl = document.getElementById('chat');
|
||||
if (chatEl) chatEl.removeEventListener('scroll', _chatUsageScrollHandler);
|
||||
_chatUsageScrollHandler = null;
|
||||
}
|
||||
_chatUsageKeyId = null;
|
||||
if (this._closeMenu) {
|
||||
document.removeEventListener('click', this._closeMenu);
|
||||
this._closeMenu = null;
|
||||
|
||||
Reference in new issue
Block a user