Gemini - Botón de Copiar Directo

Agrega un botón de 'Copiar' visible en la barra de acciones de cada respuesta de Gemini y en la barra de herramientas del Canvas.

Versión del día 4/7/2025. Echa un vistazo a la versión más reciente.

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.

Necesitarás 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.

Necesitará instalar una extensión como Tampermonkey para 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)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

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

// ==UserScript==
// @name         Gemini - Botón de Copiar Directo
// @namespace    http://tampermonkey.net/
// @version      1.9
// @description  Agrega un botón de 'Copiar' visible en la barra de acciones de cada respuesta de Gemini y en la barra de herramientas del Canvas.
// @author       Gemini
// @match        https://gemini.google.com/app*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=gemini.google.com
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Inyectamos CSS para los nuevos botones y los estados de los íconos.
    GM_addStyle(`
        .copiar-script-button {
            margin-right: 8px;
        }
        .copiar-script-button .copied-icon, .copiar-canvas-button .copied-icon {
            color: #6dd58c; /* Verde para el ícono de "check" */
            font-variation-settings: 'FILL' 1;
        }
        .copiar-canvas-button {
            margin-right: 8px;
        }
    `);

    /**
     * Crea y agrega el botón de copiar a un contenedor de respuesta de chat.
     * @param {HTMLElement} responseContainer - El elemento <div> que contiene la respuesta del modelo.
     */
    function agregarBotonDeCopiaChat(responseContainer) {
        const actionsContainer = responseContainer.querySelector('.buttons-container-v2');
        const shareButtonWrapper = responseContainer.querySelector('share-button');

        if (!actionsContainer || !shareButtonWrapper) return;

        const botonCopiar = document.createElement('button');
        botonCopiar.setAttribute('mat-icon-button', '');
        botonCopiar.setAttribute('mattooltip', 'Copiar contenido');
        botonCopiar.setAttribute('aria-label', 'Copiar contenido');
        botonCopiar.className = 'mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-unthemed copiar-script-button';

        const icono = document.createElement('mat-icon');
        icono.className = 'mat-icon notranslate google-symbols mat-ligature-font mat-icon-no-color';
        icono.textContent = 'content_copy';
        botonCopiar.appendChild(icono);

        botonCopiar.addEventListener('click', (e) => {
            e.stopPropagation();
            e.preventDefault();
            const contentElement = responseContainer.querySelector('.markdown');
            if (contentElement) {
                navigator.clipboard.writeText(contentElement.innerText).then(() => {
                    icono.textContent = 'check';
                    icono.classList.add('copied-icon');
                    setTimeout(() => {
                        icono.textContent = 'content_copy';
                        icono.classList.remove('copied-icon');
                    }, 2000);
                }).catch(err => console.error('Error al copiar el texto del chat: ', err));
            }
        });

        actionsContainer.insertBefore(botonCopiar, shareButtonWrapper);
    }

    /**
     * Crea y agrega el botón de copiar a la barra de herramientas del panel de código (Canvas).
     * @param {HTMLElement} panelCanvas - El elemento <code-immersive-panel>.
     */
    function agregarBotonDeCopiaCanvas(panelCanvas) {
        const actionsContainer = panelCanvas.querySelector('toolbar .action-buttons');
        const shareButtonTrigger = panelCanvas.querySelector('toolbar share-button button');

        if (!actionsContainer || !shareButtonTrigger) {
            return;
        }

        const botonCopiar = document.createElement('button');
        botonCopiar.setAttribute('mat-icon-button', '');
        botonCopiar.setAttribute('mattooltip', 'Copiar código');
        botonCopiar.setAttribute('aria-label', 'Copiar código');
        botonCopiar.className = 'mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-mdc-tooltip-trigger icon-button copiar-canvas-button mat-unthemed';

        const icono = document.createElement('mat-icon');
        icono.setAttribute('role', 'img');
        icono.className = 'mat-icon notranslate google-symbols mat-ligature-font mat-icon-no-color';
        icono.textContent = 'content_copy';
        botonCopiar.appendChild(icono);

        botonCopiar.addEventListener('click', (e) => {
            e.stopPropagation();
            e.preventDefault();

            // Simula un clic en el botón "Compartir" para abrir el menú
            shareButtonTrigger.click();

            // Espera un instante para que el menú se renderice en el DOM
            setTimeout(() => {
                // Busca el botón de copiar real dentro del panel del menú que acaba de aparecer
                const menuPanel = document.querySelector('.mat-mdc-menu-panel.mat-mdc-menu-panel');
                if (menuPanel) {
                    const originalCopyButton = menuPanel.querySelector('copy-button button');
                    if (originalCopyButton) {
                        // Simula un clic en el botón de copiar original
                        originalCopyButton.click();

                        // Feedback visual de éxito en nuestro botón
                        icono.textContent = 'check';
                        icono.classList.add('copied-icon');
                        setTimeout(() => {
                            icono.textContent = 'content_copy';
                            icono.classList.remove('copied-icon');
                        }, 2000);
                    }
                }
                // El menú se cierra solo al hacer clic en una opción.
            }, 50); // Un pequeño delay es suficiente
        });

        actionsContainer.insertBefore(botonCopiar, shareButtonTrigger.parentElement.parentElement);
    }

    /**
     * Busca nuevos elementos en la página para agregarles los botones correspondientes.
     */
    function procesarNuevosNodos() {
        // --- Lógica para las respuestas del chat ---
        document.querySelectorAll('model-response:not([data-copy-button-added])').forEach(container => {
            if (container.querySelector('.markdown')) {
                agregarBotonDeCopiaChat(container);
                container.dataset.copyButtonAdded = 'true';
            }
        });

        // --- Lógica para el panel de código (Canvas) ---
        document.querySelectorAll('code-immersive-panel:not([data-canvas-copy-button-added])').forEach(panel => {
            agregarBotonDeCopiaCanvas(panel);
            panel.dataset.canvasCopyButtonAdded = 'true';
        });
    }

    // El MutationObserver vigila por cambios en el DOM para ejecutar nuestro código.
    const observer = new MutationObserver(() => {
        setTimeout(procesarNuevosNodos, 500);
    });

    // Empezamos a observar el cuerpo del documento.
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    // Ejecutamos la función una vez al inicio.
    setTimeout(procesarNuevosNodos, 1000);

})();