您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Display Gemini response results next to Bing search results and speed up searches by eliminating intermediate URLs.
当前为
- // ==UserScript==
- // @name Bing Plus
- // @version 5.2
- // @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',
- BUTTON_BORDER: '#ccc',
- DARK_BACKGROUND: '#202124',
- DARK_BORDER: '#5f6368',
- DARK_TEXT: '#fff',
- CODE_BLOCK_BG: '#f0f0f0', // 라이트 모드 코드 블록 배경색
- DARK_CODE_BLOCK_BG: '#555' // 다크 모드 코드 블록 배경색
- },
- BORDER: '1px solid #e0e0e0',
- BORDER_RADIUS: '4px',
- FONT_SIZE: {
- TEXT: '14px',
- TITLE: '18px'
- },
- ICON_SIZE: '20px',
- LOGO_SIZE: '24px',
- SMALL_ICON_SIZE: '16px'
- },
- ASSETS: {
- GOOGLE_LOGO: 'https://www.gstatic.com/marketing-cms/assets/images/bc/1a/a310779347afa1927672dc66a98d/g.png=s48-fcrop64=1,00000000ffffffff-rw',
- GEMINI_LOGO: 'https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg',
- REFRESH_ICON: 'https://www.svgrepo.com/show/533704/refresh-cw-alt-3.svg'
- },
- MESSAGE_KEYS: {
- PROMPT: 'prompt',
- ENTER_API_KEY: 'enterApiKey',
- GEMINI_EMPTY: 'geminiEmpty',
- PARSE_ERROR: 'parseError',
- NETWORK_ERROR: 'networkError',
- TIMEOUT: 'timeout',
- LOADING: 'loading',
- UPDATE_TITLE: 'updateTitle',
- UPDATE_NOW: 'updateNow',
- SEARCH_ON_GOOGLE: 'searchongoogle'
- }
- };
- // 지역화 모듈
- const Localization = {
- MESSAGES: {
- [Config.MESSAGE_KEYS.PROMPT]: {
- ko: `"${'${query}'}"에 대한 정보를 찾아줘`,
- zh: `请以标记格式填写有关\"${'${query}'}\"的信息。`,
- default: `Please write information about \"${'${query}'}\" in markdown format`
- },
- [Config.MESSAGE_KEYS.ENTER_API_KEY]: {
- ko: 'Gemini API 키를 입력하세요:',
- zh: '请输入 Gemini API 密钥:',
- default: 'Please enter your Gemini API key:'
- },
- [Config.MESSAGE_KEYS.GEMINI_EMPTY]: {
- ko: '⚠️ Gemini 응답이 비어있습니다.',
- zh: '⚠️ Gemini 返回为空。',
- default: '⚠️ Gemini response is empty.'
- },
- [Config.MESSAGE_KEYS.PARSE_ERROR]: {
- ko: '❌ 파싱 오류:',
- zh: '❌ 解析错误:',
- default: '❌ Parsing error:'
- },
- [Config.MESSAGE_KEYS.NETWORK_ERROR]: {
- ko: '❌ 네트워크 오류:',
- zh: '❌ 网络错误:',
- default: '❌ Network error:'
- },
- [Config.MESSAGE_KEYS.TIMEOUT]: {
- ko: '❌ 요청 시간이 초과되었습니다.',
- zh: '❌ 请求超时。',
- default: '❌ Request timeout'
- },
- [Config.MESSAGE_KEYS.LOADING]: {
- ko: '불러오는 중...',
- zh: '加载中...',
- default: 'Loading...'
- },
- [Config.MESSAGE_KEYS.UPDATE_TITLE]: {
- ko: 'marked.min.js 업데이트 필요',
- zh: '需要更新 marked.min.js',
- default: 'marked.min.js update required'
- },
- [Config.MESSAGE_KEYS.UPDATE_NOW]: {
- ko: '확인',
- zh: '确认',
- default: 'OK'
- },
- [Config.MESSAGE_KEYS.SEARCH_ON_GOOGLE]: {
- ko: 'Google 에서 검색하기',
- zh: '在 Google 上搜索',
- default: 'Search on Google'
- }
- },
- getMessage(key, vars = {}) {
- const lang = navigator.language;
- const langKey = lang.includes('ko') ? 'ko' : lang.includes('zh') ? 'zh' : 'default';
- const template = this.MESSAGES[key]?.[langKey] || this.MESSAGES[key]?.default || '';
- return template.replace(/\$\{(.*?)\}/g, (_, k) => vars[k] || '');
- }
- };
- // 스타일 모듈
- const 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(`Detected 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: transparent !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;
- background: transparent !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;
- background: transparent !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;
- background: transparent !important;
- }
- }
- /* 코드 블록 스타일 */
- #gemini-content pre {
- background: ${Config.STYLES.COLORS.CODE_BLOCK_BG} !important;
- padding: ${Config.UI.DEFAULT_MARGIN + 2}px;
- border-radius: ${Config.STYLES.BORDER_RADIUS};
- overflow-x: auto;
- }
- /* 다크 모드에서 코드 블록 배경색 */
- [data-theme="dark"] #gemini-content pre,
- .dark #gemini-content pre,
- .b_dark #gemini-content pre,
- [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_BLOCK_BG} !important;
- }
- @media (prefers-color-scheme: dark) {
- #gemini-content pre,
- #b_context #gemini-content pre,
- .b_right #gemini-content pre {
- background: ${Config.STYLES.COLORS.DARK_CODE_BLOCK_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;
- background: transparent !important;
- }
- #google-search-btn {
- width: 100%;
- max-width: 100%;
- font-size: ${Config.STYLES.FONT_SIZE.TEXT};
- padding: ${Config.UI.DEFAULT_MARGIN}px;
- margin-bottom: ${Config.UI.DEFAULT_MARGIN * 1.25}px;
- cursor: pointer;
- border: 1px solid ${Config.STYLES.COLORS.BUTTON_BORDER};
- border-radius: ${Config.STYLES.BORDER_RADIUS};
- background-color: ${Config.STYLES.COLORS.BUTTON_BG};
- color: ${Config.STYLES.COLORS.TITLE};
- font-family: sans-serif;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: ${Config.UI.DEFAULT_MARGIN}px;
- transition: transform 0.2s ease; /* 확대 애니메이션 부드럽게 */
- }
- #google-search-btn img {
- width: ${Config.STYLES.SMALL_ICON_SIZE};
- height: ${Config.STYLES.SMALL_ICON_SIZE};
- vertical-align: middle;
- transition: transform 0.2s ease; /* 아이콘 확대 애니메이션 */
- }
- /* PC에서 버튼 호버 시 확대 효과 */
- @media (min-width: 769px) {
- #google-search-btn:hover {
- transform: scale(1.1); /* 버튼 전체 확대 */
- }
- #google-search-btn:hover img {
- transform: scale(1.1); /* 아이콘 추가 확대 */
- }
- }
- #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,
- codeBlockBg: Config.STYLES.COLORS.CODE_BLOCK_BG
- },
- dark: {
- background: Config.STYLES.COLORS.DARK_BACKGROUND,
- text: Config.STYLES.COLORS.DARK_TEXT,
- border: Config.STYLES.COLORS.DARK_BORDER,
- codeBlockBg: Config.STYLES.COLORS.DARK_CODE_BLOCK_BG
- }
- });
- // 계산된 스타일 디버깅
- 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 codeBlock = document.querySelector('#gemini-content pre');
- const bContext = document.querySelector('#b_context');
- const bContextParent = document.querySelector('.b_context');
- const bRight = document.querySelector('.b_right');
- if (geminiBox && content) {
- const computedBoxStyle = window.getComputedStyle(geminiBox);
- const computedContentStyle = window.getComputedStyle(content);
- const computedCodeBlockStyle = codeBlock ? window.getComputedStyle(codeBlock) : null;
- 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: {
- background: computedContentStyle.backgroundColor,
- color: computedContentStyle.color,
- children: Array.from(content.children).map(child => ({
- tag: child.tagName,
- color: window.getComputedStyle(child).color,
- background: child.tagName === 'PRE' ? window.getComputedStyle(child).backgroundColor : undefined
- }))
- },
- codeBlock: codeBlock ? {
- background: computedCodeBlockStyle.backgroundColor,
- padding: computedCodeBlockStyle.padding,
- borderRadius: computedCodeBlockStyle.borderRadius
- } : 'No code block found',
- bContext: bContext ? {
- background: computedBContextStyle.backgroundColor,
- color: computedBContextStyle.color,
- border: computedBContextStyle.border
- } : null,
- bContextParent: bContextParent ? {
- background: computedBContextParentStyle.backgroundColor,
- color: computedBContextParentStyle.color,
- border: computedBContextParentStyle.border
- } : null,
- bRight: bRight ? {
- background: computedBRightStyle.backgroundColor,
- color: computedBRightStyle.color,
- border: computedBRightStyle.border
- } : null
- });
- } else {
- console.log('Elements not found for computed style check', {
- geminiBox: !!geminiBox,
- content: !!content,
- codeBlock: !!codeBlock,
- 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();
- })();