ChatGPT Zero

Enhancements for ChatGPT

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         ChatGPT Zero
// @namespace    https://github.com/NextDev65/
// @version      0.60
// @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        GM_xmlhttpRequest
// @grant        unsafeWindow
// @connect      artificialanalysis.ai
// ==/UserScript==

(function () {
    'use strict';

    // --- Configuration ---
    const PREFERRED_MODEL_KEY = 'preferredChatGPTModel';
    const SETTINGS_KEY = 'chatgptZeroSettings';
    const DEFAULT_MODEL = 'auto';
    const MODELS = {
        'gpt-5-3': {
            label: 'GPT 5.3',
            aaSlug: 'gpt-5-3-codex'
        },
        'gpt-5-mini': {
            label: 'GPT 5 Mini',
            aaSlug: 'gpt-5-mini-minimal'
        },
        'gpt-5-t-mini': {
            label: 'GPT 5 Thinking Mini',
            aaSlug: 'gpt-5-mini'
        },
        'auto': {
            label: 'Auto',
            aaSlug: null
        }
    };

    // Cache configuration for Artificial Analysis stats
    const AA_CACHE_KEY = 'aa_model_stats';
    const AA_CACHE_TTL = 1000 * 60 * 60 * 24; // 24 hours

    // User-provided API key storage
    const AA_API_KEY_STORAGE = 'aa_api_key';

    /**
     * Initializes the model stats Promise (if not already created)
     */
    let modelStatsPromise = null;
    function initStats() {
        if (!modelStatsPromise) {
            modelStatsPromise = getStats(MODELS).catch(err => {
                console.warn('Stats fetch failed', err);
                return {};
            });
        }
    }

    /**
     * Gets the user-provided API key from localStorage
     * @returns {string|null}
     */
    function getApiKey() {
        try {
            return localStorage.getItem(AA_API_KEY_STORAGE) || null;
        } catch {
            return null;
        }
    }

    /**
     * Sets or removes the API key in localStorage
     * @param {string|null} key - The API key to store, or null/empty to remove
     */
    function setApiKey(key) {
        try {
            if (key) {
                localStorage.setItem(AA_API_KEY_STORAGE, key);
            } else {
                localStorage.removeItem(AA_API_KEY_STORAGE);
            }
        } catch (e) {
            console.warn('Failed to store API key', e);
        }
    }

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

    // Storage helpers (Settings)
    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 };
        }
    }

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

    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 settings menu.
     * @returns {HTMLDivElement}
     */
    function createApiKeyInput() {
        const container = document.createElement('div');
        container.className = 'api-key-container';

        const label = document.createElement('label');
        label.className = 'api-key-label';
        label.textContent = 'Artificial Analysis API Key';

        const input = document.createElement('input');
        input.type = 'password';
        input.className = 'api-key-input';
        input.placeholder = 'API key (for model stats)';
        input.value = getApiKey() || '';

        input.addEventListener('change', () => {
            const value = input.value.trim();
            // Only store if value starts with 'aa_' (Artificial Analysis key prefix)
            if (value.startsWith('aa_')) {
                setApiKey(value);
            } else {
                // Clear invalid key
                setApiKey('');
                input.value = '';
                console.warn('API key should start with \'aa_\'')
            }
        });

        container.appendChild(label);
        container.appendChild(input);

        return container;
    }

    function createSettingsMenu() {
        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);

        // Add API key input
        menu.appendChild(createApiKeyInput());

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

        return menu;
    }

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

        // 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);
        transition: background-color var(--anim-fast)   var(--easing-standard),
                    box-shadow       var(--anim-slow)   var(--easing-standard),
                    transform        var(--anim-normal) var(--easing-transform);
    }
    #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);
        transform: translateX(0.75px) translateY(-0.75px) var(--cog-rotate);
    }
    #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: var(--cog-icon);
        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;
        transition: background-color var(--anim-normal) var(--easing-slider-bg),
                    transform        var(--anim-normal) var(--easing-transform);
    }
    .toggle-slider:before {
        content: "";
        position: absolute;
        height: 18px;
        width: 18px;
        left: 3px;
        bottom: 3px;
        background-color: white;
        border-radius: 50%;
        transition: transform var(--anim-normal) var(--easing-transform);
    }

    .api-key-container {
        display: flex;
        flex-direction: column;
        gap: 6px;
        margin-bottom: 12px;
    }
    .api-key-container:last-child {
        margin-bottom: 0;
    }

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

    .api-key-input {
        width: 100%;
        padding: 8px 12px;
        border: 1px solid #555;
        border-radius: 4px;
        background-color: #1a1a1a;
        color: #fff;
        font-size: 13px;
        box-sizing: border-box;
    }
    .api-key-input:focus {
        outline: none;
        border-color: #4CAF50;
    }
    .api-key-input::placeholder {
        color: #777;
    }
`;
        document.head.appendChild(style);
    }

    /**
     * Updates animation styles based on current settings with CSS custom properties
     */
    function updateAnimationStyles() {
        const root = document.documentElement;
        const animate = settings.animations;
        // Durations
        root.style.setProperty('--anim-fast',   animate ? '0.2s' : '0s');
        root.style.setProperty('--anim-normal', animate ? '0.3s' : '0s');
        root.style.setProperty('--anim-slow',   animate ? '0.4s' : '0s');
        // Easing functions
        root.style.setProperty('--easing-standard',  animate ? 'cubic-bezier(0.4,   0,    0.2,  1)' : '');
        root.style.setProperty('--easing-transform', animate ? 'cubic-bezier(0.68, -0.55, 0.27, 1.55)' : '');
        root.style.setProperty('--easing-slider-bg', animate ? 'cubic-bezier(0.68, -0.1,  0.27, 1.1)' : '');
        // Cog styles
        root.style.setProperty('--cog-rotate', animate ? 'rotate(45deg)' : 'rotate(0deg)');
        root.style.setProperty('--cog-icon', animate ? "'⚙️'" : "'⚙'");
        // Model Switcher glow (initially invisible, transitions from this on hover)
        root.style.setProperty('--initial-glow', animate ? `0 0 0   0 rgba(33, 33, 33, 0) inset,
                                                            0 0 5px 0 rgba(33, 33, 33, 0)` : 'none');
    }

    function updateStreamerModeStyles() {
        injectStreamerModeStyles();
        document.body.classList.toggle('streamer-mode', settings.streamerMode);
    }

    function injectStreamerModeStyles() {
        if (document.getElementById('streamer-styles')) return;

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

        style.textContent = `
        /* inactive chats */
        .streamer-mode #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);
            transition: background-color var(--anim-fast) var(--easing-standard),
                        box-shadow       var(--anim-slow) var(--easing-standard);
        }
        
        /* inactive chat titles */
        .streamer-mode #history .__menu-item:not([data-active]) .truncate span {
            opacity: 0;
            transition: opacity    var(--anim-fast) var(--easing-standard),
                        box-shadow var(--anim-slow) var(--easing-standard);
        }
        .streamer-mode #history .__menu-item:not([data-active]):hover .truncate span {
            opacity: 1;
        }
        
        /* accounts profile */
        .streamer-mode [data-testid="accounts-profile-button"] {
            display: none !important;
        }
        `;

        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;
        box-shadow: var(--initial-glow);
        transition: background-color var(--anim-fast) var(--easing-standard),
                    box-shadow       var(--anim-slow) var(--easing-standard);
    }
    #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);
    }


    /**
     * Unified function to get model stats from Artificial Analysis API.
     * Uses caching to avoid repeated API calls.
     * @param {object} modelConfig - The MODELS configuration object
     * @returns {Promise<object>} - Promise resolving to stats object { slug: index }
     */
    function getStats(modelConfig) {
        const API_KEY = getApiKey();
        
        // If no API key, fail silently and return empty stats
        if (!API_KEY) {
            return Promise.resolve({});
        }
        
        const URL = "https://artificialanalysis.ai/api/v2/data/llms/models";

        const now = Date.now();

        let cache = null;
        try {
            cache = JSON.parse(localStorage.getItem(AA_CACHE_KEY));
        } catch {}

        const cachedStats = cache?.stats || {};
        const timestamp = cache?.timestamp || 0;
        const isExpired = (now - timestamp) > AA_CACHE_TTL;

        // Determine required models
        const missing = Object.entries(modelConfig)
            .filter(([slug, cfg]) => cfg.aaSlug && cachedStats[slug] === undefined)
            .map(([slug]) => slug);

        // Use cache if valid and complete
        if (!isExpired && missing.length === 0) {
            return Promise.resolve(cachedStats);
        }

        // Fetch from API
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: URL,
                headers: { "x-api-key": API_KEY },
                onload: function (response) {
                    try {
                        const json = JSON.parse(response.responseText);
                        const newStats = { ...cachedStats };

                        for (const [slug, config] of Object.entries(modelConfig)) {
                            if (!config.aaSlug) continue;

                            const model = json.data.find(m => m.slug === config.aaSlug);

                            if (!model) {
                                console.warn(config.aaSlug, "not found");
                                continue;
                            }

                            newStats[slug] =
                                model.evaluations?.artificial_analysis_intelligence_index ?? null;
                        }

                        // Save updated cache
                        localStorage.setItem(AA_CACHE_KEY, JSON.stringify({
                            timestamp: now,
                            stats: newStats
                        }));

resolve(newStats);
                    } catch (err) {
                        reject(err);
                    }
                },
                onerror: reject
            });
        });
    }


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

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

        // Fetch stats
        let stats = {};
        try {
            stats = await modelStatsPromise;
        } catch (e) {
            console.warn('Failed to load stats', e);
        }

        // Populate dropdown with model options
        for (const [slug, config] of Object.entries(MODELS)) {
            const option = document.createElement('option');
            option.value = slug;
            option.textContent = config.label || slug;

            if (slug === currentModel) {
                option.selected = true;
            }

            // Tooltip with index (only show if data is available)
            if (stats[slug] != null) {
                option.title = `AA Index: ${stats[slug]}`;
            } else {
                option.title = '';
            }

            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');
            if (!modelSwitcher) return; // Wait until the model switcher is available

            let cog = document.getElementById('settings-cog');
            let menu = document.getElementById('settings-menu');

            // Create menu if it doesn't exist yet
            if (!menu) {
                menu = createSettingsMenu();
            }
            // Create cog + Insert cog before toolbar
            if (!cog) {
                cog = createSettingsCog(menu);
                //modelSwitcher.after(cog);
                
                document.getElementById('page-header').lastChild.prepend(cog); // last child of page header
            }
        }, 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.
     */
    async function injectModelSwitcher() {
        const checkInterval = setInterval(async () => {
            const nativeModelSwitchers = document.querySelectorAll('[data-testid="model-switcher-dropdown-button"]');
            let switcher = document.getElementById('chatgpt-model-switcher');
            const getPlusClassName = ['absolute start-1/2 flex flex-col items-center gap-2 ltr:-translate-x-1/2 rtl:translate-x-1/2',
                                      'pointer-events-none absolute start-0 flex flex-col items-center gap-2 lg:start-1/2 ltr:-translate-x-1/2 rtl:translate-x-1/2'];
            // Create switcher
            if (!switcher) {
                const savedModel = localStorage.getItem(PREFERRED_MODEL_KEY) || DEFAULT_MODEL;
                switcher = await 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);

                        // move "Get Plus" button
                        let getPlus = null;
                        for (let className of getPlusClassName) {
                            let elements = document.getElementsByClassName(className);
                            if (elements.length > 0) {
                                // give getPlus styling to switcher
                                switcher.className = getPlusClassName;
                                getPlus = elements[0];
                                break;
                            }
                        }
                        nativeModelSwitcher.parentNode.appendChild(getPlus);
                        getPlus.className = '';
                        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 = unsafeWindow.fetch;
        unsafeWindow.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
    initStats();
    injectModelSwitcher();
    overrideModelInRequest();
    updateStreamerModeStyles();
    injectSettingsMenu();
    updateAnimationStyles();
})();