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