Mammouth.ai Scroll to Bottom Button

Ajoute deux boutons dans la barre de saisie pour défiler vers le bas du chat et activer/désactiver l'auto-scroll.

Від 18.03.2025. Дивіться остання версія.

// ==UserScript==
// @name         Mammouth.ai Scroll to Bottom Button
// @name:en      Mammouth.ai Scroll to Bottom Button
// @name:es      Mammouth.ai Scroll to Bottom Button
// @namespace    http://violentmonkey.github.io/
// @version      2.0.3
// @description  Ajoute deux boutons dans la barre de saisie pour défiler vers le bas du chat et activer/désactiver l'auto-scroll.
// @description:en Add two buttons in the input bar to scroll the chat down and toggle auto-scroll.
// @description:es Añade dos botones en la barra de entrada para ir al final del chat y activar/desactivar el auto-scroll.
// @author       Assistant IA
// @match        https://mammouth.ai/app/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  // Localization dictionary for UI strings.
  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"];

  // Auto-scroll is enabled by default.
  let autoScrollActive = true;
  let scrollableElement = null;
  let scrollCheck = null;
  let lastScrollHeight = 0;
  let userManuallyScrolled = false;
  let lastUserInteractionTime = 0;

  // Inject styles for the buttons.
  // The buttons are sized and positioned to match the message send button.
  const style = document.createElement("style");
  style.innerHTML = `
    .mammouth-button {
      position: absolute;
      top: 8px;
      width: 32px;
      height: 32px;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.2s;
    }

    .mammouth-button.visible {
      opacity: 1;
      pointer-events: auto;
    }

    /* Positioning: the send button is at right:8px, so the buttons are placed just to its left */
    #mammouth-auto-scroll-button {
      right: 48px;
    }

    #mammouth-scroll-button {
      right: 88px;
    }

    .mammouth-button svg {
      width: 20px;
      height: 20px;
      color: #a16207; /* similar to light brown */
      transition: color 0.2s;
    }

    .mammouth-button:hover svg {
      color: #92400e; /* similar to brown */
    }

    /* Active indicator (green) for the auto-scroll button */
    #mammouth-auto-scroll-button.active svg {
      color: #2eae66;
    }
  `;
  document.head.appendChild(style);

  /**
   * Creates the scroll and auto-scroll buttons and inserts them into the input container.
   */
  function createButtons() {
    // Select the container that holds the text area and the send message button.
    const inputContainer = document.querySelector("div.w-full.relative");
    if (!inputContainer) return;

    // Create the "scroll to bottom" button if it does not already exist.
    if (!document.getElementById("mammouth-scroll-button")) {
      const scrollButton = document.createElement("button");
      scrollButton.id = "mammouth-scroll-button";
      scrollButton.className = "mammouth-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", () => {
        if (scrollableElement) {
          scrollToBottom();
          updateButtonsVisibility();
        }
      });
      inputContainer.appendChild(scrollButton);
    }

    // Create the "auto-scroll toggle" button if it does not already exist.
    if (!document.getElementById("mammouth-auto-scroll-button")) {
      const autoScrollButton = document.createElement("button");
      autoScrollButton.id = "mammouth-auto-scroll-button";
      autoScrollButton.className = "mammouth-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);
      if (autoScrollActive) {
        autoScrollButton.classList.add("active");
      }
      inputContainer.appendChild(autoScrollButton);
    }
  }

  /**
   * Returns true if the chat element is near the bottom.
   * @returns {boolean}
   */
  function isNearBottom() {
    if (!scrollableElement) return true;
    const scrollTop = scrollableElement.scrollTop;
    const scrollHeight = scrollableElement.scrollHeight;
    const clientHeight = scrollableElement.clientHeight;
    return scrollHeight - scrollTop - clientHeight < 50;
  }

  /**
   * Scrolls the chat element to the bottom.
   */
  function scrollToBottom() {
    if (scrollableElement) {
      scrollableElement.scrollTop = scrollableElement.scrollHeight;
      userManuallyScrolled = false;
    }
  }

  /**
   * Toggles auto-scroll on and off.
   */
  function toggleAutoScroll() {
    autoScrollActive = !autoScrollActive;
    const button = document.getElementById("mammouth-auto-scroll-button");
    if (button) {
      if (autoScrollActive) {
        button.classList.add("active");
        scrollToBottom(); // Scroll immediately when enabling
      } else {
        button.classList.remove("active");
      }
    }
    updateButtonsVisibility();
  }

  /**
   * Checks for new messages and scrolls to the bottom if appropriate.
   */
  function checkForNewContent() {
    if (!scrollableElement || !autoScrollActive) return;
    const currentScrollHeight = scrollableElement.scrollHeight;
    if (currentScrollHeight > lastScrollHeight && !userManuallyScrolled) {
      scrollToBottom();
    }
    lastScrollHeight = currentScrollHeight;
  }

  /**
   * Updates the visibility of the buttons.
   */
  function updateButtonsVisibility() {
    const scrollButton = document.getElementById("mammouth-scroll-button");
    const autoScrollButton = document.getElementById("mammouth-auto-scroll-button");
    if (!scrollButton || !autoScrollButton || !scrollableElement) return;
    if (isNearBottom() || autoScrollActive) {
      scrollButton.classList.remove("visible");
    } else {
      scrollButton.classList.add("visible");
    }
    autoScrollButton.classList.add("visible");
  }

  /**
   * Handles manual scrolling by the user.
   */
  function handleUserScroll() {
    if (!isNearBottom()) {
      userManuallyScrolled = true;
      if (autoScrollActive) {
        toggleAutoScroll();
      }
    } else {
      userManuallyScrolled = false;
    }
    lastUserInteractionTime = Date.now();
    updateButtonsVisibility();
  }

  /**
   * Initializes the buttons and event listeners.
   */
  function initScrollButtons() {
    // Select the scrolling element where messages are displayed.
    const chatElement = document.querySelector(
      "div.overflow-auto.overflow-x-hidden.w-full.flex.flex-col.gap-5.scrollable"
    );
    if (chatElement) {
      scrollableElement = chatElement;
      lastScrollHeight = chatElement.scrollHeight;

      // Insert the buttons into the input container.
      createButtons();

      if (scrollCheck) clearInterval(scrollCheck);
      scrollableElement.addEventListener("scroll", handleUserScroll);
      scrollCheck = setInterval(() => {
        checkForNewContent();
        updateButtonsVisibility();
      }, 300);

      updateButtonsVisibility();

      if (autoScrollActive) {
        scrollToBottom();
      }
    } else {
      scrollableElement = null;
      const scrollButton = document.getElementById("mammouth-scroll-button");
      const autoScrollButton = document.getElementById("mammouth-auto-scroll-button");
      if (scrollButton) scrollButton.classList.remove("visible");
      if (autoScrollButton) autoScrollButton.classList.remove("visible");
    }
  }

  // Monitor DOM changes to update our buttons in dynamic or AJAX-loaded interfaces.
  const observer = new MutationObserver((mutations) => {
    for (const mutation of mutations) {
      if (mutation.type === "childList" || mutation.type === "subtree") {
        initScrollButtons();
        break;
      }
    }
  });
  observer.observe(document.body, { childList: true, subtree: true });

  // Periodically check for button initialization (useful for AJAX navigation)
  setInterval(initScrollButtons, 2000);

  if (
    document.readyState === "complete" ||
    document.readyState === "interactive"
  ) {
    initScrollButtons();
  } else {
    window.addEventListener("DOMContentLoaded", initScrollButtons);
  }
})();