Bing Plus

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

// ==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();
})();