Qwen Chat: Wide Layout + Wider Scrollbar + Sticky Code Buttons

Removes max-width, stretches the chat, increases the scrollbar width, and makes the code copy buttons sticky.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Qwen Chat: Wide Layout + Wider Scrollbar + Sticky Code Buttons
// @name:ru      Qwen Chat: Широкий макет + толстая прокрутка + липкие кнопки кода
// @namespace    http://tampermonkey.net/
// @version      1.1
// @author       a114_you
// @description     Removes max-width, stretches the chat, increases the scrollbar width, and makes the code copy buttons sticky.
// @description:ru  Убирает max-width, растягивает чат, увеличивает ширину полосы прокрутки и делает кнопки копирования кода липкими
// @match        https://chat.qwen.ai/*
// @grant        none
// @run-at       document-idle
// @license MIT
// ==/UserScript==


(function() {
  'use strict';

  // --- НАСТРОЙКИ ---
  const CFG = {
    top: 40,      // Отступ сверху
    right: 15,    // Отступ справа
    size: '14px', // Размер значков
    bg: '#2b2b31' // Цвет фона
  };

  const css = `
    /* Широкий макет */
    html, body { width: 100vw!important; max-width: 100vw!important; overflow-x: hidden!important; }
    .mx-auto, #chat-message-container, #chat-message-container > div, .chat-messages {
      width: 100%!important; max-width: none!important; margin: 0!important;
    }

    /* Липкие кнопки */
    .sticky-buttons-clone {
      position: fixed!important; z-index: 99999; display: flex; gap: 10px;
      background: ${CFG.bg}!important; border-radius: 6px; padding: 6px 8px;
      opacity: 0; transition: opacity .1s; pointer-events: auto;
      border: none!important; box-shadow: none!important;
    }
    .sticky-buttons-clone.visible { opacity: 1; }
    .sticky-buttons-clone div { cursor: pointer; display: flex; align-items: center; }

    /* Значки */
    .sticky-buttons-clone svg {
      color: rgba(255,255,255,.8)!important; fill: rgba(255,255,255,.8)!important;
      width: ${CFG.size}!important; height: ${CFG.size}!important;
    }
    .sticky-buttons-clone div:hover svg { color: #fff!important; fill: #fff!important; }
    .hidden-by-sticky { opacity: 0!important; pointer-events: none!important; }

    /* Скроллбар */
    .chat-messages::-webkit-scrollbar { width: 16px!important; }
    .chat-messages::-webkit-scrollbar-thumb {
      background: rgba(130,130,130,.7)!important; border-radius: 8px;
      border: 4px solid transparent; background-clip: content-box;
    }
  `;

  const style = document.createElement('style');
  style.textContent = css;
  document.head.appendChild(style);

  const activeClones = new Map();
  const knownBlocks = new Set(); // Список всех блоков кода в текущем чате

  const update = () => {
    // Проверка всех известных блоков
    knownBlocks.forEach(block => {
      // Если блока больше нет в документе (сменили чат) - удаляем
      if (!document.contains(block)) {
        const data = activeClones.get(block);
        if (data) data.clone.remove();
        activeClones.delete(block);
        knownBlocks.delete(block);
        return;
      }

      const header = block.querySelector('.qwen-markdown-code-header');
      const actions = block.querySelector('.qwen-markdown-code-header-actions');
      if (!header || !actions) return;

      const bRect = block.getBoundingClientRect();
      const hRect = header.getBoundingClientRect();

      // Условие: заголовок ушел вверх, но низ блока еще виден
      const shouldShow = hRect.bottom < (CFG.top + 2) && bRect.bottom > 50;

      let data = activeClones.get(block);

      if (shouldShow) {
        if (!data) {
          const clone = actions.cloneNode(true);
          clone.className = 'sticky-buttons-clone';
          const origBtns = actions.querySelectorAll('.qwen-markdown-code-header-action-item');
          clone.querySelectorAll('.qwen-markdown-code-header-action-item').forEach((btn, i) => {
            btn.onclick = (e) => { e.stopPropagation(); origBtns[i]?.click(); };
          });
          document.body.appendChild(clone);
          requestAnimationFrame(() => clone.classList.add('visible'));
          data = { clone, actions };
          activeClones.set(block, data);
        }

        actions.classList.add('hidden-by-sticky');
        const h = data.clone.offsetHeight || 28;
        // Плавное прилипание к низу, когда блок уходит
        const top = Math.min(CFG.top, bRect.bottom - h - 10);
        data.clone.style.top = top + 'px';
        data.clone.style.right = (window.innerWidth - bRect.right + CFG.right) + 'px';
      } else if (data) {
        data.clone.classList.remove('visible');
        data.actions.classList.remove('hidden-by-sticky');
        setTimeout(() => {
          if (!data.clone.classList.contains('visible')) {
            data.clone.remove();
            activeClones.delete(block);
          }
        }, 100);
      }
    });
  };

  // Наблюдатель за появлением новых блоков (Discovery)
  const domObserver = new MutationObserver((mutations) => {
    mutations.forEach(m => m.addedNodes.forEach(node => {
      if (node.nodeType === 1) {
        const blocks = node.classList?.contains('qwen-markdown-code') ? [node] : node.querySelectorAll('.qwen-markdown-code');
        blocks.forEach(b => {
            knownBlocks.add(b);
            update(); // Проверяем сразу
        });
      }
    }));
  });

  // Скролл
  let frame;
  const onEvent = () => {
    if (!frame) {
      frame = requestAnimationFrame(() => {
        update();
        frame = null;
      });
    }
  };

  const init = () => {
    window.addEventListener('scroll', onEvent, { passive: true, capture: true });
    window.addEventListener('resize', onEvent, { passive: true });

    // Инициализируем уже имеющиеся на странице блоки
    document.querySelectorAll('.qwen-markdown-code').forEach(b => knownBlocks.add(b));

    domObserver.observe(document.body, { childList: true, subtree: true });
    update();
  };

  // Запуск
  if (document.readyState === 'complete') init();
  else window.addEventListener('load', init);
})();