// ── Chat Page ── 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) return; var container = document.getElementById('chatUsageBadge'); if (!container) return; var s = loadSettings(); if (!s.chatUsageBadge) { container.hidden = true; return; } try { var selectedKey = await apiGetSelectedKey(getCurrentAgentId()); if (!selectedKey || !selectedKey.selected) { container.innerHTML = ''; var noKeyBtn = document.createElement('button'); noKeyBtn.className = 'chat-usage-nokey'; noKeyBtn.innerHTML = ' Key auswählen'; noKeyBtn.addEventListener('click', function() { _showKeySwitchModal(); }); container.appendChild(noKeyBtn); container.hidden = false; return; } _chatUsageKeyId = selectedKey.keyId; _renderChatUsageBadge(container, selectedKey); 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 _checkChatPermissions() { if (!currentUser || currentUser.isAdmin) return; if (currentUser.canUseCli !== false) return; // CLI erlaubt → kein Problem try { var selectedKey = await apiGetSelectedKey(getCurrentAgentId()); if (selectedKey && selectedKey.selected) return; // Key vorhanden → kein Problem addWarningBubble('Kein API-Key zugewiesen und keine CLI-Berechtigung. Nachrichten können nicht verarbeitet werden. Bitte wende dich an einen Admin.'); } catch(e) { /* ignore */ } } async function _refreshChatUsageBadge() { if (!currentUser) return; var container = document.getElementById('chatUsageBadge'); if (!container || !_chatUsageKeyId) return; try { var selectedKey = await apiGetSelectedKey(getCurrentAgentId()); if (selectedKey) _renderChatUsageBadge(container, selectedKey); } catch(e) { /* ignore */ } } function _renderChatUsageBadge(container, key) { container.innerHTML = ''; var badge = _createChatUsageBadge(key.usage5h, key.usage7d); if (!badge) { badge = document.createElement('div'); badge.className = 'usage-badge usage-badge-nodata'; badge.innerHTML = ' ' + (key.name || 'API Key'); } badge.addEventListener('click', function(e) { e.stopPropagation(); _showKeySwitchModal(); }); badge.title = (key.name || 'API Key') + ' · ' + (key.plan || '?'); badge.classList.add('clickable'); container.appendChild(badge); // Check-Button nur für Admins if (currentUser && currentUser.isAdmin) { 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 { await apiCheckApiKey(_chatUsageKeyId); var selectedKey = await apiGetSelectedKey(getCurrentAgentId()); if (selectedKey) _renderChatUsageBadge(container, selectedKey); } 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 apiGetAvailableKeys(); } catch(e) { return; } var selectedKey = null; try { selectedKey = await apiGetSelectedKey(getCurrentAgentId()); } catch(e) { /* ignore */ } var selectedKeyId = selectedKey ? selectedKey.keyId : null; var { body, close } = _createModal('API Key wechseln', { modal: 'key-switch-modal' }); if (keys.length === 0) { var empty = document.createElement('div'); empty.className = 'empty-hint'; empty.textContent = 'Keine Keys verfügbar. Ein Admin muss Keys als öffentlich markieren.'; body.appendChild(empty); return; } var list = document.createElement('div'); list.className = 'key-switch-list'; // "Kein Key"-Option als erste Zeile var noneRow = document.createElement('div'); noneRow.className = 'key-switch-row'; if (!selectedKeyId) noneRow.classList.add('active'); var noneInfo = document.createElement('div'); noneInfo.className = 'key-switch-info'; var noneName = document.createElement('div'); noneName.className = 'key-switch-name'; noneName.textContent = 'Kein Key'; noneInfo.appendChild(noneName); var noneMeta = document.createElement('div'); noneMeta.className = 'key-switch-meta'; noneMeta.textContent = 'Agent ohne API-Key nutzen'; noneInfo.appendChild(noneMeta); noneRow.appendChild(noneInfo); if (selectedKeyId) { noneRow.classList.add('clickable'); noneRow.addEventListener('click', async function() { noneRow.classList.add('loading-state'); try { await apiSelectApiKey('none', getCurrentAgentId()); close(); await _initChatUsageBadge(); } catch(e) { noneRow.classList.remove('loading-state'); } }); } list.appendChild(noneRow); keys.forEach(function(key) { var row = document.createElement('div'); row.className = 'key-switch-row'; if (key.keyId === selectedKeyId) 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 = []; var providerLabels = { anthropic: '🟤 Anthropic', openai: '🟢 OpenAI', google: '🔵 Google' }; parts.push(providerLabels[key.provider] || key.provider || 'Anthropic'); 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.keyId !== selectedKeyId) { row.classList.add('clickable'); row.addEventListener('click', async function() { row.classList.add('loading-state'); try { await apiSelectApiKey(key.keyId, getCurrentAgentId()); close(); await _initChatUsageBadge(); } catch(e) { console.error('Key switch failed:', e); row.classList.remove('loading-state'); } }); } list.appendChild(row); }); body.appendChild(list); } function _buildEmojiPicker() { const picker = document.createElement('div'); picker.className = 'emoji-picker'; for (const [category, emojis] of Object.entries(EMOJI_USERDATA)) { const section = document.createElement('div'); section.className = 'emoji-picker-section'; const label = document.createElement('div'); label.className = 'emoji-picker-label'; label.textContent = category; section.appendChild(label); const grid = document.createElement('div'); grid.className = 'emoji-picker-grid'; for (const emoji of emojis) { const btn = document.createElement('button'); btn.className = 'emoji-picker-btn'; btn.textContent = emoji; btn.addEventListener('click', (e) => { e.stopPropagation(); const input = document.getElementById('input'); if (input) { const start = input.selectionStart; const end = input.selectionEnd; const val = input.value; input.value = val.slice(0, start) + emoji + val.slice(end); const pos = start + emoji.length; input.setSelectionRange(pos, pos); input.dispatchEvent(new Event('input')); input.focus(); } closeEmojiPicker(); }); // Fluent Emoji im Button rendern parseFluentEmojis(btn); grid.appendChild(btn); } section.appendChild(grid); picker.appendChild(section); } return picker; } function toggleEmojiPicker(anchor) { if (_emojiPickerEl) { closeEmojiPicker(); return; } const picker = _buildEmojiPicker(); document.body.appendChild(picker); _emojiPickerEl = picker; // Positionierung: über dem Button, rechts ausgerichtet const rect = anchor.getBoundingClientRect(); const pickerHeight = 320; const pickerWidth = 280; let top = rect.top - pickerHeight - 6; let left = rect.right - pickerWidth; // Falls kein Platz oben → unter dem Button if (top < 8) top = rect.bottom + 6; // Falls links abgeschnitten → linksbündig zum Button if (left < 8) left = 8; picker.style.top = top + 'px'; picker.style.left = left + 'px'; _emojiPickerCloseHandler = (e) => { if (!picker.contains(e.target)) closeEmojiPicker(); }; setTimeout(() => document.addEventListener('click', _emojiPickerCloseHandler), 0); } function closeEmojiPicker() { if (_emojiPickerEl) { _emojiPickerEl.remove(); _emojiPickerEl = null; } if (_emojiPickerCloseHandler) { document.removeEventListener('click', _emojiPickerCloseHandler); _emojiPickerCloseHandler = null; } } function startStreamingPoll(sessionId, sendBtn, chainId) { _recoveryChainId = chainId; // History-Polling stoppen – Streaming-Poll übernimmt stopHistoryPolling(); if (_streamingPollInterval) clearInterval(_streamingPollInterval); _streamingPollInterval = setInterval(async () => { try { // Neue Nachrichten laden (inkl. der noch streamenden) await pollHistory(true); const status = await apiIsStreaming(sessionId); if (!status.streaming) { clearInterval(_streamingPollInterval); _streamingPollInterval = null; isStreaming = false; _abortController = null; if (sendBtn) { sendBtn.disabled = false; sendBtn.hidden = false; } const stopEl = document.getElementById('stop'); if (stopEl) stopEl.hidden = true; // Typing-Indicator stoppen stopTypingIndicator(); // Finale Nachrichten nochmal laden await pollHistory(true); // Chain-Daten laden für Meta-Info im Chat const cid = _recoveryChainId || status.chainId; if (cid) { const chain = await apiGetChain(cid); if (chain) addChainMetaBubble(chain, false); } setMeta('Bereit'); _recoveryChainId = null; // History-Polling wieder starten startHistoryPolling(); } } catch (e) { console.error('Streaming-Poll fehlgeschlagen:', e); } }, 1500); } registerPage('chat', { async init(params) { const sessionId = params.id; if (!sessionId) return navigateTo('dashboard'); currentSessionId = sessionId; const session = await apiGetSession(sessionId); if (!session) return navigateTo('dashboard'); // Kein Agent = kein Chat if (!session.agentId) { navigateTo('session', { id: sessionId }); return; } currentAgentId = session.agentId; const titleEl = document.getElementById('sessionTitle'); if (titleEl) { titleEl.textContent = session.name || 'Unbenannt'; } // Header-Menü (3-Dot) const chatMenuBtn = document.getElementById('chatMenuBtn'); const chatMenuDropdown = document.getElementById('chatMenuDropdown'); if (chatMenuBtn && chatMenuDropdown) { chatMenuBtn.addEventListener('click', (e) => { e.stopPropagation(); chatMenuDropdown.hidden = !chatMenuDropdown.hidden; }); document.addEventListener('click', this._closeMenu = () => { chatMenuDropdown.hidden = true; }); } // Remote-Verbindungen const remoteBtn = document.getElementById('remoteBtn'); if (remoteBtn) remoteBtn.addEventListener('click', () => { if (chatMenuDropdown) chatMenuDropdown.hidden = true; showRemoteModal(sessionId); }); // System-Prompt anzeigen const systemPromptBtn = document.getElementById('systemPromptBtn'); if (systemPromptBtn) systemPromptBtn.addEventListener('click', () => { if (chatMenuDropdown) chatMenuDropdown.hidden = true; showSystemPromptModal(sessionId); }); // Payload anzeigen const payloadPreviewBtn = document.getElementById('payloadPreviewBtn'); if (payloadPreviewBtn) payloadPreviewBtn.addEventListener('click', () => { if (chatMenuDropdown) chatMenuDropdown.hidden = true; showPayloadPreviewModal(sessionId); }); // Workflow-Status anzeigen const workflowStatusBtn = document.getElementById('workflowStatusBtn'); if (workflowStatusBtn) workflowStatusBtn.addEventListener('click', () => { if (chatMenuDropdown) chatMenuDropdown.hidden = true; showWorkflowStatusModal(sessionId); }); // Archivieren const archiveBtn = document.getElementById('archiveBtn'); if (archiveBtn) archiveBtn.addEventListener('click', () => { if (chatMenuDropdown) chatMenuDropdown.hidden = true; clearChat(); }); // Custom Command Buttons ins Header-Menü einfügen if (chatMenuDropdown) { // "System"-Label ganz oben einfügen const systemLabel = document.createElement('div'); systemLabel.className = 'header-menu-label'; systemLabel.textContent = 'System'; chatMenuDropdown.prepend(systemLabel); let agentCmds = []; if (session.agentId) { try { const agent = await apiGetAgent(session.agentId); if (agent) { setCurrentAgentName(agent.name || null); if (agent.customCommands) { agentCmds = JSON.parse(agent.customCommands) || []; } } } catch(e) {} } let sessionCmds = []; try { sessionCmds = JSON.parse(session.customCommands || '[]') || []; } catch(e) {} // Sektions-Kopf (Separator + Label) const addSectionHeader = (label) => { const sep = document.createElement('div'); sep.className = 'header-menu-separator'; chatMenuDropdown.appendChild(sep); const lbl = document.createElement('div'); lbl.className = 'header-menu-label'; lbl.textContent = label; chatMenuDropdown.appendChild(lbl); }; // Navigations-Eintrag (z.B. "Agent bearbeiten" → Sprung zur Edit-Page) const addNavItem = (label, icon, target, targetParams) => { const btn = document.createElement('button'); btn.className = 'header-menu-item'; btn.innerHTML = ` ${label}`; btn.addEventListener('click', () => { chatMenuDropdown.hidden = true; navigateTo(target, targetParams); }); chatMenuDropdown.appendChild(btn); }; // Custom-Command-Buttons einer Sparte anhängen const addCmdButtons = (cmds) => { cmds.filter(c => c.name && c.command).forEach(cmd => { const type = cmd.type || 'background'; const btn = document.createElement('button'); btn.className = 'header-menu-item header-menu-cmd'; btn.textContent = cmd.name; btn.title = type === 'message' ? cmd.command : `[${type}] ${cmd.command}`; btn.addEventListener('click', async () => { chatMenuDropdown.hidden = true; if (type === 'message') { const inputEl = document.getElementById('input'); if (inputEl) { inputEl.value = cmd.command; inputEl.dispatchEvent(new Event('input')); } sendMessage(); return; } btn.disabled = true; btn.classList.add('running'); try { if (type === 'foreground') { const result = await apiExecCommand(sessionId, cmd.command, 'foreground'); showCommandOutputModal(cmd.name, cmd.command, result); } else { await apiExecCommand(sessionId, cmd.command, 'background'); } } catch(e) { console.error('Exec failed:', e); } btn.disabled = false; btn.classList.remove('running'); }); chatMenuDropdown.appendChild(btn); }); }; // Sparte "Agent": bearbeiten + Agent-Befehle addSectionHeader('Agent'); if (currentAgentId) addNavItem('Agent bearbeiten', 'fa-pencil', 'agent', { id: currentAgentId, returnTo: 'chat', returnId: sessionId }); addCmdButtons(agentCmds); // Sparte "Session": bearbeiten + Session-Befehle addSectionHeader('Session'); addNavItem('Session bearbeiten', 'fa-pencil', 'session', { id: sessionId, returnTo: 'chat', returnId: sessionId }); addCmdButtons(sessionCmds); } // Zurück zum Dashboard const backBtn = document.getElementById('chatBackBtn'); if (backBtn) { backBtn.addEventListener('click', () => { if (history.state && history.length > 2) { history.back(); } else { navigateTo('dashboard'); } }); } // Send / Stop const sendBtn = document.getElementById('send'); if (sendBtn) sendBtn.addEventListener('click', sendMessage); const stopBtn = document.getElementById('stop'); if (stopBtn) stopBtn.addEventListener('click', stopStreaming); const inputEl = document.getElementById('input'); if (inputEl) { inputEl.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); sendMessage(); } }); inputEl.addEventListener('input', function () { this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 150) + 'px'; }); inputEl.dispatchEvent(new Event('input')) } // Emoji Picker const emojiBtn = document.getElementById('emojiBtn'); if (emojiBtn) { emojiBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleEmojiPicker(emojiBtn); }); } // Workspace Explorer const explorerBtn = document.getElementById('explorerBtn'); if (explorerBtn) { explorerBtn.addEventListener('click', () => { openFileExplorer({ sessionId, onSelect(ref) { addPendingRef(ref); document.getElementById('input')?.focus(); } }); }); } // Unified Modus-Menü (Aus / Plan / Ultracode / Fusion / MoA) – aus der Session initialisieren. // Klick öffnet ein Dropdown (wie der Emoji-Picker), aus dem der Modus direkt gewählt wird. const compositeModeBtn = document.getElementById('planModeBtn'); if (compositeModeBtn) { const hasPanelAgents = !!(session.panelAgentIds && String(session.panelAgentIds).trim()); initCompositeMode(hasPanelAgents); compositeModeBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleCompositeMenu(compositeModeBtn); }); } // File Upload const fileBtn = document.getElementById('fileBtn'); const fileInput = document.getElementById('fileInput'); if (fileBtn && fileInput) { fileBtn.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) handleMultipleFiles(e.target.files); }); } // Drag & Drop const chatContainer = document.querySelector('.chat-page'); if (chatContainer) { chatContainer.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); }); chatContainer.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer.files.length > 0) handleMultipleFiles(e.dataTransfer.files); }); } // Paste if (inputEl) { inputEl.addEventListener('paste', (e) => { const items = e.clipboardData?.items; if (!items) return; for (const item of items) { if (item.kind === 'file') { e.preventDefault(); handleFileSelect(item.getAsFile()); return; } } }); } // State zurücksetzen und History laden lastKnownMessageId = 0; loadedArchiveCount = 0; _oldestLoadedSortKey = 0; _hasMoreOlderMessages = false; isStreaming = false; await loadHistory(); // Usage Badge laden (Admin only) _initChatUsageBadge(); // Warnung anzeigen wenn kein Key und keine CLI-Berechtigung _checkChatPermissions(); // Nach Font-Loading nochmal scrollen (Schriftart kann Höhe verändern) document.fonts.ready.then(() => { requestAnimationFrame(() => scrollDown()); }); // Prüfen ob Backend gerade noch streamt (z.B. nach Page-Refresh) try { const status = await apiIsStreaming(sessionId); if (status.streaming) { isStreaming = true; if (sendBtn) { sendBtn.disabled = true; sendBtn.hidden = true; } const stopEl = document.getElementById('stop'); if (stopEl) stopEl.hidden = false; startTypingIndicator(); startStreamingPoll(sessionId, sendBtn, status.chainId); } } catch (e) { console.error('Streaming-Status prüfen fehlgeschlagen:', e); } }, destroy() { // Stream nicht abbrechen (Backend soll weiterlaufen), // aber Frontend-Processing stoppen _chatAbandoned = true; stopTypingIndicator(); setCurrentAgentName(null); currentAgentId = 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; } if (_streamingPollInterval) { clearInterval(_streamingPollInterval); _streamingPollInterval = null; } stopHistoryPolling(); lastKnownMessageId = 0; loadedArchiveCount = 0; isStreaming = false; clearPendingFiles(); clearPendingRefs(); clearReply(); } });