ChatGPT Zero

Enhancements for ChatGPT

À partir de 2025-07-03. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         ChatGPT Zero
// @namespace    https://github.com/NextDev65/
// @version      0.42
// @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',
        'gpt-4o-mini',
        'gpt-4-1-mini',
        'gpt-4o',
        'o4-mini'
    ];

    // Default settings
    const DEFAULT_SETTINGS = {
        modelSwitcher: 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 animationsToggle = createToggleSwitch('Animations', settings.animations, (e) => {
            settings.animations = e.target.checked;
            saveSettings(settings);
            updateAnimationStyles();
        });

        menu.appendChild(modelSwitcherToggle);
        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() {
            const cogRect = cog.getBoundingClientRect();
            const viewportWidth = window.innerWidth;

            menu.style.position = 'fixed';
            menu.style.top = `${cogRect.bottom + 5}px`;
            menu.style.zIndex = '10000';

            if (!settings.animations) {
                // Temporarily show menu offscreen to measure width
                menu.style.visibility = 'hidden';
                menu.style.display = 'block';
                menu.style.left = '0px';
                menu.style.right = 'auto';

                const menuWidth = menu.offsetWidth;

                // Calculate left position with clamping to viewport
                let left = cogRect.left;
                if (left + menuWidth > viewportWidth) {
                    left = viewportWidth - menuWidth - 5; // 5px padding from edge
                }

                // Apply final position and show menu visibly
                menu.style.left = `${left}px`;
                menu.style.visibility = 'visible';
                // Don't change display here; caller controls show/hide
                menu.style.opacity = '1';        // fully visible
                menu.style.transform = 'none';
                menu.style.transition = '';      // kill any animation
            } else {
                // Animations enabled: slide/fade in from right, ending with right edges aligned
                const cogRight = cogRect.left + cogRect.width;
                const rightOffset = viewportWidth - cogRight;

                // prepare initial state
                menu.style.right = `${rightOffset}px`;
                menu.style.left = 'auto';
                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;*/

                // then slide into place, Animate on next frame
                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);
        ` : ''}
    }
    #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();
        }
    }

    /**
     * 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();
})();