Better Tencent YuanBao

Enhanced UI for Tencent YuanBao chat

// ==UserScript==
// @name         Better Tencent YuanBao
// @namespace    http://tampermonkey.net/
// @version      2025-06-06
// @description  Enhanced UI for Tencent YuanBao chat
// @author       AAur
// @match        https://yuanbao.tencent.com/chat/**
// @icon         https://www.google.com/s2/favicons?sz=64&domain=yuanbao.tencent.com
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ========== 通用工具函数 ==========
    const waitForElement = (selector) => {
        return new Promise(resolve => {
            const el = document.querySelector(selector);
            if (el) return resolve(el);

            const container = document.querySelector('.agent-chat__container') || document.body;
            const observer = new MutationObserver((_, obs) => {
                const target = document.querySelector(selector);
                if (target) {
                    obs.disconnect();
                    resolve(target);
                }
            });

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

    const debounce = (fn, delay) => {
        let timer;
        return (...args) => {
            clearTimeout(timer);
            timer = setTimeout(() => fn.apply(this, args), delay);
        };
    };

    const createStyle = (css) => {
        const style = document.createElement('style');
        style.textContent = css;
        document.head.appendChild(style);
        return style;
    };

    // ========== 功能1: 底栏收起按钮 ==========
    const initToggleButton = async () => {
        const inputBox = await waitForElement('.agent-dialogue__content--common__input.agent-chat__input-box');

        // 添加相关样式
        createStyle(`
            #inputToggleBtn {
                position: fixed;
                left: 50%;
                transform: translateX(-50%);
                z-index: 9999;
                padding: 2px 15px;
                background: #3db057;
                color: white;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                font-size: 14px;
                opacity: 0.7;
                transition: opacity 0.2s, top 0.2s;
            }
            #inputToggleBtn:hover {
                opacity: 1;
            }
            .hidden-input {
                display: none !important;
            }
        `);

        // 创建按钮
        const toggleBtn = document.createElement('button');
        toggleBtn.id = 'inputToggleBtn';
        toggleBtn.textContent = 'Hide';

        inputBox.parentNode.insertBefore(toggleBtn, inputBox);

        // 更新按钮位置函数
        const updateButtonPosition = () => {
            const rect = inputBox.getBoundingClientRect();
            const btnWidth = toggleBtn.offsetWidth;
            const leftPosition = rect.left + (rect.width - btnWidth) / 2;

            toggleBtn.style.left = `${leftPosition}px`;
            toggleBtn.style.top = `${rect.top + window.scrollY - 25}px`;
        };

        // 监听输入框大小变化
        const observeInputBoxChanges = () => {
            const resizeObserver = new ResizeObserver(debounce(() => {
                if (!inputBox.classList.contains('hidden-input')) {
                    updateButtonPosition();
                }
            }, 100));
            resizeObserver.observe(inputBox);
            return resizeObserver;
        };

        let boxObserver = observeInputBoxChanges();
        updateButtonPosition();

        // 按钮点击事件
        toggleBtn.addEventListener('click', () => {
            if (inputBox.classList.contains('hidden-input')) {
                inputBox.classList.remove('hidden-input');
                toggleBtn.textContent = 'Hide';
                boxObserver = observeInputBoxChanges();
                updateButtonPosition();
            } else {
                inputBox.classList.add('hidden-input');
                toggleBtn.textContent = 'Show';
                boxObserver.disconnect();
                toggleBtn.style.top = `${document.documentElement.scrollHeight - 40}px`;
            }
        });

        // 窗口大小调整时更新按钮位置
        window.addEventListener('resize', debounce(() => {
            if (inputBox.classList.contains('hidden-input')) {
                toggleBtn.style.top = `${document.documentElement.scrollHeight - 30}px`;
            } else {
                updateButtonPosition();
            }
        }, 100));
    };

    // ========== 功能2: 整理有序列表 ==========
    const initOlProcessor = () => {
        // 创建按钮样式
        createStyle(`
            #processOlBtn {
                position: fixed;
                bottom: 5px;
                right: 20px;
                z-index: 9999;
                padding: 5px 10px;
                background: #3db057;
                color: white;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                font-size: 12px;
                opacity: 0.8;
                transition: opacity 0.2s;
            }
            #processOlBtn:hover {
                opacity: 1;
            }
            .numbered-item {
                display: block;
                margin-bottom: 8px;
                line-height: 1.5;
                position: relative;
                padding-left: 1.5em;
            }
        `);

        // 创建并添加整理按钮
        const processOlBtn = document.createElement('button');
        processOlBtn.id = 'processOlBtn';
        processOlBtn.textContent = '整理OL';
        document.body.appendChild(processOlBtn);

        // 计算节点内字符数
        const countTextContent = (element) => {
            let count = 0;
            const walker = document.createTreeWalker(
                element,
                NodeFilter.SHOW_TEXT,
                null,
                false
            );

            while (walker.nextNode()) {
                count += walker.currentNode.textContent.trim().length;
            }
            return count;
        };

        // 转换单个OL元素
        const processOlElement = (ol) => {
            // Mark as processed to avoid duplicate processing
            ol.classList.add('processed-ol');

            // Get direct child LI elements only
            const lis = Array.from(ol.querySelectorAll(':scope > li'));
            const fragment = document.createDocumentFragment();

            lis.forEach((li, index) => {
                // Create a new div to replace the li
                const numberedDiv = document.createElement('div');
                numberedDiv.className = 'numbered-item';

                // Find the first text node inside the li (ignoring whitespace)
                function findFirstTextNode(element) {
                    // 遍历所有子节点
                    for (const child of element.childNodes) {
                        if (child.nodeType === Node.TEXT_NODE && child.textContent.trim() !== '') {
                            return child; // 找到目标文本节点
                        } else if (child.nodeType === Node.ELEMENT_NODE) {
                            const found = findFirstTextNode(child); // 递归搜索子元素
                            if (found) return found;
                        }
                    }
                    return null; // 未找到
                }

                const firstTextNode = findFirstTextNode(li);

                if (firstTextNode) {
                    // 在文本节点前插入序号(保留原文本)
                    firstTextNode.textContent = `${index + 1}. ${firstTextNode.textContent.trim()}`;
                } else {
                    // 如果没有文本节点,则在 <li> 开头插入序号
                    const textNode = document.createTextNode(`${index + 1}. `);
                    li.prepend(textNode);
                }

                // Move all of li's children to the new div
                while (li.firstChild) {
                    numberedDiv.appendChild(li.firstChild);
                }

                fragment.appendChild(numberedDiv);
            });

            // Replace the OL with our new structure
            ol.replaceWith(fragment);
        };

        // 处理所有OL元素
        const processAllOls = () => {
            const ols = document.querySelectorAll('ol:not(.processed-ol)');
            let processedCount = 0;

            ols.forEach(ol => {
                if (countTextContent(ol) > 200) {
                    processOlElement(ol);
                    processedCount++;
                }
            });

            // 显示处理结果
            if (processedCount > 0) {
                processOlBtn.textContent = `已整理${processedCount}个OL`;
                setTimeout(() => {
                    processOlBtn.textContent = '整理OL';
                }, 2000);
            } else {
                processOlBtn.textContent = '未发现需整理的OL';
                setTimeout(() => {
                    processOlBtn.textContent = '整理OL';
                }, 2000);
            }
        };

        // 观察内容变化并延迟处理
        const observeContentChanges = () => {
            const contentElement = document.querySelector('.agent-dialogue__content--common__content');
            if (!contentElement) return;

            let changeTimer;
            const observer = new MutationObserver((mutations) => {
                // 检查是否有实际内容变化
                const hasRelevantChange = mutations.some(mutation =>
                    mutation.type === 'childList' ||
                    (mutation.type === 'characterData' && mutation.target.textContent.trim())
                );

                if (hasRelevantChange) {
                    clearTimeout(changeTimer);
                    changeTimer = setTimeout(() => {
                        processAllOls();
                    }, 500);
                }
            });

            observer.observe(contentElement, {
                childList: true,
                subtree: true,
                characterData: true
            });

            return () => observer.disconnect();
        };

        // 初始化内容观察
        let cleanupObserver;
        const initObserver = () => {
            if (cleanupObserver) cleanupObserver();
            cleanupObserver = observeContentChanges();
        };

        // 延迟初始化观察器,确保元素已加载
        setTimeout(initObserver, 1000);

        // 按钮点击事件
        processOlBtn.addEventListener('click', processAllOls);

        // 返回initObserver以便外部调用
        return initObserver;
    };

    // ========== 主初始化函数 ==========
    const main = () => {
        const initOlObserver = initOlProcessor(); // 获取返回的initObserver函数
        Promise.all([
            initToggleButton(),
            initOlObserver(),
            initOlObserver && initOlObserver() // 如果initOlProcessor返回了initObserver就调用
        ]).catch(error => {
            console.error('Better Tencent YuanBao initialization error:', error);
        });
    };

    // ========== 启动脚本 ==========
    if ('requestIdleCallback' in window) {
        window.requestIdleCallback(main);
    } else {
        setTimeout(main, 500);
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址