// ── Chat Page ── let _streamingPollInterval = null; let _recoveryChainId = null; let _emojiPickerEl = null; let _emojiPickerCloseHandler = null; 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; // 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; } 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; }); } // System-Prompt anzeigen const systemPromptBtn = document.getElementById('systemPromptBtn'); if (systemPromptBtn) systemPromptBtn.addEventListener('click', () => { if (chatMenuDropdown) chatMenuDropdown.hidden = true; showSystemPromptModal(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 && agent.customCommands) { agentCmds = JSON.parse(agent.customCommands) || []; } } catch(e) {} } let sessionCmds = []; try { sessionCmds = JSON.parse(session.customCommands || '[]') || []; } catch(e) {} const addCmdSection = (cmds, label) => { const validCmds = cmds.filter(c => c.name && c.command); if (validCmds.length === 0) return; 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); validCmds.forEach(cmd => { if (!cmd.name || !cmd.command) return; 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); }); }; addCmdSection(agentCmds, 'Agent'); addCmdSection(sessionCmds, 'Session'); } // Zurück zum Dashboard const backBtn = document.getElementById('chatBackBtn'); if (backBtn) { backBtn.addEventListener('click', () => history.back()); } // 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(); } }); }); } // 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 messages.length = 0; lastKnownMessageId = 0; loadedArchiveCount = 0; _oldestLoadedMessageId = 0; _hasMoreOlderMessages = false; isStreaming = false; await loadHistory(); // 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; setMetaHtml(' Streaming...'); 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; closeEmojiPicker(); if (this._closeMenu) { document.removeEventListener('click', this._closeMenu); this._closeMenu = null; } if (_streamingPollInterval) { clearInterval(_streamingPollInterval); _streamingPollInterval = null; } stopHistoryPolling(); messages.length = 0; lastKnownMessageId = 0; loadedArchiveCount = 0; isStreaming = false; clearPendingFiles(); clearPendingRefs(); clearReply(); } });