Switch Bug Team Model

Bug Team —— 好用、爱用 ♥

目前為 2025-04-20 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

(function() {
    'use strict';

    const modelMap = {
        "o3 ": "o3",
        "o4-mini-high": "o4-mini-high",
        "o4-mini": "o4-mini",
        "gpt-4.5 (preview)": "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);

    let dropdownElement = null;
    let isDropdownVisible = false;

    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 getCurrentModelInfo() {
        const params = new URLSearchParams(window.location.search);
        const currentModelId = params.get('model');
        let currentDisplayName = "Select Model";
        let currentIndex = -1;

        if (currentModelId) {
            const index = modelIds.indexOf(currentModelId);
            if (index !== -1) {
                currentIndex = index;
                currentDisplayName = modelDisplayNames[index];
            } else {
                currentDisplayName = `Model: ${currentModelId.substring(0, 10)}${currentModelId.length > 10 ? '...' : ''}`;
                currentIndex = -1;
            }
        } else {
             if (modelDisplayNames.length > 0) {
                 currentDisplayName = modelDisplayNames[0];
                 currentIndex = 0;
             }
        }
        return { currentId: currentModelId, displayName: currentDisplayName, index: currentIndex };
    }

    function createModelSwitcher() {
        if (modelDisplayNames.length === 0) {
            console.warn("Model Switcher: modelMap is empty. Cannot create switcher.");
            return null;
        }

        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 currentInfo = getCurrentModelInfo();
        button.textContent = currentInfo.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 ((currentInfo.currentId && currentInfo.currentId === modelId) || (!currentInfo.currentId && index === 0)) {
                 item.classList.add('active');
            }

            item.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                const selectedModelId = e.target.dataset.modelId;
                if (selectedModelId) {
                    const url = new URL(window.location.href);
                    url.searchParams.set('model', selectedModelId);
                    if (getCurrentModelInfo().currentId !== selectedModelId) {
                        window.location.href = url.toString();
                    } else {
                         hideDropdown();
                    }
                } else {
                    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) return;

        const buttonRect = button.getBoundingClientRect();
        const scrollX = window.scrollX || window.pageXOffset;
        const scrollY = window.scrollY || window.pageYOffset;

        document.body.appendChild(dropdownElement);
        isDropdownVisible = true;

        const dropdownHeight = dropdownElement.offsetHeight;
        const spaceAbove = buttonRect.top;
        const spaceBelow = window.innerHeight - buttonRect.bottom;
        const margin = 5;

        let top, left = buttonRect.left + scrollX;

        if (spaceBelow >= dropdownHeight + margin || spaceBelow >= spaceAbove) {
            top = buttonRect.bottom + scrollY + margin;
        } else {
            top = buttonRect.top + scrollY - dropdownHeight - margin;
        }

        if (top < scrollY + margin) top = scrollY + margin;
        if (left < scrollX + margin) left = scrollX + margin;

        const dropdownWidth = dropdownElement.offsetWidth;
        if (left + dropdownWidth > window.innerWidth + scrollX - margin) {
            left = window.innerWidth + scrollX - dropdownWidth - margin;
        }

        dropdownElement.style.top = `${top}px`;
        dropdownElement.style.left = `${left}px`;

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

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

        if (dropdownElement.parentNode === document.body) {
            document.body.removeChild(dropdownElement);
        }
        isDropdownVisible = false;

        document.removeEventListener('click', handleClickOutside, true);
        window.removeEventListener('resize', hideDropdown);
        window.removeEventListener('scroll', hideDropdown, true);
    }

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

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

    function insertSwitcherButton() {
        const existingContainer = document.getElementById('model-switcher-container');

        if (existingContainer) {
            const button = document.getElementById('model-switcher-button');
            const currentInfo = getCurrentModelInfo();

            if(button && button.textContent !== currentInfo.displayName) {
                button.textContent = currentInfo.displayName;

                if (dropdownElement) {
                    const items = dropdownElement.querySelectorAll('.model-switcher-item');
                    items.forEach((item, index) => {
                        item.classList.remove('active');
                        const modelId = item.dataset.modelId;
                        if ((currentInfo.currentId && currentInfo.currentId === modelId) || (!currentInfo.currentId && index === 0)) {
                            item.classList.add('active');
                        }
                    });
                }
            }
            return true;
        }

        const attachmentButton = document.querySelector('button[aria-label="Upload files and more"]');
        if (!attachmentButton) {
            return false;
        }

        const targetContainer = attachmentButton.closest('div[style*="--vt-composer-attach-file-action"]');
        if (!targetContainer) {
            console.warn('Model Switcher: Could not find the target container (div with view-transition-name) for the attachment button.');
            const directParent = attachmentButton.parentElement;
            if (directParent && directParent.parentElement) {
                 const switcherContainer = createModelSwitcher();
                 if (!switcherContainer) return false;
                 directParent.parentElement.insertBefore(switcherContainer, directParent.nextSibling);
                 console.log('Model Switcher: Inserted after attachment button\'s direct parent (fallback).');
                 return true;
            }
            console.error('Model Switcher: Fallback insertion also failed.');
            return false;
        }

        const switcherContainer = createModelSwitcher();
        if (!switcherContainer) return false;

        targetContainer.insertAdjacentElement('afterend', switcherContainer);
        console.log('Model Switcher: Button inserted after the attachment button container.');
        return true;
    }


    let insertionAttempted = false;
    const observer = new MutationObserver((mutationsList, obs) => {
        const targetButtonExists = document.querySelector('button[aria-label="Upload files and more"]');

        if (targetButtonExists) {
            if (!document.getElementById('model-switcher-container')) {
                 if (insertSwitcherButton()) {
                     insertionAttempted = true;
                 } else if (!insertionAttempted) {
                     console.error('Model Switcher: Found attachment button, but failed to insert switcher next to it.');
                     insertionAttempted = true;
                 }
            } else {
                insertSwitcherButton();
                insertionAttempted = true;
            }
        }

        if (insertionAttempted && !document.getElementById('model-switcher-container')) {
             console.log("Model Switcher: Button container removed (UI update?), resetting for re-insertion attempt...");
             insertionAttempted = false;
             hideDropdown();
        }
    });

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

    setTimeout(insertSwitcherButton, 1500);

})();