✨ Gemini Key: One-Click Web Intelligence & Spark Magic

Add a spark of magic to your browsing. Background fetch page content with Ctrl+Middle Click or Alt+G, and seamlessly send it to Gemini with automatic model switching.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name               ✨ Gemini Key: One-Click Web Intelligence & Spark Magic
// @name:en            ✨ Gemini Key: One-Click Web Intelligence & Spark Magic
// @name:zh-TW         ✨ Gemini Key:一鍵網頁文章傳送與智慧火花
// @name:zh-CN         ✨ Gemini Key:一键网页文章传送与智慧火花
// @name:ja            ✨ Gemini Key: ワンクリックウェブ記事転送&スパークマジック
// @name:ko            ✨ Gemini Key: 원클릭 웹 문서 전송 및 스파크 매직
// @name:es            ✨ Gemini Key: Inteligencia web con un clic y magia Spark
// @author             WellsTsai
// @namespace          wellstsai.com
// @version            2026.5.20.1
// @description        Add a spark of magic to your browsing. Background fetch page content with Ctrl+Middle Click or Alt+G, and seamlessly send it to Gemini with automatic model switching.
// @description:en     Add a spark of magic to your browsing. Background fetch page content with Ctrl+Middle Click or Alt+G, and seamlessly send it to Gemini with automatic model switching.
// @description:zh-TW 為您的瀏覽體驗注入智慧魔法。按住 Ctrl+中鍵或 Alt+G 鍵,即可在背景抓取網頁內容並自動切換模型傳送至 Gemini 進行深度分析。
// @description:zh-CN 为您的浏览体验注入智慧魔法。按住 Ctrl+中键或 Alt+G 键,即可在背景抓取网页内容并自动切换模型传送至 Gemini 进行深度分析。
// @description:ja    ネットサーフィンにスパークマジックを。Ctrl+中クリックまたはAlt+Gキーでコンテンツを抽出し、自動モデル切り替え機能でGeminiに送信して要約・分析します。
// @description:ko    브라우징에 스파크 매직을 더해보세요. Ctrl+휠 클릭 또는 Alt+G로 본문을 백그라운드에서 추출하여 모델 자동 전환과 함께 Gemini로 전송합니다.
// @description:es    Añade un toque de magia Spark a tu navegación. Extrae contenido en segundo plano con Ctrl+Clic Central o Alt+G, y envíalo a Gemini con cambio de modelo automático.
// @match             *://*/*
// @grant             GM_openInTab
// @grant             GM_xmlhttpRequest
// @grant             GM_setValue
// @grant             GM_getValue
// @grant             GM_deleteValue
// @grant             GM_registerMenuCommand
// @connect           *
// @license           MIT
// ==/UserScript==

(function() {
    'use strict';

    if (window.top !== window.self) return;

    // ==========================================
    // [ Configurations & Constants ]
    // ==========================================
    const CONFIG = {
        MAX_LEN: 25000,
        MIN_LEN: 100,
        TARGET_IDX: 1, // Target model index (1-based: 1 represents the first option in the menu)
        KEY: 'pending_gemini_prompt',
        GEMINI_URL: 'https://gemini.google.com'
    };

    const SELECTORS = {
        READY: 'h1[data-test-id="message"], .zero-state-container',
        MENU_BTN: 'button[data-test-id="bard-mode-menu-button"]',
        MENU_ITEM: 'gem-menu-item',
        EDITOR: '.ql-editor[contenteditable="true"], div[contenteditable="true"]',
        SEND_BTN: 'button:has(mat-icon[data-mat-icon-name="arrow_upward"]), button:has(mat-icon[data-mat-icon-name="send"])'
    };

    // ==========================================
    // [ Utility Helpers ]
    // ==========================================
    const sleep = ms => new Promise(r => setTimeout(r, ms));

    // Lightweight DOM observer targeting a specific CSS selector
    const waitEl = (sel, timeout = 10000) => new Promise((res, rej) => {
        let el = document.querySelector(sel);
        if (el) return res(el);
        const obs = new MutationObserver(() => {
            if (el = document.querySelector(sel)) { obs.disconnect(); res(el); }
        });
        obs.observe(document, { childList: true, subtree: true });
        setTimeout(() => { obs.disconnect(); rej(new Error(`Timeout: ${sel}`)); }, timeout);
    });

    // High-fidelity click simulator designed to bypass Angular/Material MDC event boundaries
    const click = el => {
        if (!el) return;
        el.focus?.();
        const win = el.ownerDocument?.defaultView || window;
        for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
            const Cls = type.startsWith('pointer') ? PointerEvent : MouseEvent;
            const init = { bubbles: true, cancelable: true, composed: true, detail: 1, buttons: type.includes('down') ? 1 : 0 };
            try {
                el.dispatchEvent(new Cls(type, { ...init, view: win }));
            } catch {
                el.dispatchEvent(new Cls(type, init)); // Fallback mode for restricted sandboxed environments
            }
        }
    };

    const cleanText = root => {
        const clone = root.cloneNode(true);
        clone.querySelectorAll('script, style, noscript, nav, header, footer, aside, iframe, .ads, .menu').forEach(el => el.remove());
        return clone.innerText.replace(/\n\s*\n/g, '\n').replace(/\s{2,}/g, ' ').trim();
    };

    const sendToGemini = (title, url, text) => {
        let promptText = text;
        if (promptText.length > CONFIG.MAX_LEN) {
            promptText = promptText.substring(0, CONFIG.MAX_LEN) + '\n\n...(文章過長,已自動截斷)';
        }

        const finalPrompt = `請忽略所有客套話與開場白。這是一篇網路文章,請幫我萃取精華,但【絕對不要過度簡化而破壞了作者原本的思路與邏輯脈絡】,使用台灣正體中文,豐富地說明呈現。\n\n` +
            `請以排版清晰、易於掃讀的方式,提供以下三點資訊:\n` +
            `1. 🎯【一句話總結】:精準點出這篇文章到底在說什麼、或解決什麼問題。\n` +
            `2. 📝【重點解析】:條列文章中的全部關鍵資訊。\n` +
            `   - 如果是新聞/科技:列出新功能、規格、影響或重點數據。\n` +
            `   - 如果是論壇/討論:列出樓主痛點、網友主要共識或正反意見。\n` +
            `   - 如果是評測/食記:列出優缺點、特色或踩雷警告。\n` +
            `3. 💡【結論與後續】:文章最終的結論、解決方案,或是值得讀者注意的事項。\n\n` +
            `【文章標題】:${title}\n【來源網址】:${url}\n【原文內容】:\n${promptText}`;

        GM_setValue(CONFIG.KEY, finalPrompt);
        GM_openInTab(`${CONFIG.GEMINI_URL}/app`, { active: false, insert: true, setParent: true });
        console.log('[Universal->Gemini] Task prepared. New Gemini tab spawned.');
    };

    // ==========================================
    // [ Phase 1 ] Source Page Logic
    // ==========================================
    const initSource = () => {
        const fetchAndSend = (title, url) => {
            if (!url || url.startsWith('javascript:')) return;
            document.body.style.cursor = 'wait';

            GM_xmlhttpRequest({
                method: "GET", url,
                onload: ({ responseText }) => {
                    document.body.style.cursor = 'default';
                    const doc = new DOMParser().parseFromString(responseText, "text/html");
                    const text = cleanText(doc.body);
                    if (text.length < CONFIG.MIN_LEN) return alert('⚠️ 抓取到的文字過少,請直接進入網頁使用 Alt+G。');
                    sendToGemini(title || doc.title || 'Untitled Article', url, text);
                },
                onerror: () => {
                    document.body.style.cursor = 'default';
                    alert('Failed to retrieve article content.');
                }
            });
        };

        const sendCurrent = () => sendToGemini(document.title, location.href, cleanText(document.body));

        GM_registerMenuCommand('✨ 摘要當前網頁並傳送至 Gemini (Alt+G)', sendCurrent);

        window.addEventListener('keydown', e => {
            if (e.altKey && e.key.toLowerCase() === 'g') {
                e.preventDefault(); sendCurrent();
            }
        });

        const onMiddleClick = e => {
            if (e.button === 1 && e.ctrlKey) {
                const a = e.target.closest('a');
                if (a?.href) {
                    e.preventDefault(); e.stopPropagation();
                    if (e.type === 'auxclick') fetchAndSend(a.innerText.trim() || a.title || 'Untitled Link', a.href);
                }
            }
        };

        window.addEventListener('mousedown', onMiddleClick);
        window.addEventListener('auxclick', onMiddleClick);
    };

    // ==========================================
    // [ Phase 2 ] Gemini Page Logic
    // ==========================================
    const initGemini = async () => {
        const prompt = GM_getValue(CONFIG.KEY);
        if (!prompt) return;

        GM_deleteValue(CONFIG.KEY);
        console.log('[Universal->Gemini] Pending task detected. Waiting for page to load...');

        try {
            await waitEl(SELECTORS.READY);
            await sleep(300);
        } catch (e) {
            console.warn('[Universal->Gemini] Welcome UI timeout. Proceeding with execution...', e);
        }

        // Switch Model
        try {
            const trigger = await waitEl(SELECTORS.MENU_BTN);
            click(trigger);
            await sleep(200); // Yield execution to allow transition of menu items

            await waitEl(SELECTORS.MENU_ITEM);
            const items = document.querySelectorAll(SELECTORS.MENU_ITEM);
            const target = items[CONFIG.TARGET_IDX - 1];

            if (target) {
                click(target);
                console.log(`[Universal->Gemini] Model switched successfully.`);
            } else {
                click(document.body); // Attempt to dismiss potential stray overlays
            }
            await sleep(500);
        } catch (e) {
            console.warn("[Universal->Gemini] Model switching process failed or timed out", e);
        }

        // Inject Prompt & Dispatch
        try {
            const editor = await waitEl(SELECTORS.EDITOR);
            editor.focus();
            document.execCommand('insertText', false, prompt);
            editor.dispatchEvent(new Event('input', { bubbles: true }));

            await sleep(800);
            const sendBtn = await waitEl(SELECTORS.SEND_BTN);
            click(sendBtn);
            console.log('[Universal->Gemini] Message dispatched automatically.');
        } catch (e) {
            console.error("[Universal->Gemini] Text injection or dispatch process failed", e);
        }
    };

    if (location.origin === CONFIG.GEMINI_URL) {
        initGemini();
    } else {
        initSource();
    }
})();