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: 4px;
  38. margin-right: 4px;
  39. align-self: center;
  40. }
  41.  
  42. #model-switcher-button {
  43. display: inline-flex;
  44. align-items: center;
  45. justify-content: center;
  46. height: 36px;
  47. min-width: 36px;
  48. padding: 0 12px;
  49. border-radius: 9999px;
  50. border: 1px solid var(--token-border-light, #E5E5E5);
  51. font-size: 14px;
  52. font-weight: 500;
  53. color: var(--token-text-secondary, #666666);
  54. background-color: var(--token-main-surface-primary, #FFFFFF);
  55. cursor: pointer;
  56. white-space: nowrap;
  57. transition: background-color 0.2s ease;
  58. box-sizing: border-box;
  59. }
  60.  
  61. #model-switcher-button:hover {
  62. background-color: var(--token-main-surface-secondary, #F7F7F8);
  63. }
  64.  
  65. #model-switcher-dropdown {
  66. position: fixed;
  67. display: block;
  68. background-color: var(--token-main-surface-primary, white);
  69. border: 1px solid var(--token-border-medium, #E5E5E5);
  70. border-radius: 8px;
  71. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  72. z-index: 1050;
  73. min-width: 180px;
  74. overflow-y: auto;
  75. padding: 4px 0;
  76. }
  77.  
  78. .model-switcher-item {
  79. display: block;
  80. padding: 8px 16px;
  81. color: var(--token-text-primary, #171717);
  82. text-decoration: none;
  83. white-space: nowrap;
  84. cursor: pointer;
  85. font-size: 14px;
  86. }
  87.  
  88. .model-switcher-item:hover {
  89. background-color: var(--token-main-surface-secondary, #F7F7F8);
  90. }
  91.  
  92. .model-switcher-item.active {
  93. font-weight: bold;
  94. }
  95. `);
  96.  
  97. function getCurrentModelInfo() {
  98. const params = new URLSearchParams(window.location.search);
  99. const currentModelId = params.get('model');
  100. let currentDisplayName = "Select Model";
  101. let currentIndex = -1;
  102.  
  103. if (currentModelId) {
  104. const index = modelIds.indexOf(currentModelId);
  105. if (index !== -1) {
  106. currentIndex = index;
  107. currentDisplayName = modelDisplayNames[index];
  108. } else {
  109. currentDisplayName = `Model: ${currentModelId.substring(0, 10)}${currentModelId.length > 10 ? '...' : ''}`;
  110. currentIndex = -1;
  111. }
  112. } else {
  113. if (modelDisplayNames.length > 0) {
  114. currentDisplayName = modelDisplayNames[0];
  115. currentIndex = 0;
  116. }
  117. }
  118. return { currentId: currentModelId, displayName: currentDisplayName, index: currentIndex };
  119. }
  120.  
  121. function createModelSwitcher() {
  122. if (modelDisplayNames.length === 0) {
  123. console.warn("Model Switcher: modelMap is empty. Cannot create switcher.");
  124. return null;
  125. }
  126.  
  127. const container = document.createElement('div');
  128. container.className = 'model-switcher-container';
  129. container.id = 'model-switcher-container';
  130.  
  131. const button = document.createElement('button');
  132. button.id = 'model-switcher-button';
  133. button.type = 'button';
  134.  
  135. const dropdown = document.createElement('div');
  136. dropdown.className = 'model-switcher-dropdown';
  137. dropdown.id = 'model-switcher-dropdown';
  138.  
  139. const currentInfo = getCurrentModelInfo();
  140. button.textContent = currentInfo.displayName;
  141.  
  142. modelDisplayNames.forEach((name, index) => {
  143. const modelId = modelIds[index];
  144. const item = document.createElement('a');
  145. item.className = 'model-switcher-item';
  146. item.textContent = name;
  147. item.dataset.modelId = modelId;
  148. item.href = '#';
  149.  
  150. if ((currentInfo.currentId && currentInfo.currentId === modelId) || (!currentInfo.currentId && index === 0)) {
  151. item.classList.add('active');
  152. }
  153.  
  154. item.addEventListener('click', (e) => {
  155. e.preventDefault();
  156. e.stopPropagation();
  157. const selectedModelId = e.target.dataset.modelId;
  158. if (selectedModelId) {
  159. const url = new URL(window.location.href);
  160. url.searchParams.set('model', selectedModelId);
  161. if (getCurrentModelInfo().currentId !== selectedModelId) {
  162. window.location.href = url.toString();
  163. } else {
  164. hideDropdown();
  165. }
  166. } else {
  167. hideDropdown();
  168. }
  169. });
  170. dropdown.appendChild(item);
  171. });
  172.  
  173. button.addEventListener('click', (e) => {
  174. e.stopPropagation();
  175. toggleDropdown();
  176. });
  177.  
  178. container.appendChild(button);
  179. dropdownElement = dropdown;
  180.  
  181. return container;
  182. }
  183.  
  184. function showDropdown() {
  185. if (!dropdownElement || isDropdownVisible) return;
  186.  
  187. const button = document.getElementById('model-switcher-button');
  188. if (!button) return;
  189.  
  190. const buttonRect = button.getBoundingClientRect();
  191. const scrollX = window.scrollX || window.pageXOffset;
  192. const scrollY = window.scrollY || window.pageYOffset;
  193.  
  194. document.body.appendChild(dropdownElement);
  195. isDropdownVisible = true;
  196.  
  197. const dropdownHeight = dropdownElement.offsetHeight;
  198. const spaceAbove = buttonRect.top;
  199. const spaceBelow = window.innerHeight - buttonRect.bottom;
  200. const margin = 5;
  201.  
  202. let top, left = buttonRect.left + scrollX;
  203.  
  204. if (spaceBelow >= dropdownHeight + margin || spaceBelow >= spaceAbove) {
  205. top = buttonRect.bottom + scrollY + margin;
  206. } else {
  207. top = buttonRect.top + scrollY - dropdownHeight - margin;
  208. }
  209.  
  210. if (top < scrollY + margin) top = scrollY + margin;
  211. if (left < scrollX + margin) left = scrollX + margin;
  212.  
  213. const dropdownWidth = dropdownElement.offsetWidth;
  214. if (left + dropdownWidth > window.innerWidth + scrollX - margin) {
  215. left = window.innerWidth + scrollX - dropdownWidth - margin;
  216. }
  217.  
  218. dropdownElement.style.top = `${top}px`;
  219. dropdownElement.style.left = `${left}px`;
  220.  
  221. document.addEventListener('click', handleClickOutside, true);
  222. window.addEventListener('resize', hideDropdown);
  223. window.addEventListener('scroll', hideDropdown, true);
  224. }
  225.  
  226. function hideDropdown() {
  227. if (!dropdownElement || !isDropdownVisible) return;
  228.  
  229. if (dropdownElement.parentNode === document.body) {
  230. document.body.removeChild(dropdownElement);
  231. }
  232. isDropdownVisible = false;
  233.  
  234. document.removeEventListener('click', handleClickOutside, true);
  235. window.removeEventListener('resize', hideDropdown);
  236. window.removeEventListener('scroll', hideDropdown, true);
  237. }
  238.  
  239. function toggleDropdown() {
  240. if (isDropdownVisible) {
  241. hideDropdown();
  242. } else {
  243. showDropdown();
  244. }
  245. }
  246.  
  247. function handleClickOutside(event) {
  248. const button = document.getElementById('model-switcher-button');
  249. if (dropdownElement && dropdownElement.parentNode === document.body && button &&
  250. !button.contains(event.target) && !dropdownElement.contains(event.target)) {
  251. hideDropdown();
  252. }
  253. }
  254.  
  255. function insertSwitcherButton() {
  256. const existingContainer = document.getElementById('model-switcher-container');
  257.  
  258. if (existingContainer) {
  259. const button = document.getElementById('model-switcher-button');
  260. const currentInfo = getCurrentModelInfo();
  261.  
  262. if(button && button.textContent !== currentInfo.displayName) {
  263. button.textContent = currentInfo.displayName;
  264.  
  265. if (dropdownElement) {
  266. const items = dropdownElement.querySelectorAll('.model-switcher-item');
  267. items.forEach((item, index) => {
  268. item.classList.remove('active');
  269. const modelId = item.dataset.modelId;
  270. if ((currentInfo.currentId && currentInfo.currentId === modelId) || (!currentInfo.currentId && index === 0)) {
  271. item.classList.add('active');
  272. }
  273. });
  274. }
  275. }
  276. return true;
  277. }
  278.  
  279. const attachmentButton = document.querySelector('button[aria-label="Upload files and more"]');
  280. if (!attachmentButton) {
  281. return false;
  282. }
  283.  
  284. const targetContainer = attachmentButton.closest('div[style*="--vt-composer-attach-file-action"]');
  285. if (!targetContainer) {
  286. console.warn('Model Switcher: Could not find the target container (div with view-transition-name) for the attachment button.');
  287. const directParent = attachmentButton.parentElement;
  288. if (directParent && directParent.parentElement) {
  289. const switcherContainer = createModelSwitcher();
  290. if (!switcherContainer) return false;
  291. directParent.parentElement.insertBefore(switcherContainer, directParent.nextSibling);
  292. console.log('Model Switcher: Inserted after attachment button\'s direct parent (fallback).');
  293. return true;
  294. }
  295. console.error('Model Switcher: Fallback insertion also failed.');
  296. return false;
  297. }
  298.  
  299. const switcherContainer = createModelSwitcher();
  300. if (!switcherContainer) return false;
  301.  
  302. targetContainer.insertAdjacentElement('afterend', switcherContainer);
  303. console.log('Model Switcher: Button inserted after the attachment button container.');
  304. return true;
  305. }
  306.  
  307.  
  308. let insertionAttempted = false;
  309. const observer = new MutationObserver((mutationsList, obs) => {
  310. const targetButtonExists = document.querySelector('button[aria-label="Upload files and more"]');
  311.  
  312. if (targetButtonExists) {
  313. if (!document.getElementById('model-switcher-container')) {
  314. if (insertSwitcherButton()) {
  315. insertionAttempted = true;
  316. } else if (!insertionAttempted) {
  317. console.error('Model Switcher: Found attachment button, but failed to insert switcher next to it.');
  318. insertionAttempted = true;
  319. }
  320. } else {
  321. insertSwitcherButton();
  322. insertionAttempted = true;
  323. }
  324. }
  325.  
  326. if (insertionAttempted && !document.getElementById('model-switcher-container')) {
  327. console.log("Model Switcher: Button container removed (UI update?), resetting for re-insertion attempt...");
  328. insertionAttempted = false;
  329. hideDropdown();
  330. }
  331. });
  332.  
  333. observer.observe(document.body, {
  334. childList: true,
  335. subtree: true
  336. });
  337.  
  338. setTimeout(insertSwitcherButton, 1500);
  339.  
  340. })();