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.

Versione datata 04/07/2025. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

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

})();