ChatGPT Thinking / Instant Toggle (Pointer Click)

Toggle between ChatGPT "Thinking" and "Instant" with Ctrl+Alt+.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         ChatGPT Thinking / Instant Toggle (Pointer Click)
// @namespace    https://chatgpt.com/
// @version      2.3
// @description  Toggle between ChatGPT "Thinking" and "Instant" with Ctrl+Alt+.
// @match        https://chatgpt.com/*
// @grant        none
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // ---- Helpers ----

  function log(...args) {
    console.log('[MODEL-TOGGLE]', ...args);
  }

  function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  function waitFor(predicate, timeout = 3000) {
    return new Promise((resolve, reject) => {
      const start = performance.now();
      (function check() {
        try {
          const val = predicate();
          if (val) return resolve(val);
        } catch (e) { /* ignore */ }
        if (performance.now() - start > timeout) {
          return reject(new Error('waitFor timeout'));
        }
        requestAnimationFrame(check);
      })();
    });
  }

  function isVisible(el) {
    if (!el) return false;
    const style = window.getComputedStyle(el);
    if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
    const rect = el.getBoundingClientRect();
    return rect.width > 0 && rect.height > 0;
  }

  // NEW (v2.3): Simulate a POINTER event (focus, pointerdown, pointerup, click)
  function simulateClick(element) {
    if (!element) return;
    log('Simulating POINTER click (focus, pointerdown, pointerup, click)');
    try {
      // 1. Focus the element
      element.focus();

      // 2. Simulate Pointer Down
      const pointerdownEvent = new PointerEvent('pointerdown', {
        bubbles: true,
        cancelable: true,
        view: window,
        pointerType: 'mouse',
        isPrimary: true
      });
      element.dispatchEvent(pointerdownEvent);

      // 3. Simulate Pointer Up
      const pointerupEvent = new PointerEvent('pointerup', {
        bubbles: true,
        cancelable: true,
        view: window,
        pointerType: 'mouse',
        isPrimary: true
      });
      element.dispatchEvent(pointerupEvent);

      // 4. Trigger standard click for good measure
      element.click();
    } catch (e) {
      log('Error during simulateClick, falling back', e);
      if (element.click) element.click(); // Fallback
    }
  }

  // Get the visible model selector button
  function getModelButton() {
    const buttons = document.querySelectorAll("button[data-testid='model-switcher-dropdown-button']");
    for (const btn of buttons) {
      if (isVisible(btn)) return btn;
    }
    return null;
  }

  // Determine if current model is Thinking or Instant
  function isCurrentlyThinking(btn) {
    if (!btn) return null;
    const label = (
      btn.getAttribute('aria-label') ||
      btn.textContent ||
      ''
    ).toLowerCase();

    if (label.includes('thinking')) return true;
    if (label.includes('instant')) return false;
    return null;
  }

  // Open dropdown and return the menu container element
  async function openModelMenu() {
    let btn = getModelButton();
    if (!btn) {
      log('❌ No visible model button found.');
      return null;
    }

    if (btn.getAttribute('aria-expanded') !== 'true') {
      simulateClick(btn); // <-- USE NEW POINTER CLICK
    }

    // Wait for aria-expanded="true"
    try {
        await waitFor(() => {
            const b = getModelButton();
            return b && b.getAttribute('aria-expanded') === 'true';
        }, 3000);
    } catch (e) {
        log('❌ Menu did not report aria-expanded="true" in time.');
        return null;
    }

    btn = getModelButton();
    if (!btn || btn.getAttribute('aria-expanded') !== 'true') {
      return null;
    }

    // Find menu container
    const contentId = btn.getAttribute('aria-controls');
    if (contentId) {
      const menu = document.getElementById(contentId);
      if (menu && isVisible(menu)) return menu;
    }

    const fallbackMenu = document.querySelector('[data-radix-menu-content][data-state="open"]');
    if (fallbackMenu && isVisible(fallbackMenu)) return fallbackMenu;

    return null;
  }

  // Find Instant / Thinking item inside a given menu
  function findMenuItemByVariant(menu, variant /* 'instant' | 'thinking' */) {
    const v = variant.toLowerCase();
    const selector = `[role="menuitem"][data-testid^="model-switcher-"][data-testid$="-${v}"]`;
    let el = menu.querySelector(selector);
    if (el) return el;

    // Fallback: text search
    const candidates = Array.from(menu.querySelectorAll('[role="menuitem"], [role="option"]'));
    for (const c of candidates) {
      if (!isVisible(c)) continue;
      const text = (c.textContent || '').toLowerCase();
      if (text.includes(v)) return c;
    }
    return null;
  }

  let toggleInProgress = false;

  async function toggleModel() {
    if (toggleInProgress) return;
    toggleInProgress = true;

    try {
      const btn = getModelButton();
      if (!btn) {
        log('❌ No visible model button (before toggle).');
        return;
      }

      const currentThinking = isCurrentlyThinking(btn);
      const targetVariant =
        currentThinking === null
          ? 'thinking'
          : currentThinking
            ? 'instant'
            : 'thinking';

      log('Current mode thinking?:', currentThinking, '→ target:', targetVariant);

      const menu = await openModelMenu();
      if (!menu) {
        log('❌ Could not open/find model dropdown menu.');
        return;
      }

      await sleep(50);

      const item = findMenuItemByVariant(menu, targetVariant);
      if (!item) {
        log('❌ Could not find menu item for', targetVariant);
        return;
      }

      item.click();
      log('✅ Switched to', targetVariant);
    } catch (err) {
      log('❌ Error while toggling model:', err);
    } finally {
      toggleInProgress = false;
    }
  }

  // Hotkey: Ctrl + Alt + .
  window.addEventListener(
    'keydown',
    (e) => {
      if (!e.ctrlKey || !e.altKey || e.shiftKey) return;
      if (e.key !== '.' && e.code !== 'Period') return;

      const t = e.target;
      const tag = t && t.tagName;
      if (
        tag === 'INPUT' ||
        tag === 'TEXTAREA' ||
        (t && t.isContentEditable)
      ) {
        return;
      }

      e.preventDefault();
      toggleModel();
    },
    true
  );

  log('Userscript loaded (v2.3 - Pointer Click): Ctrl+Alt+. to toggle.');
})();