ChatGPT Zero

Enhancements for ChatGPT

// ==UserScript==
// @name         ChatGPT Zero
// @namespace    https://github.com/NextDev65/
// @version      0.5
// @description  Enhancements for ChatGPT
// @author       NextDev65
// @homepageURL  https://github.com/NextDev65/ChatGPT-0
// @supportURL   https://github.com/NextDev65/ChatGPT-0
// @match        https://chatgpt.com/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // --- Configuration ---
    const PREFERRED_MODEL_KEY = 'preferredChatGPTModel';
    const SETTINGS_KEY = 'chatgptZeroSettings';
    const DEFAULT_MODEL = 'gpt-4o-mini';
    const MODELS = [
        'gpt-3.5-turbo',
        'text-davinci-002-render-sha',
        'text-davinci-002-render-sha-mobile',
        'gpt-4-mobile',
        'gpt-4o-mini',
        'gpt-4-1-mini',
        'gpt-4o',
        'o4-mini'
    ];

    // Default settings
    const DEFAULT_SETTINGS = {
        modelSwitcher: true,
        streamerMode: true,
        animations: true
    };

    // Load settings from localStorage
    function loadSettings() {
        try {
            const saved = localStorage.getItem(SETTINGS_KEY);
            return saved ? { ...DEFAULT_SETTINGS, ...JSON.parse(saved) } : { ...DEFAULT_SETTINGS };
        } catch (e) {
            console.warn('Failed to load settings, using defaults', e);
            return { ...DEFAULT_SETTINGS };
        }
    }

    // Save settings to localStorage
    function saveSettings(settings) {
        try {
            localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
        } catch (e) {
            console.warn('Failed to save settings', e);
        }
    }

    // Global settings object
    let settings = loadSettings();

    /**
     * Creates a toggle switch element
     * @param {string} label - The label text for the toggle
     * @param {boolean} checked - Initial checked state
     * @param {Function} onChange - Callback when toggle changes
     * @returns {HTMLDivElement}
     */
    function createToggleSwitch(label, checked, onChange) {
        const container = document.createElement('div');
        container.className = 'toggle-container';

        const labelElement = document.createElement('label');
        labelElement.className = 'toggle-label';
        labelElement.textContent = label;

        const switchContainer = document.createElement('label');
        switchContainer.className = 'toggle-switch';

        const input = document.createElement('input');
        input.type = 'checkbox';
        input.checked = checked;
        input.className = 'toggle-input';
        input.addEventListener('change', onChange);

        const slider = document.createElement('span');
        slider.className = 'toggle-slider';

        switchContainer.appendChild(input);
        switchContainer.appendChild(slider);
        container.appendChild(labelElement);
        container.appendChild(switchContainer);

        return container;
    }

    /**
     * Creates and returns a <button> element with an attached settings menu.
     * @returns {HTMLButtonElement}
     */
    function createSettingsMenu() {
        const cog = document.createElement('button');
        cog.id = 'settings-cog';
        //cog.textContent = settings.animations ? '⚙️' : '⚙';
        cog.setAttribute('aria-label', 'Settings');

        const menu = document.createElement('div');
        menu.id = 'settings-menu';
        menu.className = 'settings-dropdown';
        menu.style.display = 'none';

        // Create toggle switches
        const modelSwitcherToggle = createToggleSwitch('Model Switcher', settings.modelSwitcher, (e) => {
            settings.modelSwitcher = e.target.checked;
            saveSettings(settings);
            updateModelSwitcherVisibility();
        });
        
        const streamerModeToggle = createToggleSwitch(
          'Streamer Mode',
          settings.streamerMode ?? true,
          (e) => {
            settings.streamerMode = e.target.checked;
            saveSettings(settings);
            updateStreamerModeStyles();
          }
        );

        const animationsToggle = createToggleSwitch('Animations', settings.animations, (e) => {
            settings.animations = e.target.checked;
            saveSettings(settings);
            updateAnimationStyles();
        });

        menu.appendChild(modelSwitcherToggle);
        menu.appendChild(streamerModeToggle);
        menu.appendChild(animationsToggle);

        // Append menu to body to avoid positioning issues
        document.body.appendChild(menu);

        // Toggle menu visibility
        cog.addEventListener('click', (e) => {
            e.stopPropagation();
            //const isVisible = window.getComputedStyle(menu).display !== 'none';
            if (menu.style.display === 'block')
            {
                menu.style.display = 'none';
            }
            else {
                positionMenu();
                menu.style.display = 'block';
            }
        });

        // Close menu when clicking outside
        document.addEventListener('click', (e) => {
            if (!cog.contains(e.target) && !menu.contains(e.target)) {
                menu.style.display = 'none';
            }
        });

        // Position menu relative to cog
        function positionMenu() {
            // cog bounds, changes when cog is rotated (animations enabled) -> alignment inconsistencies
            const cogRect = cog.getBoundingClientRect();
            // page header bounds
            const parentRect = cog.parentElement.getBoundingClientRect();
            const viewportWidth = window.innerWidth;

            menu.style.position = 'fixed';
            menu.style.top = `${parentRect.bottom - 5}px`; // 5px above `page-header`
            menu.style.zIndex = '10000';
            
            const cogRight = cogRect.left + cogRect.width;
            const rightOffset = viewportWidth - cogRight;

            // prepare initial state
            menu.style.right = `${rightOffset}px`;
            menu.style.left = 'auto';
            if (settings.animations) {
                menu.style.opacity = '0';
                menu.style.transform = 'translateX(10px)';
                menu.style.transition = 'opacity 0.3s ease, transform 0.3s ease';

                /*// force a reflow so the browser registers the start state
                // eslint-disable-next-line @microsoft/sdl/no-document-domain -- reflow hack
                void menu.offsetWidth;*/

                // slide into place
                requestAnimationFrame(() => {
                    menu.style.opacity = '1';
                    menu.style.transform = 'translateX(0)';
                });
            }
        }

        // Inject CSS for settings menu and toggle switches
        injectSettingsStyles();

        return cog;
    }

    /**
     * Injects CSS styles for the settings menu and components
     */
    function injectSettingsStyles() {
        if (document.getElementById('settings-styles')) return;

        const style = document.createElement('style');
        style.id = 'settings-styles';

        style.textContent = `
    #settings-cog {
        font-size: 20px;
        margin-left: 12px;
        padding: 4px 5px;
        border: none;
        border-radius: 50%;
        background-color: #212121;
        color: #fff;
        cursor: pointer;
        box-shadow: 0 0 0 0 rgba(33, 33, 33, 0) inset,
                    0 0 5px 0 rgba(33, 33, 33, 0);
        display: flex;
        align-items: center;
        justify-content: center;
        position: relative;
        transform: translateX(0.75px) translateY(-0.75px);
        ${settings.animations ? `
        transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
                    box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1);
        ` : ''}
        ${settings.animations ? `
        transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55);
        ` : ''}
    }
    #settings-cog:hover {
        background-color: #2f2f2f;
        box-shadow: 0 0 2.5px 0 rgba(255, 255, 255, 0) inset,
                    0 0 5px 0 rgba(255, 255, 255, 0.2);
        ${settings.animations ? `
        transform: translateX(0.75px) translateY(-0.75px) rotate(45deg);
        ` : `
        transform: translateX(0.75px) translateY(-0.75px);
        `}
    }
    #settings-cog:focus {
        outline: none;
        box-shadow: 0 0 2.5px 0 rgba(255, 255, 255, 0.5) inset,
                    0 0 5px 0 rgba(255, 255, 255, 0.5);
    }

    #settings-cog::before {
        content: '${settings.animations ? '⚙️' : '⚙'}';
        transform-origin: center;
        transform: translateX(0.75px) translateY(-0.75px);
    }

    .settings-dropdown {
        display: none;
        background-color: #2a2a2a;
        border: 1px solid #444;
        border-radius: 8px;
        padding: 12px;
        min-width: 200px;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
    }


    .toggle-container {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 12px;
    }
    .toggle-container:last-child {
        margin-bottom: 0;
    }

    .toggle-label {
        color: #fff;
        font-size: 14px;
    }

    .toggle-switch {
        position: relative;
        display: inline-block;
        width: 44px;
        height: 24px;
    }

    .toggle-input {
        position: absolute;
        opacity: 0;
        width: 100%;
        height: 100%;
        cursor: pointer;
        z-index: 1;
    }
    .toggle-input:checked + .toggle-slider {
        background-color: #4CAF50;
    }
    .toggle-input:checked + .toggle-slider:before {
        transform: translateX(20px);
    }
    .toggle-input:checked + .toggle-slider:hover {
        background-color: #45a049;
    }

    .toggle-slider {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: #555;
        border-radius: 24px;
        ${settings.animations ? `
        transition: background-color 0.3s cubic-bezier(0.68, -0.1, 0.27, 1.1),
                    transform 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55);
        ` : ''}
    }
    .toggle-slider:before {
        content: "";
        position: absolute;
        height: 18px;
        width: 18px;
        left: 3px;
        bottom: 3px;
        background-color: white;
        border-radius: 50%;
        ${settings.animations ? `
        transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55);
        ` : ''}
    }
`;
        document.head.appendChild(style);
    }

    /**
     * Updates animation styles based on current settings
     */
    function updateAnimationStyles() {
        // Remove existing styles and re-inject with updated animation settings
        document.getElementById('settings-styles')?.remove();
        document.getElementById('model-switcher-styles')?.remove();

        injectSettingsStyles();
        // Re-inject model switcher styles if it exists
        const modelSwitcher = document.getElementById('chatgpt-model-switcher');
        if (modelSwitcher) {
            injectModelSwitcherStyles();
        }
        if (settings.streamerMode) {
            updateStreamerModeStyles();
        }
    }

    function updateStreamerModeStyles() {
        injectStreamerModeStyles();
    }

    function injectStreamerModeStyles() {
        // Remove old rules
        document.getElementById('streamer-styles')?.remove();

        if (!settings.streamerMode) return;  // nothing to do if disabled

        const style = document.createElement('style');
        style.id = 'streamer-styles';

        style.textContent = `
        /* inactive chats */
        #history .__menu-item:not([data-active]) {
            box-shadow: 0 0 2.5px 0 rgba(255, 255, 255, 0) inset,
                        0 0 5px 0 rgba(255, 255, 255, 0.2);
            ${settings.animations ? `
            transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
                        box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1);
            ` : ''}
        }
        
        /* inactive chat titles */
        #history .__menu-item:not([data-active]) .truncate span {
            opacity: 0;
            ${settings.animations ? `
            transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
                        box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1);
            ` : ''}
        }
        #history .__menu-item:not([data-active]):hover .truncate span {
            opacity: 1;
        }
        `;

        document.head.appendChild(style);
    }

    /**
     * Updates model switcher visibility based on settings
     */
    function updateModelSwitcherVisibility() {
        const modelSwitcher = document.getElementById('chatgpt-model-switcher');
        if (modelSwitcher) {
            modelSwitcher.style.display = settings.modelSwitcher ? 'block' : 'none';
        }
    }

    /**
     * Injects CSS styles for the model switcher
     */
    function injectModelSwitcherStyles() {
        if (document.getElementById('model-switcher-styles')) return;

        const style = document.createElement('style');
        style.id = 'model-switcher-styles';

        style.textContent = `
    #chatgpt-model-switcher {
        margin: auto;
        padding: 4px 8px;
        border: none;
        border-radius: 6px;
        background-color: #212121;
        color: #fff;
        outline: none;
        ${settings.animations ? `
        box-shadow: 0 0 0 0 rgba(33, 33, 33, 0) inset,
                    0 0 5px 0 rgba(33, 33, 33, 0);
        transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
                    box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1);
        ` : ''}
    }
    #chatgpt-model-switcher:hover {
        background-color: #2f2f2f;
        box-shadow: 0 0 2.5px 0 rgba(255, 255, 255, 0) inset,
                    0 0 5px 0 rgba(255, 255, 255, 0.2);
    }
    #chatgpt-model-switcher:focus {
        outline: none;
        box-shadow: 0 0 2.5px 0 rgba(255, 255, 255, 0.5) inset,
                    0 0 5px 0 rgba(255, 255, 255, 0.5);
    }
`;
        document.head.appendChild(style);
    }

    /**
     * Creates and returns a <select> element configured as the model switcher.
     * @param {string} currentModel - Model to pre-select in the dropdown.
     * @returns {HTMLSelectElement}
     */
    function createModelSwitcher(currentModel) {
        const select = document.createElement('select');
        select.id = 'chatgpt-model-switcher';

        // Inject CSS for base styling, hover, focus, and transition effects
        injectModelSwitcherStyles();

        // Populate dropdown with model options
        MODELS.forEach(model => {
            const option = document.createElement('option');
            option.value = model;
            option.textContent = model;
            if (model === currentModel) option.selected = true;
            select.appendChild(option);
        });

        // Save selection to localStorage on change
        select.addEventListener('change', () => {
            localStorage.setItem(PREFERRED_MODEL_KEY, select.value);
        });

        // Set initial visibility based on settings
        select.style.display = settings.modelSwitcher ? 'block' : 'none';

        return select;
    }

    /**
     * Finds our model switcher in the UI and inserts the settings cog after it.
     * Retries every second until our model switcher is visible.
     */
    function injectSettingsMenu() {
        const checkInterval = setInterval(() => {
            const modelSwitcher = document.getElementById('chatgpt-model-switcher');
            let cog = document.getElementById('settings-cog');

            // Create cog if it doesn't exist yet
            if (!cog) {
                cog = createSettingsMenu();
            }
            // Insert cog after visible model switcher
            if (modelSwitcher && !cog.parentNode && modelSwitcher.parentNode) {
                modelSwitcher.parentNode.insertBefore(cog, modelSwitcher.nextSibling);
            }
        }, 1000);
    }

    /**
     * Finds the native model switcher in the UI and inserts our custom switcher beside it.
     * Retries every second until the native element is visible.
     */
    function injectModelSwitcher() {
        const checkInterval = setInterval(() => {
            const nativeModelSwitchers = document.querySelectorAll('[data-testid="model-switcher-dropdown-button"]');
            let switcher = document.getElementById('chatgpt-model-switcher');

            // Create switcher
            if (!switcher) {
                const savedModel = localStorage.getItem(PREFERRED_MODEL_KEY) || DEFAULT_MODEL;
                switcher = createModelSwitcher(savedModel);
            }
            // Insert switcher next to the first visible native button
            if (!switcher.parentNode) {
                for (let nativeModelSwitcher of nativeModelSwitchers) {
                    if (nativeModelSwitcher.checkVisibility && nativeModelSwitcher.checkVisibility()) {
                        nativeModelSwitcher.parentNode.after(switcher);
                        break;
                    }
                }
            }
        }, 1000);
    }

    /**
     * Overrides window.fetch to intercept conversation requests and replace the model
     * property in the request body with the user-selected model.
     */
    function overrideModelInRequest() {
        // Only override if model switcher is enabled
        if (!settings.modelSwitcher) return;

        const origFetch = window.fetch;
        window.fetch = async (...args) => {
            const [resource, config] = args;
            const savedModel = localStorage.getItem(PREFERRED_MODEL_KEY) || DEFAULT_MODEL;

            // Target only conversation API calls
            if (
                typeof resource === 'string' &&
                resource.includes('/backend-api/f/conversation') &&
                config?.body
            ) {
                try {
                    const body = JSON.parse(config.body);
                    if (body && body.model) {
                        // Overwrite model
                        body.model = savedModel;
                        config.body = JSON.stringify(body);
                    }
                } catch (e) {
                    console.warn('Model switcher failed to parse request body', e);
                }
            }

            return origFetch(resource, config);
        };
    }

    // Initialize the userscript
    injectModelSwitcher();
    overrideModelInRequest();
    injectSettingsMenu();
    updateStreamerModeStyles();

})();

QingJ © 2025

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