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.

As of 2025-07-04. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);

})();