Switch Bug Team Model

Bug Team,好用、爱用 ♥

// ==UserScript==
// @name         Switch Bug Team Model
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Bug Team,好用、爱用 ♥
// @author       wandouyu
// @match        *://chat.voct.dev/*
// @match        *://chatgpt.com/*
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    let dropdownElement = null;
    let isDropdownVisible = false;
    let insertionAttempted = false;
    const SCRIPT_PREFIX = "模型切换器:";
    const DEBOUNCE_DELAY = 300;
    
    const modelMap = {
        "o3 ": "o3",
        "o4-mini-high": "o4-mini-high",
        "o4-mini": "o4-mini",
        "gpt-4.5": "gpt-4-5",
        "gpt-4o": "gpt-4o",
        "gpt-4o-mini": "gpt-4o-mini",
        "gpt-4o (tasks)": "gpt-4o-jawbone",
        "gpt-4": "gpt-4"
    };
    const modelDisplayNames = Object.keys(modelMap);
    const modelIds = Object.values(modelMap);

    GM_addStyle(`
        .model-switcher-container {
            position: relative;
            display: inline-block;
            margin-left: 4px;
            margin-right: 4px;
            align-self: center;
        }

        #model-switcher-button {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            height: 36px;
            min-width: 36px;
            padding: 0 12px;
            border-radius: 9999px;
            border: 1px solid var(--token-border-light, #E5E5E5);
            font-size: 14px;
            font-weight: 500;
            color: var(--token-text-secondary, #666666);
            background-color: var(--token-main-surface-primary, #FFFFFF);
            cursor: pointer;
            white-space: nowrap;
            transition: background-color 0.2s ease;
            box-sizing: border-box;
        }

        #model-switcher-button:hover {
            background-color: var(--token-main-surface-secondary, #F7F7F8);
        }

        #model-switcher-dropdown {
            position: fixed;
            display: block;
            background-color: var(--token-main-surface-primary, white);
            border: 1px solid var(--token-border-medium, #E5E5E5);
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
            z-index: 1050;
            min-width: 180px;
            overflow-y: auto;
            padding: 4px 0;
        }

        .model-switcher-item {
            display: block;
            padding: 8px 16px;
            color: var(--token-text-primary, #171717);
            text-decoration: none;
            white-space: nowrap;
            cursor: pointer;
            font-size: 14px;
        }

        .model-switcher-item:hover {
            background-color: var(--token-main-surface-secondary, #F7F7F8);
        }

        .model-switcher-item.active {
            font-weight: bold;
        }
    `);

    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    function getCurrentModelInfo() {
        const params = new URLSearchParams(window.location.search);
        const currentModelIdFromUrl = params.get('model');

        let effectiveModelId = modelIds[0];
        let currentIndex = 0;

        if (currentModelIdFromUrl) {
            const foundIndex = modelIds.indexOf(currentModelIdFromUrl);
            if (foundIndex !== -1) {
                effectiveModelId = currentModelIdFromUrl;
                currentIndex = foundIndex;
            }
        }

        const currentDisplayName = modelDisplayNames[currentIndex];
        return { currentId: effectiveModelId, displayName: currentDisplayName, index: currentIndex };
    }

    function createModelSwitcher() {
        const container = document.createElement('div');
        container.className = 'model-switcher-container';
        container.id = 'model-switcher-container';

        const button = document.createElement('button');
        button.id = 'model-switcher-button';
        button.type = 'button';

        const dropdown = document.createElement('div');
        dropdown.className = 'model-switcher-dropdown';
        dropdown.id = 'model-switcher-dropdown';

        const initialModelInfo = getCurrentModelInfo();
        button.textContent = initialModelInfo.displayName;

        modelDisplayNames.forEach((name, index) => {
            const modelId = modelIds[index];
            const item = document.createElement('a');
            item.className = 'model-switcher-item';
            item.textContent = name;
            item.dataset.modelId = modelId;
            item.href = '#';

            if (initialModelInfo.currentId === modelId) {
                item.classList.add('active');
            }

            item.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();

                const selectedModelId = e.target.dataset.modelId;
                const currentModelIdNow = getCurrentModelInfo().currentId;

                if (currentModelIdNow !== selectedModelId) {
                    const url = new URL(window.location.href);
                    url.searchParams.set('model', selectedModelId);
                    console.log(`${SCRIPT_PREFIX} 切换目标: ${name} (${selectedModelId})`);
                    window.location.href = url.toString();
                    // hideDropdown();
                } else {
                    console.log(`${SCRIPT_PREFIX} 模型无需切换`);
                    hideDropdown();
                }
            });
            dropdown.appendChild(item);
        });

        button.addEventListener('click', (e) => {
            e.stopPropagation();
            toggleDropdown();
        });

        container.appendChild(button);
        dropdownElement = dropdown;
        return container;
    }

    function showDropdown() {
        if (!dropdownElement || isDropdownVisible) return;

        const button = document.getElementById('model-switcher-button');
        if (!button) {
            console.error(`${SCRIPT_PREFIX} 找不到添加的模型切换器按钮,无法定位下拉菜单。`);
            return;
        }

        if (!dropdownElement.parentElement) {
            document.body.appendChild(dropdownElement);
        }
        isDropdownVisible = true;

        const buttonRect = button.getBoundingClientRect();
        const dropdownHeight = dropdownElement.offsetHeight;
        const dropdownWidth = dropdownElement.offsetWidth;
        const spaceAbove = buttonRect.top;
        const spaceBelow = window.innerHeight - buttonRect.bottom;
        const margin = 5;
        let top, left = buttonRect.left;
        if (spaceBelow >= dropdownHeight + margin || spaceBelow >= spaceAbove) {
            top = buttonRect.bottom + margin;
        } else {
            top = buttonRect.top - dropdownHeight - margin;
        }
        top = Math.max(margin, Math.min(top, window.innerHeight - dropdownHeight - margin));
        left = Math.max(margin, Math.min(left, window.innerWidth - dropdownWidth - margin));
        dropdownElement.style.top = `${top}px`;
        dropdownElement.style.left = `${left}px`;

        document.addEventListener('click', handleClickOutside, true);
        window.addEventListener('resize', debouncedHideDropdown);
        window.addEventListener('scroll', debouncedHideDropdownOnScroll, true);
    }

    function hideDropdown() {
        if (!dropdownElement || !isDropdownVisible) return;
        if (dropdownElement.parentElement) {
            dropdownElement.remove();
        }
        isDropdownVisible = false;
        document.removeEventListener('click', handleClickOutside, true);
        window.removeEventListener('resize', debouncedHideDropdown);
        window.removeEventListener('scroll', debouncedHideDropdownOnScroll, true);
    }
    const debouncedHideDropdown = debounce(hideDropdown, 150);
    const debouncedHideDropdownOnScroll = debounce(hideDropdown, 150);

    function toggleDropdown() {
        if (isDropdownVisible) {
            hideDropdown();
        } else {
            showDropdown();
        }
    }

    function handleClickOutside(event) {
        const button = document.getElementById('model-switcher-button');
        if (dropdownElement && dropdownElement.parentNode && button &&
            !button.contains(event.target) && !dropdownElement.contains(event.target)) {
             hideDropdown();
        }
    }

    function updateSwitcherState() {
        const button = document.getElementById('model-switcher-button');
        if (!button) return;

        const currentInfo = getCurrentModelInfo();

        if (button.textContent !== currentInfo.displayName) {
            button.textContent = currentInfo.displayName;
            console.log(`${SCRIPT_PREFIX} 按钮文本更新为: ${currentInfo.displayName}`);
        }

        if (dropdownElement) {
            const items = dropdownElement.querySelectorAll('.model-switcher-item');
            let changed = false;
            items.forEach((item) => {
                const modelId = item.dataset.modelId;
                const shouldBeActive = (currentInfo.currentId === modelId);
                if (item.classList.contains('active') !== shouldBeActive) {
                    if (shouldBeActive) {
                        item.classList.add('active');
                    } else {
                        item.classList.remove('active');
                    }
                    changed = true;
                }
            });
            if (changed) {
                 console.log(`${SCRIPT_PREFIX} 下拉菜单激活状态已更新`);
            }
        }
    }

    function checkAndInsertSwitcher() {
        const composerSelector = 'form[data-type="unified-composer"]';
        const toolbarContainer = document.querySelector(composerSelector);

        if (!toolbarContainer) {
            const existingSwitcher = document.getElementById('model-switcher-container');
            if (existingSwitcher && !existingSwitcher.closest(composerSelector)) {
                console.log(`${SCRIPT_PREFIX} 切换器存在于预期容器之外,移除。`);
                existingSwitcher.remove();
                insertionAttempted = false;
                hideDropdown();
            }
            return;
        }

        const existingSwitcher = document.getElementById('model-switcher-container');
        const targetSelector = 'div[style*="--vt-composer-attach-file-action"]';
        const insertionTarget = toolbarContainer.querySelector(targetSelector);

        if (existingSwitcher) {
            if (!toolbarContainer.contains(existingSwitcher)) {
                 console.log(`${SCRIPT_PREFIX} 切换器存在但不在预期容器内,移动它...`);
                 if (insertionTarget) {
                     insertionTarget.insertAdjacentElement('afterend', existingSwitcher);
                 } else {
                     toolbarContainer.appendChild(existingSwitcher);
                     console.log(`${SCRIPT_PREFIX} 未找到附件按钮,切换器移动到工具栏末尾`);
                 }
            }
            updateSwitcherState();
            insertionAttempted = true;
        } else {
            if (insertionTarget) {
                console.log(`${SCRIPT_PREFIX} 找到插入点 (${targetSelector}),尝试插入切换器...`);
                const switcherContainer = createModelSwitcher();
                insertionTarget.insertAdjacentElement('afterend', switcherContainer);
                console.log(`${SCRIPT_PREFIX} 成功插入至附件上传按钮之后`);
                insertionAttempted = true;
            } else {
                 console.warn(`${SCRIPT_PREFIX} 在工具栏内未找到附件上传按钮 (${targetSelector}),尝试添加到工具栏末尾`);
                 const switcherContainer = createModelSwitcher();
                 toolbarContainer.appendChild(switcherContainer);
                 console.log(`${SCRIPT_PREFIX} 成功插入至工具栏末尾`);
                 insertionAttempted = true;
            }
        }

        const currentSwitcherExists = !!document.getElementById('model-switcher-container');
        if (insertionAttempted && !currentSwitcherExists) {
            console.log(`${SCRIPT_PREFIX} 切换器容器似乎已被移除, 将在下次检查时尝试重新插入`);
            insertionAttempted = false;
            hideDropdown();
        }
    }

    const debouncedCheckAndInsert = debounce(checkAndInsertSwitcher, DEBOUNCE_DELAY);

    const observer = new MutationObserver((mutationsList, obs) => {
        debouncedCheckAndInsert();
    });

    console.log(`${SCRIPT_PREFIX} 正在监控 DOM 变化`);
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    console.log(`${SCRIPT_PREFIX} 计划在 500ms 后进行首次检查`);
    setTimeout(debouncedCheckAndInsert, 500);

})();