Bing Plus

Add Gemini response, improve speed to search results, add Google search buttons

目前為 2025-05-13 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Bing Plus
// @version      6.1
// @description  Add Gemini response, improve speed to search results, add Google search buttons
// @author       lanpod
// @match        https://www.bing.com/search*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @require      https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.7/marked.min.js
// @license      MIT
// @namespace http://tampermonkey.net/
// ==/UserScript==


(function () {
    'use strict';

    // 설정 모듈: 전역 설정값 관리 (API, 스타일, 메시지 등)
    const Config = {
        API: {
            GEMINI_MODEL: 'gemini-2.0-flash',
            GEMINI_URL: 'https://generativelanguage.googleapis.com/v1beta/models/',
            MARKED_CDN_URL: 'https://api.cdnjs.com/libraries/marked'
        },
        VERSIONS: {
            MARKED_VERSION: '15.0.7'
        },
        CACHE: {
            PREFIX: 'gemini_cache_'
        },
        STORAGE_KEYS: {
            CURRENT_VERSION: 'markedCurrentVersion',
            LATEST_VERSION: 'markedLatestVersion',
            LAST_NOTIFIED: 'markedLastNotifiedVersion'
        },
        UI: {
            DEFAULT_MARGIN: 8,
            DEFAULT_PADDING: 16,
            Z_INDEX: 9999
        },
        STYLES: {
            COLORS: {
                BACKGROUND: '#fff',
                BORDER: '#e0e0e0',
                TEXT: '#000',
                TITLE: '#000',
                BUTTON_BG: '#f0f3ff',
                BUTTON_BORDER: '#ccc',
                DARK_BACKGROUND: '#202124',
                DARK_BORDER: '#5f6368',
                DARK_TEXT: '#fff',
                CODE_BLOCK_BG: '#f0f0f0',
                DARK_CODE_BLOCK_BG: '#555'
            },
            BORDER: '1px solid #e0e0e0',
            BORDER_RADIUS: '4px',
            FONT_SIZE: {
                TEXT: '14px',
                TITLE: '18px'
            },
            ICON_SIZE: '20px',
            LOGO_SIZE: '24px',
            SMALL_ICON_SIZE: '16px'
        },
        ASSETS: {
            GOOGLE_LOGO: 'https://www.gstatic.com/marketing-cms/assets/images/bc/1a/a310779347afa1927672dc66a98d/g.png=s48-fcrop64=1,00000000ffffffff-rw',
            GEMINI_LOGO: 'https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg',
            REFRESH_ICON: 'https://www.svgrepo.com/show/533704/refresh-cw-alt-3.svg'
        },
        MESSAGE_KEYS: {
            PROMPT: 'prompt',
            ENTER_API_KEY: 'enterApiKey',
            GEMINI_EMPTY: 'geminiEmpty',
            PARSE_ERROR: 'parseError',
            NETWORK_ERROR: 'networkError',
            TIMEOUT: 'timeout',
            LOADING: 'loading',
            UPDATE_TITLE: 'updateTitle',
            UPDATE_NOW: 'updateNow',
            SEARCH_ON_GOOGLE: 'searchongoogle'
        }
    };

    // 지역화 모듈: 다국어 메시지 처리
    const Localization = {
        // 다국어 메시지 객체
        MESSAGES: {
            [Config.MESSAGE_KEYS.PROMPT]: {
                ko: `"${'${query}'}"에 대한 정보를 찾아줘`,
                zh: `请以标记格式填写有关\"${'${query}'}\"的信息。`,
                default: `Please write information about \"${'${query}'}\" in markdown format`
            },
            [Config.MESSAGE_KEYS.ENTER_API_KEY]: {
                ko: 'Gemini API 키를 입력하세요:',
                zh: '请输入 Gemini API 密钥:',
                default: 'Please enter your Gemini API key:'
            },
            [Config.MESSAGE_KEYS.GEMINI_EMPTY]: {
                ko: '⚠️ Gemini 응답이 비어있습니다.',
                zh: '⚠️ Gemini 返回为空。',
                default: '⚠️ Gemini response is empty.'
            },
            [Config.MESSAGE_KEYS.PARSE_ERROR]: {
                ko: '❌ 파싱 오류:',
                zh: '❌ 解析错误:',
                default: '❌ Parsing error:'
            },
            [Config.MESSAGE_KEYS.NETWORK_ERROR]: {
                ko: '❌ 네트워크 오류:',
                zh: '❌ 网络错误:',
                default: '❌ Network error:'
            },
            [Config.MESSAGE_KEYS.TIMEOUT]: {
                ko: '❌ 요청 시간이 초과되었습니다.',
                zh: '❌ 请求超时。',
                default: '❌ Request timeout'
            },
            [Config.MESSAGE_KEYS.LOADING]: {
                ko: '불러오는 중...',
                zh: '加载中...',
                default: 'Loading...'
            },
            [Config.MESSAGE_KEYS.UPDATE_TITLE]: {
                ko: 'marked.min.js 업데이트 필요',
                zh: '需要更新 marked.min.js',
                default: 'marked.min.js update required'
            },
            [Config.MESSAGE_KEYS.UPDATE_NOW]: {
                ko: '확인',
                zh: '确认',
                default: 'OK'
            },
            [Config.MESSAGE_KEYS.SEARCH_ON_GOOGLE]: {
                ko: 'Google 에서 검색하기',
                zh: '在 Google 上搜索',
                default: 'Search on Google'
            }
        },

        // 주어진 키와 변수를 사용하여 지역화된 메시지 반환
        getMessage(key, vars = {}) {
            const lang = navigator.language;
            const langKey = lang.includes('ko') ? 'ko' : lang.includes('zh') ? 'zh' : 'default';
            const template = this.MESSAGES[key]?.[langKey] || this.MESSAGES[key]?.default || '';
            return template.replace(/\$\{(.*?)\}/g, (_, k) => vars[k] || '');
        }
    };

    // 디바이스 감지 모듈: 디바이스 타입 감지 (데스크톱, 모바일, 태블릿)
    const DeviceDetector = {
        // 캐싱 변수
        _cache: {
            isDesktop: null,
            isMobile: null,
            isTablet: null,
            isGeminiAvailable: null
        },

        // 디바이스가 Android 또는 iPhone인지 확인
        isAndroidOrIPhone() {
            return /Android|iPhone/i.test(navigator.userAgent);
        },

        // UserAgent에 "Mobile" 키워드가 있는지 확인
        hasMobileKeyword() {
            return /Mobile/i.test(navigator.userAgent);
        },

        // 데스크톱 환경인지 확인 (Android 또는 iPhone이 아닌 경우)
        isDesktop() {
            if (this._cache.isDesktop === null) {
                this._cache.isDesktop = !this.isAndroidOrIPhone();
            }
            return this._cache.isDesktop;
        },

        // 모바일 환경인지 확인 (Android 또는 iPhone이면서 "Mobile" 키워드 포함)
        isMobile() {
            if (this._cache.isMobile === null) {
                this._cache.isMobile = this.isAndroidOrIPhone() && this.hasMobileKeyword();
            }
            return this._cache.isMobile;
        },

        // 태블릿 환경인지 확인 (Android이지만 "Mobile" 키워드 없음)
        isTablet() {
            if (this._cache.isTablet === null) {
                this._cache.isTablet = /Android/i.test(navigator.userAgent) && !this.hasMobileKeyword();
            }
            return this._cache.isTablet;
        },

        // Gemini UI를 표시할 수 있는 환경인지 확인 (데스크톱 환경에서만 가능)
        isGeminiAvailable() {
            if (this._cache.isGeminiAvailable === null) {
                const hasBContext = !!document.getElementById('b_context');
                const hasBRight = !!document.querySelector('.b_right');
                this._cache.isGeminiAvailable = this.isDesktop() && (hasBContext || hasBRight);
            }
            return this._cache.isGeminiAvailable;
        },

        // 캐시 초기화
        resetCache() {
            this._cache = {
                isDesktop: null,
                isMobile: null,
                isTablet: null,
                isGeminiAvailable: null
            };
        }
    };

    // 스타일 생성 모듈: CSS 스타일 정의
    const StyleGenerator = {
        // 공통 스타일 정의
        commonStyles: {
            '#b_results > li.b_ad a': { 'color': 'green !important' },
            '#b_context, .b_context, .b_right': {
                'color': 'initial !important',
                'border': 'none !important',
                'border-width': '0 !important',
                'border-style': 'none !important',
                'border-collapse': 'separate !important',
                'background': 'transparent !important'
            }
        },

        // Gemini 박스 스타일 정의
        geminiBoxStyles: {
            '#gemini-box': {
                'width': '100%',
                'max-width': '100%',
                'background': `${Config.STYLES.COLORS.BACKGROUND} !important`,
                'border': `${Config.STYLES.BORDER} !important`,
                'border-style': 'solid !important',
                'border-width': '1px !important',
                'border-radius': Config.STYLES.BORDER_RADIUS,
                'padding': `${Config.UI.DEFAULT_PADDING}px`,
                'margin-bottom': `${Config.UI.DEFAULT_MARGIN * 2.5}px`,
                'font-family': 'sans-serif',
                'overflow-x': 'auto',
                'position': 'relative',
                'box-sizing': 'border-box',
                'color': 'initial !important'
            }
        },

        // 테마별 스타일 정의 (라이트/다크 모드)
        themeStyles: {
            '[data-theme="light"] #gemini-box, .light #gemini-box': {
                'background': `${Config.STYLES.COLORS.BACKGROUND} !important`,
                'border': `1px solid ${Config.STYLES.COLORS.BORDER} !important`
            },
            '[data-theme="light"] #gemini-box h3, .light #gemini-box h3': {
                'color': `${Config.STYLES.COLORS.TITLE} !important`
            },
            '[data-theme="light"] #gemini-content, [data-theme="light"] #gemini-content *, .light #gemini-content, .light #gemini-content *': {
                'color': `${Config.STYLES.COLORS.TEXT} !important`,
                'background': 'transparent !important'
            },
            '[data-theme="light"] #gemini-divider, .light #gemini-divider': {
                'background': `${Config.STYLES.COLORS.BORDER} !important`
            },
            '[data-theme="dark"] #gemini-box, .dark #gemini-box, .b_dark #gemini-box': {
                'background': `${Config.STYLES.COLORS.DARK_BACKGROUND} !important`,
                'border': `1px solid ${Config.STYLES.COLORS.DARK_BORDER} !important`
            },
            '@media (prefers-color-scheme: dark)': {
                '#gemini-box': {
                    'background': `${Config.STYLES.COLORS.DARK_BACKGROUND} !important`,
                    'border': `1px solid ${Config.STYLES.COLORS.DARK_BORDER} !important`
                },
                '#gemini-box h3': {
                    'color': `${Config.STYLES.COLORS.DARK_TEXT} !important`
                },
                '#gemini-content, #gemini-content *': {
                    'color': `${Config.STYLES.COLORS.DARK_TEXT} !important`,
                    'background': 'transparent !important'
                },
                '#gemini-content pre': {
                    'background': `${Config.STYLES.COLORS.DARK_CODE_BLOCK_BG} !important`
                },
                '#gemini-divider': {
                    'background': `${Config.STYLES.COLORS.DARK_BORDER} !important`
                },
                '#marked-update-popup': {
                    'background': `${Config.STYLES.COLORS.DARK_BACKGROUND} !important`,
                    'color': `${Config.STYLES.COLORS.DARK_TEXT} !important`
                }
            }
        },

        // Gemini 콘텐츠 스타일 정의
        contentStyles: {
            '#gemini-content': {
                'font-size': Config.STYLES.FONT_SIZE.TEXT,
                'line-height': '1.6',
                'white-space': 'pre-wrap',
                'word-wrap': 'break-word',
                'background': 'transparent !important'
            },
            '#gemini-content pre': {
                'background': `${Config.STYLES.COLORS.CODE_BLOCK_BG} !important`,
                'padding': `${Config.UI.DEFAULT_MARGIN + 2}px`,
                'border-radius': Config.STYLES.BORDER_RADIUS,
                'overflow-x': 'auto'
            },
            '[data-theme="dark"] #gemini-content pre, .dark #gemini-content pre, .b_dark #gemini-content pre': {
                'background': `${Config.STYLES.COLORS.DARK_CODE_BLOCK_BG} !important`
            }
        },

        // Gemini 헤더 스타일 정의
        headerStyles: {
            '#gemini-header': {
                'display': 'flex',
                'align-items': 'center',
                'justify-content': 'space-between',
                'margin-bottom': `${Config.UI.DEFAULT_MARGIN}px`
            },
            '#gemini-title-wrap': {
                'display': 'flex',
                'align-items': 'center'
            },
            '#gemini-logo': {
                'width': Config.STYLES.LOGO_SIZE,
                'height': Config.STYLES.LOGO_SIZE,
                'margin-right': `${Config.UI.DEFAULT_MARGIN}px`
            },
            '#gemini-box h3': {
                'margin': '0',
                'font-size': Config.STYLES.FONT_SIZE.TITLE,
                'font-weight': 'bold'
            },
            '#gemini-refresh-btn': {
                'width': Config.STYLES.ICON_SIZE,
                'height': Config.STYLES.ICON_SIZE,
                'cursor': 'pointer',
                'opacity': '0.6',
                'transition': 'transform 0.5s ease'
            },
            '#gemini-refresh-btn:hover': {
                'opacity': '1',
                'transform': 'rotate(360deg)'
            },
            '#gemini-divider': {
                'height': '1px',
                'margin': `${Config.UI.DEFAULT_MARGIN}px 0`
            }
        },

        // Google 검색 버튼 스타일 정의
        googleButtonStyles: {
            '#google-search-btn': {
                'width': '100%',
                'max-width': '100%',
                'font-size': Config.STYLES.FONT_SIZE.TEXT,
                'padding': `${Config.UI.DEFAULT_MARGIN}px`,
                'margin-bottom': `${Config.UI.DEFAULT_MARGIN * 1.25}px`,
                'cursor': 'pointer',
                'border': `1px solid ${Config.STYLES.COLORS.BUTTON_BORDER}`,
                'border-radius': Config.STYLES.BORDER_RADIUS,
                'background-color': Config.STYLES.COLORS.BUTTON_BG,
                'color': Config.STYLES.COLORS.TITLE,
                'font-family': 'sans-serif',
                'display': 'flex',
                'align-items': 'center',
                'justify-content': 'center',
                'gap': `${Config.UI.DEFAULT_MARGIN}px`,
                'transition': 'transform 0.2s ease'
            },
            '#google-search-btn img': {
                'width': Config.STYLES.SMALL_ICON_SIZE,
                'height': Config.STYLES.SMALL_ICON_SIZE,
                'vertical-align': 'middle',
                'transition': 'transform 0.2s ease'
            },
            '.desktop-useragent #google-search-btn:hover': {
                'transform': 'scale(1.1)'
            },
            '.desktop-useragent #google-search-btn:hover img': {
                'transform': 'scale(1.1)'
            }
        },

        // 업데이트 팝업 스타일 정의
        popupStyles: {
            '#marked-update-popup': {
                'position': 'fixed',
                'top': '30%',
                'left': '50%',
                'transform': 'translate(-50%, -50%)',
                'background': Config.STYLES.COLORS.BACKGROUND,
                'padding': `${Config.UI.DEFAULT_PADDING * 1.25}px`,
                'z-index': Config.UI.Z_INDEX,
                'border': `1px solid ${Config.STYLES.COLORS.BUTTON_BORDER}`,
                'box-shadow': '0 2px 10px rgba(0,0,0,0.1)',
                'text-align': 'center'
            },
            '[data-theme="dark"] #marked-update-popup, .dark #marked-update-popup, .b_dark #marked-update-popup': {
                'background': `${Config.STYLES.COLORS.DARK_BACKGROUND} !important`,
                'color': `${Config.STYLES.COLORS.DARK_TEXT} !important`
            },
            '#marked-update-popup button': {
                'margin-top': `${Config.UI.DEFAULT_MARGIN * 1.25}px`,
                'padding': `${Config.UI.DEFAULT_MARGIN}px ${Config.UI.DEFAULT_PADDING}px`,
                'cursor': 'pointer',
                'border': `1px solid ${Config.STYLES.COLORS.BUTTON_BORDER}`,
                'border-radius': Config.STYLES.BORDER_RADIUS,
                'background-color': Config.STYLES.COLORS.BUTTON_BG,
                'color': Config.STYLES.COLORS.TITLE,
                'font-family': 'sans-serif'
            }
        },

        // 모바일 환경 스타일 정의
        mobileStyles: {
            '.mobile-useragent #google-search-btn': {
                'max-width': '96%',
                'margin': `${Config.UI.DEFAULT_MARGIN}px auto`,
                'padding': `${Config.UI.DEFAULT_PADDING * 0.75}px`,
                'border-radius': '16px'
            },
            '.mobile-useragent #gemini-box': {
                'padding': `${Config.UI.DEFAULT_PADDING * 0.75}px`,
                'border-radius': '16px'
            }
        },

        // 모든 스타일을 하나의 CSS 문자열로 변환
        generateStyles() {
            const styles = [
                this.commonStyles,
                this.geminiBoxStyles,
                this.themeStyles,
                this.contentStyles,
                this.headerStyles,
                this.googleButtonStyles,
                this.popupStyles,
                this.mobileStyles
            ];

            return styles.reduce((css, styleObj) => {
                for (const [selector, props] of Object.entries(styleObj)) {
                    css += `${selector} {`;
                    for (const [prop, value] of Object.entries(props)) {
                        css += `${prop}: ${value};`;
                    }
                    css += '}';
                }
                return css;
            }, '');
        }
    };

    // 테마 관리 모듈: 테마 변경 감지 및 적용
    const ThemeManager = {
        // 테마 변경 감지 및 스타일 적용
        applyTheme() {
            const isDarkTheme = document.documentElement.getAttribute('data-theme') === 'dark' ||
                                document.documentElement.classList.contains('dark') ||
                                document.documentElement.classList.contains('b_dark') ||
                                window.matchMedia('(prefers-color-scheme: dark)').matches;

            const geminiBox = document.querySelector('#gemini-box');
            if (geminiBox) {
                geminiBox.style.background = isDarkTheme
                    ? Config.STYLES.COLORS.DARK_BACKGROUND
                    : Config.STYLES.COLORS.BACKGROUND;
                geminiBox.style.borderColor = isDarkTheme
                    ? Config.STYLES.COLORS.DARK_BORDER
                    : Config.STYLES.COLORS.BORDER;
            }
        },

        // 테마 변경 감지 설정
        observeThemeChange() {
            const observer = new MutationObserver(() => this.applyTheme());
            const targetElement = document.querySelector('#b_context') || document.querySelector('.b_right') || document.documentElement;

            observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });
            if (targetElement !== document.documentElement) {
                observer.observe(targetElement, { attributes: true, attributeFilter: ['style', 'class'] });
            }

            window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => this.applyTheme());
        }
    };

    // 스타일 관리 모듈: 스타일 초기화 및 적용
    const Styles = {
        // 스타일 초기화
        initStyles() {
            const styleElement = document.createElement('style');
            styleElement.id = 'bing-plus-styles';
            styleElement.textContent = StyleGenerator.generateStyles();
            document.head.appendChild(styleElement);
            this.applyMobileStyles();
        },

        // 모바일 환경 스타일 적용
        applyMobileStyles() {
            if (DeviceDetector.isMobile()) {
                document.documentElement.classList.add('mobile-useragent');
            } else if (DeviceDetector.isDesktop()) {
                document.documentElement.classList.add('desktop-useragent');
            }
        }
    };

    // 유틸리티 모듈: 공통 유틸리티 함수
    const Utils = {
        // 검색 쿼리 추출
        getQuery() {
            return new URLSearchParams(location.search).get('q');
        },

        // Gemini API 키 가져오기 또는 입력받기
        getApiKey() {
            let key = localStorage.getItem('geminiApiKey');
            if (!key) {
                key = prompt(Localization.getMessage(Config.MESSAGE_KEYS.ENTER_API_KEY));
                if (key) localStorage.setItem('geminiApiKey', key);
            }
            return key;
        }
    };

    // UI 생성 모듈: DOM 요소 생성
    const UI = {
        // Google 검색 버튼 생성
        createGoogleButton(query) {
            const btn = document.createElement('button');
            btn.id = 'google-search-btn';
            btn.innerHTML = `
                <img src="${Config.ASSETS.GOOGLE_LOGO}" alt="Google Logo">
                ${Localization.getMessage(Config.MESSAGE_KEYS.SEARCH_ON_GOOGLE)}
            `;
            btn.onclick = () => window.open(`https://www.google.com/search?q=${encodeURIComponent(query)}`, '_blank');
            return btn;
        },

        // Gemini 박스 생성
        createGeminiBox(query, apiKey) {
            const box = document.createElement('div');
            box.id = 'gemini-box';
            box.innerHTML = `
                <div id="gemini-header">
                    <div id="gemini-title-wrap">
                        <img id="gemini-logo" src="${Config.ASSETS.GEMINI_LOGO}" alt="Gemini Logo">
                        <h3>Gemini Search Results</h3>
                    </div>
                    <img id="gemini-refresh-btn" title="Refresh" src="${Config.ASSETS.REFRESH_ICON}" />
                </div>
                <hr id="gemini-divider">
                <div id="gemini-content">${Localization.getMessage(Config.MESSAGE_KEYS.LOADING)}</div>
            `;
            box.querySelector('#gemini-refresh-btn').onclick = () => GeminiAPI.fetch(query, box.querySelector('#gemini-content'), apiKey, true);

            if (DeviceDetector.isDesktop()) {
                VersionChecker.checkMarkedJsVersion();
            }

            return box;
        },

        // Gemini UI 전체 생성 (Google 버튼 + Gemini 박스)
        createGeminiUI(query, apiKey) {
            const wrapper = document.createElement('div');
            wrapper.id = 'gemini-wrapper';
            wrapper.appendChild(this.createGoogleButton(query));
            wrapper.appendChild(this.createGeminiBox(query, apiKey));
            return wrapper;
        },

        // 기존 UI 요소 제거
        removeExistingElements() {
            document.querySelectorAll('#gemini-wrapper, #google-search-btn').forEach(el => el.remove());
        }
    };

    // Gemini API 모듈: Gemini API 호출 및 응답 처리
    const GeminiAPI = {
        // Gemini API 호출
        fetch(query, container, apiKey, force = false) {
            const cacheKey = `${Config.CACHE.PREFIX}${query}`;
            const cached = force ? null : sessionStorage.getItem(cacheKey);

            if (cached) {
                if (container) container.innerHTML = marked.parse(cached);
                return;
            }

            if (container) container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.LOADING);

            GM_xmlhttpRequest({
                method: 'POST',
                url: `${Config.API.GEMINI_URL}${Config.API.GEMINI_MODEL}:generateContent?key=${apiKey}`,
                headers: { 'Content-Type': 'application/json' },
                data: JSON.stringify({
                    contents: [{ parts: [{ text: Localization.getMessage(Config.MESSAGE_KEYS.PROMPT, { query }) }] }]
                }),
                onload({ responseText }) {
                    try {
                        const text = JSON.parse(responseText)?.candidates?.[0]?.content?.parts?.[0]?.text;
                        if (text) {
                            sessionStorage.setItem(cacheKey, text);
                            if (container) container.innerHTML = marked.parse(text);
                        } else {
                            if (container) container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.GEMINI_EMPTY);
                        }
                    } catch (e) {
                        if (container) container.textContent = `${Localization.getMessage(Config.MESSAGE_KEYS.PARSE_ERROR)} ${e.message}`;
                    }
                },
                onerror(err) {
                    if (container) container.textContent = `${Localization.getMessage(Config.MESSAGE_KEYS.NETWORK_ERROR)} ${err.finalUrl}`;
                },
                ontimeout() {
                    if (container) container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.TIMEOUT);
                }
            });
        }
    };

    // 링크 정리 모듈: 중간 URL 제거
    const LinkCleaner = {
        // URL 디코딩
        decodeRealUrl(url, key) {
            const param = new URL(url).searchParams.get(key)?.replace(/^a1/, '');
            if (!param) return null;
            try {
                const decoded = decodeURIComponent(atob(param.replace(/_/g, '/').replace(/-/g, '+')));
                return decoded.startsWith('/') ? location.origin + decoded : decoded;
            } catch {
                return null;
            }
        },

        // 실제 URL로 변환
        resolveRealUrl(url) {
            const rules = [
                { pattern: /bing\.com\/(ck\/a|aclick)/, key: 'u' },
                { pattern: /so\.com\/search\/eclk/, key: 'aurl' }
            ];
            for (const { pattern, key } of rules) {
                if (pattern.test(url)) {
                    const real = this.decodeRealUrl(url, key);
                    if (real && real !== url) return real;
                }
            }
            return url;
        },

        // 모든 링크를 실제 URL로 변환
        convertLinksToReal(root) {
            root.querySelectorAll('a[href]').forEach(a => {
                const realUrl = this.resolveRealUrl(a.href);
                if (realUrl && realUrl !== a.href) a.href = realUrl;
            });
        }
    };

    // 버전 확인 모듈: marked.js 버전 체크
    const VersionChecker = {
        // 버전 비교
        compareVersions(current, latest) {
            const currentParts = current.split('.').map(Number);
            const latestParts = latest.split('.').map(Number);
            for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
                const c = currentParts[i] || 0;
                const l = latestParts[i] || 0;
                if (c < l) return -1;
                if (c > l) return 1;
            }
            return 0;
        },

        // marked.js 버전 체크 및 업데이트 알림 표시
        checkMarkedJsVersion() {
            localStorage.setItem(Config.STORAGE_KEYS.CURRENT_VERSION, Config.VERSIONS.MARKED_VERSION);

            GM_xmlhttpRequest({
                method: 'GET',
                url: Config.API.MARKED_CDN_URL,
                onload: ({ responseText }) => {
                    try {
                        const latest = JSON.parse(responseText).version;
                        localStorage.setItem(Config.STORAGE_KEYS.LATEST_VERSION, latest);

                        const lastNotified = localStorage.getItem(Config.STORAGE_KEYS.LAST_NOTIFIED);
                        if (this.compareVersions(Config.VERSIONS.MARKED_VERSION, latest) < 0 &&
                            (!lastNotified || this.compareVersions(lastNotified, latest) < 0)) {
                            const existingPopup = document.getElementById('marked-update-popup');
                            if (existingPopup) existingPopup.remove();

                            const popup = document.createElement('div');
                            popup.id = 'marked-update-popup';
                            popup.innerHTML = `
                                <p><b>${Localization.getMessage(Config.MESSAGE_KEYS.UPDATE_TITLE)}</b></p>
                                <p>Current: ${Config.VERSIONS.MARKED_VERSION}<br>Latest: ${latest}</p>
                                <button>${Localization.getMessage(Config.MESSAGE_KEYS.UPDATE_NOW)}</button>
                            `;
                            popup.querySelector('button').onclick = () => {
                                localStorage.setItem(Config.STORAGE_KEYS.LAST_NOTIFIED, latest);
                                popup.remove();
                            };
                            document.body.appendChild(popup);
                        }
                    } catch (e) {
                        console.warn('marked.min.js version check error:', e.message);
                    }
                },
                onerror: () => console.warn('marked.min.js version check request failed')
            });
        }
    };

    // 이벤트 핸들러 모듈: URL 및 DOM 변경 감지
    const EventHandler = {
        // URL 변경 감지 및 처리
        observeUrlChange(onChangeCallback) {
            let lastUrl = location.href;

            const checkUrlChange = () => {
                if (location.href !== lastUrl) {
                    lastUrl = location.href;
                    onChangeCallback();
                }
            };

            const originalPushState = history.pushState;
            history.pushState = function (...args) {
                originalPushState.apply(this, args);
                checkUrlChange();
            };

            const originalReplaceState = history.replaceState;
            history.replaceState = function (...args) {
                originalReplaceState.apply(this, args);
                checkUrlChange();
            };

            window.addEventListener('popstate', checkUrlChange);

            const observer = new MutationObserver(checkUrlChange);
            const targetNode = document.querySelector('head > title') || document.body;
            observer.observe(targetNode, { childList: true, subtree: true });
        }
    };

    // 렌더링 상태 관리 모듈: UI 렌더링 상태 관리
    const RenderState = {
        isRendering: false,

        // 렌더링 시작
        startRendering() {
            if (this.isRendering) return false;
            this.isRendering = true;
            return true;
        },

        // 렌더링 완료
        finishRendering() {
            this.isRendering = false;
        }
    };

    // UI 렌더링 모듈: UI 렌더링 로직
    const UIRenderer = {
        // 데스크톱 환경 렌더링
        renderDesktop(query, apiKey) {
            // Gemini UI가 표시될 수 있는지 확인 (데스크톱에서 강제로 렌더링 보장)
            const contextTarget = document.getElementById('b_context') || document.querySelector('.b_right');
            if (!contextTarget) return false;

            requestAnimationFrame(() => {
                const wrapper = UI.createGeminiUI(query, apiKey);
                contextTarget.prepend(wrapper);

                window.requestIdleCallback(() => {
                    const content = wrapper.querySelector('#gemini-content');
                    if (content) {
                        const cache = sessionStorage.getItem(`${Config.CACHE.PREFIX}${query}`);
                        if (cache) {
                            content.innerHTML = marked.parse(cache);
                        } else {
                            window.requestIdleCallback(() => GeminiAPI.fetch(query, content, apiKey));
                        }
                    }
                    RenderState.finishRendering();
                });
            });
            return true;
        },

        // 모바일 환경 렌더링
        renderMobile(query) {
            const contentTarget = document.getElementById('b_content');
            if (!contentTarget) return false;

            requestAnimationFrame(() => {
                const googleBtn = UI.createGoogleButton(query);
                contentTarget.parentNode.insertBefore(googleBtn, contentTarget);
                RenderState.finishRendering();
            });
            return true;
        },

        // 태블릿 환경 렌더링 (아무 UI도 표시하지 않음, Google 검색 버튼 포함)
        renderTablet() {
            // 태블릿에서는 Gemini UI와 Google 검색 버튼 모두 표시하지 않음
            RenderState.finishRendering();
            return true;
        },

        // 메인 렌더링 함수
        render() {
            if (!RenderState.startRendering()) return;

            const query = Utils.getQuery();
            if (!query) {
                RenderState.finishRendering();
                return;
            }

            UI.removeExistingElements();

            if (DeviceDetector.isDesktop()) {
                const apiKey = Utils.getApiKey();
                if (!apiKey) {
                    RenderState.finishRendering();
                    return;
                }
                this.renderDesktop(query, apiKey);
            } else if (DeviceDetector.isMobile()) {
                this.renderMobile(query);
            } else if (DeviceDetector.isTablet()) {
                this.renderTablet();
            } else {
                RenderState.finishRendering();
            }
        }
    };

    // 초기화 모듈: 스크립트 초기화
    const Initializer = {
        // 초기화 실행
        init() {
            const initialize = () => {
                Styles.initStyles();
                ThemeManager.applyTheme();
                LinkCleaner.convertLinksToReal(document);
                // DOM 준비 완료 후 렌더링
                const checkAndRender = () => {
                    if (document.getElementById('b_context') || document.querySelector('.b_right')) {
                        UIRenderer.render();
                    } else {
                        setTimeout(checkAndRender, 100); // 100ms 후 재시도
                    }
                };
                checkAndRender();
                EventHandler.observeUrlChange(() => {
                    UIRenderer.render();
                    LinkCleaner.convertLinksToReal(document);
                });
                ThemeManager.observeThemeChange();
            };

            if (document.readyState === 'complete' || document.readyState === 'interactive') {
                setTimeout(initialize, 1);
            } else {
                document.addEventListener('DOMContentLoaded', initialize);
            }
        }
    };

    // 스크립트 실행
    Initializer.init();
})();