Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save drobbins/5ea8d0232dc8c8580ee3d6d00c333186 to your computer and use it in GitHub Desktop.

Select an option

Save drobbins/5ea8d0232dc8c8580ee3d6d00c333186 to your computer and use it in GitHub Desktop.
BoodleBox Human Message Navigator Userscript
// ==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