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;