diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css
index 46a6806..ff563e4 100644
--- a/wwwroot/css/chat.css
+++ b/wwwroot/css/chat.css
@@ -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);
+}
diff --git a/wwwroot/index.html b/wwwroot/index.html
index 7e3d685..1967bc1 100644
--- a/wwwroot/index.html
+++ b/wwwroot/index.html
@@ -379,6 +379,7 @@
+
diff --git a/wwwroot/js/history.js b/wwwroot/js/history.js
index 9f2af22..5f104eb 100644
--- a/wwwroot/js/history.js
+++ b/wwwroot/js/history.js
@@ -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);
diff --git a/wwwroot/js/pages/chat-page.js b/wwwroot/js/pages/chat-page.js
index 67e99ba..56ffbb4 100644
--- a/wwwroot/js/pages/chat-page.js
+++ b/wwwroot/js/pages/chat-page.js
@@ -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 = '
';
+ 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;