Google Plus & Bing Plus

Add Gemini response, improve speed to search results(Bing), add Google/Bing search buttons, Gemini On/Off toggle

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Google Plus & Bing Plus
// @version      8.3
// @description  Add Gemini response, improve speed to search results(Bing), add Google/Bing search buttons, Gemini On/Off toggle
// @author       monit8280
// @match        https://www.bing.com/search*
// @match        https://www.google.com/search*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @connect      generativelanguage.googleapis.com
// @connect      api.cdnjs.com
// @require      https://cdnjs.cloudflare.com/ajax/libs/marked/16.3.0/lib/marked.umd.js
// @license      MIT
// @namespace http://tampermonkey.net/
// ==/UserScript==

(function () {
    'use strict';

    /**
     * ==========================================================================================
     * 1. CONFIGURATION & CONSTANTS
     * ==========================================================================================
     */
    const Config = {
        API: {
            DEFAULT_MODEL: 'gemini-2.5-flash',
            BASE_URL: 'https://generativelanguage.googleapis.com/v1beta/models/',
            MARKED_CDN: 'https://api.cdnjs.com/libraries/marked'
        },
        MODELS: [
            { id: 'gemini-2.5-flash', name: '2.5 Flash' },
            { id: 'gemini-2.5-flash-lite', name: '2.5 Flash Lite' },
            { id: 'gemini-2.5-pro', name: '2.5 Pro' },
            { id: 'gemini-3-flash-preview', name: '3.0 Flash' },
            { id: 'gemini-3-pro-preview', name: '3.0 Pro' }
        ],
        STORAGE: {
            PREFIX: 'gemini_',
            API_KEY: 'geminiApiKey',
            ENABLED: 'geminiEnabled',
            THEME: 'themeMode',
            MODEL: 'selectedModel',
            LAST_NOTIFIED: 'markedLastNotifiedVersion',
            CURRENT_VERSION: 'markedCurrentVersion',
            LATEST_VERSION: 'markedLatestVersion'
        },
        UI: {
            MARGIN: 8,
            PADDING: 16,
            BORDER_RADIUS: '12px', // Rounded aesthetics
            ICON_SIZE: '20px',
            LOGO_SIZE: '24px',
            SMALL_ICON_SIZE: '16px',
            ANIMATION_SPEED: '0.2s'
        },
        ASSETS: {
            GOOGLE_LOGO: 'https://www.gstatic.com/images/branding/searchlogo/ico/favicon.ico',
            BING_LOGO: 'https://account.microsoft.com/favicon.ico',
            GEMINI_LOGO: 'https://www.gstatic.com/lamda/images/gemini_sparkle_aurora_33f86dc0c0257da337c63.svg',
            REFRESH_ICON: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik00LjA2MTg5IDEzQzQuMDIxMDQgMTIuNjcyNCA0IDEyLjMzODcgNCAxMkM0IDcuNTgxNzIgNy41ODE3MiA0IDEyIDRDMTQuNTAwNiA0IDE2LjczMzIgNS4xNDcyNyAxOC4yMDAyIDYuOTQ0MTZNMTkuOTM4MSAxMUMxOS45NzkgMTEuMzI3NiAyMCAxMS42NjEzIDIwIDEyQzIwIDE2LjQxODMgMTYuNDE4MyAyMCAxMiAyMEM5LjYxMDYxIDIwIDcuNDY1ODkgMTguOTUyNSA2IDE3LjI5MTZNOSAxN0g2VjE3LjI5MTZNMTguMjAwMiA0VjYuOTQ0MTZNMTguMjAwMiA2Ljk0NDE2VjYuOTk5OTNMMTUuMjAwMiA3TTYgMjBWMTcuMjkxNiIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPg0KPC9zdmc+',
            LIGHT_ICON: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gVXBsb2FkZWQgdG86IFNWRyBSZXBvLCB3d3cuc3ZncmVwby5jb20sIEdlbmVyYXRvcjogU1ZHIFJlcG8gTWl4ZXIgVG9vbHMgLS0+DQo8c3ZnIHdpZHRoPSI4MDBweCIgaGVpZ2h0PSI4MDBweCIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgY2xhc3M9Imljb24iICB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTg2MSA2NTYuN2wxNDQuNi0xNDQuNkw4NjEgMzY3LjZWMTYzLjFINjU2LjZMNTEyIDE4LjYgMzY3LjQgMTYzLjFIMTYzdjIwNC41TDE4LjQgNTEyLjEgMTYzIDY1Ni43djIwNC40aDIwNC40TDUxMiAxMDA1LjdsMTQ0LjYtMTQ0LjZIODYxeiIgZmlsbD0iI0ZDRDE3MCIgLz48cGF0aCBkPSJNNTEyIDEwMTUuN2MtMi42IDAtNS4xLTEtNy4xLTIuOUwzNjMuMyA4NzEuMUgxNjNjLTUuNSAwLTEwLTQuNS0xMC0xMFY2NjAuOEwxMS40IDUxOS4yYy0xLjktMS45LTIuOS00LjQtMi45LTcuMSAwLTIuNyAxLjEtNS4yIDIuOS03LjFMMTUzIDM2My40VjE2My4xYzAtNS41IDQuNS0xMCAxMC0xMGgyMDAuM0w1MDQuOSAxMS41YzEuOS0xLjkgNC40LTIuOSA3LjEtMi45czUuMiAxLjEgNy4xIDIuOWwxNDEuNiAxNDEuNkg4NjFjNS41IDAgMTAgNC41IDEwIDEwdjIwMC4zTDEwMTIuNiA1MDVjMS45IDEuOSAyLjkgNC40IDIuOSA3LjEgMCAyLjctMS4xIDUuMi0yLjkgNy4xTDg3MSA2NjAuOHYyMDAuM2MwIDUuNS00LjUgMTAtMTAgMTBINjYwLjdsLTE0MS42IDE0MS42Yy0yIDItNC41IDMtNy4xIDN6TTE3MyA4NTEuMWgxOTQuNGMyLjcgMCA1LjIgMS4xIDcuMSAyLjlMNTEyIDk5MS42bDEzNy41LTEzNy41YzEuOS0xLjkgNC40LTIuOSA3LjEtMi45SDg1MVY2NTYuN2MwLTIuNyAxLjEtNS4yIDIuOS03LjFsMTM3LjUtMTM3LjUtMTM3LjUtMTM3LjVjLTEuOS0xLjktMi45LTQuNC0yLjktNy4xVjE3My4xSDY1Ni42Yy0yLjcgMC01LjItMS4xLTcuMS0yLjlMNTEyIDMyLjcgMzc0LjUgMTcwLjJjLTEuOSAxLjktNC40IDIuOS03LjEgMi45SDE3M3YxOTQuNGMwIDIuNy0xLjEgNS4yLTIuOSA3LjFMMzIuNiA1MTIuMWwxMzcuNSAxMzcuNWMxLjkgMS45IDIuOSA0LjQgMi45IDcuMXYxOTQuNHoiIGZpbGw9IiIgLz48cGF0aCBkPSJNNTEyIDUxMi4xbS0yNTcuOCAwYTI1Ny44IDI1Ny44IDAgMSAwIDUxNS42IDAgMjU3LjggMjU3LjggMCAxIDAtNTE1LjYgMFoiIGZpbGw9IiNGN0REQUQiIC8+PHBhdGggZD0iTTUxMiA3NzkuOWMtNzEuNSAwLTEzOC44LTI3LjktMTg5LjQtNzguNC01MC42LTUwLjYtNzguNC0xMTcuOC03OC40LTE4OS40czI3LjktMTM4LjggNzguNC0xODkuNGM1MC42LTUwLjYgMTE3LjgtNzguNCAxODkuNC03OC40IDcxLjUgMCAxMzguOCAyNy45IDE4OS40IDc4LjQgNTAuNiA1MC42IDc4LjQgMTE3LjggNzguNCAxODkuNFM3NTIgNjUwLjkgNzAxLjQgNzAxLjUgNTgzLjUgNzc5LjkgNTEyIDc3OS45eiBtMC01MTUuNmMtNjYuMiAwLTEyOC40IDI1LjgtMTc1LjIgNzIuNi00Ni44IDQ2LjgtNzIuNiAxMDktNzIuNiAxNzUuMnMyNS44IDEyOC40IDcyLjYgMTc1LjJjNDYuOCA0Ni44IDEwOSA3Mi42IDE3NS4yIDcyLjYgNjYuMiAwIDEyOC40LTI1LjggMTc1LjItNzIuNiA0Ni44LTQ2LjggNzIuNiAxMDkgNzIuNi0xNzUuMlM3MzQgMzgzLjcgNjg3LjIgMzM2LjljLTQ2LjgtNDYuOC0xMDktNzIuNi0xNzUuMi03Mi42eiIgZmlsbD0iIiAvPjwvc3ZnPg==',
            DARK_ICON: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xOS45MDAxIDIuMzA3MTlDMTkuNzM5MiAxLjg5NzYgMTkuMTYxNiAxLjg5NzYgMTkuMDAwNyAyLjMwNzE5TDE4LjU3MDMgMy40MDI0N0MxOC41MjEyIDMuNTI3NTIgMTguNDIyNiAzLjYyNjUxIDE4LjI5OCAzLjY3NTgzTDE3LjIwNjcgNC4xMDc4QzE2Ljc5ODYgNC4yNjkzNCAxNi43OTg2IDQuODQ5IDE3LjIwNjcgNS4wMTA1NEwxOC4yOTggNS40NDI1MkMxOC40MjI2IDUuNDkxODQgMTguNTIxMiA1LjU5MDgyIDE4LjU3MDMgNS43MTU4N0wxOS4wMDA3IDYuODExMTVDMTkuMTYxNiA3LjIyMDc0IDE5LjczOTIgNy4yMjA3NCAxOS45MDAxIDYuODExMTZMMjAuMzMwNSA1LjcxNTg3QzIwLjM3OTYgNS41OTA4MiAyMC40NzgyIDUuNDkxODQgMjAuNjAyOCA1LjQ0MjUyTDIxLjY5NDEgNS4wMTA1NEMyMi4xMDIyIDQuODQ5IDIyLjEwMjIgNC4yNjkzNCAyMS42OTQxIDQuMTA3OEwyMC42MDI4IDMuNjc1ODNDMjAuNDc4MiAzLjYyNjUxIDIwLjM3OTYgMy41Mjc1MiAyMC4zMzA1IDMuNDAyNDdMMTkuOTAwMSAyLjMwNzE5WiIgZmlsbD0iIzFDMjc0QyIvPg0KPHBhdGggZD0iTTE2LjAzMjggOC4xMjk2N0MxNS44NzE4IDcuNzIwMDkgMTUuMjk0MyA3LjcyMDA5IDE1LjEzMzMgOC4xMjk2N0wxNC45NzY0IDguNTI5MDJDMTQuOTI3MyA4LjY1NDA3IDE0LjgyODcgOC43NTMwNSAxNC43MDQxIDguODAyMzdMMTQuMzA2MiA4Ljk1OTg3QzEzLjg5ODEgOS4xMjE0MSAxMy44OTgxIDkuNzAxMDcgMTQuMzA2MiA5Ljg2MjYxTDE0LjcwNDEgMTAuMDIwMUMxNC44Mjg3IDEwLjA2OTQgMTQuOTI3MyAxMC4xNjg0IDE0Ljk3NjQgMTAuMjkzNUwxNS4xMzMzIDEwLjY5MjhDMTUuMjk0MyAxMS4xMDI0IDE1Ljg3MTggMTEuMTAyNCAxNi4wMzI4IDEwLjY5MjhMMTYuMTg5NyAxMC4yOTM1QzE2LjIzODggMTAuMTY4NCAxNi4zMzc0IDEwLjA2OTQgMTYuNDYyIDEwLjAyMDFMMTYuODU5OSA5Ljg2MjYxQzE3LjI2OCA5LjcwMTA3IDE3LjI2OCA5LjEyMTQxIDE2Ljg1OTkgOC45NTk4N0wxNi40NjIgOC44MDIzN0MxNi4zMzc0IDguNzUzMDUgMTYuMjM4OCA4LjY1NDA3IDE2LjE4OTcgOC41MjkwMkwxNi4wMzI4IDguMTI5NjdaIiBmaWxsPSIjMUMyNzRDIi8+DQo8cGF0aCBkPSJNMTIgMjJDMTcuNTIyOCAyMiAyMiAxNy41MjI4IDIyIDEyQzIyIDExLjUzNzMgMjEuMzA2NSAxMS40NjA4IDIxLjA2NzIgMTEuODU2OEMxOS45Mjg5IDEzLjc0MDYgMTcuODYxNSAxNSAxNS41IDE1QzExLjkxMDEgMTUgOSAxMi4wODk5IDkgOC41QzkgNi4xMzg0NSAxMC4yNTk0IDQuMDcxMDUgMTIuMTQzMiAyLjkzMjc2QzEyLjUzOTIgMi42OTM0NyAxMi40NjI3IDIgMTIgMkM2LjQ3NzE1IDIgMiA2LjQ3NzE1IDIgMTJDMiAxNy41MjI4IDYuNDc3MTUgMjIgMTIgMjJaIiBmaWxsPSIjMUMyNzRDIi8+DQo8L3N2Zz4='
        }
    };

    /**
     * ==========================================================================================
     * 2. CORE UTILITIES
     * ==========================================================================================
     */
    const Utils = {
        Storage: {
            get: (key) => localStorage.getItem(key),
            set: (key, val) => localStorage.setItem(key, val),
            getBool: (key, def = true) => {
                const val = localStorage.getItem(key);
                return val === null ? def : val === 'true';
            }
        },
        Env: {
            isGoogle: () => window.location.hostname.includes('google.com'),
            isBing: () => window.location.hostname.includes('bing.com'),
            getQuery: () => new URLSearchParams(window.location.search).get('q'),
            isMobile: () => {
                const ua = navigator.userAgent;
                const topWidth = window.innerWidth;
                const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
                return (/Android|iPhone|Mobile/i.test(ua)) || (isTouch && topWidth <= 1024);
            }
        },
        I18n: {
            MESSAGES: {
                prompt: { ko: `"${'${query}'}"에 대한 정보를 찾아줘`, default: `Please write information about "${'${query}'}" in markdown format` },
                enterApiKey: { ko: 'Gemini API 키 입력:', default: 'Enter Gemini API Key:' },
                geminiEmpty: { ko: '⚠️ 응답이 비어있습니다.', default: '⚠️ Response is empty.' },
                loading: { ko: '불러오는 중...', default: 'Loading...' },
                error: { ko: '오류 발생:', default: 'Error:' },
                geminiOff: { ko: 'Gemini가 꺼져있습니다.', default: 'Gemini is OFF' },
                searchGoogle: { ko: 'Google 검색', default: 'Search on Google' },
                searchBing: { ko: 'Bing 검색', default: 'Search on Bing' }
            },
            get(key, vars = {}) {
                const lang = navigator.language.includes('ko') ? 'ko' : 'default';
                const tmpl = this.MESSAGES[key]?.[lang] || this.MESSAGES[key]?.default || '';
                return tmpl.replace(/\$\{(.*?)\}/g, (_, k) => vars[k] || '');
            }
        }
    };

    /**
     * ==========================================================================================
     * 3. STATE MANAGEMENT (Theme & Settings)
     * ==========================================================================================
     */
    const State = {
        theme: 'light',
        init() {
            const saved = Utils.Storage.get(Config.STORAGE.THEME);
            this.theme = saved || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
            this.applyTheme();
            window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
                this.theme = e.matches ? 'dark' : 'light';
                this.applyTheme();
            });
        },
        toggleTheme() {
            this.theme = this.theme === 'light' ? 'dark' : 'light';
            Utils.Storage.set(Config.STORAGE.THEME, this.theme);
            this.applyTheme();
            return this.theme;
        },
        applyTheme() {
            document.documentElement.classList.toggle('gemini-dark-mode', this.theme === 'dark');
            const btn = document.getElementById('gemini-theme-toggle-btn');
            if (btn) btn.src = this.getThemeIcon();
        },
        getThemeIcon() {
            return this.theme === 'light' ? Config.ASSETS.DARK_ICON : Config.ASSETS.LIGHT_ICON;
        },
        // Model & API Key
        getApiKey: () => Utils.Storage.get(Config.STORAGE.API_KEY),
        setApiKey: (key) => Utils.Storage.set(Config.STORAGE.API_KEY, key),
        getModel: () => Utils.Storage.get(Config.STORAGE.MODEL) || Config.API.DEFAULT_MODEL,
        setModel: (model) => Utils.Storage.set(Config.STORAGE.MODEL, model),
        isEnabled: () => Utils.Storage.getBool(Config.STORAGE.ENABLED),
        setEnabled: (v) => Utils.Storage.set(Config.STORAGE.ENABLED, v ? 'true' : 'false')
    };

    /**
     * ==========================================================================================
     * 4. STYLES
     * ==========================================================================================
     */
    const Styles = {
        inject() {
            const css = `
                :root {
                    --g-bg: #fff; --g-border: #e0e0e0; --g-text: #000; --g-title: #000;
                    --g-btn-bg: #f0f3ff; --g-btn-border: #ccc; --g-code-bg: #f0f0f0;
                    --g-icon-filter: none;
                }
                .gemini-dark-mode {
                    --g-bg: #282c34; --g-border: #444; --g-text: #e0e0e0; --g-title: #fff;
                    --g-btn-bg: #3a3f4b; --g-btn-border: #555; --g-code-bg: #3b3b3b;
                    --g-icon-filter: invert(1);
                }
                /* Layout */
                #gemini-wrapper { margin-bottom: 20px; font-family: sans-serif; }
                #gemini-top-row { display: flex; gap: 8px; margin-bottom: 12px; width: 100%; }

                /* Buttons & Selects */
                .gemini-btn, .gemini-select {
                    height: 40px; border-radius: ${Config.UI.BORDER_RADIUS}; font-size: 13px;
                    display: flex; align-items: center; justify-content: center;
                    cursor: pointer; transition: all ${Config.UI.ANIMATION_SPEED};
                    border: 1px solid var(--g-btn-border);
                    background: var(--g-btn-bg) !important; /* Bing 스타일 오버라이드 */
                    color: var(--g-title) !important;
                    box-sizing: border-box; overflow: hidden; /* 오버플로우 방지 */
                }
                .gemini-btn img {
                    width: 16px !important; height: 16px !important;
                    object-fit: contain; /* 이미지 비율 유지 */
                }
                .gemini-btn:hover, .gemini-select:hover { transform: translateY(-2px); box-shadow: 0 2px 5px rgba(0,0,0,0.15); }
                #google-search-btn, #bing-search-btn { flex: 8; gap: 8px; } /* gap으로 간격 조정 */
                #gemini-model-select-top {
                    flex: 2; appearance: none; text-align-last: center; text-align: center;
                    outline: none; padding: 0 !important; margin: 0;
                }
                #gemini-model-select-top option { text-align: center; }

                /* Main Box */
                #gemini-box {
                    width: 100%; padding: ${Config.UI.PADDING}px;
                    border: 1px solid var(--g-border); border-radius: ${Config.UI.BORDER_RADIUS};
                    background: var(--g-bg); color: var(--g-text);
                    box-sizing: border-box; overflow: hidden;
                    position: relative; /* 자식 요소 위치 기준 */
                }
                #gemini-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
                #gemini-title-wrap { display: flex; align-items: center; gap: 8px; text-decoration: none; color: inherit; }
                #gemini-title-wrap h3 { font-weight: bold; margin: 0; font-size: 16px; color: var(--g-title); }
                #gemini-time-info { font-size: 12px; color: #888; font-weight: normal; margin-left: 6px; }
                #gemini-logo { width: 24px; height: 24px; cursor: pointer; transition: transform 0.2s; }
                #gemini-logo:hover { transform: scale(1.1); }
                #gemini-actions { display: flex; align-items: center; gap: 10px; }

                /* Content & Markdown */
                #gemini-content {
                    font-size: 14px; line-height: 1.6; color: var(--g-text);
                    padding-top: 4px; padding-right: 8px; /* 스크롤바 공간 확보 */
                    max-height: 650px; overflow-y: auto; scrollbar-width: thin;
                }
                #gemini-content p { margin: 0 0 12px 0; } /* 문단 간격 */
                #gemini-content::-webkit-scrollbar { width: 6px; }
                #gemini-content::-webkit-scrollbar-thumb { background-color: #ccc; border-radius: 3px; }
                .gemini-dark-mode #gemini-content::-webkit-scrollbar-thumb { background-color: #555; }
                #gemini-content h1, #gemini-content h2, #gemini-content h3 { margin-top: 20px; margin-bottom: 10px; }
                #gemini-content ul, #gemini-content ol { padding-left: 20px; margin: 10px 0; }
                #gemini-content li { margin-bottom: 5px; }
                #gemini-content pre { background: var(--g-code-bg); padding: 12px; border-radius: 6px; overflow-x: auto; margin: 10px 0; position: relative; }
                #gemini-divider { height: 1px; border: 0; background: var(--g-border); margin: 8px 0 16px 0; }

                /* Code Copy Button */
                .code-copy-btn {
                    position: absolute; top: 8px; right: 8px;
                    background: rgba(128, 128, 128, 0.2); color: var(--g-text);
                    border: 1px solid var(--g-border); border-radius: 4px;
                    font-size: 11px; padding: 4px 8px; cursor: pointer;
                    opacity: 0; transition: opacity 0.2s;
                }
                #gemini-content pre:hover .code-copy-btn { opacity: 1; }
                .code-copy-btn:hover { background: rgba(128, 128, 128, 0.4); }

                /* Icons - Specific Animations */
                .gemini-icon-btn {
                    width: 20px; height: 20px; cursor: pointer; opacity: 0.6; transition: 0.3s ease;
                    vertical-align: middle;
                }
                .gemini-icon-btn:hover { opacity: 1; }

                #gemini-theme-toggle-btn:hover { transform: scale(1.2); }

                #gemini-refresh-btn { transition: transform 0.6s ease-in-out; filter: var(--g-icon-filter); }
                #gemini-refresh-btn:hover { transform: rotate(360deg); }

                /* Toggle Switch */
                .g-toggle { width: 44px; height: 24px; border-radius: 12px; position: relative; cursor: pointer; transition: background 0.2s; }
                .g-knob { width: 22px; height: 22px; background: #fff; border-radius: 50%; position: absolute; top: 1px; left: 1px; transition: left 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.2); }
                .g-toggle.on { background: #d1d5db; } .g-toggle.off { background: #3c4043; }
                .g-toggle.on .g-knob { left: 21px; }

                /* API Key Input */
                #gemini-api-container { display: flex; flex-direction: column; gap: 10px; padding: 10px 0; }
                #gemini-api-input { width: 95%; padding: 10px; border-radius: 8px; border: 1px solid var(--g-border); background: var(--g-bg); color: var(--g-text); }
                #gemini-api-save-btn { align-self: flex-end; padding: 8px 16px; background: #4285f4; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; }

                /* Device Specific */
                .mobile-ua #gemini-box { border-radius: 16px; }
                #b_context, .b_right { width: 432px !important; }

                /* Skeleton Loading Animation */
                @keyframes shimmer {
                    0% { background-position: 200% 0; }
                    100% { background-position: -200% 0; }
                }
                .skeleton-loader { display: flex; flex-direction: column; gap: 12px; padding: 4px 0; }
                .skeleton-line {
                    height: 14px; width: 100%; border-radius: 4px;
                    background: linear-gradient(90deg, var(--g-bg) 25%, var(--g-border) 50%, var(--g-bg) 75%);
                    background-size: 200% 100%; animation: shimmer 1.5s infinite;
                }
                .skeleton-line.short { width: 60%; }
                .skeleton-line.medium { width: 85%; }
            `;
            GM_addStyle(css);
            document.documentElement.classList.add(Utils.Env.isMobile() ? 'mobile-ua' : 'desktop-ua');
        }
    };

    /**
     * ==========================================================================================
     * 5. API & SERVICES
     * ==========================================================================================
     */
    const GeminiAPI = {
        fetch(query, container, apiKey) {
            container.innerHTML = `
                <div class="skeleton-loader">
                    <div class="skeleton-line medium"></div>
                    <div class="skeleton-line"></div>
                    <div class="skeleton-line short"></div>
                </div>
            `;

            // 시간 표시 초기화
            const timeSpan = document.getElementById('gemini-time-info');
            if (timeSpan) timeSpan.textContent = '(0.00s)';

            const model = State.getModel();
            const prompt = Utils.I18n.get('prompt', { query });
            const startTime = performance.now();
            let timerId = null;

            // 스톱워치 시작
            if (timeSpan) {
                timerId = setInterval(() => {
                    const current = ((performance.now() - startTime) / 1000).toFixed(2);
                    timeSpan.textContent = `(${current}s)`;
                }, 10);
            }

            GM_xmlhttpRequest({
                method: 'POST',
                url: `${Config.API.BASE_URL}${model}:generateContent?key=${apiKey}`,
                headers: { 'Content-Type': 'application/json' },
                data: JSON.stringify({
                    contents: [{ parts: [{ text: prompt }] }]
                }),
                onload: (res) => {
                    if (timerId) clearInterval(timerId); // 타이머 정지
                    try {
                        const endTime = performance.now();
                        const duration = ((endTime - startTime) / 1000).toFixed(2);
                        if (timeSpan) timeSpan.textContent = `(${duration}s)`;

                        const data = JSON.parse(res.responseText);
                        const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
                        if (text) {
                            sessionStorage.setItem(`${Config.STORAGE.PREFIX}${query}`, text);
                            sessionStorage.setItem(`${Config.STORAGE.PREFIX}${query}_time`, duration); // 시간 저장
                            container.innerHTML = marked.parse(text);
                            UI.addCodeCopyButtons(container);
                        } else {
                            throw new Error(data.error?.message || 'Empty response');
                        }
                    } catch (e) {
                        container.textContent = `${Utils.I18n.get('error')} ${e.message}`;
                    }
                },
                onerror: () => {
                    if (timerId) clearInterval(timerId);
                    container.textContent = Utils.I18n.get('geminiEmpty');
                }
            });
        }
    };

    const LinkCleaner = {
        clean(root = document) {
            root.querySelectorAll('a[href]').forEach(a => {
                try {
                    const url = new URL(a.href);
                    // Google
                    if (url.hostname.includes('google.com') && url.pathname === '/url') {
                        const q = url.searchParams.get('q') || url.searchParams.get('url');
                        if (q) a.href = decodeURIComponent(q);
                    }
                    // Bing
                    else if (url.hostname.includes('bing.com') && url.pathname.includes('/ck')) {
                        const u = url.searchParams.get('u');
                        if (u) {
                            // Bing uses 'a1' prefix followed by base64 encoded URL
                            const target = u.replace(/^a1/, '');
                            // Base64 decoding might fail if padding is missing or characters are invalid
                            // Also need to handle URL safe base64 if necessary (Bing usually uses standard or simple replacement)
                            // Usually just atob working is fine for standard links.
                            // Some characters might be replaced: - -> +, _ -> /
                            const b64 = target.replace(/-/g, '+').replace(/_/g, '/');
                            // Add padding if needed
                            const pad = b64.length % 4;
                            const padded = pad ? b64 + '='.repeat(4 - pad) : b64;

                            try {
                                a.href = decodeURIComponent(atob(padded));
                            } catch (e) {
                                // If base64 fails, fallback to simple request or leave it
                            }
                        }
                    }
                } catch (e) { }
            });
        }
    };

    /**
     * ==========================================================================================
     * 6. UI COMPONENTS
     * ==========================================================================================
     */
    const UI = {
        create(tag, { id, className, style, html, attributes, events } = {}) {
            const el = document.createElement(tag);
            if (id) el.id = id;
            if (className) el.className = className;
            if (style) Object.assign(el.style, style);
            if (html) el.innerHTML = html;
            if (attributes) Object.entries(attributes).forEach(([k, v]) => el.setAttribute(k, v));
            if (events) Object.entries(events).forEach(([k, v]) => el.addEventListener(k, v));
            return el;
        },

        Controls: {
            createToggle() {
                const enabled = State.isEnabled();
                const wrap = UI.create('div', { style: { display: 'flex', alignItems: 'center', cursor: 'pointer', gap: '8px' } });

                // Text Label
                const label = UI.create('span', {
                    html: enabled ? 'ON' : 'OFF',
                    style: { fontSize: '13px', fontWeight: 'bold', color: enabled ? 'var(--g-title)' : '#888' }
                });

                // Switch
                const switchEl = UI.create('div', { className: `g-toggle ${enabled ? 'on' : 'off'}` });
                switchEl.appendChild(UI.create('div', { className: 'g-knob' }));

                wrap.onclick = () => {
                    const next = !State.isEnabled();
                    State.setEnabled(next);

                    // Update UI
                    switchEl.className = `g-toggle ${next ? 'on' : 'off'}`;
                    label.textContent = next ? 'ON' : 'OFF';
                    label.style.color = next ? 'var(--g-title)' : '#888';

                    App.refresh();
                };

                wrap.append(label, switchEl);
                return wrap;
            }
        },

        createTopRow(query) {
            const row = UI.create('div', { id: 'gemini-top-row' });

            // 1. Search Switch Button
            const isGoogle = Utils.Env.isGoogle();
            const btn = UI.create('div', {
                id: isGoogle ? 'bing-search-btn' : 'google-search-btn',
                className: 'gemini-btn',
                html: `<img src="${isGoogle ? Config.ASSETS.BING_LOGO : Config.ASSETS.GOOGLE_LOGO}"> <span>${Utils.I18n.get(isGoogle ? 'searchBing' : 'searchGoogle')}</span>`,
                events: { click: () => window.open(`https://${isGoogle ? 'bing' : 'google'}.com/search?q=${encodeURIComponent(query)}`) }
            });

            // 2. Model Selector
            const select = UI.create('select', {
                id: 'gemini-model-select-top',
                className: 'gemini-select',
                events: { change: (e) => { State.setModel(e.target.value); App.refresh(true); } }
            });
            Config.MODELS.forEach(m => {
                const opt = document.createElement('option');
                opt.value = m.id; opt.textContent = m.name;
                opt.selected = m.id === State.getModel();
                select.appendChild(opt);
            });

            row.appendChild(btn);
            row.appendChild(select);
            return row;
        },

        createApiKeyInput() {
            const container = UI.create('div', { id: 'gemini-api-container' });
            container.innerHTML = `<p style="margin:0; font-size:13px; font-weight:bold;">${Utils.I18n.get('enterApiKey')}</p>`;

            const input = UI.create('input', { id: 'gemini-api-input', type: 'password', attributes: { placeholder: 'AIza...' } });
            const btn = UI.create('button', {
                id: 'gemini-api-save-btn', html: 'Save & Refresh', events: {
                    click: () => {
                        const val = input.value.trim();
                        if (val) { State.setApiKey(val); App.refresh(true); }
                    }
                }
            });

            container.appendChild(input);
            container.appendChild(btn);
            return container;
        },

        createMainBox(query) {
            const box = UI.create('div', { id: 'gemini-box' });

            // Header
            const header = UI.create('div', { id: 'gemini-header' });

            // Title Container (Wrapper)
            const titleContainer = UI.create('div', { style: { display: 'flex', alignItems: 'center', gap: '0px' } });

            // Link (Clickable)
            const titleLink = UI.create('a', {
                id: 'gemini-title-link',
                attributes: { href: 'https://gemini.google.com/', target: '_blank' },
                style: { display: 'flex', alignItems: 'center', gap: '8px', textDecoration: 'none', color: 'inherit' }
            });
            titleLink.innerHTML = `<img id="gemini-logo" src="${Config.ASSETS.GEMINI_LOGO}"> <h3 style="margin:0; font-size:16px; font-weight:bold; color:var(--g-title);">Gemini Results</h3>`;

            // Time Info (Non-clickable)
            const timeSpan = UI.create('span', { id: 'gemini-time-info' });

            titleContainer.append(titleLink, timeSpan);

            const actions = UI.create('div', { id: 'gemini-actions' });
            const toggle = this.Controls.createToggle();
            const themeBtn = UI.create('img', {
                id: 'gemini-theme-toggle-btn', className: 'gemini-icon-btn',
                attributes: { src: State.getThemeIcon(), title: 'Toggle Theme' },
                events: { click: () => { State.toggleTheme(); themeBtn.src = State.getThemeIcon(); } }
            });
            const refreshBtn = UI.create('img', {
                id: 'gemini-refresh-btn', className: 'gemini-icon-btn',
                attributes: { src: Config.ASSETS.REFRESH_ICON, title: 'Refresh' },
                events: { click: () => App.refresh(true) }
            });

            actions.append(toggle, themeBtn, refreshBtn);
            header.append(titleContainer, actions);

            // Content
            const content = UI.create('div', { id: 'gemini-content' });
            const divider = UI.create('hr', { id: 'gemini-divider' });

            box.append(header, divider, content);
            return { box, content, refreshBtn };
        },

        addCodeCopyButtons(container) {
            container.querySelectorAll('pre').forEach(pre => {
                if (pre.querySelector('.code-copy-btn')) return;

                const btn = UI.create('button', {
                    className: 'code-copy-btn',
                    html: 'Copy',
                    events: {
                        click: () => {
                            const code = pre.querySelector('code')?.innerText || pre.innerText;
                            navigator.clipboard.writeText(code).then(() => {
                                btn.textContent = 'Copied!';
                                setTimeout(() => btn.textContent = 'Copy', 2000);
                            });
                        }
                    }
                });
                pre.appendChild(btn);
            });
        }
    };

    /**
     * ==========================================================================================
     * 7. MAIN APP CONTROLLER
     * ==========================================================================================
     */
    const App = {
        elements: null,

        init() {
            State.init();
            Styles.inject();
            try { marked.setOptions({ breaks: true, gfm: true }); } catch (e) { } // 마크다운 옵션 설정
            this.handleNavigation();
            LinkCleaner.clean();

            // Delayed init to wait for page load
            const onReady = () => {
                const target = Utils.Env.isGoogle() ? document.getElementById('rhs') : (document.getElementById('b_context') || document.querySelector('.b_right'));
                // Mobile check first
                if (Utils.Env.isMobile()) {
                    this.render(null); // Container not needed for mobile logic
                } else if (target) {
                    this.render(target);
                } else if (Utils.Env.isGoogle()) {
                    // Force rhs ...
                    // Force RHS creation if missing on Google
                    const rcnt = document.getElementById('rcnt');
                    if (rcnt) {
                        const rhs = UI.create('div', { id: 'rhs', style: { marginLeft: '16px', width: '432px' } });
                        rcnt.appendChild(rhs);
                        this.render(rhs);
                    }
                }
            };

            if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', onReady);
            else onReady();
        },

        render(container) {
            const query = Utils.Env.getQuery();
            if (!query) return;

            // 1. Mobile/Tablet Mode (Search Button Only)
            if (Utils.Env.isMobile()) {
                document.getElementById('mobile-search-btn-wrapper')?.remove();
                const btn = UI.create('div', {
                    id: 'mobile-search-btn-wrapper',
                    style: { margin: '0', padding: '15px 16px', background: 'var(--g-bg)' }
                });

                // 모바일용 버튼 생성 (UI.createTopRow에서 버튼만 추출하여 사용하거나 새로 생성)
                const isGoogle = Utils.Env.isGoogle();
                const searchBtn = UI.create('div', {
                    id: isGoogle ? 'bing-search-btn' : 'google-search-btn',
                    className: 'gemini-btn',
                    html: `<img src="${isGoogle ? Config.ASSETS.BING_LOGO : Config.ASSETS.GOOGLE_LOGO}"> <span>${Utils.I18n.get(isGoogle ? 'searchBing' : 'searchGoogle')}</span>`,
                    events: { click: () => window.open(`https://${isGoogle ? 'bing' : 'google'}.com/search?q=${encodeURIComponent(query)}`) },
                    style: { width: '100%' } // 모바일은 꽉 차게
                });
                btn.appendChild(searchBtn);

                // Insert Position Logic
                if (isGoogle) {
                    const target = document.getElementById('main') || document.getElementById('rcnt');
                    if (target) target.parentNode.insertBefore(btn, target);
                } else {
                    const target = document.getElementById('b_content');
                    if (target) target.prepend(btn);
                }
                return; // Gemini 로직 중단
            }

            // 2. Desktop Mode (Existing Logic)
            if (!container) return;

            // Google Width Enforcement
            if (Utils.Env.isGoogle() && container.id === 'rhs') {
                container.style.width = '432px';
                container.style.minWidth = '432px';
            }

            // Remove existing
            document.getElementById('gemini-wrapper')?.remove();

            // Structure
            const wrapper = UI.create('div', { id: 'gemini-wrapper' });
            wrapper.appendChild(UI.createTopRow(query));

            const { box, content, refreshBtn } = UI.createMainBox(query);
            this.elements = { content, refreshBtn }; // Cache for updates
            wrapper.appendChild(box);

            container.prepend(wrapper);
            this.loadContent(query);
        },

        loadContent(query, forceRefresh = false) {
            if (!this.elements) return;
            const { content, refreshBtn } = this.elements;
            const apiKey = State.getApiKey();

            if (!apiKey) {
                content.innerHTML = '';
                content.appendChild(UI.createApiKeyInput());
                refreshBtn.style.display = 'none';
                return;
            }
            refreshBtn.style.display = 'block';

            if (!State.isEnabled()) {
                content.textContent = Utils.I18n.get('geminiOff');
                return;
            }

            const cached = !forceRefresh && sessionStorage.getItem(`${Config.STORAGE.PREFIX}${query}`);
            if (cached) {
                const cachedTime = sessionStorage.getItem(`${Config.STORAGE.PREFIX}${query}_time`);
                const timeSpan = document.getElementById('gemini-time-info');
                if (timeSpan && cachedTime) timeSpan.textContent = `(${cachedTime}s)`;

                content.innerHTML = marked.parse(cached);
                UI.addCodeCopyButtons(content);
            } else {
                GeminiAPI.fetch(query, content, apiKey);
            }
        },

        refresh(force = false) {
            if (force) {
                // 모든 Gemini 관련 캐시(sessionStorage) 삭제
                Object.keys(sessionStorage).forEach(key => {
                    if (key.startsWith(Config.STORAGE.PREFIX)) {
                        sessionStorage.removeItem(key);
                    }
                });
            }
            const query = Utils.Env.getQuery();
            if (query) this.loadContent(query, force);
        },

        handleNavigation() {
            // SPA Navigation Watcher
            let lastUrl = location.href;
            const check = () => {
                if (location.href !== lastUrl) {
                    lastUrl = location.href;
                    setTimeout(() => {
                        const target = document.getElementById('rhs') || document.querySelector('#b_context') || document.querySelector('.b_right');
                        this.render(target);
                        LinkCleaner.clean();
                    }, 500);
                }
            };
            const obs = new MutationObserver(check);
            obs.observe(document.body, { childList: true, subtree: true });
        }
    };

    // Run
    App.init();

})();