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 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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