Switch Bug Team Model

Bug Team —— 好用、爱用 ♥

As of 20/04/2025. See the latest version.

// ==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: 8px; 
        }

        #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;
            max-height: 300px;
            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);
                    window.location.href = url.toString(); 
                }
                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 (spaceAbove > dropdownHeight + margin || spaceAbove >= spaceBelow) {
            top = buttonRect.top + scrollY - dropdownHeight - margin;
        } else {
            top = buttonRect.bottom + scrollY + 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 findCommentNode(parentElement, commentText) {
        const iterator = document.createNodeIterator(parentElement, NodeFilter.SHOW_COMMENT);
        let currentNode;
        while (currentNode = iterator.nextNode()) {
            if (currentNode.nodeValue.trim() === commentText) {
                return currentNode;
            }
        }
        return null;
    }

    
    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 switcherContainer = createModelSwitcher(); 
        if (!switcherContainer) return false; 

        
        
        const toolbar = document.querySelector('.max-xs\\:gap-1.flex.items-center.gap-2.overflow-x-auto');
        if (toolbar) {
            const commentNode = findCommentNode(toolbar, 'Insert code here');
            if (commentNode && commentNode.parentNode) {
                commentNode.parentNode.insertBefore(switcherContainer, commentNode);
                console.log('Model Switcher: Button inserted before comment.');
                return true;
            }
        }

        
        
        
        const toolsButton = document.querySelector('button[aria-label="Use a tool"]');
        const toolsButtonWrapper = toolsButton?.closest('div[class*="relative"]'); 
        if (toolsButtonWrapper && toolsButtonWrapper.parentNode && toolbar && toolbar.contains(toolsButtonWrapper)) {
            toolsButtonWrapper.parentNode.insertBefore(switcherContainer, toolsButtonWrapper);
            console.warn('Model Switcher: Comment not found. Inserted button before potential Tools button container.');
            return true;
        }

        
        if (toolbar) {
            toolbar.appendChild(switcherContainer);
            console.warn('Model Switcher: Comment and specific Tools container not found. Appended button to toolbar.');
            return true;
        }

        
        
        const composerArea = document.querySelector('textarea[tabindex="0"]')?.parentNode?.parentNode; 
         if (composerArea) {
             
             
             
             console.warn('Model Switcher: Toolbar not found. Attempting insertion near composer (may fail).');
             
             
         }


        console.error('Model Switcher: Could not find a suitable insertion point for the button.');
        return false; 
    }

    

    let insertionAttempted = false;
    const observer = new MutationObserver((mutationsList, obs) => {
        
        const targetParentExists = document.querySelector('.max-xs\\:gap-1.flex.items-center.gap-2.overflow-x-auto') || 
                                   document.querySelector('button[aria-label="Use a tool"]')?.closest('div'); 

        
        if (targetParentExists) {
            
            if (!document.getElementById('model-switcher-container')) {
                 if (insertSwitcherButton()) {
                     insertionAttempted = true; 
                     console.log("Model Switcher: Button check/insertion successful.");
                 } else if (!insertionAttempted) {
                     
                     console.error('Model Switcher: Found toolbar area, but failed to insert button container.');
                     insertionAttempted = true; 
                 }
            } else {
                
                insertSwitcherButton(); 
                insertionAttempted = true; 
            }
        }

        
        if (insertionAttempted && !document.getElementById('model-switcher-container')) {
             console.log("Model Switcher: Button container removed by UI update, attempting re-insertion...");
             insertionAttempted = false; 
             hideDropdown(); 
             
             setTimeout(insertSwitcherButton, 200);
        }
    });

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

    
    setTimeout(insertSwitcherButton, 1500); 

})();