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 {
|
.chat-page .page-content {
|
||||||
align-items: stretch;
|
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 {
|
#chat {
|
||||||
@@ -1328,3 +1375,41 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
|
<div id="chatUsageBadge" class="chat-usage-overlay" hidden></div>
|
||||||
<div id="chat"></div>
|
<div id="chat"></div>
|
||||||
<div class="meta-bar" id="meta"></div>
|
<div class="meta-bar" id="meta"></div>
|
||||||
<div id="filePreview" class="file-preview" hidden></div>
|
<div id="filePreview" class="file-preview" hidden></div>
|
||||||
|
|||||||
@@ -482,6 +482,7 @@ async function pollHistory(force = false) {
|
|||||||
if (newMessages.length > 0) {
|
if (newMessages.length > 0) {
|
||||||
renderMessages(newMessages, true);
|
renderMessages(newMessages, true);
|
||||||
scrollDown();
|
scrollDown();
|
||||||
|
if (typeof _refreshChatUsageBadge === 'function') _refreshChatUsageBadge();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('History poll fehlgeschlagen:', e);
|
console.error('History poll fehlgeschlagen:', e);
|
||||||
|
|||||||
@@ -4,6 +4,183 @@ let _streamingPollInterval = null;
|
|||||||
let _recoveryChainId = null;
|
let _recoveryChainId = null;
|
||||||
let _emojiPickerEl = null;
|
let _emojiPickerEl = null;
|
||||||
let _emojiPickerCloseHandler = 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() {
|
function _buildEmojiPicker() {
|
||||||
const picker = document.createElement('div');
|
const picker = document.createElement('div');
|
||||||
@@ -374,6 +551,9 @@ registerPage('chat', {
|
|||||||
|
|
||||||
await loadHistory();
|
await loadHistory();
|
||||||
|
|
||||||
|
// Usage Badge laden (Admin only)
|
||||||
|
_initChatUsageBadge();
|
||||||
|
|
||||||
// Nach Font-Loading nochmal scrollen (Schriftart kann Höhe verändern)
|
// Nach Font-Loading nochmal scrollen (Schriftart kann Höhe verändern)
|
||||||
document.fonts.ready.then(() => {
|
document.fonts.ready.then(() => {
|
||||||
requestAnimationFrame(() => scrollDown());
|
requestAnimationFrame(() => scrollDown());
|
||||||
@@ -402,6 +582,13 @@ registerPage('chat', {
|
|||||||
stopTypingIndicator();
|
stopTypingIndicator();
|
||||||
setCurrentAgentName(null);
|
setCurrentAgentName(null);
|
||||||
closeEmojiPicker();
|
closeEmojiPicker();
|
||||||
|
// Usage Badge cleanup
|
||||||
|
if (_chatUsageScrollHandler) {
|
||||||
|
var chatEl = document.getElementById('chat');
|
||||||
|
if (chatEl) chatEl.removeEventListener('scroll', _chatUsageScrollHandler);
|
||||||
|
_chatUsageScrollHandler = null;
|
||||||
|
}
|
||||||
|
_chatUsageKeyId = null;
|
||||||
if (this._closeMenu) {
|
if (this._closeMenu) {
|
||||||
document.removeEventListener('click', this._closeMenu);
|
document.removeEventListener('click', this._closeMenu);
|
||||||
this._closeMenu = null;
|
this._closeMenu = null;
|
||||||
|
|||||||
Reference in new issue
Block a user