javascript

Кнопка «К началу ответа» для ChatGPT, Qwen, DeepSeek, Claude, Gemini, Grok и Perplexity: как я побе…

  • вторник, 30 июня 2026 г. в 00:00:11
https://habr.com/ru/articles/1053326/

Спойлер: коды готовы — вставьте и пользуйтесь.

Логотипы ChatGPT, DeepSeek, Qwen и кнопка
Пример кнопки. Логотипы являются товарными знаками компаний.

Знаете, что меня расстраивало больше всего в чатах с нейросетями? Сидишь, читаешь длинный ответ в момент генерации. Дошёл до середины, понял, что упустил какую-то деталь в начале, крутишь скролл вверх. А бот в этот момент дописывает новый абзац, и весь текст уезжает обратно вниз.

Штатная стрелочка “наверх” тут не спасает. Она кидает к шапке сайта, а не к началу конкретного сообщения ассистента. Приходится ловить текст вручную. Сейчас стали выкатывать что-то вроде истории запросов справа от чата, похожее на закладки, но мне они не нравятся по той же причине: надо приглядываться и целиться в анимированный интерфейс.

Чтобы не дергать страницу туда-сюда, я набросал около 10 юзерскриптов для всех популярных чатов. Они вешают в углу экрана кнопку, которая телепортирует прямо к первой строчке именно последнего ответа. Заодно вычищают визуальный мусор, который разработчики добавляют в интерфейс без возможности отключения штатными методами.

Под капотом логика везде одинаковая. Скрипт ищет в DOM-дереве последний блок с ответом, рисует поверх всего кнопку с position: fixed, а по клику вызывает обычный scrollIntoView. Чтобы было понятно, куда именно мы прыгнули, блок на секунду подсвечивается. Можно придумать любые эффекты и доработки оригинального интерфейса. Я начал с кнопки, а потом решил почистить интерфейс.

Поскольку все эти чаты работают как SPA, просто повесить обработчик на загрузку страницы не получится. Приходится слушать изменения через MutationObserver, иначе при переходе в новый диалог кнопка отвалится.

Самое интересное начинается на этапе поиска нужного div. У каждой платформы свои заморочки.

В ChatGPT и Claude жизнь облегчает наличие нормальных data-* атрибутов или предсказуемых классов. Нашел [data-message-author-role="assistant"] — и можно расслабиться.

Честно говоря, я не сильно вникал в код. Как только видел, что кнопка работает, то переходил к настройке другого ИИ чата. Поэтому, если кому интересно сделать надёжнее, пишите в комментариях как улучшить код для уменьшения вероятности отвала после обновлений сайтов с чатами.

С Gemini и Grok тоже более-менее понятно: там либо веб-компоненты вроде model-response, либо обычные markdown-контейнеры.

А вот DeepSeek любит использовать хеш-классы. Сегодня ты ищешь сообщение по .ds-message, а завтра верстак обновился, и классы превратились в случайный набор букв. Приходится делать кучу фолбэков, как в Qwen, где скрипт перебирает несколько вариантов селекторов, пока не наткнется на нужный. В Perplexity вообще приходится искать контент внутри специфичных renderer-блоков.

Ниже полный рабочий userscript для ChatGPT.

// ==UserScript==
// @name         ChatGPT
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Кнопка к началу ответа на chatgpt.com
// @match        https://chatgpt.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // Скрываем блоки CSS
    const style = document.createElement('style');
    style.textContent = `
        /* Reasoning */
        .reasoning,
        .model-reasoning,
        .thought,
        .chain-of-thought,
        [data-reasoning="true"],
        [class*="reasoning"],
        [class*="thought"] {
            display: none !important;
        }
        .thinking-indicator,
        .model-thinking {
            display: none !important;
        }

        /* Follow-up suggestions */
        p:has(+ ul[data-is-last-node][data-is-only-node]) {
            display: none !important;
        }
        ul[data-is-last-node][data-is-only-node] {
            display: none !important;
        }
        ul:has(li span.entity-underline) {
            display: none !important;
        }
        p:has(span:contains("следующим шагом")) {
            display: none !important;
        }
        .follow-up,
        .suggestion,
        .suggested-actions {
            display: none !important;
        }

        /* Дисклеймер */
        .text-caption-regular {
            display: none !important;
        }
        [class*="text-token-text-tertiary"] .text-caption-regular {
            display: none !important;
        }

        /* Кнопка "Принять предложение" */
        div.mx-3\\.5.mt-1.mb-2:has(button[aria-label="Принять предложение"]) {
            display: none !important;
        }
    `;
    document.head.appendChild(style);

    // Динамическое скрытие (для follow-up)
    function hideUnwantedElements() {
        document.querySelectorAll('ul[data-is-last-node][data-is-only-node]').forEach(el => {
            el.style.display = 'none';
        });
        document.querySelectorAll('p:has(+ ul[data-is-last-node][data-is-only-node])').forEach(el => {
            el.style.display = 'none';
        });
        document.querySelectorAll('p').forEach(el => {
            if (el.textContent.includes('следующим шагом')) {
                const next = el.nextElementSibling;
                if (next && next.tagName === 'UL' && next.querySelector('span.entity-underline')) {
                    el.style.display = 'none';
                    next.style.display = 'none';
                }
            }
        });
    }

    hideUnwantedElements();

    const observer = new MutationObserver(() => {
        hideUnwantedElements();
    });
    observer.observe(document.body, { childList: true, subtree: true });

    // Поиск последнего ответа ассистента
    function getLastAssistantMessage() {
        const messages = document.querySelectorAll('[data-message-author-role="assistant"]');
        if (messages.length > 0) {
            return messages[messages.length - 1];
        }
        const turns = document.querySelectorAll('[data-turn="assistant"]');
        if (turns.length > 0) {
            return turns[turns.length - 1];
        }
        return null;
    }

    // Создание кнопки
    const btn = document.createElement('button');
    btn.textContent = 'К началу ответа';        // ← Текст кнопки (меняйте)
    btn.title = 'Прокрутить к началу последнего ответа ИИ';
    btn.style.cssText = `
        position: fixed;
        bottom: 28px;                          /* ← Отступ снизу */
        right: 28px;                           /* ← Отступ справа */
        z-index: 10000;
        padding: 10px 18px;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); /* ← Цвета градиента (меняйте) */
        color: white;
        border: none;
        border-radius: 8px;
        cursor: pointer;
        font-size: 13px;
        font-weight: 500;
        box-shadow: 0 4px 14px rgba(102, 126, 234, 0.35);
        transition: all 0.2s ease;
        backdrop-filter: blur(6px);
    `;

    btn.onmouseenter = () => {
        btn.style.transform = 'translateY(-2px)';
        btn.style.boxShadow = '0 6px 18px rgba(102, 126, 234, 0.5)';
    };
    btn.onmouseleave = () => {
        btn.style.transform = 'translateY(0)';
        btn.style.boxShadow = '0 4px 14px rgba(102, 126, 234, 0.35)';
    };

    btn.onclick = () => {
        const target = getLastAssistantMessage();
        if (target) {
            target.scrollIntoView({ behavior: 'smooth', block: 'start' });
            const origBorder = target.style.borderTop;
            const origTrans = target.style.transition;
            target.style.transition = 'border-top 0.2s ease';
            target.style.borderTop = '3px solid #667eea';   // ← Цвет подсветки (меняйте)
            setTimeout(() => {
                target.style.borderTop = origBorder;
                target.style.transition = origTrans;
            }, 1500);
        } else {
            console.warn('[ChatGPT Scroll] Assistant message not found');
            window.scrollTo({ top: 0, behavior: 'smooth' });
        }
    };

    // Добавление кнопки в DOM
    function initButton() {
        if (document.body) {
            document.body.appendChild(btn);
        } else {
            setTimeout(initButton, 100);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initButton);
    } else {
        initButton();
    }

    // Поддержка SPA
    let lastUrl = location.href;
    new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            if (!document.body.contains(btn)) {
                initButton();
            }
        }
    }).observe(document, { subtree: true, childList: true });
})();

Применить скрипт легко: Установите расширение Tampermonkey для вашего браузера. Нажмите на иконку Tampermonkey и “Создать новый скрипт”. Удалите всё в шаблоне и вставьте скрипт. Сохраните (Ctrl+S) и обновите страницу нужного чата. Наслаждайтесь кнопкой!

Расположение кнопки на разных экранах может отличаться. Если она встала не туда, то просто поправьте значения bottom и right в коде за 5 секунд.

Настройка под себя: Цвет кнопки - замените #667eea и #764ba2 на свои hex-коды. Расположение - поменяйте значения bottom и right. Текст кнопки - замените “К началу ответа” на что угодно. Скрытие подписей - удалите или закомментируйте строки с селекторами.

Минус у такого подхода со скриптами один — мы парсим чужую верстку. Как только платформа решает провести редизайн (вряд ли это будет очень часто) или просто меняет нейминг классов, то скрипт сразу же ломается и хеш-классы отваливаются первыми. Data-атрибуты живут дольше, но и они не вечны.

Это не история из разряда «поставил и забыл». Скорее, «поставил, пользуешься, пока не прилетит обновление от вендора». Но меня это устраивает. Для меня секундное дело зайти в DevTools и глянуть код.

Вчера как раз DeepSeek выкатил минорный апдейт, и кнопка исчезла. Пришлось открывать консоль, искать новый класс для блока с мыслями и править CSS. Зато теперь интерфейс снова не режет глаз, и я очень рад своему небольшому изобретению. Мелочь, а приятно!

Все скрипты кнопок для разных ИИ лежат у меня на GitHub: button-AI.


Коротко обо мне: я архитектор и дизайнер интерьера, и моя страсть к удобству распространяется не только на архитектурные пространства, но и на цифровые интерфейсы. Меня зовут Артём Ерыков. Если интересно — загляните на мой сайт. Буду рад познакомиться.