Spaces:
Running
on
Zero
Running
on
Zero
| (function () { | |
| console.log("Keyboard shortcuts script loaded"); | |
| // --- Autoscroll control --- | |
| let lastSendTs = 0; | |
| let userPinnedScroll = false; | |
| let chatContainer = null; | |
| let chatObserver = null; | |
| const now = () => Date.now(); | |
| const SEND_AUTOSCROLL_WINDOW_MS = 2500; | |
| const NEAR_BOTTOM_PX = 120; | |
| const getChatContainer = () => { | |
| if (chatContainer && document.body.contains(chatContainer)) return chatContainer; | |
| // Prefer explicit elem_id container | |
| const root = document.getElementById('chatbot') || document.querySelector('#chatbot'); | |
| // Fallbacks: any visible gr-chatbot within left pane | |
| const candidates = []; | |
| if (root) candidates.push(root); | |
| candidates.push( | |
| document.querySelector('#left-pane .gr-chatbot'), | |
| document.querySelector('.gr-chatbot'), | |
| document.querySelector('#left-pane [data-testid="chatbot"]') | |
| ); | |
| for (const el of candidates) { | |
| if (!el) continue; | |
| // Try common inner scroll area | |
| let container = el.querySelector('[data-testid="bot"]') || el.querySelector('[data-testid="chatbot"]') || el; | |
| // Walk down to the element that actually scrolls | |
| const stack = [container]; | |
| while (stack.length) { | |
| const cur = stack.shift(); | |
| if (!cur) continue; | |
| const style = cur instanceof Element ? getComputedStyle(cur) : null; | |
| const canScroll = style && (style.overflowY === 'auto' || style.overflowY === 'scroll'); | |
| if (canScroll && cur.scrollHeight > cur.clientHeight + 10) { | |
| chatContainer = cur; | |
| break; | |
| } | |
| if (cur.children && cur.children.length) stack.push(...cur.children); | |
| } | |
| if (!chatContainer) chatContainer = container; | |
| if (chatContainer) break; | |
| } | |
| return chatContainer; | |
| }; | |
| const isNearBottom = (el) => { | |
| if (!el) return true; | |
| const distance = el.scrollHeight - el.scrollTop - el.clientHeight; | |
| return distance <= NEAR_BOTTOM_PX; | |
| }; | |
| const scrollToBottom = (el) => { | |
| if (!el) return; | |
| el.scrollTo({ top: el.scrollHeight, behavior: 'auto' }); | |
| }; | |
| const attachScrollListener = () => { | |
| const el = getChatContainer(); | |
| if (!el) return; | |
| el.addEventListener('scroll', () => { | |
| // If the user scrolls up away from bottom, pin the position | |
| userPinnedScroll = !isNearBottom(el); | |
| }, { passive: true }); | |
| }; | |
| const observeChat = () => { | |
| const el = getChatContainer(); | |
| if (!el) return; | |
| if (chatObserver) chatObserver.disconnect(); | |
| chatObserver = new MutationObserver(() => { | |
| const withinSendWindow = now() - lastSendTs < SEND_AUTOSCROLL_WINDOW_MS; | |
| const shouldScroll = withinSendWindow || (!userPinnedScroll && isNearBottom(el)); | |
| if (shouldScroll) scrollToBottom(el); | |
| }); | |
| chatObserver.observe(el, { childList: true, subtree: true, characterData: true }); | |
| }; | |
| const send = () => { | |
| // Try multiple selectors to find the send button | |
| const selectors = [ | |
| '#send-btn', | |
| 'button[id="send-btn"]', | |
| '.gr-button:contains("Send")', | |
| 'button:contains("Send")', | |
| '#send-btn button', | |
| '[data-testid*="send"]', | |
| 'button[variant="primary"]' | |
| ]; | |
| let btn = null; | |
| for (let selector of selectors) { | |
| try { | |
| if (selector.includes(':contains')) { | |
| // Handle :contains selector manually | |
| const buttons = document.querySelectorAll('button'); | |
| for (let button of buttons) { | |
| if (button.textContent.trim() === 'Send') { | |
| btn = button; | |
| break; | |
| } | |
| } | |
| } else { | |
| btn = document.querySelector(selector); | |
| } | |
| if (btn) { | |
| console.log("Found send button with selector:", selector); | |
| break; | |
| } | |
| } catch (e) { | |
| // Skip invalid selectors | |
| } | |
| } | |
| if (btn) { | |
| console.log("Clicking send button"); | |
| lastSendTs = now(); | |
| // When sending, allow autoscroll for initial response | |
| userPinnedScroll = false; | |
| // Ensure observers are up | |
| setTimeout(() => { attachScrollListener(); observeChat(); }, 0); | |
| btn.click(); | |
| return true; | |
| } else { | |
| console.log("Send button not found"); | |
| // Debug: log all buttons | |
| const allButtons = document.querySelectorAll('button'); | |
| console.log("All buttons found:", allButtons); | |
| return false; | |
| } | |
| }; | |
| const setupKeyboardShortcuts = () => { | |
| console.log("Setting up keyboard shortcuts"); | |
| // Initialize observers once UI is present | |
| setTimeout(() => { attachScrollListener(); observeChat(); }, 50); | |
| setTimeout(() => { attachScrollListener(); observeChat(); }, 400); | |
| document.addEventListener('keydown', (e) => { | |
| const isCmdEnter = (e.metaKey || e.ctrlKey) && e.key === 'Enter'; | |
| const isEnter = e.key === 'Enter' && !e.shiftKey && !e.metaKey && !e.ctrlKey; | |
| const isInputFocused = document.activeElement && | |
| (document.activeElement.matches('#chat-input textarea') || | |
| document.activeElement.matches('.chat-input-box textarea')); | |
| if ((isCmdEnter || (isEnter && isInputFocused)) && isInputFocused) { | |
| console.log("Send triggered"); | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| const success = send(); | |
| if (!success) { | |
| console.log("Failed to send, trying again in 100ms"); | |
| setTimeout(send, 100); | |
| } | |
| } | |
| if (e.key === 'Escape') { | |
| const input = document.querySelector('#chat-input textarea') || document.querySelector('.chat-input-box textarea'); | |
| if (input) { | |
| input.focus(); | |
| console.log("Focused input field"); | |
| } | |
| } | |
| }, true); | |
| console.log("Keyboard shortcuts set up"); | |
| }; | |
| // Multiple attempts to set up shortcuts | |
| const attempts = [100, 500, 1000, 2000]; | |
| attempts.forEach(delay => { | |
| setTimeout(() => { | |
| console.log(`Attempting setup after ${delay}ms`); | |
| setupKeyboardShortcuts(); | |
| }, delay); | |
| }); | |
| // Force light theme - disable dark mode switching | |
| const forceTheme = () => { | |
| // Remove any dark theme classes that might be added | |
| document.documentElement.classList.remove('dark'); | |
| document.body.classList.remove('dark'); | |
| // Set light color scheme | |
| document.documentElement.style.colorScheme = 'light'; | |
| // Override any theme preference that might be set | |
| if (window.matchMedia) { | |
| const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)'); | |
| // Override the matches property to always return false | |
| Object.defineProperty(darkModeQuery, 'matches', { | |
| value: false, | |
| writable: false | |
| }); | |
| } | |
| }; | |
| // Apply theme override immediately and on any DOM changes | |
| forceTheme(); | |
| // Watch for any changes and reapply theme override | |
| const observer = new MutationObserver(() => { | |
| forceTheme(); | |
| }); | |
| observer.observe(document.documentElement, { | |
| attributes: true, | |
| attributeFilter: ['class', 'data-theme', 'theme'] | |
| }); | |
| // Also set up immediately if DOM is ready | |
| if (document.readyState !== 'loading') { | |
| setupKeyboardShortcuts(); | |
| } else { | |
| document.addEventListener('DOMContentLoaded', setupKeyboardShortcuts); | |
| } | |
| })(); | |