ChatGPT Zero

Enhancements for ChatGPT

  1. // ==UserScript==
  2. // @name ChatGPT Zero
  3. // @namespace https://github.com/NextDev65/
  4. // @version 0.5
  5. // @description Enhancements for ChatGPT
  6. // @author NextDev65
  7. // @homepageURL https://github.com/NextDev65/ChatGPT-0
  8. // @supportURL https://github.com/NextDev65/ChatGPT-0
  9. // @match https://chatgpt.com/*
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. // --- Configuration ---
  17. const PREFERRED_MODEL_KEY = 'preferredChatGPTModel';
  18. const SETTINGS_KEY = 'chatgptZeroSettings';
  19. const DEFAULT_MODEL = 'gpt-4o-mini';
  20. const MODELS = [
  21. 'gpt-3.5-turbo',
  22. 'text-davinci-002-render-sha',
  23. 'text-davinci-002-render-sha-mobile',
  24. 'gpt-4-mobile',
  25. 'gpt-4o-mini',
  26. 'gpt-4-1-mini',
  27. 'gpt-4o',
  28. 'o4-mini'
  29. ];
  30.  
  31. // Default settings
  32. const DEFAULT_SETTINGS = {
  33. modelSwitcher: true,
  34. streamerMode: true,
  35. animations: true
  36. };
  37.  
  38. // Load settings from localStorage
  39. function loadSettings() {
  40. try {
  41. const saved = localStorage.getItem(SETTINGS_KEY);
  42. return saved ? { ...DEFAULT_SETTINGS, ...JSON.parse(saved) } : { ...DEFAULT_SETTINGS };
  43. } catch (e) {
  44. console.warn('Failed to load settings, using defaults', e);
  45. return { ...DEFAULT_SETTINGS };
  46. }
  47. }
  48.  
  49. // Save settings to localStorage
  50. function saveSettings(settings) {
  51. try {
  52. localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
  53. } catch (e) {
  54. console.warn('Failed to save settings', e);
  55. }
  56. }
  57.  
  58. // Global settings object
  59. let settings = loadSettings();
  60.  
  61. /**
  62. * Creates a toggle switch element
  63. * @param {string} label - The label text for the toggle
  64. * @param {boolean} checked - Initial checked state
  65. * @param {Function} onChange - Callback when toggle changes
  66. * @returns {HTMLDivElement}
  67. */
  68. function createToggleSwitch(label, checked, onChange) {
  69. const container = document.createElement('div');
  70. container.className = 'toggle-container';
  71.  
  72. const labelElement = document.createElement('label');
  73. labelElement.className = 'toggle-label';
  74. labelElement.textContent = label;
  75.  
  76. const switchContainer = document.createElement('label');
  77. switchContainer.className = 'toggle-switch';
  78.  
  79. const input = document.createElement('input');
  80. input.type = 'checkbox';
  81. input.checked = checked;
  82. input.className = 'toggle-input';
  83. input.addEventListener('change', onChange);
  84.  
  85. const slider = document.createElement('span');
  86. slider.className = 'toggle-slider';
  87.  
  88. switchContainer.appendChild(input);
  89. switchContainer.appendChild(slider);
  90. container.appendChild(labelElement);
  91. container.appendChild(switchContainer);
  92.  
  93. return container;
  94. }
  95.  
  96. /**
  97. * Creates and returns a <button> element with an attached settings menu.
  98. * @returns {HTMLButtonElement}
  99. */
  100. function createSettingsMenu() {
  101. const cog = document.createElement('button');
  102. cog.id = 'settings-cog';
  103. //cog.textContent = settings.animations ? '⚙️' : '⚙';
  104. cog.setAttribute('aria-label', 'Settings');
  105.  
  106. const menu = document.createElement('div');
  107. menu.id = 'settings-menu';
  108. menu.className = 'settings-dropdown';
  109. menu.style.display = 'none';
  110.  
  111. // Create toggle switches
  112. const modelSwitcherToggle = createToggleSwitch('Model Switcher', settings.modelSwitcher, (e) => {
  113. settings.modelSwitcher = e.target.checked;
  114. saveSettings(settings);
  115. updateModelSwitcherVisibility();
  116. });
  117. const streamerModeToggle = createToggleSwitch(
  118. 'Streamer Mode',
  119. settings.streamerMode ?? true,
  120. (e) => {
  121. settings.streamerMode = e.target.checked;
  122. saveSettings(settings);
  123. updateStreamerModeStyles();
  124. }
  125. );
  126.  
  127. const animationsToggle = createToggleSwitch('Animations', settings.animations, (e) => {
  128. settings.animations = e.target.checked;
  129. saveSettings(settings);
  130. updateAnimationStyles();
  131. });
  132.  
  133. menu.appendChild(modelSwitcherToggle);
  134. menu.appendChild(streamerModeToggle);
  135. menu.appendChild(animationsToggle);
  136.  
  137. // Append menu to body to avoid positioning issues
  138. document.body.appendChild(menu);
  139.  
  140. // Toggle menu visibility
  141. cog.addEventListener('click', (e) => {
  142. e.stopPropagation();
  143. //const isVisible = window.getComputedStyle(menu).display !== 'none';
  144. if (menu.style.display === 'block')
  145. {
  146. menu.style.display = 'none';
  147. }
  148. else {
  149. positionMenu();
  150. menu.style.display = 'block';
  151. }
  152. });
  153.  
  154. // Close menu when clicking outside
  155. document.addEventListener('click', (e) => {
  156. if (!cog.contains(e.target) && !menu.contains(e.target)) {
  157. menu.style.display = 'none';
  158. }
  159. });
  160.  
  161. // Position menu relative to cog
  162. function positionMenu() {
  163. // cog bounds, changes when cog is rotated (animations enabled) -> alignment inconsistencies
  164. const cogRect = cog.getBoundingClientRect();
  165. // page header bounds
  166. const parentRect = cog.parentElement.getBoundingClientRect();
  167. const viewportWidth = window.innerWidth;
  168.  
  169. menu.style.position = 'fixed';
  170. menu.style.top = `${parentRect.bottom - 5}px`; // 5px above `page-header`
  171. menu.style.zIndex = '10000';
  172. const cogRight = cogRect.left + cogRect.width;
  173. const rightOffset = viewportWidth - cogRight;
  174.  
  175. // prepare initial state
  176. menu.style.right = `${rightOffset}px`;
  177. menu.style.left = 'auto';
  178. if (settings.animations) {
  179. menu.style.opacity = '0';
  180. menu.style.transform = 'translateX(10px)';
  181. menu.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
  182.  
  183. /*// force a reflow so the browser registers the start state
  184. // eslint-disable-next-line @microsoft/sdl/no-document-domain -- reflow hack
  185. void menu.offsetWidth;*/
  186.  
  187. // slide into place
  188. requestAnimationFrame(() => {
  189. menu.style.opacity = '1';
  190. menu.style.transform = 'translateX(0)';
  191. });
  192. }
  193. }
  194.  
  195. // Inject CSS for settings menu and toggle switches
  196. injectSettingsStyles();
  197.  
  198. return cog;
  199. }
  200.  
  201. /**
  202. * Injects CSS styles for the settings menu and components
  203. */
  204. function injectSettingsStyles() {
  205. if (document.getElementById('settings-styles')) return;
  206.  
  207. const style = document.createElement('style');
  208. style.id = 'settings-styles';
  209.  
  210. style.textContent = `
  211. #settings-cog {
  212. font-size: 20px;
  213. margin-left: 12px;
  214. padding: 4px 5px;
  215. border: none;
  216. border-radius: 50%;
  217. background-color: #212121;
  218. color: #fff;
  219. cursor: pointer;
  220. box-shadow: 0 0 0 0 rgba(33, 33, 33, 0) inset,
  221. 0 0 5px 0 rgba(33, 33, 33, 0);
  222. display: flex;
  223. align-items: center;
  224. justify-content: center;
  225. position: relative;
  226. transform: translateX(0.75px) translateY(-0.75px);
  227. ${settings.animations ? `
  228. transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
  229. box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  230. ` : ''}
  231. ${settings.animations ? `
  232. transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55);
  233. ` : ''}
  234. }
  235. #settings-cog:hover {
  236. background-color: #2f2f2f;
  237. box-shadow: 0 0 2.5px 0 rgba(255, 255, 255, 0) inset,
  238. 0 0 5px 0 rgba(255, 255, 255, 0.2);
  239. ${settings.animations ? `
  240. transform: translateX(0.75px) translateY(-0.75px) rotate(45deg);
  241. ` : `
  242. transform: translateX(0.75px) translateY(-0.75px);
  243. `}
  244. }
  245. #settings-cog:focus {
  246. outline: none;
  247. box-shadow: 0 0 2.5px 0 rgba(255, 255, 255, 0.5) inset,
  248. 0 0 5px 0 rgba(255, 255, 255, 0.5);
  249. }
  250.  
  251. #settings-cog::before {
  252. content: '${settings.animations ? '⚙️' : ''}';
  253. transform-origin: center;
  254. transform: translateX(0.75px) translateY(-0.75px);
  255. }
  256.  
  257. .settings-dropdown {
  258. display: none;
  259. background-color: #2a2a2a;
  260. border: 1px solid #444;
  261. border-radius: 8px;
  262. padding: 12px;
  263. min-width: 200px;
  264. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  265. }
  266.  
  267.  
  268. .toggle-container {
  269. display: flex;
  270. justify-content: space-between;
  271. align-items: center;
  272. margin-bottom: 12px;
  273. }
  274. .toggle-container:last-child {
  275. margin-bottom: 0;
  276. }
  277.  
  278. .toggle-label {
  279. color: #fff;
  280. font-size: 14px;
  281. }
  282.  
  283. .toggle-switch {
  284. position: relative;
  285. display: inline-block;
  286. width: 44px;
  287. height: 24px;
  288. }
  289.  
  290. .toggle-input {
  291. position: absolute;
  292. opacity: 0;
  293. width: 100%;
  294. height: 100%;
  295. cursor: pointer;
  296. z-index: 1;
  297. }
  298. .toggle-input:checked + .toggle-slider {
  299. background-color: #4CAF50;
  300. }
  301. .toggle-input:checked + .toggle-slider:before {
  302. transform: translateX(20px);
  303. }
  304. .toggle-input:checked + .toggle-slider:hover {
  305. background-color: #45a049;
  306. }
  307.  
  308. .toggle-slider {
  309. position: absolute;
  310. top: 0;
  311. left: 0;
  312. right: 0;
  313. bottom: 0;
  314. background-color: #555;
  315. border-radius: 24px;
  316. ${settings.animations ? `
  317. transition: background-color 0.3s cubic-bezier(0.68, -0.1, 0.27, 1.1),
  318. transform 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55);
  319. ` : ''}
  320. }
  321. .toggle-slider:before {
  322. content: "";
  323. position: absolute;
  324. height: 18px;
  325. width: 18px;
  326. left: 3px;
  327. bottom: 3px;
  328. background-color: white;
  329. border-radius: 50%;
  330. ${settings.animations ? `
  331. transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55);
  332. ` : ''}
  333. }
  334. `;
  335. document.head.appendChild(style);
  336. }
  337.  
  338. /**
  339. * Updates animation styles based on current settings
  340. */
  341. function updateAnimationStyles() {
  342. // Remove existing styles and re-inject with updated animation settings
  343. document.getElementById('settings-styles')?.remove();
  344. document.getElementById('model-switcher-styles')?.remove();
  345.  
  346. injectSettingsStyles();
  347. // Re-inject model switcher styles if it exists
  348. const modelSwitcher = document.getElementById('chatgpt-model-switcher');
  349. if (modelSwitcher) {
  350. injectModelSwitcherStyles();
  351. }
  352. if (settings.streamerMode) {
  353. updateStreamerModeStyles();
  354. }
  355. }
  356.  
  357. function updateStreamerModeStyles() {
  358. injectStreamerModeStyles();
  359. }
  360.  
  361. function injectStreamerModeStyles() {
  362. // Remove old rules
  363. document.getElementById('streamer-styles')?.remove();
  364.  
  365. if (!settings.streamerMode) return; // nothing to do if disabled
  366.  
  367. const style = document.createElement('style');
  368. style.id = 'streamer-styles';
  369.  
  370. style.textContent = `
  371. /* inactive chats */
  372. #history .__menu-item:not([data-active]) {
  373. box-shadow: 0 0 2.5px 0 rgba(255, 255, 255, 0) inset,
  374. 0 0 5px 0 rgba(255, 255, 255, 0.2);
  375. ${settings.animations ? `
  376. transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
  377. box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  378. ` : ''}
  379. }
  380. /* inactive chat titles */
  381. #history .__menu-item:not([data-active]) .truncate span {
  382. opacity: 0;
  383. ${settings.animations ? `
  384. transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
  385. box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  386. ` : ''}
  387. }
  388. #history .__menu-item:not([data-active]):hover .truncate span {
  389. opacity: 1;
  390. }
  391. `;
  392.  
  393. document.head.appendChild(style);
  394. }
  395.  
  396. /**
  397. * Updates model switcher visibility based on settings
  398. */
  399. function updateModelSwitcherVisibility() {
  400. const modelSwitcher = document.getElementById('chatgpt-model-switcher');
  401. if (modelSwitcher) {
  402. modelSwitcher.style.display = settings.modelSwitcher ? 'block' : 'none';
  403. }
  404. }
  405.  
  406. /**
  407. * Injects CSS styles for the model switcher
  408. */
  409. function injectModelSwitcherStyles() {
  410. if (document.getElementById('model-switcher-styles')) return;
  411.  
  412. const style = document.createElement('style');
  413. style.id = 'model-switcher-styles';
  414.  
  415. style.textContent = `
  416. #chatgpt-model-switcher {
  417. margin: auto;
  418. padding: 4px 8px;
  419. border: none;
  420. border-radius: 6px;
  421. background-color: #212121;
  422. color: #fff;
  423. outline: none;
  424. ${settings.animations ? `
  425. box-shadow: 0 0 0 0 rgba(33, 33, 33, 0) inset,
  426. 0 0 5px 0 rgba(33, 33, 33, 0);
  427. transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
  428. box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  429. ` : ''}
  430. }
  431. #chatgpt-model-switcher:hover {
  432. background-color: #2f2f2f;
  433. box-shadow: 0 0 2.5px 0 rgba(255, 255, 255, 0) inset,
  434. 0 0 5px 0 rgba(255, 255, 255, 0.2);
  435. }
  436. #chatgpt-model-switcher:focus {
  437. outline: none;
  438. box-shadow: 0 0 2.5px 0 rgba(255, 255, 255, 0.5) inset,
  439. 0 0 5px 0 rgba(255, 255, 255, 0.5);
  440. }
  441. `;
  442. document.head.appendChild(style);
  443. }
  444.  
  445. /**
  446. * Creates and returns a <select> element configured as the model switcher.
  447. * @param {string} currentModel - Model to pre-select in the dropdown.
  448. * @returns {HTMLSelectElement}
  449. */
  450. function createModelSwitcher(currentModel) {
  451. const select = document.createElement('select');
  452. select.id = 'chatgpt-model-switcher';
  453.  
  454. // Inject CSS for base styling, hover, focus, and transition effects
  455. injectModelSwitcherStyles();
  456.  
  457. // Populate dropdown with model options
  458. MODELS.forEach(model => {
  459. const option = document.createElement('option');
  460. option.value = model;
  461. option.textContent = model;
  462. if (model === currentModel) option.selected = true;
  463. select.appendChild(option);
  464. });
  465.  
  466. // Save selection to localStorage on change
  467. select.addEventListener('change', () => {
  468. localStorage.setItem(PREFERRED_MODEL_KEY, select.value);
  469. });
  470.  
  471. // Set initial visibility based on settings
  472. select.style.display = settings.modelSwitcher ? 'block' : 'none';
  473.  
  474. return select;
  475. }
  476.  
  477. /**
  478. * Finds our model switcher in the UI and inserts the settings cog after it.
  479. * Retries every second until our model switcher is visible.
  480. */
  481. function injectSettingsMenu() {
  482. const checkInterval = setInterval(() => {
  483. const modelSwitcher = document.getElementById('chatgpt-model-switcher');
  484. let cog = document.getElementById('settings-cog');
  485.  
  486. // Create cog if it doesn't exist yet
  487. if (!cog) {
  488. cog = createSettingsMenu();
  489. }
  490. // Insert cog after visible model switcher
  491. if (modelSwitcher && !cog.parentNode && modelSwitcher.parentNode) {
  492. modelSwitcher.parentNode.insertBefore(cog, modelSwitcher.nextSibling);
  493. }
  494. }, 1000);
  495. }
  496.  
  497. /**
  498. * Finds the native model switcher in the UI and inserts our custom switcher beside it.
  499. * Retries every second until the native element is visible.
  500. */
  501. function injectModelSwitcher() {
  502. const checkInterval = setInterval(() => {
  503. const nativeModelSwitchers = document.querySelectorAll('[data-testid="model-switcher-dropdown-button"]');
  504. let switcher = document.getElementById('chatgpt-model-switcher');
  505.  
  506. // Create switcher
  507. if (!switcher) {
  508. const savedModel = localStorage.getItem(PREFERRED_MODEL_KEY) || DEFAULT_MODEL;
  509. switcher = createModelSwitcher(savedModel);
  510. }
  511. // Insert switcher next to the first visible native button
  512. if (!switcher.parentNode) {
  513. for (let nativeModelSwitcher of nativeModelSwitchers) {
  514. if (nativeModelSwitcher.checkVisibility && nativeModelSwitcher.checkVisibility()) {
  515. nativeModelSwitcher.parentNode.after(switcher);
  516. break;
  517. }
  518. }
  519. }
  520. }, 1000);
  521. }
  522.  
  523. /**
  524. * Overrides window.fetch to intercept conversation requests and replace the model
  525. * property in the request body with the user-selected model.
  526. */
  527. function overrideModelInRequest() {
  528. // Only override if model switcher is enabled
  529. if (!settings.modelSwitcher) return;
  530.  
  531. const origFetch = window.fetch;
  532. window.fetch = async (...args) => {
  533. const [resource, config] = args;
  534. const savedModel = localStorage.getItem(PREFERRED_MODEL_KEY) || DEFAULT_MODEL;
  535.  
  536. // Target only conversation API calls
  537. if (
  538. typeof resource === 'string' &&
  539. resource.includes('/backend-api/f/conversation') &&
  540. config?.body
  541. ) {
  542. try {
  543. const body = JSON.parse(config.body);
  544. if (body && body.model) {
  545. // Overwrite model
  546. body.model = savedModel;
  547. config.body = JSON.stringify(body);
  548. }
  549. } catch (e) {
  550. console.warn('Model switcher failed to parse request body', e);
  551. }
  552. }
  553.  
  554. return origFetch(resource, config);
  555. };
  556. }
  557.  
  558. // Initialize the userscript
  559. injectModelSwitcher();
  560. overrideModelInRequest();
  561. injectSettingsMenu();
  562. updateStreamerModeStyles();
  563.  
  564. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址