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
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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