GPT Message Top Scroll

The “↑” button next to Copy/Edit scrolls only large messages (>=700 characters) to their top, aligned per message; supports both user and assistant.

// ==UserScript==
// @name         GPT Message Top Scroll
// @namespace    https://greasyfork.org/users/your-id-or-homepage
// @version      1.3
// @description  The “↑” button next to Copy/Edit scrolls only large messages (>=700 characters) to their top, aligned per message; supports both user and assistant.
// @author       Madhuvrata
// @match        https://chatgpt.com/*
// @grant        none
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(() => {
  'use strict';

  // CSS
  const style = document.createElement('style');
  style.textContent = `
    .tm-scroll-top-btn {
      width: 32px!important;
      height: 32px!important;
      margin: 0 4px!important;
      background: rgba(0,0,0,0.05)!important;
      border: 1px solid rgba(0,0,0,0.1)!important;
      border-radius: 6px!important;
      display: inline-flex!important;
      align-items: center!important;
      justify-content: center!important;
      cursor: pointer!important;
      opacity: .7!important;
      transition: opacity .2s, background .2s;
      pointer-events: auto!important;
    }
    .tm-scroll-top-btn:hover { opacity: 1!important; background: rgba(0,0,0,0.1)!important; }
    .tm-force-panel {
      opacity: 1!important;
      pointer-events: auto!important;
      mask-image: none!important;
      -webkit-mask-image: none!important;
    }
  `;
  document.head.appendChild(style);

  const svgArrow = `
    <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
      <path d="M10 4l-6 6h4v6h4v-6h4l-6-6z"/>
    </svg>`;

  // панель кнопок для user и assistant
  const PANEL_SELECTOR = `
    article[data-testid^="conversation-turn-"] .flex.justify-end,
    article[data-testid^="conversation-turn-"] .flex.justify-start
  `.trim().replace(/\s+/g,' ');

  function injectButtons() {
    document.querySelectorAll(PANEL_SELECTOR).forEach(panel => {
      panel.classList.add('tm-force-panel');
      const inner = panel.querySelector(':scope > div');
      if (!inner || inner.querySelector('.tm-scroll-top-btn')) return;
      inner.classList.add('tm-force-panel');

      // найдём связанный article и контейнер markdown
      const article = panel.closest('article[data-testid^="conversation-turn-"]');
      if (!article) return;
      const md = article.querySelector('.markdown') || article;

      // длина текста
      const txtLen = (md.innerText || '').length;

      // создаём кнопку
      const btn = document.createElement('button');
      btn.className = 'tm-scroll-top-btn';
      btn.innerHTML = svgArrow;
      btn.title = 'Scroll message to top';
      btn.addEventListener('click', e => {
        e.stopPropagation();
        // если короткое сообщение — игнорируем
        if (txtLen < 700) return;
        article.scrollIntoView({ behavior: 'smooth', block: 'start' });
      });

      inner.appendChild(btn);
    });
  }

  injectButtons();
  new MutationObserver(injectButtons)
    .observe(document.body, { childList: true, subtree: true });
})();