Greasy Fork is available in English.

ChatGPT Visual Enhancements with message Navigation

Enhances the ChatGPT interface by visually highlighting the active chat link, styling message boxes, and adding top-centered navigation buttons to traverse messages.

// ==UserScript==
// @name         ChatGPT Visual Enhancements with message Navigation
// @namespace    http://tampermonkey.net/
// @version      1.8
// @description  Enhances the ChatGPT interface by visually highlighting the active chat link, styling message boxes, and adding top-centered navigation buttons to traverse messages.
// @author       noushadBug
// @match        https://chatgpt.com/*
// @icon         https://chatgpt.com/favicon.ico
// @license      MIT
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // Initialize variables for navigation
    let messageElements = [];
    let currentIndex = -1;

    // Create and style navigation buttons
    function createNavigationButtons() {
        const navContainer = document.createElement('div');
        navContainer.id = 'custom-nav-container';
        navContainer.style.position = 'absolute';
        navContainer.style.top = '10px';
        navContainer.style.left = '50%';
        navContainer.style.transform = 'translateX(-50%)';
        navContainer.style.display = 'flex';
        navContainer.style.gap = '10px';
        navContainer.style.zIndex = '1000';

        const prevButton = document.createElement('button');
        prevButton.id = 'custom-prev-button';
        prevButton.innerHTML = '‹';
        styleNavButton(prevButton, 'Previous Message');

        const nextButton = document.createElement('button');
        nextButton.id = 'custom-next-button';
        nextButton.innerHTML = '›';
        styleNavButton(nextButton, 'Next Message');

        navContainer.appendChild(prevButton);
        navContainer.appendChild(nextButton);
        document.body.appendChild(navContainer);

        // Add event listeners
        prevButton.addEventListener('click', () => navigateMessages(-1));
        nextButton.addEventListener('click', () => navigateMessages(1));
    }

    // Style individual navigation buttons
    function styleNavButton(button, ariaLabel) {
        // Apply the new styles as per your specifications
        button.style.padding = '4px 19px';
        button.style.fontSize = '20px';
        button.style.border = 'none';
        button.style.borderRadius = '50px';
        button.style.cursor = 'pointer';
        button.style.backgroundColor = 'rgb(65, 76, 59)';
        button.style.color = 'rgb(255, 255, 255)';
        button.style.boxShadow = 'rgba(0, 0, 0, 0.3) 0px 2px 5px';
        button.style.transition = 'background-color 0.3s, transform 0.2s';
        button.style.transform = 'scale(1)';

        // Accessibility
        button.setAttribute('aria-label', ariaLabel);

        // Hover effects
        button.addEventListener('mouseover', () => {
            button.style.backgroundColor = '#5a6e57';
            button.style.transform = 'scale(1.1)';
        });

        button.addEventListener('mouseout', () => {
            button.style.backgroundColor = 'rgb(65, 76, 59)';
            button.style.transform = 'scale(1)';
        });
    }

    // Function to update the list of message elements
    function updateMessageElements() {
        // Select all message containers; adjust the selector if necessary
        messageElements = Array.from(document.querySelectorAll('[data-message-author-role]'));
    }

    // Function to navigate through messages
    function navigateMessages(direction) {
        if (messageElements.length === 0) return;

        // Remove highlight from the current message
        if (currentIndex >= 0 && currentIndex < messageElements.length) {
            const currentMessage = messageElements[currentIndex];
            currentMessage.style.border = '';
            currentMessage.style.boxShadow = '';
        }

        // Update the current index
        currentIndex += direction;
        if (currentIndex < 0) {
            currentIndex = messageElements.length - 1; // Wrap to last
        } else if (currentIndex >= messageElements.length) {
            currentIndex = 0; // Wrap to first
        }

        const targetMessage = messageElements[currentIndex];

        // Ensure the targetMessage exists
        if (targetMessage) {
            // Scroll to the message container, aligning it to the top
            targetMessage.scrollIntoView({ behavior: 'smooth', block: 'start' });

            // Highlight the target message
            targetMessage.style.border = '2px solid #2f2f2f';
            targetMessage.style.boxShadow = 'rgb(123 166 175) 0px 0px 10px';

            // Focus on the message for better accessibility
            targetMessage.setAttribute('tabindex', '-1'); // Make it focusable
            targetMessage.focus({ preventScroll: true });
        }
    }

    // Function to update the active <li> state
    function updateActiveState() {
        const listItems = document.querySelectorAll('li.relative');
        const currentPath = window.location.pathname;

        listItems.forEach(li => {
            const anchor = li.querySelector('a'); // Find the <a> inside <li>
            if (anchor && anchor.getAttribute('href') === currentPath) {
                // If href matches the current path, style it as active
                li.classList.add('active');
                li.style.background = 'black';
                li.style.borderRadius = '20px';
            } else {
                // Remove active styles from other <li> elements
                li.classList.remove('active');
                li.style.background = '';
                li.style.borderRadius = '';
            }
        });
    }

    // Function to style assistant messages
    function styleAssistantMessages() {
        const messages = document.querySelectorAll('[data-message-author-role="assistant"]');

        messages.forEach(message => {
            if (!message.classList.contains('custom-assistant-message-container')) {
                // Add custom class and apply styles
                message.classList.add('custom-assistant-message-container');
                message.style.background = '#414c3b';
                message.style.padding = '1em';
                message.style.borderRadius = '10px';
                message.style.color = '#ffffff'; // Ensure text is readable
                message.setAttribute('tabindex', '-1'); // Make it focusable
            }
        });
    }

    // Function to highlight user messages (optional enhancement)
    function styleUserMessages() {
        const userMessages = document.querySelectorAll('[data-message-author-role="user"]');

        userMessages.forEach(message => {
            if (!message.classList.contains('custom-user-message-container')) {
                // Add custom class and apply styles
                message.classList.add('custom-user-message-container');
                message.style.background = '#2a2a2a';
                message.style.padding = '1em';
                message.style.borderRadius = '10px';
                message.style.color = '#ffffff'; // Ensure text is readable
                message.setAttribute('tabindex', '-1'); // Make it focusable
            }
        });
    }

    // Debounce function to limit the rate of function execution
    function debounce(func, wait) {
        let timeout;
        return function (...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }

    // Observe URL and DOM changes with debounced callback
    const observer = new MutationObserver(debounce(() => {
        updateActiveState();
        styleAssistantMessages();
        styleUserMessages();
        updateMessageElements();
    }, 300));

    // Start observing changes to the document body
    observer.observe(document.body, { childList: true, subtree: true });

    // Initial setup
    updateActiveState();
    styleAssistantMessages();
    styleUserMessages();
    updateMessageElements();
    createNavigationButtons();


    // Clean up when the script is removed
    window.addEventListener('unload', () => observer.disconnect());

    // Additional CSS Styling
    const otherCSS = `.h-header-height{background-color: var(--sidebar-surface-primary);}`;
    const styleTag = document.createElement('style');
    styleTag.textContent = otherCSS;
    document.head.appendChild(styleTag);
})();