YouMind Chat Navigation

添加 "上一问" / "下一问" 悬浮按钮,方便在长对话中快速定位 (兼容单开/分屏/SPA)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         YouMind Chat Navigation
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  添加 "上一问" / "下一问" 悬浮按钮,方便在长对话中快速定位 (兼容单开/分屏/SPA)
// @author       YouMind User
// @match        https://youmind.com/*
// @match        https://*.youmind.com/*
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    // 配置:选择器
    const SELECTORS = {
        // 用户提问的容器类名
        USER_MSG: '.ym-ask-user-content',
        // 核心滚动容器类名
        SCROLL_CONTAINER_CLASS: 'overflow-y-scroll'
    };

    // 图标 (SVG)
    const ICONS = {
        UP: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>`,
        DOWN: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>`
    };

    const css = `
        /* 导航控制器容器 */
        #ym-chat-nav {
            position: absolute; 
            bottom: 20px;       /* 距离底部仅 20px (因为注入到不含输入框的容器中) */
            right: 20px;        /* 右下角 */
            display: flex;
            flex-direction: column;
            gap: 6px;
            z-index: 50;
            opacity: 0.3;
            transition: opacity 0.3s ease;
            pointer-events: auto;
        }

        #ym-chat-nav:hover {
            opacity: 1;
        }

        .ym-nav-btn {
            width: 32px;
            height: 32px;
            background: #ffffff;
            border: 1px solid #e5e7eb;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
            color: #6b7280;
            transition: all 0.2s ease;
            user-select: none;
        }

        .ym-nav-btn:hover {
            background: #f3f4f6;
            color: #111827;
            transform: translateY(-1px);
        }

        .ym-nav-btn:active {
            transform: translateY(0);
        }
    `;

    // 注入样式
    const style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    // 核心函数:寻找并注入
    function checkAndInject() {
        if (document.getElementById('ym-chat-nav')) return; // 已存在

        // 策略:从 .ym-ask-user-content 向上找最稳定的挂载点
        // 1. 找到滚动容器 (.overflow-y-scroll)
        // 2. 注入到 滚动容器的 父级 (这个父级通常是 flex-1, 高度 = 此容器 + 0,不含 input)

        let scrollContainer = document.querySelector('.' + SELECTORS.SCROLL_CONTAINER_CLASS);

        // 如果找不到通用的 scroll class,尝试通过内容反向查找
        if (!scrollContainer) {
            const userMsg = document.querySelector(SELECTORS.USER_MSG);
            if (userMsg) {
                let curr = userMsg.parentElement;
                while (curr && curr !== document.body) {
                    const overflow = window.getComputedStyle(curr).overflowY;
                    if (overflow === 'auto' || overflow === 'scroll') {
                        scrollContainer = curr;
                        break;
                    }
                    curr = curr.parentElement;
                }
            }
        }

        if (!scrollContainer) return; // 还没加载出来

        // 找到了滚动容器,现在找它的父级作为注入点
        const targetContainer = scrollContainer.parentElement;
        if (!targetContainer) return;

        // 确保父级有定位上下文
        const style = window.getComputedStyle(targetContainer);
        if (style.position === 'static') {
            targetContainer.style.position = 'relative';
        }

        const nav = document.createElement('div');
        nav.id = 'ym-chat-nav';

        const prevBtn = document.createElement('div');
        prevBtn.className = 'ym-nav-btn';
        prevBtn.innerHTML = ICONS.UP;
        prevBtn.title = '上一问';
        prevBtn.onclick = (e) => { e.stopPropagation(); scrollToMessage('prev', scrollContainer); };

        const nextBtn = document.createElement('div');
        nextBtn.className = 'ym-nav-btn';
        nextBtn.innerHTML = ICONS.DOWN;
        nextBtn.title = '下一问';
        nextBtn.onclick = (e) => { e.stopPropagation(); scrollToMessage('next', scrollContainer); };

        nav.appendChild(prevBtn);
        nav.appendChild(nextBtn);
        targetContainer.appendChild(nav);
    }

    // 滚动逻辑
    // 滚动逻辑
    function scrollToMessage(direction, scrollContainer) {
        const messages = Array.from(scrollContainer.querySelectorAll(SELECTORS.USER_MSG));
        if (messages.length === 0) return;

        // 使用 scrollTop 计算更精准,不受父级 offset 影响
        const currentScroll = scrollContainer.scrollTop;

        // 我们需要找到每个 message 相对于 content 流顶部的位置
        const containerRect = scrollContainer.getBoundingClientRect();

        let targetEl = null;

        if (direction === 'prev') {
            // 逆序找第一个位置在当前视口之上的
            for (let i = messages.length - 1; i >= 0; i--) {
                const rect = messages[i].getBoundingClientRect();
                // rect.top 是元素顶部距视窗顶部的距离
                // containerRect.top 是容器顶部距视窗顶部的距离
                // diff < -50 说明元素头部已经滚出去了
                if (rect.top - containerRect.top < -50) {
                    targetEl = messages[i];
                    break;
                }
            }
            if (!targetEl && currentScroll > 50) targetEl = messages[0];
        } else {
            // 正序找第一个位置在当前视口之下的
            for (let i = 0; i < messages.length; i++) {
                const rect = messages[i].getBoundingClientRect();
                // diff > 100 说明元素在下面
                if (rect.top - containerRect.top > 100) {
                    targetEl = messages[i];
                    break;
                }
            }

            // 如果没有找到下一个目标 (说明后面没有提问了),且当前已经在看最后一个提问或其回答
            if (!targetEl) {
                // 滚到底部
                scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior: 'smooth' });
                return;
            }
        }

        if (targetEl) {
            targetEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
        }
    }

    // 强力监听:使用 MutationObserver 应对所有 SPA 页面切换
    const observer = new MutationObserver((mutations) => {
        // 简单粗暴:只要 DOM 变了且按钮不存在,就尝试注入
        if (!document.getElementById('ym-chat-nav')) {
            checkAndInject();
        }
    });

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

    // 初始尝试
    checkAndInject();

    // 键盘快捷键绑定: PageUp / PageDown
    document.addEventListener('keydown', (e) => {
        // 确保不是在输入框中按键 (避免干扰打字)
        const activeTag = document.activeElement ? document.activeElement.tagName.toLowerCase() : '';
        if (activeTag === 'input' || activeTag === 'textarea' || document.activeElement.isContentEditable) {
            return;
        }

        if (e.key === 'PageUp') {
            const btn = document.querySelector('#ym-chat-nav .ym-nav-btn[title="上一问"]');
            if (btn && btn.offsetParent !== null) { // 存在且可见
                e.preventDefault();
                btn.click();
            }
        } else if (e.key === 'PageDown') {
            const btn = document.querySelector('#ym-chat-nav .ym-nav-btn[title="下一问"]');
            if (btn && btn.offsetParent !== null) { // 存在且可见
                e.preventDefault();
                btn.click();
            }
        }
    });

})();