Mammouth.ai Floating Scroll Buttons

Ajoute des boutons flottants pour défiler vers le bas du chat et activer/désactiver l'auto-scroll.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         Mammouth.ai Floating Scroll Buttons
// @name:en      Mammouth.ai Floating Scroll Buttons
// @name:es      Botones flotantes de desplazamiento para Mammouth.ai
// @namespace    http://violentmonkey.github.io/
// @version      4.0.0
// @description  Ajoute des boutons flottants pour défiler vers le bas du chat et activer/désactiver l'auto-scroll.
// @description:en Adds floating buttons to scroll to the bottom of the chat and toggle auto-scroll.
// @description:es Añade botones flotantes para desplazarse al final del chat y activar/desactivar el auto-scroll.
// @author       AI Script Assistant
// @match        https://mammouth.ai/app/*
// @match        https://mammouth.ai/*
// @grant        none
// @license      MIT
// @compatible   firefox Violentmonkey
// @compatible   chrome Violentmonkey
// ==/UserScript==

(function() {
    'use strict';

    /**
     * Localization dictionary for UI strings.
     * Provides translations for button tooltips in multiple languages.
     * @type {Object}
     */
    const locales = {
        en: {
            scrollButton: "Scroll to bottom",
            autoScrollButton: "Toggle auto-scroll",
        },
        fr: {
            scrollButton: "Défiler vers le bas",
            autoScrollButton: "Activer/désactiver le suivi automatique",
        },
        es: {
            scrollButton: "Desplazarse al final",
            autoScrollButton: "Activar/desactivar auto-scroll",
        },
    };

    // Detect user's browser language and fallback to English.
    const userLang = (navigator.language || navigator.userLanguage || "en")
        .toLowerCase()
        .slice(0, 2);
    const messages = locales[userLang] || locales["en"];

    // State variables to track scroll behavior and elements
    let autoScrollActive = true;                // Auto-scroll is enabled by default
    let scrollableElement = null;               // Reference to the scrollable container
    let scrollInterval = null;                  // Interval for checking new content
    let lastScrollHeight = 0;                   // Track content height changes

    /**
     * Creates and injects CSS styles for the floating buttons.
     * Styles are designed to ensure buttons are visible and accessible.
     */
    function injectStyles() {
        // Check if styles are already injected
        if (document.getElementById('mammouth-scroll-styles')) {
            return;
        }

        const style = document.createElement('style');
        style.id = 'mammouth-scroll-styles';
        style.textContent = `
            /* Container for floating buttons */
            #mammouth-floating-buttons {
                position: fixed;
                bottom: 20px;
                right: 20px;
                display: flex;
                flex-direction: column;
                gap: 10px;
                z-index: 9999;
            }
            
            /* Individual button styling */
            .mammouth-floating-button {
                width: 42px;
                height: 42px;
                border-radius: 50%;
                background: rgba(255, 255, 255, 0.9);
                box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
                border: none;
                cursor: pointer;
                display: flex;
                align-items: center;
                justify-content: center;
                transition: transform 0.2s, background-color 0.2s;
            }
            
            /* Hover effect */
            .mammouth-floating-button:hover {
                transform: scale(1.1);
            }
            
            /* Icon styling */
            .mammouth-floating-button svg {
                width: 24px;
                height: 24px;
                color: #555;
            }
            
            /* Active state for auto-scroll button */
            #auto-scroll-button.active {
                background-color: #2eae66;
            }
            
            #auto-scroll-button.active svg {
                color: white;
            }
        `;
        document.head.appendChild(style);
        console.log('[Mammouth Scroll] Styles injected');
    }

    /**
     * Creates the floating buttons and adds them to the document.
     * Buttons are positioned in the bottom-right corner of the viewport.
     */
    function createFloatingButtons() {
        // Check if buttons already exist to avoid duplicates
        if (document.getElementById('mammouth-floating-buttons')) {
            return;
        }

        // Create container for floating buttons
        const container = document.createElement('div');
        container.id = 'mammouth-floating-buttons';

        // Create scroll down button
        const scrollButton = document.createElement('button');
        scrollButton.className = 'mammouth-floating-button';
        scrollButton.id = 'scroll-button';
        scrollButton.title = messages.scrollButton;
        scrollButton.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
            </svg>
        `;
        scrollButton.addEventListener('click', scrollToBottom);

        // Create auto-scroll toggle button
        const autoScrollButton = document.createElement('button');
        autoScrollButton.className = 'mammouth-floating-button';
        autoScrollButton.id = 'auto-scroll-button';
        autoScrollButton.title = messages.autoScrollButton;
        autoScrollButton.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"></path>
            </svg>
        `;
        autoScrollButton.addEventListener('click', toggleAutoScroll);
        
        // Set initial active state
        if (autoScrollActive) {
            autoScrollButton.classList.add('active');
        }

        // Add buttons to container
        container.appendChild(autoScrollButton);
        container.appendChild(scrollButton);

        // Add container to document body
        document.body.appendChild(container);
        
        console.log('[Mammouth Scroll] Floating buttons created');
    }

    /**
     * Finds the scrollable element containing the chat messages.
     * Uses multiple strategies to identify the correct element.
     * @returns {Element|null} - The scrollable element or null if not found
     */
    function findScrollableElement() {
        console.log('[Mammouth Scroll] Searching for scrollable element...');
        
        // Strategy 1: Find all visible scrollable elements
        const allElements = document.querySelectorAll('*');
        const candidates = [];
        
        for (const element of allElements) {
            const style = window.getComputedStyle(element);
            // Look for elements that are scrollable and tall enough to be a chat container
            if ((style.overflowY === 'auto' || style.overflowY === 'scroll') && 
                element.scrollHeight > element.clientHeight &&
                element.clientHeight > 200) {
                candidates.push({
                    element,
                    height: element.clientHeight,
                    scrollHeight: element.scrollHeight
                });
            }
        }
        
        // Sort candidates by scroll height (largest first)
        candidates.sort((a, b) => b.scrollHeight - a.scrollHeight);
        
        if (candidates.length > 0) {
            console.log('[Mammouth Scroll] Found scrollable candidates:', candidates);
            return candidates[0].element;
        }
        
        // Strategy 2: Try specific selectors that might match the chat container
        const specificSelectors = [
            'main .overflow-y-auto',
            '.conversation-container',
            '[role="log"]',
            '.messages-container',
            '.overflow-y-auto',
            '.scrollable'
        ];
        
        for (const selector of specificSelectors) {
            const element = document.querySelector(selector);
            if (element) {
                console.log(`[Mammouth Scroll] Found element with selector "${selector}":`, element);
                return element;
            }
        }
        
        console.log('[Mammouth Scroll] No scrollable element found');
        return null;
    }

    /**
     * Scrolls the chat element to the bottom.
     */
    function scrollToBottom() {
        if (scrollableElement) {
            scrollableElement.scrollTop = scrollableElement.scrollHeight;
            console.log('[Mammouth Scroll] Scrolled to bottom');
        } else {
            console.log('[Mammouth Scroll] Cannot scroll: no scrollable element found');
        }
    }

    /**
     * Toggles auto-scroll on and off.
     * Updates button appearance to reflect current state.
     */
    function toggleAutoScroll() {
        autoScrollActive = !autoScrollActive;
        const button = document.getElementById('auto-scroll-button');
        
        if (button) {
            if (autoScrollActive) {
                button.classList.add('active');
                scrollToBottom(); // Scroll immediately when enabling
                console.log('[Mammouth Scroll] Auto-scroll enabled');
            } else {
                button.classList.remove('active');
                console.log('[Mammouth Scroll] Auto-scroll disabled');
            }
        }
    }

    /**
     * Checks for new content and scrolls to the bottom if appropriate.
     * Called periodically to handle new messages.
     */
    function checkScrolling() {
        if (!scrollableElement) {
            // Try to find the scrollable element if we don't have one
            scrollableElement = findScrollableElement();
            return;
        }
        
        const currentScrollHeight = scrollableElement.scrollHeight;
        
        // If content height increased and auto-scroll is active, scroll to bottom
        if (currentScrollHeight > lastScrollHeight && autoScrollActive) {
            scrollToBottom();
        }
        
        lastScrollHeight = currentScrollHeight;
    }

    /**
     * Handles manual scrolling by the user.
     * Disables auto-scroll if user scrolls away from the bottom.
     */
    function handleManualScroll() {
        if (!scrollableElement) return;
        
        // Check if we're near the bottom of the scroll area
        const { scrollTop, scrollHeight, clientHeight } = scrollableElement;
        const isNearBottom = scrollHeight - scrollTop - clientHeight < 50;
        
        // If user scrolled away from bottom and auto-scroll was active, disable it
        if (!isNearBottom && autoScrollActive) {
            autoScrollActive = false;
            const button = document.getElementById('auto-scroll-button');
            if (button) {
                button.classList.remove('active');
            }
            console.log('[Mammouth Scroll] Auto-scroll disabled due to manual scrolling');
        }
    }

    /**
     * Main initialization function.
     * Sets up the UI elements and event listeners.
     */
    function initialize() {
        console.log('[Mammouth Scroll] Initializing...');
        injectStyles();
        createFloatingButtons();
        
        // Find the scrollable element
        scrollableElement = findScrollableElement();
        
        if (scrollableElement) {
            lastScrollHeight = scrollableElement.scrollHeight;
            // Add event listener for manual scrolling
            scrollableElement.addEventListener('scroll', handleManualScroll);
            console.log('[Mammouth Scroll] Scrollable element initialized:', scrollableElement);
        }
        
        // Set up interval for checking content and scroll state
        if (scrollInterval) clearInterval(scrollInterval);
        scrollInterval = setInterval(checkScrolling, 500);
        
        // Initial scroll to bottom if auto-scroll is enabled
        if (autoScrollActive) {
            setTimeout(scrollToBottom, 1000);
        }
    }

    /**
     * Sets up an observer to watch for DOM changes.
     * Ensures buttons are recreated if removed and handles UI changes.
     */
    function setupObserver() {
        const observer = new MutationObserver((mutations) => {
            // Check if our buttons were removed
            if (!document.getElementById('mammouth-floating-buttons')) {
                createFloatingButtons();
            }
            
            // Check if the scrollable element has changed
            const newScrollable = findScrollableElement();
            if (newScrollable !== scrollableElement) {
                console.log('[Mammouth Scroll] Scrollable element changed, reinitializing...');
                initialize();
            }
        });
        
        // Monitor the body for structural changes
        observer.observe(document.body, { 
            childList: true, 
            subtree: true,
            attributes: false,
            characterData: false
        });
    }

    // Start the script when the page is ready
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        initialize();
        setupObserver();
    } else {
        window.addEventListener('DOMContentLoaded', () => {
            initialize();
            setupObserver();
        });
    }

    // Also run after a short delay to ensure dynamic content has loaded
    setTimeout(() => {
        initialize();
        setupObserver();
    }, 2000);
})();