Switch Bug Team Model

Bug Team —— 好用、爱用 ♥

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

  1. // ==UserScript==
  2. // @name Switch Bug Team Model
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Bug Team —— 好用、爱用 ♥
  6. // @author wandouyu
  7. // @match *://chatgpt.com/*
  8. // @match *://chat.openai.com/*
  9. // @match *://chat.voct.dev/*
  10. // @grant GM_addStyle
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. const modelMap = {
  18. "o3 ": "o3",
  19. "o4-mini-high": "o4-mini-high",
  20. "o4-mini": "o4-mini",
  21. "gpt-4.5 (preview)": "gpt-4-5",
  22. "gpt-4o": "gpt-4o",
  23. "gpt-4o-mini": "gpt-4o-mini",
  24. "gpt-4o (tasks)": "gpt-4o-jawbone",
  25. "gpt-4": "gpt-4"
  26. };
  27. const modelDisplayNames = Object.keys(modelMap);
  28. const modelIds = Object.values(modelMap);
  29.  
  30. let dropdownElement = null;
  31. let isDropdownVisible = false;
  32.  
  33. GM_addStyle(`
  34. .model-switcher-container {
  35. position: relative;
  36. display: inline-block;
  37. margin-left: 8px;
  38. }
  39.  
  40. #model-switcher-button {
  41. display: inline-flex;
  42. align-items: center;
  43. justify-content: center;
  44. height: 36px;
  45. min-width: 36px;
  46. padding: 0 12px;
  47. border-radius: 9999px;
  48. border: 1px solid var(--token-border-light, #E5E5E5);
  49. font-size: 14px;
  50. font-weight: 500;
  51. color: var(--token-text-secondary, #666666);
  52. background-color: var(--token-main-surface-primary, #FFFFFF);
  53. cursor: pointer;
  54. white-space: nowrap;
  55. transition: background-color 0.2s ease;
  56. box-sizing: border-box;
  57. }
  58.  
  59. #model-switcher-button:hover {
  60. background-color: var(--token-main-surface-secondary, #F7F7F8);
  61. }
  62.  
  63. #model-switcher-dropdown {
  64. position: fixed;
  65. display: block;
  66. background-color: var(--token-main-surface-primary, white);
  67. border: 1px solid var(--token-border-medium, #E5E5E5);
  68. border-radius: 8px;
  69. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  70. z-index: 1050;
  71. min-width: 180px;
  72. max-height: 300px;
  73. overflow-y: auto;
  74. padding: 4px 0;
  75. }
  76.  
  77. .model-switcher-item {
  78. display: block;
  79. padding: 8px 16px;
  80. color: var(--token-text-primary, #171717);
  81. text-decoration: none;
  82. white-space: nowrap;
  83. cursor: pointer;
  84. font-size: 14px;
  85. }
  86.  
  87. .model-switcher-item:hover {
  88. background-color: var(--token-main-surface-secondary, #F7F7F8);
  89. }
  90.  
  91. .model-switcher-item.active {
  92. font-weight: bold;
  93. }
  94. `);
  95.  
  96. function getCurrentModelInfo() {
  97. const params = new URLSearchParams(window.location.search);
  98. const currentModelId = params.get('model');
  99. let currentDisplayName = "Select Model";
  100. let currentIndex = -1;
  101.  
  102. if (currentModelId) {
  103. const index = modelIds.indexOf(currentModelId);
  104. if (index !== -1) {
  105. currentIndex = index;
  106. currentDisplayName = modelDisplayNames[index];
  107. } else {
  108. currentDisplayName = `Model: ${currentModelId.substring(0, 10)}${currentModelId.length > 10 ? '...' : ''}`;
  109. currentIndex = -1;
  110. }
  111. } else {
  112. if (modelDisplayNames.length > 0) {
  113. currentDisplayName = modelDisplayNames[0];
  114. currentIndex = 0;
  115. }
  116. }
  117. return { currentId: currentModelId, displayName: currentDisplayName, index: currentIndex };
  118. }
  119.  
  120. function createModelSwitcher() {
  121. if (modelDisplayNames.length === 0) {
  122. console.warn("Model Switcher: modelMap is empty. Cannot create switcher.");
  123. return null;
  124. }
  125.  
  126. const container = document.createElement('div');
  127. container.className = 'model-switcher-container';
  128. container.id = 'model-switcher-container';
  129.  
  130. const button = document.createElement('button');
  131. button.id = 'model-switcher-button';
  132. button.type = 'button';
  133.  
  134. const dropdown = document.createElement('div');
  135. dropdown.className = 'model-switcher-dropdown';
  136. dropdown.id = 'model-switcher-dropdown';
  137.  
  138. const currentInfo = getCurrentModelInfo();
  139. button.textContent = currentInfo.displayName;
  140.  
  141. modelDisplayNames.forEach((name, index) => {
  142. const modelId = modelIds[index];
  143. const item = document.createElement('a');
  144. item.className = 'model-switcher-item';
  145. item.textContent = name;
  146. item.dataset.modelId = modelId;
  147. item.href = '#';
  148.  
  149. if ((currentInfo.currentId && currentInfo.currentId === modelId) || (!currentInfo.currentId && index === 0)) {
  150. item.classList.add('active');
  151. }
  152.  
  153. item.addEventListener('click', (e) => {
  154. e.preventDefault();
  155. e.stopPropagation();
  156. const selectedModelId = e.target.dataset.modelId;
  157. if (selectedModelId) {
  158. const url = new URL(window.location.href);
  159. url.searchParams.set('model', selectedModelId);
  160. window.location.href = url.toString();
  161. }
  162. hideDropdown();
  163. });
  164. dropdown.appendChild(item);
  165. });
  166.  
  167. button.addEventListener('click', (e) => {
  168. e.stopPropagation();
  169. toggleDropdown();
  170. });
  171.  
  172. container.appendChild(button);
  173. dropdownElement = dropdown;
  174.  
  175. return container;
  176. }
  177.  
  178. function showDropdown() {
  179. if (!dropdownElement || isDropdownVisible) return;
  180.  
  181. const button = document.getElementById('model-switcher-button');
  182. if (!button) return;
  183.  
  184. const buttonRect = button.getBoundingClientRect();
  185. const scrollX = window.scrollX || window.pageXOffset;
  186. const scrollY = window.scrollY || window.pageYOffset;
  187.  
  188. document.body.appendChild(dropdownElement);
  189. isDropdownVisible = true;
  190.  
  191. const dropdownHeight = dropdownElement.offsetHeight;
  192. const spaceAbove = buttonRect.top;
  193. const spaceBelow = window.innerHeight - buttonRect.bottom;
  194. const margin = 5;
  195.  
  196. let top, left = buttonRect.left + scrollX;
  197.  
  198. if (spaceAbove > dropdownHeight + margin || spaceAbove >= spaceBelow) {
  199. top = buttonRect.top + scrollY - dropdownHeight - margin;
  200. } else {
  201. top = buttonRect.bottom + scrollY + margin;
  202. }
  203.  
  204. if (top < scrollY + margin) top = scrollY + margin;
  205. if (left < scrollX + margin) left = scrollX + margin;
  206. const dropdownWidth = dropdownElement.offsetWidth;
  207. if (left + dropdownWidth > window.innerWidth + scrollX - margin) {
  208. left = window.innerWidth + scrollX - dropdownWidth - margin;
  209. }
  210.  
  211.  
  212. dropdownElement.style.top = `${top}px`;
  213. dropdownElement.style.left = `${left}px`;
  214.  
  215. document.addEventListener('click', handleClickOutside, true);
  216. window.addEventListener('resize', hideDropdown);
  217. window.addEventListener('scroll', hideDropdown, true);
  218.  
  219. }
  220.  
  221. function hideDropdown() {
  222. if (!dropdownElement || !isDropdownVisible) return;
  223.  
  224. if (dropdownElement.parentNode === document.body) {
  225. document.body.removeChild(dropdownElement);
  226. }
  227. isDropdownVisible = false;
  228.  
  229. document.removeEventListener('click', handleClickOutside, true);
  230. window.removeEventListener('resize', hideDropdown);
  231. window.removeEventListener('scroll', hideDropdown, true);
  232.  
  233. }
  234.  
  235. function toggleDropdown() {
  236. if (isDropdownVisible) {
  237. hideDropdown();
  238. } else {
  239. showDropdown();
  240. }
  241. }
  242.  
  243. function handleClickOutside(event) {
  244. const button = document.getElementById('model-switcher-button');
  245. if (dropdownElement && dropdownElement.parentNode === document.body && button && !button.contains(event.target) && !dropdownElement.contains(event.target)) {
  246. hideDropdown();
  247. }
  248. }
  249.  
  250.  
  251. function findCommentNode(parentElement, commentText) {
  252. const iterator = document.createNodeIterator(parentElement, NodeFilter.SHOW_COMMENT);
  253. let currentNode;
  254. while (currentNode = iterator.nextNode()) {
  255. if (currentNode.nodeValue.trim() === commentText) {
  256. return currentNode;
  257. }
  258. }
  259. return null;
  260. }
  261.  
  262. function insertSwitcherButton() {
  263. const existingContainer = document.getElementById('model-switcher-container');
  264.  
  265. if (existingContainer) {
  266. const button = document.getElementById('model-switcher-button');
  267. const currentInfo = getCurrentModelInfo();
  268. if(button && button.textContent !== currentInfo.displayName) {
  269. button.textContent = currentInfo.displayName;
  270.  
  271. if (dropdownElement) {
  272. const items = dropdownElement.querySelectorAll('.model-switcher-item');
  273. items.forEach((item, index) => {
  274. item.classList.remove('active');
  275. const modelId = item.dataset.modelId;
  276. if ((currentInfo.currentId && currentInfo.currentId === modelId) || (!currentInfo.currentId && index === 0)) {
  277. item.classList.add('active');
  278. }
  279. });
  280. }
  281. }
  282. return true;
  283. }
  284.  
  285. const switcherContainer = createModelSwitcher();
  286. if (!switcherContainer) return false;
  287.  
  288. const toolbar = document.querySelector('.max-xs\\:gap-1.flex.items-center.gap-2.overflow-x-auto');
  289. if (toolbar) {
  290. const commentNode = findCommentNode(toolbar, 'Insert code here');
  291. if (commentNode && commentNode.parentNode) {
  292. commentNode.parentNode.insertBefore(switcherContainer, commentNode);
  293. console.log('Model Switcher: Button inserted before comment.');
  294. return true;
  295. }
  296. }
  297.  
  298. const toolsButton = document.querySelector('button[aria-label="Use a tool"]');
  299. const toolsButtonWrapper = toolsButton?.closest('div[class*="relative"]');
  300. if (toolsButtonWrapper && toolsButtonWrapper.parentNode && toolbar && toolbar.contains(toolsButtonWrapper)) {
  301. toolsButtonWrapper.parentNode.insertBefore(switcherContainer, toolsButtonWrapper);
  302. console.warn('Model Switcher: Comment not found. Inserted button before potential Tools button container.');
  303. return true;
  304. }
  305.  
  306. if (toolbar) {
  307. toolbar.appendChild(switcherContainer);
  308. console.warn('Model Switcher: Comment and specific Tools container not found. Appended button to toolbar.');
  309. return true;
  310. }
  311.  
  312. const composerArea = document.querySelector('textarea[tabindex="0"]')?.parentNode?.parentNode;
  313. if (composerArea) {
  314. console.warn('Model Switcher: Toolbar not found. Attempting insertion near composer (may fail).');
  315. }
  316.  
  317.  
  318. console.error('Model Switcher: Could not find a suitable insertion point for the button.');
  319. return false;
  320. }
  321.  
  322.  
  323. let insertionAttempted = false;
  324. const observer = new MutationObserver((mutationsList, obs) => {
  325. const targetParentExists = document.querySelector('.max-xs\\:gap-1.flex.items-center.gap-2.overflow-x-auto') ||
  326. document.querySelector('button[aria-label="Use a tool"]')?.closest('div');
  327.  
  328. if (targetParentExists) {
  329. if (!document.getElementById('model-switcher-container')) {
  330. if (insertSwitcherButton()) {
  331. insertionAttempted = true;
  332. console.log("Model Switcher: Button check/insertion successful.");
  333. } else if (!insertionAttempted) {
  334. console.error('Model Switcher: Found toolbar area, but failed to insert button container.');
  335. insertionAttempted = true;
  336. }
  337. } else {
  338. insertSwitcherButton();
  339. insertionAttempted = true;
  340. }
  341. }
  342.  
  343. if (insertionAttempted && !document.getElementById('model-switcher-container')) {
  344. console.log("Model Switcher: Button container removed by UI update, attempting re-insertion...");
  345. insertionAttempted = false;
  346. hideDropdown();
  347. setTimeout(insertSwitcherButton, 200);
  348. }
  349. });
  350.  
  351. observer.observe(document.body, {
  352. childList: true,
  353. subtree: true
  354. });
  355.  
  356. setTimeout(insertSwitcherButton, 1500);
  357.  
  358. })();