Gemini - Smart Copy Buttons

Enhances Gemini's copy function. The native chat 'Copy' button now intelligently copies only code blocks if present. Also adds a direct 'Copy' button to the Canvas/code editor toolbar.

目前為 2025-08-01 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Gemini - Smart Copy Buttons
// @name:es      Gemini - Botones de Copiado Inteligentes
// @namespace    http://tampermonkey.net/
// @version      3.1.0
// @description  Enhances Gemini's copy function. The native chat 'Copy' button now intelligently copies only code blocks if present. Also adds a direct 'Copy' button to the Canvas/code editor toolbar.
// @description:es Mejora la función de copiado de Gemini. El botón nativo 'Copiar' del chat ahora copia de forma inteligente solo los bloques de código si existen. También añade un botón de 'Copiar' directo a la barra de herramientas del Canvas/editor de código.
// @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 el ícono de feedback en el botón del Canvas.
    GM_addStyle(`
        .copiar-canvas-button .copied-icon {
            color: #6dd58c; /* Color verde para el ícono de "check" */
            font-variation-settings: 'FILL' 1;
        }
        .copiar-canvas-button {
            margin-right: 8px; /* Espacio para el botón en el Canvas */
        }
    `);

    /**
     * Modifica el comportamiento del botón de copiar nativo en las respuestas del chat.
     * @param {HTMLElement} responseContainer - El elemento <model-response> que contiene la respuesta.
     */
    function modificarBotonDeCopiaChat(responseContainer) {
        // Selector para el botón de "Copiar respuesta" en el pie de página del mensaje.
        const botonCopiarNativo = responseContainer.querySelector('.response-container-footer copy-button > button');

        if (!botonCopiarNativo) return;

        // Clonamos el botón para eliminar de forma segura los event listeners existentes.
        const botonClonado = botonCopiarNativo.cloneNode(true);
        botonCopiarNativo.parentNode.replaceChild(botonClonado, botonCopiarNativo);

        const icono = botonClonado.querySelector('mat-icon');

        botonClonado.addEventListener('click', (e) => {
            e.stopPropagation();
            e.preventDefault(); // Prevenimos la acción de copiado original.

            const bloquesDeCodigo = responseContainer.querySelectorAll('div.code-block');
            let textoACopiar = '';

            if (bloquesDeCodigo.length > 0) {
                // Si hay bloques de código, copia solo el código, concatenando si hay varios.
                bloquesDeCodigo.forEach(bloque => {
                    const elementoCodigo = bloque.querySelector('code');
                    if (elementoCodigo) {
                        textoACopiar += elementoCodigo.innerText + '\n\n';
                    }
                });
                textoACopiar = textoACopiar.trim(); // Limpia espacios/saltos de línea extra al final.
            } else {
                // Si no hay código, copia el texto completo de la respuesta.
                const elementoContenido = responseContainer.querySelector('.markdown');
                if (elementoContenido) {
                    textoACopiar = elementoContenido.innerText;
                }
            }

            if (textoACopiar) {
                navigator.clipboard.writeText(textoACopiar).then(() => {
                    if (icono) {
                        const iconoOriginal = icono.textContent;
                        icono.textContent = 'check'; // Feedback visual de éxito.
                        setTimeout(() => {
                            icono.textContent = iconoOriginal;
                        }, 2000);
                    }
                }).catch(err => console.error('Error al copiar el contenido modificado: ', err));
            }
        });

        // Marcamos el contenedor como modificado para no volver a procesarlo.
        responseContainer.dataset.nativeCopyModified = 'true';
    }

    /**
     * Agrega un botón de copiar directo a la barra de herramientas del Canvas (editor de código).
     * @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');

        // Si no existe el contenedor o el botón de compartir, o si nuestro botón ya fue agregado, no hace nada.
        if (!actionsContainer || !shareButtonTrigger || panelCanvas.querySelector('.copiar-canvas-button')) return;

        const botonCopiar = document.createElement('button');
        botonCopiar.className = 'mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-mdc-tooltip-trigger icon-button copiar-canvas-button mat-unthemed';
        botonCopiar.setAttribute('mat-icon-button', '');
        botonCopiar.setAttribute('mattooltip', 'Copiar código');
        botonCopiar.addEventListener('click', (e) => {
            e.stopPropagation();
            e.preventDefault();
            // Simula un clic en el botón "Compartir" para abrir el menú contextual.
            shareButtonTrigger.click();
            
            // Espera un instante a que el menú aparezca en el DOM.
            setTimeout(() => {
                const menuPanel = document.querySelector('.mat-mdc-menu-panel.mat-mdc-menu-panel');
                if (menuPanel) {
                    // Busca el botón de copia original dentro del menú.
                    const originalCopyButton = menuPanel.querySelector('copy-button button');
                    if (originalCopyButton) {
                        // Simula un clic en el botón de copia original para usar la lógica de Gemini.
                        originalCopyButton.click();
                        
                        // Proporciona feedback visual en nuestro propio botón.
                        const icono = botonCopiar.querySelector('mat-icon');
                        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.
        });

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

        // Inserta nuestro nuevo botón antes del botón de compartir.
        actionsContainer.insertBefore(botonCopiar, shareButtonTrigger.parentElement.parentElement);
        panelCanvas.dataset.canvasCopyButtonAdded = 'true';
    }

    // MutationObserver vigila los cambios en la página para aplicar las mejoras dinámicamente.
    const observer = new MutationObserver(() => {
        // Modifica el botón de copia en las respuestas del chat que no hayan sido procesadas.
        document.querySelectorAll('model-response:not([data-native-copy-modified])').forEach(modificarBotonDeCopiaChat);

        // Agrega el botón de copia en el panel de código (Canvas) si aún no lo tiene.
        document.querySelectorAll('code-immersive-panel:not([data-canvas-copy-button-added])').forEach(agregarBotonDeCopiaCanvas);
    });

    // Inicia la observación del cuerpo del documento para detectar nuevos elementos.
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
})();