Google Plus & Bing Plus

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         Google Plus & Bing Plus
// @version      8.6
// @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*
// @match        https://www.google.co.kr/search*
// @match        https://*.google.com/search*
// @match        https://*.google.co.kr/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-3.1-flash-lite',
            BASE_URL: 'https://generativelanguage.googleapis.com/v1beta/models/',
            MARKED_CDN: 'https://api.cdnjs.com/libraries/marked'
        },
        MODELS: [
            { id: 'gemini-3.1-flash-lite', name: '3.1 Flash Lite' },
            { id: 'gemini-3.5-flash', name: '3.5 Flash' },
            { id: 'gemini-3.1-pro-preview', name: '3.1 Pro' }
        ],
        STORAGE: {
            PREFIX: 'gemini_',
            API_KEY: 'geminiApiKey',
            ENABLED: 'geminiEnabled',
            RAG_ENABLED: 'geminiRagEnabled',
            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.'),
            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'),
        isRagEnabled: () => Utils.Storage.getBool(Config.STORAGE.RAG_ENABLED, true),
        setRagEnabled: (v) => Utils.Storage.set(Config.STORAGE.RAG_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: #1e1e1e;
                    color: #abb2bf;
                    padding: 38px 12px 12px 12px;
                    border-radius: 8px;
                    overflow-x: auto;
                    margin: 12px 0;
                    position: relative;
                    border: 1px solid var(--g-border);
                }
                .gemini-dark-mode #gemini-content pre {
                    background: #181a1f;
                }
                .code-header {
                    position: absolute;
                    top: 0; left: 0; right: 0;
                    height: 30px;
                    background: rgba(0, 0, 0, 0.3);
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    padding: 0 12px;
                    font-size: 11px;
                    font-family: monospace;
                    color: #888;
                    border-bottom: 1px solid rgba(255, 255, 255, 0.05);
                }
                .code-copy-btn-new {
                    background: transparent;
                    color: #abb2bf;
                    border: none;
                    font-size: 11px;
                    cursor: pointer;
                    padding: 2px 6px;
                    border-radius: 4px;
                    transition: background 0.2s;
                }
                .code-copy-btn-new:hover {
                    background: rgba(255, 255, 255, 0.1);
                }
                #gemini-divider { height: 1px; border: 0; background: var(--g-border); margin: 8px 0 16px 0; }

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

                /* RAG OFF diagonal slash */
                #gemini-rag-toggle-btn { position: relative; }
                #gemini-rag-toggle-btn.rag-off::after {
                    content: ''; position: absolute; inset: 0; pointer-events: none;
                    background: linear-gradient(
                        to bottom left,
                        transparent calc(50% - 1px),
                        var(--g-btn-border) calc(50% - 1px),
                        var(--g-btn-border) calc(50% + 1px),
                        transparent calc(50% + 1px)
                    );
                }
            `;
            GM_addStyle(css);
            document.documentElement.classList.add(Utils.Env.isMobile() ? 'mobile-ua' : 'desktop-ua');
        }
    };

    /**
     * ==========================================================================================
     * 5. API & SERVICES
     * ==========================================================================================
     */
    const SearchScraper = {
        getResults() {
            if (!State.isRagEnabled()) return []; // RAG 비활성화 시 빈 결과 반환
            const results = [];
            const isGoogle = Utils.Env.isGoogle();
            const isBing = Utils.Env.isBing();

            if (isGoogle) {
                // 구글 검색 결과 아이템 셀렉터 (div.g, div.MjjYud, .tF2Cxc 등 포함)
                const items = document.querySelectorAll('div.g, div.MjjYud, .tF2Cxc');
                items.forEach(item => {
                    const titleEl = item.querySelector('h3');
                    // 다양한 스니펫 셀렉터 대응 (새 레이아웃 대응용 line-clamp 스타일 div 및 추가 클래스 포함)
                    const snippetEl = item.querySelector('div.VwiC3b, span.aCOpbc, div.MUxGec, div[style*="-webkit-line-clamp"], .yXM1m, .kb0pb');
                    if (titleEl && snippetEl) {
                        const title = titleEl.textContent.trim();
                        const snippet = snippetEl.textContent.trim();
                        if (title && snippet) {
                            results.push({ title, snippet });
                        }
                    }
                });
            } else if (isBing) {
                // 빙 검색 결과 아이템 셀렉터 (li.b_algo)
                const items = document.querySelectorAll('li.b_algo');
                items.forEach(item => {
                    const titleEl = item.querySelector('h2');
                    const snippetEl = item.querySelector('div.b_caption p, .b_algoSlug, div.b_attribution + p, p');
                    if (titleEl && snippetEl) {
                        const title = titleEl.textContent.trim();
                        const snippet = snippetEl.textContent.trim();
                        if (title && snippet) {
                            results.push({ title, snippet });
                        }
                    }
                });
            }

            // 중복 제거 및 상위 5개 결과 추출
            const unique = [];
            const seen = new Set();
            for (const res of results) {
                const key = res.title + res.snippet;
                if (!seen.has(key)) {
                    seen.add(key);
                    unique.push(res);
                }
            }
            return unique.slice(0, 5);
        }
    };

    const GeminiAPI = {
        buildRagPrompt(query, results) {
            const defaultPrompt = Utils.I18n.get('prompt', { query });
            if (!results || results.length === 0) {
                return defaultPrompt;
            }

            let context = "Below are the top search results from the search engine for this query:\n\n";
            results.forEach((res, index) => {
                context += `[Search Result ${index + 1}]\nTitle: ${res.title}\nSnippet: ${res.snippet}\n\n`;
            });

            const lang = navigator.language.includes('ko') ? 'ko' : 'default';
            if (lang === 'ko') {
                return `${context}
위 제공된 검색 결과 요약(Search Results)들을 신뢰성 있는 주요 정보원으로 사용하여, 다음 질문에 친절하고 상세하게 한국어로 답변해 주세요.
답변할 때 다음 지침을 지키십시오:
1. 제공된 검색 결과의 핵심 팩트를 우선적으로 정리하고 왜곡하지 마세요.
2. 필요하다면 본인의 일반 지식(우회 지식)을 결합하여 설명하되, 검색 결과와 모순되거나 잘못된 정보가 생기지 않도록 주의하세요.
3. 답변은 읽기 쉽도록 명확한 문단 구분과 마크다운(Markdown) 형식을 사용해 작성해 주세요.

질문: ${query}`;
            } else {
                return `${context}
Using the provided search results as the primary reliable sources of information, write a helpful and detailed response to the query: "${query}"
Please follow these guidelines:
1. Synthesize the core facts from the search results accurately without distortion.
2. If necessary, combine with your general knowledge, but ensure there are no contradictions with the search results.
3. Write the response in a structured and easy-to-read markdown format.`;
            }
        },

        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 results = SearchScraper.getResults();

            // RAG 뱃지 노출 상태 업데이트
            const badge = document.getElementById('gemini-rag-badge');
            if (badge) {
                if (results.length > 0) {
                    badge.style.display = 'inline-block';
                    badge.title = `${results.length}개의 검색 결과를 반영하여 보강된 답변입니다.`;
                } else {
                    badge.style.display = 'none';
                }
            }

            const prompt = this.buildRagPrompt(query, results);
            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); // 시간 저장
                            sessionStorage.setItem(`${Config.STORAGE.PREFIX}${query}_rag_count`, results.length); // RAG 개수 저장
                            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.') && 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) {
                            try {
                                // Bing uses 'a1' prefix followed by base64 encoded URL
                                const target = u.replace(/^a1/, '');
                                const b64 = target.replace(/-/g, '+').replace(/_/g, '/');
                                // Add padding if needed
                                const pad = b64.length % 4;
                                const padded = pad ? b64 + '='.repeat(4 - pad) : b64;

                                const decoded = decodeURIComponent(atob(padded));
                                if (decoded.startsWith('http://') || decoded.startsWith('https://')) {
                                    a.href = decoded;
                                }
                            } catch (e) {
                                // If base64 fails, fallback to standard link
                            }
                        }
                    }
                } 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)}`) },
                style: { flex: '6' }
            });

            // 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); } },
                style: { flex: '2' }
            });
            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);
            });

            // 3. RAG Toggle Button (ON/OFF 상태를 극명하게 구별하는 디자인 적용)
            const ragEnabled = State.isRagEnabled();
            const ragBtn = UI.create('div', {
                id: 'gemini-rag-toggle-btn',
                className: ragEnabled ? 'gemini-btn' : 'gemini-btn rag-off',
                html: '<span>RAG</span>',
                style: {
                    flex: '2',
                    background: ragEnabled
                        ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important'
                        : 'rgba(239, 68, 68, 0.08) !important',
                    color: ragEnabled
                        ? '#fff !important'
                        : '#ef4444 !important',
                    border: '1px solid var(--g-btn-border)'
                },
                attributes: { title: ragEnabled ? 'RAG ON (실시간 검색 보강 사용 중)' : 'RAG OFF (실시간 검색 보강 사용 안 함)' },
                events: {
                    click: () => {
                        const next = !State.isRagEnabled();
                        State.setRagEnabled(next);
                        ragBtn.classList.toggle('rag-off', !next);
                        ragBtn.style.background = next ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'rgba(239, 68, 68, 0.08)';
                        ragBtn.style.color = next ? '#fff' : '#ef4444';
                        ragBtn.style.border = '1px solid var(--g-btn-border)';
                        ragBtn.setAttribute('title', next ? 'RAG ON (실시간 검색 보강 사용 중)' : 'RAG OFF (실시간 검색 보강 사용 안 함)');
                        App.refresh(true);
                    }
                }
            });

            row.appendChild(btn);
            row.appendChild(select);
            row.appendChild(ragBtn);
            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 saveKey = () => {
                const val = input.value.trim();
                if (val) { State.setApiKey(val); App.refresh(true); }
            };

            const btn = UI.create('button', {
                id: 'gemini-api-save-btn', html: 'Save & Refresh', events: {
                    click: saveKey
                }
            });

            input.addEventListener('keydown', (e) => {
                if (e.key === 'Enter') {
                    saveKey();
                }
            });

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

            // RAG Status Badge (Premium Styling)
            const ragBadge = UI.create('span', {
                id: 'gemini-rag-badge',
                html: 'RAG',
                style: {
                    fontSize: '10px',
                    background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
                    color: '#fff',
                    padding: '2px 6px',
                    borderRadius: '4px',
                    fontWeight: 'bold',
                    marginLeft: '8px',
                    display: 'none',
                    verticalAlign: 'middle',
                    boxShadow: '0 1px 3px rgba(0,0,0,0.12)'
                }
            });

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

            titleContainer.append(titleLink, ragBadge, 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-header')) return;

                // Extract language from class if marked added it, e.g. class="language-javascript"
                const codeEl = pre.querySelector('code');
                let lang = 'code';
                if (codeEl) {
                    const match = [...codeEl.classList].find(c => c.startsWith('language-'));
                    if (match) {
                        lang = match.replace('language-', '');
                    }
                }

                const header = UI.create('div', {
                    className: 'code-header',
                    html: `<span class="code-lang">${lang.toUpperCase()}</span>`
                });

                const btn = UI.create('button', {
                    className: 'code-copy-btn-new',
                    html: 'Copy',
                    events: {
                        click: () => {
                            const code = codeEl?.innerText || pre.innerText;
                            const fallbackCopy = (text) => {
                                const textArea = document.createElement("textarea");
                                textArea.value = text;
                                textArea.style.position = "fixed";
                                document.body.appendChild(textArea);
                                textArea.focus();
                                textArea.select();
                                try {
                                    document.execCommand('copy');
                                    btn.textContent = 'Copied!';
                                } catch (err) {
                                    btn.textContent = 'Failed';
                                }
                                document.body.removeChild(textArea);
                                setTimeout(() => btn.textContent = 'Copy', 2000);
                            };

                            if (navigator.clipboard && navigator.clipboard.writeText) {
                                navigator.clipboard.writeText(code).then(() => {
                                    btn.textContent = 'Copied!';
                                    setTimeout(() => btn.textContent = 'Copy', 2000);
                                }).catch(() => fallbackCopy(code));
                            } else {
                                fallbackCopy(code);
                            }
                        }
                    }
                });

                header.appendChild(btn);
                pre.prepend(header);
            });
        }
    };

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

                // 캐시된 RAG 뱃지 상태 복원
                const cachedRagCount = sessionStorage.getItem(`${Config.STORAGE.PREFIX}${query}_rag_count`);
                const badge = document.getElementById('gemini-rag-badge');
                if (badge) {
                    if (cachedRagCount && parseInt(cachedRagCount) > 0) {
                        badge.style.display = 'inline-block';
                        badge.title = `${cachedRagCount}개의 검색 결과를 반영하여 보강된 답변입니다.`;
                    } else {
                        badge.style.display = 'none';
                    }
                }

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

            // Listen to standard popstate and hashchange
            window.addEventListener('popstate', check);
            window.addEventListener('hashchange', check);

            // Hook History API pushState/replaceState
            const originalPush = history.pushState;
            history.pushState = function (...args) {
                originalPush.apply(this, args);
                check();
            };
            const originalReplace = history.replaceState;
            history.replaceState = function (...args) {
                originalReplace.apply(this, args);
                check();
            };
        }
    };

    // Run
    App.init();

})();