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:
Meleki committed 2026-03-12 20:03:30 +01:00
1 parent d33c1c370c
commit 3ec9ebba45
4 files changed
+274

No files matched your search

+85
View File
@@ -1,5 +1,52 @@
.chat-page .page-content {
align-items: stretch;
position: relative;
}
/* Usage Badge Overlay */
.chat-usage-overlay {
position: absolute;
top: var(--page-spacing);
left: 50%;
transform: translateX(-50%);
z-index: 50;
display: flex;
align-items: center;
gap: calc(var(--badge-spacing) / 2);
opacity: 1;
transition: opacity var(--transition-duration), transform var(--transition-duration);
cursor: pointer;
}
.chat-usage-overlay[hidden] {
display: none;
}
.chat-usage-overlay.hidden {
opacity: 0;
transform: translateX(-50%) translateY(-1em);
pointer-events: none;
}
.chat-usage-overlay .usage-badge {
margin-top: 0;
background: var(--chat-bg);
border-color: var(--f3-border);
}
.chat-usage-overlay .chat-usage-check {
background: var(--chat-bg);
border: var(--border-width) solid var(--f3-border);
border-radius: var(--border-radius);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.chat-usage-overlay .chat-usage-check:hover {
background: var(--f2-bg);
}
#chat {
@@ -1328,3 +1375,41 @@
text-overflow: ellipsis;
white-space: nowrap;
}
/* ── Key Switch Modal ── */
.key-switch-list {
display: flex;
flex-direction: column;
gap: var(--badge-spacing);
}
.key-switch-row {
display: flex;
align-items: center;
gap: var(--badge-spacing);
padding: var(--badge-padding);
border: var(--border-width) solid var(--f3-border);
border-radius: var(--border-radius);
background: var(--f3-bg);
}
.key-switch-row.active {
border-color: var(--primary);
background: rgb(from var(--primary) r g b / .2);
}
.key-switch-info {
flex: 1;
min-width: 0;
}
.key-switch-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.key-switch-meta {
opacity: var(--semi-transparent);
}
+1
View File
@@ -379,6 +379,7 @@
</div>
</header>
<div class="page-content">
<div id="chatUsageBadge" class="chat-usage-overlay" hidden></div>
<div id="chat"></div>
<div class="meta-bar" id="meta"></div>
<div id="filePreview" class="file-preview" hidden></div>
+1
View File
@@ -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);
+187
View File
@@ -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;