Better Tencent YuanBao

Enhanced UI for Tencent YuanBao chat

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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);
    }
})();