// ==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/ // @downloadURL none // ==/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 = ` 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 = `

Gemini Search Results


${Localization.getMessage(Config.MESSAGE_KEYS.LOADING)}
`; 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 = `

${Localization.getMessage(Config.MESSAGE_KEYS.UPDATE_TITLE)}

Current: ${Config.VERSIONS.MARKED_VERSION}
Latest: ${latest}

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