Greasy Fork is available in English.

Gemini Model Switcher Buttons

Replaces Gemini model selection dropdown with easily accessible buttons, and reapplies your selection on conversation reload

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         Gemini Model Switcher Buttons
// @namespace    http://tampermonkey.net/
// @version      2.4
// @description  Replaces Gemini model selection dropdown with easily accessible buttons, and reapplies your selection on conversation reload
// @author       treescandal & Gemini 3.0 Pro
// @match        https://gemini.google.com/*
// @license      MIT
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // ============ CONFIGURATION ============

    const MODE_CONFIG = {
        modes: [
            { icon: '⚡', label: 'Flash', index: 0 },
            { icon: '🧠', label: 'Think', index: 1 },
            { icon: '✨', label: 'Pro', index: 2 }
        ],
        containerSelector: '.leading-actions-wrapper',
        checkInterval: 300,
        compactBreakpoint: 620,
        reapplyDelay: 1500,
        minReapplyInterval: 4000,
        maxReapplyAttempts: 3
    };

    // ============ STATE MANAGEMENT ============

    let savedModeIndex = -1;
    let languageMap = null;
    let isSwitching = false;
    let switchTimeout = null;
    let reapplyAttempts = 0;
    let lastReapplyTime = 0;
    let conversationId = null;
    let lastContainerCheck = null;
    let resizeObserver = null;

    try {
        const savedMap = localStorage.getItem('gqs_language_map');
        if (savedMap) {
            languageMap = JSON.parse(savedMap);
        }

        const savedIndex = localStorage.getItem('gqs_last_index');
        if (savedIndex !== null) {
            savedModeIndex = parseInt(savedIndex, 10);
            console.log('Gemini Switcher: Loaded saved preference:', savedModeIndex);
        }
    } catch (e) {
        console.warn('Gemini Switcher: Could not load saved state', e);
    }

    // ============ STYLES ============

    const styles = `
        #gemini-quick-switch-bar {
            display: flex;
            gap: 6px;
            align-items: center;
            margin-left: auto;
            margin-right: 8px;
            height: 40px;
            z-index: 999;
        }

        .gqs-btn {
            background: rgba(0,0,0,0.05);
            border: 1px solid transparent;
            border-radius: 100px;
            padding: 0 14px 0 10px;
            height: 32px;
            font-size: 13px;
            font-weight: 500;
            color: #444746;
            cursor: pointer;
            transition: all 0.2s ease;
            white-space: nowrap;
            display: flex;
            align-items: center;
            gap: 4px;
        }

        .gqs-btn:hover {
            background: rgba(0,0,0,0.1);
        }

        .gqs-btn.active {
            background: #c2e7ff;
            color: #001d35;
        }

        .gqs-btn.compact {
            padding: 0 8px;
            min-width: 32px;
            justify-content: center;
        }

        .gqs-btn.compact .gqs-label {
            display: none;
        }

        .gqs-btn .gqs-icon {
            font-size: 16px;
            line-height: 1;
        }

        .gqs-btn .gqs-label {
            font-size: 13px;
        }

        @media (prefers-color-scheme: dark) {
            .gqs-btn {
                background: rgba(255,255,255,0.1);
                color: #e3e3e3;
            }
            .gqs-btn:hover {
                background: rgba(255,255,255,0.2);
            }
            .gqs-btn.active {
                background: #004a77;
                color: #c2e7ff;
            }
        }

        .model-picker-container {
            width: 0 !important;
            height: 0 !important;
            opacity: 0 !important;
            overflow: hidden !important;
            pointer-events: none !important;
            position: absolute !important;
        }

        .gds-mode-switch-menu {
            opacity: 0 !important;
            pointer-events: none !important;
            visibility: hidden !important;
        }

        .cdk-overlay-pane:has(> .gds-mode-switch-menu),
        .cdk-overlay-pane:has(> * > .gds-mode-switch-menu) {
            opacity: 0 !important;
            pointer-events: none !important;
            visibility: hidden !important;
        }

        .mat-bottom-sheet-container.gds-mode-switch-menu,
        .mat-bottom-sheet-container:has(.gds-mode-switch-menu) {
            opacity: 0 !important;
            pointer-events: none !important;
            visibility: hidden !important;
        }

        .mat-bottom-sheet-container:has([data-test-id^="bard-mode-option"]) {
            opacity: 0 !important;
            pointer-events: none !important;
            visibility: hidden !important;
        }

        .cdk-overlay-backdrop {
            transition: none !important;
        }

        .cdk-overlay-container:has(.gds-mode-switch-menu) .cdk-overlay-backdrop,
        .cdk-overlay-container:has([data-test-id^="bard-mode-option"]) .cdk-overlay-backdrop {
            opacity: 0 !important;
            pointer-events: none !important;
            visibility: hidden !important;
        }

        @supports not (selector(:has(*))) {
            .gds-mode-switch-menu * {
                opacity: 0 !important;
                pointer-events: none !important;
            }
        }

        @keyframes gqs-pulse {
            0% { opacity: 1; transform: scale(1); }
            50% { opacity: 0.7; transform: scale(0.98); }
            100% { opacity: 1; transform: scale(1); }
        }

        @keyframes gqs-success-flash {
            0% { background-color: #c2e7ff; }
            50% { background-color: #b7f7d6; border-color: #26a668; }
            100% { background-color: #c2e7ff; }
        }

        @media (prefers-color-scheme: dark) {
             @keyframes gqs-success-flash {
                0% { background-color: transparent; }
                50% { background-color: #22c55e; color: #ffffff; }
                100% { background-color: transparent; }
            }
        }

        .gqs-btn.working {
            animation: gqs-pulse 3s infinite ease-in-out !important;
            pointer-events: none;
            cursor: wait;
        }

        .gqs-btn.success-flash {
            animation: gqs-success-flash 0.6s ease-out;
        }
    `;

    const styleSheet = document.createElement('style');
    styleSheet.innerText = styles;

    if (document.head.firstChild) {
        document.head.insertBefore(styleSheet, document.head.firstChild);
    } else {
        document.head.appendChild(styleSheet);
    }

    // ============ URL DETECTION ============

    function getConversationId() {
        const pathParts = location.pathname.split('/').filter(p => p);

        let startIndex = 0;
        if (pathParts[0] === 'u' && !isNaN(pathParts[1])) {
            startIndex = 2;
        }

        const relevantParts = pathParts.slice(startIndex);

        if (relevantParts.length === 1) {
            return null;
        }

        if (relevantParts.length >= 2) {
            return relevantParts.slice(1).join('/');
        }

        return null;
    }

    function isOnConversationPage() {
        const path = location.pathname;
        return path.includes('/app/') || path.includes('/gem/') ||
               path.endsWith('/app') || path.endsWith('/gem');
    }

    // ============ MENU TRIGGER DETECTION ============

    function findMenuTrigger() {
        let trigger = document.querySelector('[data-test-id*="mode-menu"]');
        if (trigger) return { element: trigger, strategy: 'test-id' };

        const candidates = Array.from(document.querySelectorAll('button[aria-haspopup="true"]'));

        trigger = candidates.find(btn => {
            const hasRelevantContent = btn.querySelector('svg') ||
                                       btn.className.includes('model') ||
                                       btn.className.includes('mode');
            return hasRelevantContent;
        });

        if (trigger) return { element: trigger, strategy: 'aria-haspopup' };

        const structureMatch = Array.from(document.querySelectorAll('button')).find(btn => {
            const hasChevron = btn.querySelector('svg[viewBox*="24"]');
            return hasChevron;
        });

        if (structureMatch) return { element: structureMatch, strategy: 'structure' };

        return null;
    }

    // ============ MENU ITEMS DETECTION ============

    function findMenuItems() {
        const menuSelectors = [
            '[data-test-id^="bard-mode-option"]',
            '.gds-mode-switch-menu [role="menuitem"]',
            '.mat-bottom-sheet-container [role="menuitem"]',
            '[role="menu"] [role="menuitem"]',
            '[role="listbox"] [role="option"]',
        ];

        for (const selector of menuSelectors) {
            const items = Array.from(document.querySelectorAll(selector));
            if (items.length >= 2) {
                return items;
            }
        }

        const overlays = Array.from(document.querySelectorAll('.cdk-overlay-pane'));
        for (const overlay of overlays) {
            const rect = overlay.getBoundingClientRect();
            if (rect.height > 0) {
                const buttons = Array.from(overlay.querySelectorAll('button, [role="menuitem"], [role="option"]'));
                const validItems = buttons.filter(btn => {
                    const text = btn.innerText?.trim();
                    const btnRect = btn.getBoundingClientRect();
                    return text && text.length > 0 && btnRect.height > 20;
                });

                if (validItems.length >= 2) {
                    return validItems;
                }
            }
        }

        return [];
    }

    // ============ POLLING HELPER ============

    function waitForMenu(callback, maxWait = 500) {
        const startTime = Date.now();

        const check = () => {
            const items = findMenuItems();

            if (items.length >= 3) {
                callback(items);
            } else if (Date.now() - startTime < maxWait) {
                requestAnimationFrame(check);
            } else {
                console.warn('Gemini Switcher: Menu timeout');
                callback([]);
            }
        };

        requestAnimationFrame(check);
    }

    // ============ LANGUAGE MAP ============

    function buildLanguageMapFromMenu(menuItems) {
        const tempMap = {};

        menuItems.forEach((item, index) => {
            const titleElement = item.querySelector('.gds-title-m, [class*="title"]');
            if (titleElement) {
                const text = titleElement.innerText.trim().toLowerCase();
                if (text) {
                    tempMap[text] = index;
                }
            }
        });

        if (Object.keys(tempMap).length > 0) {
            languageMap = tempMap;
            try {
                localStorage.setItem('gqs_language_map', JSON.stringify(languageMap));
                console.log('Gemini Switcher: Language map built:', languageMap);
            } catch (e) {
                console.warn('Gemini Switcher: Could not save language map', e);
            }
        }
    }

    // ============ ACTIVE MODE DETECTION ============

    function detectActiveModeFromUI() {
        const triggerResult = findMenuTrigger();
        if (!triggerResult) return -1;

        const trigger = triggerResult.element;
        const text = trigger.innerText.toLowerCase().replace(/\s+/g, ' ').trim();

        if (!text || text.length < 2) return -1;

        if (languageMap) {
            for (const [keyword, index] of Object.entries(languageMap)) {
                if (text.includes(keyword)) {
                    return index;
                }
            }
        }

        return -1;
    }

    // ============ MODE SWITCHING ============

    function performModeSwitch(modeIndex, isAutoReapply = false) {
        if (isSwitching) {
            console.log('Gemini Switcher: Already switching, skipping');
            return;
        }

        const triggerExists = !!findMenuTrigger();
        console.log(`Gemini Switcher: performModeSwitch - mode: ${modeIndex}, isAutoReapply: ${isAutoReapply}, trigger exists: ${triggerExists}`);

        const targetBtn = document.querySelector(`.gqs-btn[data-mode-index="${modeIndex}"]`);
        if (targetBtn) targetBtn.classList.add('working');

        if (!isAutoReapply) {
            savedModeIndex = modeIndex;
            reapplyAttempts = 0;
            try {
                localStorage.setItem('gqs_last_index', modeIndex.toString());
            } catch (e) {
                console.warn('Gemini Switcher: Could not save preference', e);
            }
        } else {
            reapplyAttempts++;
            lastReapplyTime = Date.now();
        }

        isSwitching = true;
        clearTimeout(switchTimeout);

        // Update UI optimistically
        document.querySelectorAll('.gqs-btn').forEach((btn, idx) => {
            btn.classList.toggle('active', idx === modeIndex);
        });

        const triggerResult = findMenuTrigger();
        if (!triggerResult) {
            console.error('Gemini Switcher: Trigger not found');
            if (targetBtn) targetBtn.classList.remove('working');
            isSwitching = false;
            return;
        }

        console.log('Gemini Switcher: Clicking trigger with strategy:', triggerResult.strategy);
        triggerResult.element.click();

        waitForMenu((menuItems) => {
            if (menuItems.length === 0) {
                console.error('Gemini Switcher: Menu not found');
                if (targetBtn) targetBtn.classList.remove('working');
                isSwitching = false;
                return;
            }

            console.log('Gemini Switcher: Menu found with', menuItems.length, 'items');

            if (menuItems.length >= 3 && !languageMap) {
                buildLanguageMapFromMenu(menuItems);
            }

            if (menuItems.length > modeIndex) {
                console.log('Gemini Switcher: Clicking menu item', modeIndex);
                menuItems[modeIndex].click();

                if (targetBtn) {
                    targetBtn.classList.remove('working');
                    targetBtn.classList.add('success-flash');
                    setTimeout(() => targetBtn.classList.remove('success-flash'), 800);
                }

                switchTimeout = setTimeout(() => {
                    isSwitching = false;
                    console.log('Gemini Switcher: Switch completed');
                }, 800);
            } else {
                console.error(`Gemini Switcher: Only found ${menuItems.length} menu items, cannot switch to index ${modeIndex}`);
                if (targetBtn) targetBtn.classList.remove('working');
                isSwitching = false;
            }
        });
    }

    function checkAndReapply() {
        if (savedModeIndex === -1) {
            console.log('Gemini Switcher: No saved preference, skipping reapply');
            return;
        }

        if (isSwitching) {
            console.log('Gemini Switcher: Currently switching, skipping reapply');
            return;
        }

        if (reapplyAttempts >= MODE_CONFIG.maxReapplyAttempts) {
            console.log('Gemini Switcher: Max reapply attempts reached');
            return;
        }

        const now = Date.now();
        if (now - lastReapplyTime < MODE_CONFIG.minReapplyInterval) {
            console.log('Gemini Switcher: Rate limit - skipping reapply (too soon)');
            return;
        }

        const trigger = findMenuTrigger();
        if (!trigger) {
            console.log('Gemini Switcher: UI not ready for reapply (no trigger), skipping');
            return;
        }

        const currentMode = detectActiveModeFromUI();

        if (currentMode === -1) {
            console.log('Gemini Switcher: Cannot detect current mode, will build language map');
            performModeSwitch(savedModeIndex, true);
            return;
        }

        if (currentMode !== savedModeIndex) {
            console.log(`Gemini Switcher: Drift detected (current: ${currentMode}, saved: ${savedModeIndex})`);
            performModeSwitch(savedModeIndex, true);
        } else {
            console.log(`Gemini Switcher: Mode already correct (${currentMode}), no reapply needed`);
        }
    }

    // ============ UI STATE UPDATE ============

    function updateActiveState() {
        if (isSwitching) return;

        let activeIndex = savedModeIndex;

        if (activeIndex === -1) {
            activeIndex = detectActiveModeFromUI();
        }

        document.querySelectorAll('.gqs-btn').forEach((btn, idx) => {
            btn.classList.toggle('active', idx === activeIndex);
        });
    }

    // ============ RESPONSIVE LAYOUT ============

    function updateButtonLayout() {
        const container = document.querySelector(MODE_CONFIG.containerSelector);
        const buttons = document.querySelectorAll('.gqs-btn');

        if (!container || buttons.length === 0) return;

        const containerWidth = container.offsetWidth;
        const isCompact = containerWidth < MODE_CONFIG.compactBreakpoint;

        buttons.forEach(btn => {
            if (isCompact) {
                btn.classList.add('compact');
                btn.title = btn.querySelector('.gqs-label')?.textContent || '';
            } else {
                btn.classList.remove('compact');
                btn.removeAttribute('title');
            }
        });
    }

    // ============ UI INJECTION ============

    function injectButtons(container) {
        const bar = document.createElement('div');
        bar.id = 'gemini-quick-switch-bar';

        MODE_CONFIG.modes.forEach((mode, idx) => {
            const btn = document.createElement('button');
            btn.className = 'gqs-btn';

            const icon = document.createElement('span');
            icon.className = 'gqs-icon';
            icon.textContent = mode.icon;

            const label = document.createElement('span');
            label.className = 'gqs-label';
            label.textContent = mode.label;

            btn.appendChild(icon);
            btn.appendChild(label);
            btn.dataset.modeIndex = idx;
            btn.onclick = () => performModeSwitch(mode.index, false);
            bar.appendChild(btn);
        });

        container.appendChild(bar);
        console.log('Gemini Switcher: Buttons injected');

        setTimeout(() => {
            updateButtonLayout();
            updateActiveState();
        }, 100);
    }

    // ============ INITIALIZATION & MONITORING ============

    setInterval(() => {
        const container = document.querySelector(MODE_CONFIG.containerSelector);
        const existingBar = document.getElementById('gemini-quick-switch-bar');

        if (container && !existingBar) {
            injectButtons(container);
            lastContainerCheck = container;

            if (resizeObserver) {
                resizeObserver.disconnect();
            }
            resizeObserver = new ResizeObserver(updateButtonLayout);
            resizeObserver.observe(container);
        }

        if (existingBar && !document.body.contains(existingBar)) {
            lastContainerCheck = null;
            if (resizeObserver) {
                resizeObserver.disconnect();
                resizeObserver = null;
            }
        }

        if (existingBar) {
            updateActiveState();
        }
    }, MODE_CONFIG.checkInterval);

    window.addEventListener('resize', updateButtonLayout);

    // ============ NAVIGATION HANDLING ============

    let lastUrl = location.href;
    let navigationDebounceTimeout = null;
    conversationId = getConversationId();

    function handleNavigationChange() {
        const currentConversationId = getConversationId();

        if (currentConversationId !== conversationId) {
            conversationId = currentConversationId;

            console.log('Gemini Switcher: Conversation changed:', {
                id: conversationId,
                isOnConversationPage: isOnConversationPage()
            });

            if (isOnConversationPage() && savedModeIndex !== -1) {
                console.log('Gemini Switcher: On conversation page, will reapply preference');
                reapplyAttempts = 0;
                lastReapplyTime = 0;

                setTimeout(() => {
                    checkAndReapply();
                }, MODE_CONFIG.reapplyDelay);
            }
        }
    }

    new MutationObserver(() => {
        const currentUrl = location.href;

        if (currentUrl !== lastUrl) {
            lastUrl = currentUrl;
            lastContainerCheck = null;
            isSwitching = false;
            clearTimeout(switchTimeout);

            if (resizeObserver) {
                resizeObserver.disconnect();
                resizeObserver = null;
            }

            console.log('Gemini Switcher: Navigation detected');

            clearTimeout(navigationDebounceTimeout);
            navigationDebounceTimeout = setTimeout(() => {
                handleNavigationChange();
            }, 500);
        }

    }).observe(document.body, { childList: true, subtree: true });

    // ============ INITIAL LOAD HANDLING ============

    if (isOnConversationPage() && savedModeIndex !== -1) {
        console.log('Gemini Switcher: Initial load on conversation page, checking state...');

        const retryDelays = [1500, 2500, 4000];

        retryDelays.forEach(delay => {
            setTimeout(() => {
                if (reapplyAttempts < MODE_CONFIG.maxReapplyAttempts) {
                    const trigger = findMenuTrigger();
                    if (trigger) {
                        console.log(`Gemini Switcher: Trigger found at ${delay}ms, attempting reapply`);
                        checkAndReapply();
                    } else {
                        console.log(`Gemini Switcher: Trigger not found at ${delay}ms, will retry`);
                    }
                }
            }, delay);
        });
    }

})();