Instantly share code, notes, and snippets.
Created
March 6, 2026 16:42
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save drobbins/5ea8d0232dc8c8580ee3d6d00c333186 to your computer and use it in GitHub Desktop.
BoodleBox Human Message Navigator Userscript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name BoodleBox Human Message Navigator | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.0 | |
| // @description Navigate between human-generated messages in BoodleBox conversations using keyboard shortcuts | |
| // @author You | |
| // @match https://boodlebox.com/* | |
| // @match https://*.boodlebox.com/* | |
| // @match https://box.boodle.ai/* | |
| // @grant none | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| // ============================================ | |
| // CONFIGURATION | |
| // ============================================ | |
| const CONFIG = { | |
| // Keyboard shortcuts (using J/K vim-style navigation) | |
| prevKey: 'k', // Jump to previous human message | |
| nextKey: 'j', // Jump to next human message | |
| // CSS class patterns for message detection | |
| // Human messages have "styles_submission" in their class | |
| // AI messages have "styles_response" in their class | |
| humanMessageSelector: '[class*="styles_submission"]', | |
| aiMessageSelector: '[class*="styles_response"]', | |
| // Visual styling for highlighted message | |
| highlightStyle: { | |
| backgroundColor: 'rgba(59, 130, 246, 0.15)', | |
| borderLeft: '4px solid #3b82f6', | |
| transition: 'all 0.3s ease' | |
| }, | |
| // Scroll behavior | |
| scrollBehavior: 'smooth', | |
| scrollBlock: 'center' | |
| }; | |
| // ============================================ | |
| // STATE MANAGEMENT | |
| // ============================================ | |
| let currentIndex = -1; | |
| let humanMessages = []; | |
| let lastHighlightedElement = null; | |
| // ============================================ | |
| // MESSAGE COLLECTION & INDEXING | |
| // ============================================ | |
| /** | |
| * Finds all human-generated messages in the conversation | |
| * Human messages are identified by having "styles_submission" in their class name | |
| * @returns {NodeList} - Collection of human message elements | |
| */ | |
| function collectHumanMessages() { | |
| // Select elements with class containing "styles_submission" | |
| humanMessages = Array.from(document.querySelectorAll(CONFIG.humanMessageSelector)); | |
| // Filter to only get the top-level message containers (avoid nested matches) | |
| humanMessages = humanMessages.filter(el => { | |
| // Check if this element is nested inside another submission element | |
| const parent = el.parentElement?.closest(CONFIG.humanMessageSelector); | |
| return !parent; | |
| }); | |
| console.log(`[BoodleBox Navigator] Found ${humanMessages.length} human messages`); | |
| return humanMessages; | |
| } | |
| /** | |
| * Finds all AI-generated messages in the conversation (for reference/debugging) | |
| * AI messages are identified by having "styles_response" in their class name | |
| * @returns {NodeList} - Collection of AI message elements | |
| */ | |
| function collectAIMessages() { | |
| return document.querySelectorAll(CONFIG.aiMessageSelector); | |
| } | |
| // ============================================ | |
| // NAVIGATION FUNCTIONS | |
| // ============================================ | |
| /** | |
| * Navigates to the previous human message | |
| */ | |
| function goToPreviousHumanMessage() { | |
| collectHumanMessages(); // Refresh in case new messages appeared | |
| if (humanMessages.length === 0) { | |
| showNotification('No human messages found'); | |
| return; | |
| } | |
| if (currentIndex <= 0) { | |
| // Already at first message or not started | |
| currentIndex = 0; | |
| showNotification('Already at first human message'); | |
| } else { | |
| currentIndex--; | |
| } | |
| focusMessage(humanMessages[currentIndex]); | |
| } | |
| /** | |
| * Navigates to the next human message | |
| */ | |
| function goToNextHumanMessage() { | |
| collectHumanMessages(); // Refresh in case new messages appeared | |
| if (humanMessages.length === 0) { | |
| showNotification('No human messages found'); | |
| return; | |
| } | |
| if (currentIndex >= humanMessages.length - 1) { | |
| // Already at last message | |
| currentIndex = humanMessages.length - 1; | |
| showNotification('Already at last human message'); | |
| } else { | |
| currentIndex++; | |
| } | |
| focusMessage(humanMessages[currentIndex]); | |
| } | |
| // ============================================ | |
| // VISUAL FEEDBACK | |
| // ============================================ | |
| /** | |
| * Scrolls to and highlights the target message | |
| * @param {Element} messageElement - The message container to focus | |
| */ | |
| function focusMessage(messageElement) { | |
| if (!messageElement) return; | |
| // Remove highlight from previously focused message | |
| clearHighlight(); | |
| // Apply highlight to current message | |
| applyHighlight(messageElement); | |
| // Scroll message into view | |
| messageElement.scrollIntoView({ | |
| behavior: CONFIG.scrollBehavior, | |
| block: CONFIG.scrollBlock | |
| }); | |
| // Store reference for later cleanup | |
| lastHighlightedElement = messageElement; | |
| // Show position indicator | |
| showNotification(`Human message ${currentIndex + 1} of ${humanMessages.length}`); | |
| } | |
| /** | |
| * Applies visual highlight styling to an element | |
| * @param {Element} element - Element to highlight | |
| */ | |
| function applyHighlight(element) { | |
| // Store original styles for restoration | |
| element.dataset.originalBg = element.style.backgroundColor; | |
| element.dataset.originalBorder = element.style.borderLeft; | |
| element.dataset.originalTransition = element.style.transition; | |
| // Apply highlight styles | |
| Object.assign(element.style, CONFIG.highlightStyle); | |
| } | |
| /** | |
| * Removes highlight from the previously focused message | |
| */ | |
| function clearHighlight() { | |
| if (lastHighlightedElement) { | |
| lastHighlightedElement.style.backgroundColor = lastHighlightedElement.dataset.originalBg || ''; | |
| lastHighlightedElement.style.borderLeft = lastHighlightedElement.dataset.originalBorder || ''; | |
| lastHighlightedElement.style.transition = lastHighlightedElement.dataset.originalTransition || ''; | |
| } | |
| } | |
| /** | |
| * Shows a temporary notification toast | |
| * @param {string} message - Message to display | |
| */ | |
| function showNotification(message) { | |
| // Remove existing notification if present | |
| const existing = document.getElementById('bb-nav-notification'); | |
| if (existing) existing.remove(); | |
| // Create notification element | |
| const notification = document.createElement('div'); | |
| notification.id = 'bb-nav-notification'; | |
| notification.textContent = message; | |
| // Style the notification | |
| Object.assign(notification.style, { | |
| position: 'fixed', | |
| bottom: '20px', | |
| right: '20px', | |
| backgroundColor: '#1f2937', | |
| color: '#f9fafb', | |
| padding: '12px 20px', | |
| borderRadius: '8px', | |
| fontSize: '14px', | |
| fontWeight: '500', | |
| boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)', | |
| zIndex: '10000', | |
| opacity: '0', | |
| transform: 'translateY(10px)', | |
| transition: 'all 0.3s ease' | |
| }); | |
| document.body.appendChild(notification); | |
| // Animate in | |
| requestAnimationFrame(() => { | |
| notification.style.opacity = '1'; | |
| notification.style.transform = 'translateY(0)'; | |
| }); | |
| // Auto-remove after delay | |
| setTimeout(() => { | |
| notification.style.opacity = '0'; | |
| notification.style.transform = 'translateY(10px)'; | |
| setTimeout(() => notification.remove(), 300); | |
| }, 2000); | |
| } | |
| // ============================================ | |
| // KEYBOARD EVENT HANDLING | |
| // ============================================ | |
| /** | |
| * Handles keyboard events for navigation | |
| * @param {KeyboardEvent} event | |
| */ | |
| function handleKeydown(event) { | |
| // Ignore if user is typing in an input field | |
| const activeElement = document.activeElement; | |
| const isTyping = activeElement.tagName === 'INPUT' || | |
| activeElement.tagName === 'TEXTAREA' || | |
| activeElement.isContentEditable || | |
| activeElement.closest('[contenteditable="true"]'); | |
| if (isTyping) return; | |
| // Check for navigation keys | |
| if (event.key.toLowerCase() === CONFIG.prevKey) { | |
| event.preventDefault(); | |
| goToPreviousHumanMessage(); | |
| } else if (event.key.toLowerCase() === CONFIG.nextKey) { | |
| event.preventDefault(); | |
| goToNextHumanMessage(); | |
| } | |
| } | |
| // ============================================ | |
| // MUTATION OBSERVER | |
| // Watches for new messages being added to the conversation | |
| // ============================================ | |
| /** | |
| * Sets up a MutationObserver to detect new messages | |
| */ | |
| function setupMessageObserver() { | |
| const observer = new MutationObserver((mutations) => { | |
| // Check if new message elements were added | |
| const hasNewMessages = mutations.some(mutation => | |
| mutation.addedNodes.length > 0 && | |
| Array.from(mutation.addedNodes).some(node => | |
| node.nodeType === 1 && ( | |
| node.matches?.(CONFIG.humanMessageSelector) || | |
| node.matches?.(CONFIG.aiMessageSelector) || | |
| node.querySelector?.(CONFIG.humanMessageSelector) | |
| ) | |
| ) | |
| ); | |
| if (hasNewMessages) { | |
| // Refresh our message collection | |
| collectHumanMessages(); | |
| } | |
| }); | |
| // Observe the main content area for changes | |
| const chatContainer = document.querySelector('[class*="chat"], [class*="conversation"], main, #root'); | |
| if (chatContainer) { | |
| observer.observe(chatContainer, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| } | |
| } | |
| // ============================================ | |
| // INITIALIZATION | |
| // ============================================ | |
| /** | |
| * Initializes the script | |
| */ | |
| function init() { | |
| console.log('[BoodleBox Navigator] Initializing...'); | |
| console.log('[BoodleBox Navigator] Human selector:', CONFIG.humanMessageSelector); | |
| console.log('[BoodleBox Navigator] AI selector:', CONFIG.aiMessageSelector); | |
| // Initial collection of human messages | |
| collectHumanMessages(); | |
| // Set up keyboard listener | |
| document.addEventListener('keydown', handleKeydown); | |
| // Set up observer for new messages | |
| setupMessageObserver(); | |
| // Show ready notification | |
| showNotification('Navigator ready: J=next, K=previous'); | |
| console.log('[BoodleBox Navigator] Ready! Use J/K keys to navigate human messages.'); | |
| } | |
| // Wait for DOM to be ready, then initialize | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', init); | |
| } else { | |
| // Small delay to ensure BoodleBox has rendered | |
| setTimeout(init, 1000); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment