Greasy Fork

Greasy Fork is available in English.

Bing Plus

Display Gemini response results next to Bing search results and speed up searches by eliminating intermediate URLs.

当前为 2025-04-23 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Bing Plus
// @version      5.0
// @description  Display Gemini response results next to Bing search results and speed up searches by eliminating intermediate URLs.
// @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';

    // 설정 모듈
    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',
                CODE_BG: '#f5f5f5',
                BUTTON_BORDER: '#ccc',
                DARK_BACKGROUND: '#202124',
                DARK_BORDER: '#5f6368',
                DARK_CODE_BG: '#2d2d2d',
                DARK_TEXT: '#fff'
            },
            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 Styles = {
        inject() {
            console.log('Injecting styles...');
            const currentTheme = document.documentElement.getAttribute('data-theme') ||
                               (document.documentElement.classList.contains('dark') ||
                               document.documentElement.classList.contains('b_dark')) ? 'dark' :
                               (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
            console.log(`Current theme: ${currentTheme}`);
            GM_addStyle(`
                #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: none !important;
                }

                #b_context #gemini-box,
                .b_right #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;
                }

                [data-theme="light"] #b_context #gemini-box,
                [data-theme="light"] .b_right #gemini-box,
                .light #b_context #gemini-box,
                .light .b_right #gemini-box {
                    background: ${Config.STYLES.COLORS.BACKGROUND} !important;
                    border: 1px solid ${Config.STYLES.COLORS.BORDER} !important;
                    border-style: solid !important;
                    border-width: 1px !important;
                }

                [data-theme="light"] #b_context #gemini-box h3,
                [data-theme="light"] .b_right #gemini-box h3,
                .light #b_context #gemini-box h3,
                .light .b_right #gemini-box h3 {
                    color: ${Config.STYLES.COLORS.TITLE} !important;
                }

                [data-theme="light"] #b_context #gemini-content,
                [data-theme="light"] #b_context #gemini-content *,
                [data-theme="light"] .b_right #gemini-content,
                [data-theme="light"] .b_right #gemini-content *,
                .light #b_context #gemini-content,
                .light #b_context #gemini-content *,
                .light .b_right #gemini-content,
                .light .b_right #gemini-content * {
                    color: ${Config.STYLES.COLORS.TEXT} !important;
                }

                [data-theme="light"] #b_context #gemini-content pre,
                [data-theme="light"] .b_right #gemini-content pre,
                .light #b_context #gemini-content pre,
                .light .b_right #gemini-content pre {
                    background: ${Config.STYLES.COLORS.CODE_BG} !important;
                }

                [data-theme="light"] #b_context #gemini-divider,
                [data-theme="light"] .b_right #gemini-divider,
                .light #b_context #gemini-divider,
                .light .b_right #gemini-divider {
                    background: ${Config.STYLES.COLORS.BORDER} !important;
                }

                [data-theme="dark"] #b_context #gemini-box,
                [data-theme="dark"] .b_right #gemini-box,
                .dark #b_context #gemini-box,
                .dark .b_right #gemini-box,
                .b_dark #b_context #gemini-box,
                .b_dark .b_right #gemini-box {
                    background: ${Config.STYLES.COLORS.DARK_BACKGROUND} !important;
                    border: 1px solid ${Config.STYLES.COLORS.DARK_BORDER} !important;
                    border-style: solid !important;
                    border-width: 1px !important;
                }

                @media (prefers-color-scheme: dark) {
                    #b_context #gemini-box,
                    .b_right #gemini-box {
                        background: ${Config.STYLES.COLORS.DARK_BACKGROUND} !important;
                        border: 1px solid ${Config.STYLES.COLORS.DARK_BORDER} !important;
                        border-style: solid !important;
                        border-width: 1px !important;
                    }
                }

                [data-theme="dark"] #b_context #gemini-box h3,
                [data-theme="dark"] .b_right #gemini-box h3,
                .dark #b_context #gemini-box h3,
                .dark .b_right #gemini-box h3,
                .b_dark #b_context #gemini-box h3,
                .b_dark .b_right #gemini-box h3 {
                    color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
                }

                @media (prefers-color-scheme: dark) {
                    #b_context #gemini-box h3,
                    .b_right #gemini-box h3 {
                        color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
                    }
                }

                [data-theme="dark"] #b_context #gemini-content,
                [data-theme="dark"] #b_context #gemini-content *,
                [data-theme="dark"] .b_right #gemini-content,
                [data-theme="dark"] .b_right #gemini-content *,
                .dark #b_context #gemini-content,
                .dark #b_context #gemini-content *,
                .dark .b_right #gemini-content,
                .dark .b_right #gemini-content *,
                .b_dark #b_context #gemini-content,
                .b_dark #b_context #gemini-content *,
                .b_dark .b_right #gemini-content,
                .b_dark .b_right #gemini-content * {
                    color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
                }

                @media (prefers-color-scheme: dark) {
                    #b_context #gemini-content,
                    #b_context #gemini-content *,
                    .b_right #gemini-content,
                    .b_right #gemini-content * {
                        color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
                    }
                }

                [data-theme="dark"] #b_context #gemini-content pre,
                [data-theme="dark"] .b_right #gemini-content pre,
                .dark #b_context #gemini-content pre,
                .dark .b_right #gemini-content pre,
                .b_dark #b_context #gemini-content pre,
                .b_dark .b_right #gemini-content pre {
                    background: ${Config.STYLES.COLORS.DARK_CODE_BG} !important;
                }

                @media (prefers-color-scheme: dark) {
                    #b_context #gemini-content pre,
                    .b_right #gemini-content pre {
                        background: ${Config.STYLES.COLORS.DARK_CODE_BG} !important;
                    }
                }

                [data-theme="dark"] #b_context #gemini-divider,
                [data-theme="dark"] .b_right #gemini-divider,
                .dark #b_context #gemini-divider,
                .dark .b_right #gemini-divider,
                .b_dark #b_context #gemini-divider,
                .b_dark .b_right #gemini-divider {
                    background: ${Config.STYLES.COLORS.DARK_BORDER} !important;
                }

                @media (prefers-color-scheme: dark) {
                    #b_context #gemini-divider,
                    .b_right #gemini-divider {
                        background: ${Config.STYLES.COLORS.DARK_BORDER} !important;
                    }
                }

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

                #gemini-content {
                    font-size: ${Config.STYLES.FONT_SIZE.TEXT};
                    line-height: 1.6;
                    white-space: pre-wrap;
                    word-wrap: break-word;
                }

                #gemini-content pre {
                    padding: ${Config.UI.DEFAULT_MARGIN + 2}px;
                    border-radius: ${Config.STYLES.BORDER_RADIUS};
                    overflow-x: auto;
                }

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

                #google-search-btn img {
                    width: ${Config.STYLES.SMALL_ICON_SIZE};
                    height: ${Config.STYLES.SMALL_ICON_SIZE};
                    vertical-align: middle;
                }

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

                @media (prefers-color-scheme: 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;
                }

                @media (max-width: 768px) {
                    #google-search-btn {
                        max-width: 96%;
                        margin: ${Config.UI.DEFAULT_MARGIN}px auto;
                        padding: ${Config.UI.DEFAULT_PADDING * 0.75}px;
                        border-radius: 16px;
                    }
                    #gemini-box {
                        padding: ${Config.UI.DEFAULT_PADDING * 0.75}px;
                        border-radius: 16px;
                    }
                }
            `);
            console.log('Styles injected', {
                light: {
                    background: Config.STYLES.COLORS.BACKGROUND,
                    text: Config.STYLES.COLORS.TEXT,
                    title: Config.STYLES.COLORS.TITLE,
                    border: Config.STYLES.COLORS.BORDER
                },
                dark: {
                    background: Config.STYLES.COLORS.DARK_BACKGROUND,
                    text: Config.STYLES.COLORS.DARK_TEXT,
                    border: Config.STYLES.COLORS.DARK_BORDER
                }
            });

            // 계산된 스타일 디버깅
            setTimeout(() => {
                const geminiBox = document.querySelector('#b_context #gemini-box') ||
                                document.querySelector('.b_right #gemini-box');
                const content = document.querySelector('#b_context #gemini-content') ||
                               document.querySelector('.b_right #gemini-content');
                const bContext = document.querySelector('#b_context');
                const bContextParent = document.querySelector('.b_context');
                const bRight = document.querySelector('.b_right');
                if (geminiBox && content && (bContext || bRight)) {
                    const computedBoxStyle = window.getComputedStyle(geminiBox);
                    const computedContentStyle = window.getComputedStyle(content);
                    const computedBContextStyle = bContext ? window.getComputedStyle(bContext) : null;
                    const computedBContextParentStyle = bContextParent ? window.getComputedStyle(bContextParent) : null;
                    const computedBRightStyle = bRight ? window.getComputedStyle(bRight) : null;
                    console.log('Computed styles:', {
                        geminiBox: {
                            background: computedBoxStyle.backgroundColor,
                            border: computedBoxStyle.border,
                            borderStyle: computedBoxStyle.borderStyle,
                            borderWidth: computedBoxStyle.borderWidth,
                            borderColor: computedBoxStyle.borderColor
                        },
                        geminiContent: {
                            color: computedContentStyle.color,
                            children: Array.from(content.children).map(child => ({
                                tag: child.tagName,
                                color: window.getComputedStyle(child).color
                            }))
                        },
                        bContext: bContext ? {
                            color: computedBContextStyle.color,
                            border: computedBContextStyle.border,
                            borderStyle: computedBContextStyle.borderStyle,
                            borderWidth: computedBContextStyle.borderWidth,
                            borderColor: computedBContextStyle.borderColor
                        } : null,
                        bContextParent: bContextParent ? {
                            color: computedBContextParentStyle.color,
                            border: computedBContextParentStyle.border,
                            borderStyle: computedBContextParentStyle.borderStyle,
                            borderWidth: computedBContextParentStyle.borderWidth,
                            borderColor: computedBContextParentStyle.borderColor
                        } : null,
                        bRight: bRight ? {
                            color: computedBRightStyle.color,
                            border: computedBRightStyle.border,
                            borderStyle: computedBRightStyle.borderStyle,
                            borderWidth: computedBRightStyle.borderWidth,
                            borderColor: computedBRightStyle.borderColor
                        } : null
                    });
                } else {
                    console.log('Elements not found for computed style check', {
                        geminiBox: !!geminiBox,
                        content: !!content,
                        bContext: !!bContext,
                        bContextParent: !!bContextParent,
                        bRight: !!bRight
                    });
                }
            }, 2000); // 2초 지연으로 DOM 로드 대기
        }
    };

    // 유틸리티 모듈
    const Utils = {
        isDesktop() {
            const isDesktop = window.innerWidth > 768 && !/Mobi|Android/i.test(navigator.userAgent);
            console.log('isDesktop:', { width: window.innerWidth, userAgent: navigator.userAgent, result: isDesktop });
            return isDesktop;
        },
        isGeminiAvailable() {
            const hasBContext = !!document.getElementById('b_context');
            const hasBRight = !!document.querySelector('.b_right');
            console.log('Bing isGeminiAvailable:', { isDesktop: this.isDesktop(), hasBContext, hasBRight });
            return this.isDesktop() && (hasBContext || hasBRight);
        },
        getQuery() {
            const query = new URLSearchParams(location.search).get('q');
            console.log('getQuery:', { query, search: location.search });
            return query;
        },
        getApiKey() {
            let key = localStorage.getItem('geminiApiKey');
            if (!key) {
                key = prompt(Localization.getMessage(Config.MESSAGE_KEYS.ENTER_API_KEY));
                if (key) localStorage.setItem('geminiApiKey', key);
                console.log('API key:', key ? 'stored' : 'prompt failed');
            } else {
                console.log('API key retrieved');
            }
            return key;
        }
    };

    // UI 모듈
    const UI = {
        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;
        },
        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);
            return box;
        },
        createGeminiUI(query, apiKey) {
            const wrapper = document.createElement('div');
            wrapper.appendChild(this.createGoogleButton(query));
            wrapper.appendChild(this.createGeminiBox(query, apiKey));
            console.log('Gemini UI created:', { query, hasApiKey: !!apiKey });
            return wrapper;
        }
    };

    // Gemini API 모듈
    const GeminiAPI = {
        fetch(query, container, apiKey, force = false) {
            console.log('Fetching Gemini API:', { query, force });
            VersionChecker.checkMarkedJsVersion();

            const cacheKey = `${Config.CACHE.PREFIX}${query}`;
            if (!force) {
                const cached = sessionStorage.getItem(cacheKey);
                if (cached) {
                    container.innerHTML = marked.parse(cached);
                    console.log('Loaded from cache:', { query });
                    return;
                }
            }

            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);
                            container.innerHTML = marked.parse(text);
                            console.log('Gemini API success:', { query });
                        } else {
                            container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.GEMINI_EMPTY);
                            console.log('Gemini API empty response');
                        }
                    } catch (e) {
                        container.textContent = `${Localization.getMessage(Config.MESSAGE_KEYS.PARSE_ERROR)} ${e.message}`;
                        console.error('Gemini API parse error:', e.message);
                    }
                },
                onerror: err => {
                    container.textContent = `${Localization.getMessage(Config.MESSAGE_KEYS.NETWORK_ERROR)} ${err.finalUrl}`;
                    console.error('Gemini API network error:', err);
                },
                ontimeout: () => {
                    container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.TIMEOUT);
                    console.error('Gemini API timeout');
                }
            });
        }
    };

    // 링크 정리 모듈
    const LinkCleaner = {
        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;
            }
        },
        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;
        },
        convertLinksToReal(root) {
            root.querySelectorAll('a[href]').forEach(a => {
                const realUrl = this.resolveRealUrl(a.href);
                if (realUrl && realUrl !== a.href) a.href = realUrl;
            });
            console.log('Links converted');
        }
    };

    // 버전 확인 모듈
    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;
        },
        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;
                        console.log(`marked.js version: current=${Config.VERSIONS.MARKED_VERSION}, latest=${latest}`);

                        localStorage.setItem(Config.STORAGE_KEYS.LATEST_VERSION, latest);

                        const lastNotified = localStorage.getItem(Config.STORAGE_KEYS.LAST_NOTIFIED);
                        console.log(`Last notified version: ${lastNotified || 'none'}`);

                        if (this.compareVersions(Config.VERSIONS.MARKED_VERSION, latest) < 0 &&
                            (!lastNotified || this.compareVersions(lastNotified, latest) < 0)) {
                            console.log('Popup display condition met');

                            const existingPopup = document.getElementById('marked-update-popup');
                            if (existingPopup) {
                                existingPopup.remove();
                                console.log('Existing popup removed');
                            }

                            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);
                                console.log(`Notified version recorded: ${latest}`);
                                popup.remove();
                            };
                            document.body.appendChild(popup);
                            console.log('New popup displayed');
                        } else {
                            console.log('Popup display condition not met');
                        }
                    } catch (e) {
                        console.warn('marked.min.js version check error:', e.message);
                    }
                },
                onerror: () => console.warn('marked.min.js version check request failed')
            });
        }
    };

    // 메인 모듈
    const Main = {
        renderGemini() {
            console.log('renderGemini called');

            const query = Utils.getQuery();
            if (!query || document.getElementById('google-search-btn')) {
                console.log('Skipped:', { queryExists: !!query, googleBtnExists: !!document.getElementById('google-search-btn') });
                return;
            }

            if (Utils.isDesktop()) {
                if (!Utils.isGeminiAvailable()) {
                    console.log('Skipped PC: isGeminiAvailable false');
                    return;
                }

                const apiKey = Utils.getApiKey();
                if (!apiKey) {
                    console.log('Skipped PC: No API key');
                    return;
                }

                const contextTarget = document.getElementById('b_context') ||
                                    document.querySelector('.b_right');
                if (!contextTarget) {
                    console.error('Target element (#b_context or .b_right) not found for PC UI insertion');
                    return;
                }

                const ui = UI.createGeminiUI(query, apiKey);
                contextTarget.prepend(ui);
                console.log('PC: Gemini UI (with Google button) inserted into target element');

                const content = ui.querySelector('#gemini-content');
                const cache = sessionStorage.getItem(`${Config.CACHE.PREFIX}${query}`);
                content.innerHTML = cache ? marked.parse(cache) : Localization.getMessage(Config.MESSAGE_KEYS.LOADING);
                if (!cache) GeminiAPI.fetch(query, content, apiKey);

                // Gemini 박스 삽입 여부 확인
                const geminiBox = document.querySelector('#gemini-box');
                console.log('Gemini box inserted:', !!geminiBox);
            } else {
                const contentTarget = document.getElementById('b_content');
                if (!contentTarget) {
                    console.error('b_content not found for mobile Google button insertion');
                    return;
                }

                const googleBtn = UI.createGoogleButton(query);
                contentTarget.parentNode.insertBefore(googleBtn, contentTarget);
                console.log('Mobile: Google search button inserted before b_content');
            }
        },
        observeUrlChange() {
            let lastUrl = location.href;
            const observer = new MutationObserver(() => {
                if (location.href !== lastUrl) {
                    lastUrl = location.href;
                    console.log('MutationObserver triggered: URL changed');
                    this.renderGemini();
                    LinkCleaner.convertLinksToReal(document);
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
            console.log('Observing URL changes on document.body');
        },
        observeThemeChange() {
            const themeObserver = new MutationObserver(() => {
                const newTheme = document.documentElement.getAttribute('data-theme') ||
                                (document.documentElement.classList.contains('dark') ||
                                document.documentElement.classList.contains('b_dark')) ? 'dark' :
                                (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
                console.log(`Theme changed: ${newTheme}`);
                Styles.inject();
            });
            themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });

            // 시스템 테마 변경 감지
            window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
                const newTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
                console.log(`System theme changed: ${newTheme}`);
                Styles.inject();
            });

            // 타겟 요소 스타일 변경 감지
            const contextObserver = new MutationObserver(() => {
                console.log('Target element style changed, reapplying styles');
                Styles.inject();
            });
            const targetElement = document.querySelector('#b_context') ||
                                document.querySelector('.b_right');
            if (targetElement) {
                contextObserver.observe(targetElement, { attributes: true, attributeFilter: ['style', 'class'] });
            }
            console.log('Observing theme and style changes');
        },
        waitForElement(selector, callback, maxAttempts = 20, interval = 500) {
            let attempts = 0;
            const checkElement = () => {
                const element = document.querySelector(selector);
                if (element) {
                    console.log(`Element found: ${selector}`);
                    callback(element);
                } else if (attempts < maxAttempts) {
                    attempts++;
                    console.log(`Waiting for element: ${selector}, attempt ${attempts}/${maxAttempts}`);
                    setTimeout(checkElement, interval);
                } else {
                    console.error(`Element not found after ${maxAttempts} attempts: ${selector}`);
                }
            };
            checkElement();
        },
        init() {
            console.log('Bing Plus init:', { hostname: location.hostname, url: location.href });
            try {
                // 페이지 로드 완료 후 타겟 요소 대기
                this.waitForElement('#b_context, .b_right, #b_content', () => {
                    Styles.inject();
                    LinkCleaner.convertLinksToReal(document);
                    this.renderGemini();
                    this.observeUrlChange();
                    this.observeThemeChange();

                    // DOM 구조 디버깅
                    const bContext = document.getElementById('b_context');
                    const bContextParent = document.querySelector('.b_context');
                    const bRight = document.querySelector('.b_right');
                    const bContent = document.getElementById('b_content');
                    console.log('DOM structure debugging:', {
                        bContextExists: !!bContext,
                        bContextParentExists: !!bContextParent,
                        bRightExists: !!bRight,
                        bContentExists: !!bContent
                    });
                });
            } catch (e) {
                console.error('Init error:', e.message);
            }
        }
    };

    console.log('Bing Plus script loaded');
    Main.init();
})();