Gemini - Direct Copy Button

Adds a visible 'Copy' button to each Gemini response's action bar and to the Canvas toolbar for easy one-click copying.

// ==UserScript==
// @name         Gemini - Direct Copy Button
// @name:es      Gemini - Botón de Copiar Directo
// @namespace    http://tampermonkey.net/
// @version      2.1.0
// @description  Adds a visible 'Copy' button to each Gemini response's action bar and to the Canvas toolbar for easy one-click copying.
// @description:es 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 para copiar con un solo clic.
// @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';

    // Inyecta el CSS necesario para los nuevos botones y sus estados.
    GM_addStyle(`
        .copiar-script-button {
            margin-right: 8px; /* Espacio para el botón en las respuestas del chat */
        }
        .copiar-script-button .copied-icon, .copiar-canvas-button .copied-icon {
            color: #6dd58c; /* Color verde para el ícono de "check" de confirmación */
            font-variation-settings: 'FILL' 1;
        }
        .copiar-canvas-button {
            margin-right: 8px; /* Espacio para el botón en el Canvas */
        }
    `);

    /**
     * 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);
    }

    /**
     * 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 a que el menú se renderice en el DOM.
            setTimeout(() => {
                // Busca el botón de copiar original dentro del panel del menú.
                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);
                    }
                }
            }, 50); // Un pequeño delay es suficiente.
        });

        // Inserta el nuevo botón antes del contenedor del botón de compartir.
        actionsContainer.insertBefore(botonCopiar, shareButtonTrigger.parentElement.parentElement);
    }

    /**
     * Busca nuevos elementos en la página para agregarles los botones.
     */
    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);

})();