// ==UserScript==
// @name Bing Plus
// @version 3.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 = {
GEMINI_MODEL: 'gemini-2.0-flash',
MARKED_VERSION: '15.0.7',
CACHE_PREFIX: 'gemini_cache_',
STORAGE_KEYS: {
CURRENT_VERSION: 'markedCurrentVersion',
LATEST_VERSION: 'markedLatestVersion',
LAST_NOTIFIED: 'markedLastNotifiedVersion'
}
};
// 지역화 모듈
const Localization = {
MESSAGES: {
prompt: {
ko: `"${'${query}'}"에 대한 정보를 마크다운 형식으로 작성해줘`,
zh: `请以标记格式填写有关\"${'${query}'}\"的信息。`,
default: `Please write information about \"${'${query}'}\" in markdown format`
},
enterApiKey: {
ko: 'Gemini API 키를 입력하세요:',
zh: '请输入 Gemini API 密钥:',
default: 'Please enter your Gemini API key:'
},
geminiEmpty: {
ko: '⚠️ Gemini 응답이 비어있습니다.',
zh: '⚠️ Gemini 返回为空。',
default: '⚠️ Gemini response is empty.'
},
parseError: {
ko: '❌ 파싱 오류:',
zh: '❌ 解析错误:',
default: '❌ Parsing error:'
},
networkError: {
ko: '❌ 네트워크 오류:',
zh: '❌ 网络错误:',
default: '❌ Network error:'
},
timeout: {
ko: '❌ 요청 시간이 초과되었습니다.',
zh: '❌ 请求超时。',
default: '❌ Request timeout'
},
loading: {
ko: '불러오는 중...',
zh: '加载中...',
default: 'Loading...'
},
updateTitle: {
ko: 'marked.min.js 업데이트 필요',
zh: '需要更新 marked.min.js',
default: 'marked.min.js update required'
},
updateNow: {
ko: '확인',
zh: '确认',
default: 'OK'
},
searchongoogle: {
ko: 'Google 에서 검색하기',
zh: '在 Google 上搜索',
default: 'Search on Google'
}
},
/**
* 사용자의 언어에 따라 지역화된 메시지를 반환합니다.
* @param {string} key - 메시지 키
* @param {Object} vars - 메시지에 삽입할 변수
* @returns {string} 지역화된 메시지
*/
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 = {
/**
* 페이지에 CSS 스타일을 삽입합니다. (GM_addStyle 사용)
*/
inject() {
GM_addStyle(`
/* 광고 링크 스타일 */
#b_results > li.b_ad a { color: green !important; }
/* Gemini 박스 컨테이너 */
#gemini-box {
max-width: 400px;
background: #fff;
border: 1px solid #e0e0e0;
padding: 16px;
margin-bottom: 20px;
font-family: sans-serif;
overflow-x: auto;
position: relative;
}
/* Gemini 헤더 레이아웃 */
#gemini-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
/* Gemini 제목 래퍼 */
#gemini-title-wrap {
display: flex;
align-items: center;
}
/* Gemini 로고 스타일 */
#gemini-logo {
width: 24px;
height: 24px;
margin-right: 8px;
}
/* Gemini 제목 */
#gemini-box h3 {
margin: 0;
font-size: 18px;
color: #202124;
font-weight: bold;
}
/* 새로고침 버튼 스타일 */
#gemini-refresh-btn {
width: 20px;
height: 20px;
cursor: pointer;
opacity: 0.6;
transition: transform 0.5s ease;
}
/* 새로고침 버튼 호버 효과 */
#gemini-refresh-btn:hover {
opacity: 1;
transform: rotate(360deg);
}
/* 구분선 스타일 */
#gemini-divider {
height: 1px;
background: #e0e0e0;
margin: 8px 0;
}
/* Gemini 콘텐츠 스타일 */
#gemini-content {
font-size: 14px;
line-height: 1.6;
color: #333;
white-space: pre-wrap;
word-wrap: break-word;
}
/* 코드 블록 스타일 */
#gemini-content pre {
background: #f5f5f5;
padding: 10px;
border-radius: 5px;
overflow-x: auto;
}
/* Google 검색 버튼 스타일 */
#google-search-btn {
width: 100%;
font-size: 14px;
padding: 8px;
margin-bottom: 10px;
cursor: pointer;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #f0f3ff;
color: #202124;
font-family: sans-serif;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
/* Google 버튼 이미지 */
#google-search-btn img {
width: 16px;
height: 16px;
vertical-align: middle;
}
/* 버전 업데이트 팝업 스타일 */
#marked-update-popup {
position: fixed;
top: 30%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
padding: 20px;
z-index: 9999;
border: 1px solid #ccc;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
}
/* 팝업 버튼 스타일 */
#marked-update-popup button {
margin-top: 10px;
padding: 8px 16px;
cursor: pointer;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #f0f3ff;
color: #202124;
font-family: sans-serif;
}
`);
}
};
// 유틸리티 모듈
const Utils = {
/**
* 디바이스가 데스크톱인지 확인합니다. (화면 너비 > 768px, 모바일 아님)
* @returns {boolean}
*/
isDesktop() {
return window.innerWidth > 768 && !/Mobi|Android/i.test(navigator.userAgent);
},
/**
* Gemini UI를 표시할 수 있는지 확인합니다.
* @returns {boolean}
*/
isGeminiAvailable() {
return this.isDesktop() && !!document.getElementById('b_context');
},
/**
* URL 파라미터에서 검색 쿼리를 가져옵니다.
* @returns {string|null}
*/
getQuery() {
return new URLSearchParams(location.search).get('q');
},
/**
* Gemini API 키를 가져오거나 사용자에게 입력받습니다.
* @returns {string|null}
*/
getApiKey() {
let key = localStorage.getItem('geminiApiKey');
if (!key) {
key = prompt(Localization.getMessage('enterApiKey'));
if (key) localStorage.setItem('geminiApiKey', key);
}
return key;
}
};
// UI 모듈
const UI = {
/**
* Google 검색 버튼을 생성합니다.
* @param {string} query - 검색 쿼리
* @returns {HTMLElement}
*/
createGoogleButton(query) {
const btn = document.createElement('button');
btn.id = 'google-search-btn';
btn.innerHTML = `
${Localization.getMessage('searchongoogle')}
`;
btn.onclick = () => window.open(`https://www.google.com/search?q=${encodeURIComponent(query)}`, '_blank');
return btn;
},
/**
* Gemini 결과 박스를 생성합니다.
* @param {string} query - 검색 쿼리
* @param {string} apiKey - Gemini API 키
* @returns {HTMLElement}
*/
createGeminiBox(query, apiKey) {
const box = document.createElement('div');
box.id = 'gemini-box';
box.innerHTML = `
${Localization.getMessage('updateTitle')}
현재 버전: ${Config.MARKED_VERSION}
최신 버전: ${latest}