Last active
December 23, 2025 23:52
-
-
Save tuanchauict/b312713953d0d7b9a8111dc3cdaa1340 to your computer and use it in GitHub Desktop.
A short tampermonkey script for adding a Nav Bar for Claude.ai that helps navigate through conversation messages
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 Claude.ai Navigation Bar | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.0 | |
| // @description Creates a navigation bar on the right side of Claude.ai that helps navigate through conversation messages | |
| // @author You | |
| // @match https://claude.ai/* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=claude.ai | |
| // @grant GM_addStyle | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| // Add CSS styles | |
| GM_addStyle(` | |
| /* Navigation bar container */ | |
| .custom-nav-container { | |
| position: absolute; | |
| right: 10px; | |
| top: 50px; | |
| bottom: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| z-index: 9999; /* High z-index to stay on top */ | |
| opacity: 0; | |
| transition: opacity 0.3s ease; | |
| padding: 8px; | |
| border-radius: 10px; | |
| background-color: rgba(255, 255, 255, 0.1); | |
| } | |
| /* Show navigation on hover */ | |
| .custom-nav-container:hover { | |
| opacity: 1 !important; | |
| } | |
| /* Individual navigation dot */ | |
| .custom-nav-dot { | |
| width: 20px; | |
| height: 6px; | |
| border-radius: 6px; | |
| background-color: #DBDBDB; | |
| cursor: pointer; | |
| transition: transform 0.2s ease, background-color 0.2s ease; | |
| } | |
| /* Hover effect for dots */ | |
| .custom-nav-dot:hover { | |
| transform: scale(1.3); | |
| } | |
| /* Active dot style */ | |
| .custom-nav-dot.active { | |
| background-color: #C9B194; | |
| } | |
| /* Tooltip for dots */ | |
| .custom-nav-dot { | |
| position: relative; | |
| } | |
| .custom-nav-dot::after { | |
| content: attr(data-tooltip); | |
| position: absolute; | |
| right: calc(100% + 10px); | |
| top: 50%; | |
| transform: translateY(-50()); | |
| background-color: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| white-space: nowrap; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.2s ease; | |
| z-index: 10000; | |
| } | |
| .custom-nav-dot:hover::after { | |
| opacity: 1; | |
| } | |
| `); | |
| function getMessageElements() { | |
| return document.querySelectorAll('div[data-test-render-count] > .mb-1.mt-6.group'); | |
| } | |
| /** | |
| * Create a navigation bar based on the number of sections | |
| * @param {number} numSections - Number of sections to create navigation for | |
| * @param {Function} callback - Function to call when a navigation dot is clicked | |
| */ | |
| function createNavBar(numSections, callback) { | |
| // Create navigation container if it doesn't exist | |
| let navContainer = document.getElementById('custom-nav-container'); | |
| if (!navContainer) { | |
| navContainer = document.createElement('div'); | |
| navContainer.id = 'custom-nav-container'; | |
| navContainer.className = 'custom-nav-container'; | |
| const parent = document.body; | |
| parent.appendChild(navContainer); | |
| parent.style.position = 'relative'; | |
| } else { | |
| // Clear existing navigation dots | |
| navContainer.innerHTML = ''; | |
| } | |
| // Define sections - automatic or custom logic based on your need | |
| const sections = []; | |
| // Option 1: Use existing sections if they can be identified | |
| // Example: Get all h1, h2 elements as section markers | |
| const potentialSections = getMessageElements(); | |
| if (potentialSections.length === 0) { | |
| return; | |
| } | |
| // Use the potential sections found in the document | |
| const usedSections = potentialSections.length > numSections | |
| ? Array.from(potentialSections).slice(0, numSections) | |
| : potentialSections; | |
| usedSections.forEach((section, i) => { | |
| const rect = section.getBoundingClientRect(); | |
| sections.push({ | |
| element: section, | |
| top: rect.top + window.scrollY, | |
| bottom: rect.bottom + window.scrollY, | |
| index: i | |
| }); | |
| }); | |
| // Create navigation dots | |
| sections.forEach((section, i) => { | |
| const navDot = document.createElement('div'); | |
| navDot.className = 'custom-nav-dot'; | |
| navDot.dataset.index = i; | |
| // Get text content from the section and limit to 10 words, no newlines | |
| const textContent = section.element.innerText.replace(/\n/g, ' ').split(' ').slice(0, 10).join(' '); | |
| navDot.dataset.tooltip = textContent; | |
| navDot.onclick = function() { | |
| // Call the callback function with index | |
| if (typeof callback === 'function') { | |
| callback(i); | |
| } | |
| // Scroll to the section | |
| if (section.element) { | |
| section.element.scrollIntoView({ | |
| behavior: 'smooth' | |
| }); | |
| } else { | |
| // Scroll to position if no element | |
| window.scrollTo({ | |
| top: section.top, | |
| behavior: 'smooth' | |
| }); | |
| } | |
| // Update active state | |
| updateActiveNavDot(i); | |
| }; | |
| navContainer.appendChild(navDot); | |
| }); | |
| // Set the first dot as active initially | |
| if (sections.length > 0) { | |
| updateActiveNavDot(0); | |
| } | |
| // Add scroll event listener to update active nav dot | |
| window.addEventListener('scroll', function() { | |
| const scrollPosition = window.scrollY; | |
| const windowHeight = window.innerHeight; | |
| const middle = scrollPosition + (windowHeight / 2); | |
| // Find which section contains the middle of the viewport | |
| for (let i = 0; i < sections.length; i++) { | |
| const section = sections[i]; | |
| if (middle >= section.top && middle < section.bottom) { | |
| updateActiveNavDot(i); | |
| break; | |
| } | |
| } | |
| }); | |
| return { | |
| navContainer, | |
| sections, | |
| // Method to update the opacity | |
| setOpacity: function(opacity) { | |
| navContainer.style.opacity = opacity; | |
| } | |
| }; | |
| } | |
| /** | |
| * Update the active navigation dot | |
| * @param {number} activeIndex - Index of the active section | |
| */ | |
| function updateActiveNavDot(activeIndex) { | |
| const navDots = document.querySelectorAll('.custom-nav-dot'); | |
| navDots.forEach((dot, index) => { | |
| if (index === activeIndex) { | |
| dot.classList.add('active'); | |
| } else { | |
| dot.classList.remove('active'); | |
| } | |
| }); | |
| } | |
| // Create a global API | |
| window.RightNavBar = { | |
| create: createNavBar, | |
| updateActive: updateActiveNavDot | |
| }; | |
| /** | |
| * Initialize the navigation bar | |
| * @param {number} numSections - Number of sections to create navigation for | |
| * @param {Function} callback - Function to call when a navigation dot is clicked | |
| */ | |
| function initNavBar(numSections, callback) { | |
| // Default callback if none provided | |
| if (!callback) { | |
| callback = function(index) { | |
| console.log(`Navigation item ${index + 1} clicked!`); | |
| }; | |
| } | |
| // Create the navigation bar | |
| const navBar = window.RightNavBar.create(numSections, callback); | |
| // Optional: You can customize the initial opacity if needed | |
| // navBar.setOpacity(0.2); // Make it slightly visible by default | |
| return navBar; | |
| } | |
| // Track the section count to detect changes | |
| let sectionCount = 0; | |
| // Initialize and update the navigation bar periodically | |
| setInterval(() => { | |
| const messages = getMessageElements(); | |
| if (messages.length != sectionCount) { | |
| sectionCount = messages.length; | |
| initNavBar(sectionCount, (index) => { | |
| // You can add custom callback functionality here | |
| }); | |
| } | |
| }, 1000); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
