Add Gemini response, improve speed to search results(Bing), add Google/Bing search buttons, Gemini On/Off toggle
// ==UserScript==
// @name Google Plus & Bing Plus
// @version 8.6
// @description Add Gemini response, improve speed to search results(Bing), add Google/Bing search buttons, Gemini On/Off toggle
// @author monit8280
// @match https://www.bing.com/search*
// @match https://www.google.com/search*
// @match https://www.google.co.kr/search*
// @match https://*.google.com/search*
// @match https://*.google.co.kr/search*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @connect generativelanguage.googleapis.com
// @connect api.cdnjs.com
// @require https://cdnjs.cloudflare.com/ajax/libs/marked/16.3.0/lib/marked.umd.js
// @license MIT
// @namespace http://tampermonkey.net/
// ==/UserScript==
(function () {
'use strict';
/**
* ==========================================================================================
* 1. CONFIGURATION & CONSTANTS
* ==========================================================================================
*/
const Config = {
API: {
DEFAULT_MODEL: 'gemini-3.1-flash-lite',
BASE_URL: 'https://generativelanguage.googleapis.com/v1beta/models/',
MARKED_CDN: 'https://api.cdnjs.com/libraries/marked'
},
MODELS: [
{ id: 'gemini-3.1-flash-lite', name: '3.1 Flash Lite' },
{ id: 'gemini-3.5-flash', name: '3.5 Flash' },
{ id: 'gemini-3.1-pro-preview', name: '3.1 Pro' }
],
STORAGE: {
PREFIX: 'gemini_',
API_KEY: 'geminiApiKey',
ENABLED: 'geminiEnabled',
RAG_ENABLED: 'geminiRagEnabled',
THEME: 'themeMode',
MODEL: 'selectedModel',
LAST_NOTIFIED: 'markedLastNotifiedVersion',
CURRENT_VERSION: 'markedCurrentVersion',
LATEST_VERSION: 'markedLatestVersion'
},
UI: {
MARGIN: 8,
PADDING: 16,
BORDER_RADIUS: '12px', // Rounded aesthetics
ICON_SIZE: '20px',
LOGO_SIZE: '24px',
SMALL_ICON_SIZE: '16px',
ANIMATION_SPEED: '0.2s'
},
ASSETS: {
GOOGLE_LOGO: 'https://www.gstatic.com/images/branding/searchlogo/ico/favicon.ico',
BING_LOGO: 'https://account.microsoft.com/favicon.ico',
GEMINI_LOGO: 'https://www.gstatic.com/lamda/images/gemini_sparkle_aurora_33f86dc0c0257da337c63.svg',
REFRESH_ICON: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik00LjA2MTg5IDEzQzQuMDIxMDQgMTIuNjcyNCA0IDEyLjMzODcgNCAxMkM0IDcuNTgxNzIgNy41ODE3MiA0IDEyIDRDMTQuNTAwNiA0IDE2LjczMzIgNS4xNDcyNyAxOC4yMDAyIDYuOTQ0MTZNMTkuOTM4MSAxMUMxOS45NzkgMTEuMzI3NiAyMCAxMS42NjEzIDIwIDEyQzIwIDE2LjQxODMgMTYuNDE4MyAyMCAxMiAyMEM5LjYxMDYxIDIwIDcuNDY1ODkgMTguOTUyNSA2IDE3LjI5MTZNOSAxN0g2VjE3LjI5MTZNMTguMjAwMiA0VjYuOTQ0MTZNMTguMjAwMiA2Ljk0NDE2VjYuOTk5OTNMMTUuMjAwMiA3TTYgMjBWMTcuMjkxNiIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPg0KPC9zdmc+',
LIGHT_ICON: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gVXBsb2FkZWQgdG86IFNWRyBSZXBvLCB3d3cuc3ZncmVwby5jb20sIEdlbmVyYXRvcjogU1ZHIFJlcG8gTWl4ZXIgVG9vbHMgLS0+DQo8c3ZnIHdpZHRoPSI4MDBweCIgaGVpZ2h0PSI4MDBweCIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgY2xhc3M9Imljb24iICB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTg2MSA2NTYuN2wxNDQuNi0xNDQuNkw4NjEgMzY3LjZWMTYzLjFINjU2LjZMNTEyIDE4LjYgMzY3LjQgMTYzLjFIMTYzdjIwNC41TDE4LjQgNTEyLjEgMTYzIDY1Ni43djIwNC40aDIwNC40TDUxMiAxMDA1LjdsMTQ0LjYtMTQ0LjZIODYxeiIgZmlsbD0iI0ZDRDE3MCIgLz48cGF0aCBkPSJNNTEyIDEwMTUuN2MtMi42IDAtNS4xLTEtNy4xLTIuOUwzNjMuMyA4NzEuMUgxNjNjLTUuNSAwLTEwLTQuNS0xMC0xMFY2NjAuOEwxMS40IDUxOS4yYy0xLjktMS45LTIuOS00LjQtMi45LTcuMSAwLTIuNyAxLjEtNS4yIDIuOS03LjFMMTUzIDM2My40VjE2My4xYzAtNS41IDQuNS0xMCAxMC0xMGgyMDAuM0w1MDQuOSAxMS41YzEuOS0xLjkgNC40LTIuOSA3LjEtMi45czUuMiAxLjEgNy4xIDIuOWwxNDEuNiAxNDEuNkg4NjFjNS41IDAgMTAgNC41IDEwIDEwdjIwMC4zTDEwMTIuNiA1MDVjMS45IDEuOSAyLjkgNC40IDIuOSA3LjEgMCAyLjctMS4xIDUuMi0yLjkgNy4xTDg3MSA2NjAuOHYyMDAuM2MwIDUuNS00LjUgMTAtMTAgMTBINjYwLjdsLTE0MS42IDE0MS42Yy0yIDItNC41IDMtNy4xIDN6TTE3MyA4NTEuMWgxOTQuNGMyLjcgMCA1LjIgMS4xIDcuMSAyLjlMNTEyIDk5MS42bDEzNy41LTEzNy41YzEuOS0xLjkgNC40LTIuOSA3LjEtMi45SDg1MVY2NTYuN2MwLTIuNyAxLjEtNS4yIDIuOS03LjFsMTM3LjUtMTM3LjUtMTM3LjUtMTM3LjVjLTEuOS0xLjktMi45LTQuNC0yLjktNy4xVjE3My4xSDY1Ni42Yy0yLjcgMC01LjItMS4xLTcuMS0yLjlMNTEyIDMyLjcgMzc0LjUgMTcwLjJjLTEuOSAxLjktNC40IDIuOS03LjEgMi45SDE3M3YxOTQuNGMwIDIuNy0xLjEgNS4yLTIuOSA3LjFMMzIuNiA1MTIuMWwxMzcuNSAxMzcuNWMxLjkgMS45IDIuOSA0LjQgMi45IDcuMXYxOTQuNHoiIGZpbGw9IiIgLz48cGF0aCBkPSJNNTEyIDUxMi4xbS0yNTcuOCAwYTI1Ny44IDI1Ny44IDAgMSAwIDUxNS42IDAgMjU3LjggMjU3LjggMCAxIDAtNTE1LjYgMFoiIGZpbGw9IiNGN0REQUQiIC8+PHBhdGggZD0iTTUxMiA3NzkuOWMtNzEuNSAwLTEzOC44LTI3LjktMTg5LjQtNzguNC01MC42LTUwLjYtNzguNC0xMTcuOC03OC40LTE4OS40czI3LjktMTM4LjggNzguNC0xODkuNGM1MC42LTUwLjYgMTE3LjgtNzguNCAxODkuNC03OC40IDcxLjUgMCAxMzguOCAyNy45IDE4OS40IDc4LjQgNTAuNiA1MC42IDc4LjQgMTE3LjggNzguNCAxODkuNFM3NTIgNjUwLjkgNzAxLjQgNzAxLjUgNTgzLjUgNzc5LjkgNTEyIDc3OS45eiBtMC01MTUuNmMtNjYuMiAwLTEyOC40IDI1LjgtMTc1LjIgNzIuNi00Ni44IDQ2LjgtNzIuNiAxMDktNzIuNiAxNzUuMnMyNS44IDEyOC40IDcyLjYgMTc1LjJjNDYuOCA0Ni44IDEwOSA3Mi42IDE3NS4yIDcyLjYgNjYuMiAwIDEyOC40LTI1LjggMTc1LjItNzIuNiA0Ni44LTQ2LjggNzIuNiAxMDkgNzIuNi0xNzUuMlM3MzQgMzgzLjcgNjg3LjIgMzM2LjljLTQ2LjgtNDYuOC0xMDktNzIuNi0xNzUuMi03Mi42eiIgZmlsbD0iIiAvPjwvc3ZnPg==',
DARK_ICON: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xOS45MDAxIDIuMzA3MTlDMTkuNzM5MiAxLjg5NzYgMTkuMTYxNiAxLjg5NzYgMTkuMDAwNyAyLjMwNzE5TDE4LjU3MDMgMy40MDI0N0MxOC41MjEyIDMuNTI3NTIgMTguNDIyNiAzLjYyNjUxIDE4LjI5OCAzLjY3NTgzTDE3LjIwNjcgNC4xMDc4QzE2Ljc5ODYgNC4yNjkzNCAxNi43OTg2IDQuODQ5IDE3LjIwNjcgNS4wMTA1NEwxOC4yOTggNS40NDI1MkMxOC40MjI2IDUuNDkxODQgMTguNTIxMiA1LjU5MDgyIDE4LjU3MDMgNS43MTU4N0wxOS4wMDA3IDYuODExMTVDMTkuMTYxNiA3LjIyMDc0IDE5LjczOTIgNy4yMjA3NCAxOS45MDAxIDYuODExMTZMMjAuMzMwNSA1LjcxNTg3QzIwLjM3OTYgNS41OTA4MiAyMC40NzgyIDUuNDkxODQgMjAuNjAyOCA1LjQ0MjUyTDIxLjY5NDEgNS4wMTA1NEMyMi4xMDIyIDQuODQ5IDIyLjEwMjIgNC4yNjkzNCAyMS42OTQxIDQuMTA3OEwyMC42MDI4IDMuNjc1ODNDMjAuNDc4MiAzLjYyNjUxIDIwLjM3OTYgMy41Mjc1MiAyMC4zMzA1IDMuNDAyNDdMMTkuOTAwMSAyLjMwNzE5WiIgZmlsbD0iIzFDMjc0QyIvPg0KPHBhdGggZD0iTTE2LjAzMjggOC4xMjk2N0MxNS44NzE4IDcuNzIwMDkgMTUuMjk0MyA3LjcyMDA5IDE1LjEzMzMgOC4xMjk2N0wxNC45NzY0IDguNTI5MDJDMTQuOTI3MyA4LjY1NDA3IDE0LjgyODcgOC43NTMwNSAxNC43MDQxIDguODAyMzdMMTQuMzA2MiA4Ljk1OTg3QzEzLjg5ODEgOS4xMjE0MSAxMy44OTgxIDkuNzAxMDcgMTQuMzA2MiA5Ljg2MjYxTDE0LjcwNDEgMTAuMDIwMUMxNC44Mjg3IDEwLjA2OTQgMTQuOTI3MyAxMC4xNjg0IDE0Ljk3NjQgMTAuMjkzNUwxNS4xMzMzIDEwLjY5MjhDMTUuMjk0MyAxMS4xMDI0IDE1Ljg3MTggMTEuMTAyNCAxNi4wMzI4IDEwLjY5MjhMMTYuMTg5NyAxMC4yOTM1QzE2LjIzODggMTAuMTY4NCAxNi4zMzc0IDEwLjA2OTQgMTYuNDYyIDEwLjAyMDFMMTYuODU5OSA5Ljg2MjYxQzE3LjI2OCA5LjcwMTA3IDE3LjI2OCA5LjEyMTQxIDE2Ljg1OTkgOC45NTk4N0wxNi40NjIgOC44MDIzN0MxNi4zMzc0IDguNzUzMDUgMTYuMjM4OCA4LjY1NDA3IDE2LjE4OTcgOC41MjkwMkwxNi4wMzI4IDguMTI5NjdaIiBmaWxsPSIjMUMyNzRDIi8+DQo8cGF0aCBkPSJNMTIgMjJDMTcuNTIyOCAyMiAyMiAxNy41MjI4IDIyIDEyQzIyIDExLjUzNzMgMjEuMzA2NSAxMS40NjA4IDIxLjA2NzIgMTEuODU2OEMxOS45Mjg5IDEzLjc0MDYgMTcuODYxNSAxNSAxNS41IDE1QzExLjkxMDEgMTUgOSAxMi4wODk5IDkgOC41QzkgNi4xMzg0NSAxMC4yNTk0IDQuMDcxMDUgMTIuMTQzMiAyLjkzMjc2QzEyLjUzOTIgMi42OTM0NyAxMi40NjI3IDIgMTIgMkM2LjQ3NzE1IDIgMiA2LjQ3NzE1IDIgMTJDMiAxNy41MjI4IDYuNDc3MTUgMjIgMTIgMjJaIiBmaWxsPSIjMUMyNzRDIi8+DQo8L3N2Zz4='
}
};
/**
* ==========================================================================================
* 2. CORE UTILITIES
* ==========================================================================================
*/
const Utils = {
Storage: {
get: (key) => localStorage.getItem(key),
set: (key, val) => localStorage.setItem(key, val),
getBool: (key, def = true) => {
const val = localStorage.getItem(key);
return val === null ? def : val === 'true';
}
},
Env: {
isGoogle: () => window.location.hostname.includes('google.'),
isBing: () => window.location.hostname.includes('bing.com'),
getQuery: () => new URLSearchParams(window.location.search).get('q'),
isMobile: () => {
const ua = navigator.userAgent;
const topWidth = window.innerWidth;
const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
return (/Android|iPhone|Mobile/i.test(ua)) || (isTouch && topWidth <= 1024);
}
},
I18n: {
MESSAGES: {
prompt: { ko: `"${'${query}'}"에 대한 정보를 찾아줘`, default: `Please write information about "${'${query}'}" in markdown format` },
enterApiKey: { ko: 'Gemini API 키 입력:', default: 'Enter Gemini API Key:' },
geminiEmpty: { ko: '⚠️ 응답이 비어있습니다.', default: '⚠️ Response is empty.' },
loading: { ko: '불러오는 중...', default: 'Loading...' },
error: { ko: '오류 발생:', default: 'Error:' },
geminiOff: { ko: 'Gemini가 꺼져있습니다.', default: 'Gemini is OFF' },
searchGoogle: { ko: 'Google 검색', default: 'Search on Google' },
searchBing: { ko: 'Bing 검색', default: 'Search on Bing' }
},
get(key, vars = {}) {
const lang = navigator.language.includes('ko') ? 'ko' : 'default';
const tmpl = this.MESSAGES[key]?.[lang] || this.MESSAGES[key]?.default || '';
return tmpl.replace(/\$\{(.*?)\}/g, (_, k) => vars[k] || '');
}
}
};
/**
* ==========================================================================================
* 3. STATE MANAGEMENT (Theme & Settings)
* ==========================================================================================
*/
const State = {
theme: 'light',
init() {
const saved = Utils.Storage.get(Config.STORAGE.THEME);
this.theme = saved || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
this.applyTheme();
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
this.theme = e.matches ? 'dark' : 'light';
this.applyTheme();
});
},
toggleTheme() {
this.theme = this.theme === 'light' ? 'dark' : 'light';
Utils.Storage.set(Config.STORAGE.THEME, this.theme);
this.applyTheme();
return this.theme;
},
applyTheme() {
document.documentElement.classList.toggle('gemini-dark-mode', this.theme === 'dark');
const btn = document.getElementById('gemini-theme-toggle-btn');
if (btn) btn.src = this.getThemeIcon();
},
getThemeIcon() {
return this.theme === 'light' ? Config.ASSETS.DARK_ICON : Config.ASSETS.LIGHT_ICON;
},
// Model & API Key
getApiKey: () => Utils.Storage.get(Config.STORAGE.API_KEY),
setApiKey: (key) => Utils.Storage.set(Config.STORAGE.API_KEY, key),
getModel: () => Utils.Storage.get(Config.STORAGE.MODEL) || Config.API.DEFAULT_MODEL,
setModel: (model) => Utils.Storage.set(Config.STORAGE.MODEL, model),
isEnabled: () => Utils.Storage.getBool(Config.STORAGE.ENABLED),
setEnabled: (v) => Utils.Storage.set(Config.STORAGE.ENABLED, v ? 'true' : 'false'),
isRagEnabled: () => Utils.Storage.getBool(Config.STORAGE.RAG_ENABLED, true),
setRagEnabled: (v) => Utils.Storage.set(Config.STORAGE.RAG_ENABLED, v ? 'true' : 'false')
};
/**
* ==========================================================================================
* 4. STYLES
* ==========================================================================================
*/
const Styles = {
inject() {
const css = `
:root {
--g-bg: #fff; --g-border: #e0e0e0; --g-text: #000; --g-title: #000;
--g-btn-bg: #f0f3ff; --g-btn-border: #ccc; --g-code-bg: #f0f0f0;
--g-icon-filter: none;
}
.gemini-dark-mode {
--g-bg: #282c34; --g-border: #444; --g-text: #e0e0e0; --g-title: #fff;
--g-btn-bg: #3a3f4b; --g-btn-border: #555; --g-code-bg: #3b3b3b;
--g-icon-filter: invert(1);
}
/* Layout */
#gemini-wrapper { margin-bottom: 20px; font-family: sans-serif; }
#gemini-top-row { display: flex; gap: 8px; margin-bottom: 12px; width: 100%; }
/* Buttons & Selects */
.gemini-btn, .gemini-select {
height: 40px; border-radius: ${Config.UI.BORDER_RADIUS}; font-size: 13px;
display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: all ${Config.UI.ANIMATION_SPEED};
border: 1px solid var(--g-btn-border);
background: var(--g-btn-bg) !important; /* Bing 스타일 오버라이드 */
color: var(--g-title) !important;
box-sizing: border-box; overflow: hidden; /* 오버플로우 방지 */
}
.gemini-btn img {
width: 16px !important; height: 16px !important;
object-fit: contain; /* 이미지 비율 유지 */
}
.gemini-btn:hover, .gemini-select:hover { transform: translateY(-2px); box-shadow: 0 2px 5px rgba(0,0,0,0.15); }
#google-search-btn, #bing-search-btn { flex: 8; gap: 8px; } /* gap으로 간격 조정 */
#gemini-model-select-top {
flex: 2; appearance: none; text-align-last: center; text-align: center;
outline: none; padding: 0 !important; margin: 0;
}
#gemini-model-select-top option { text-align: center; }
/* Main Box */
#gemini-box {
width: 100%; padding: ${Config.UI.PADDING}px;
border: 1px solid var(--g-border); border-radius: ${Config.UI.BORDER_RADIUS};
background: var(--g-bg); color: var(--g-text);
box-sizing: border-box; overflow: hidden;
position: relative; /* 자식 요소 위치 기준 */
}
#gemini-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
#gemini-title-wrap { display: flex; align-items: center; gap: 8px; text-decoration: none; color: inherit; }
#gemini-title-wrap h3 { font-weight: bold; margin: 0; font-size: 16px; color: var(--g-title); }
#gemini-time-info { font-size: 12px; color: #888; font-weight: normal; margin-left: 6px; }
#gemini-logo { width: 24px; height: 24px; cursor: pointer; transition: transform 0.2s; }
#gemini-logo:hover { transform: scale(1.1); }
#gemini-actions { display: flex; align-items: center; gap: 10px; }
/* Content & Markdown */
#gemini-content {
font-size: 14px; line-height: 1.6; color: var(--g-text);
padding-top: 4px; padding-right: 8px; /* 스크롤바 공간 확보 */
max-height: 650px; overflow-y: auto; scrollbar-width: thin;
}
#gemini-content p { margin: 0 0 12px 0; } /* 문단 간격 */
#gemini-content::-webkit-scrollbar { width: 6px; }
#gemini-content::-webkit-scrollbar-thumb { background-color: #ccc; border-radius: 3px; }
.gemini-dark-mode #gemini-content::-webkit-scrollbar-thumb { background-color: #555; }
#gemini-content h1, #gemini-content h2, #gemini-content h3 { margin-top: 20px; margin-bottom: 10px; }
#gemini-content ul, #gemini-content ol { padding-left: 20px; margin: 10px 0; }
#gemini-content li { margin-bottom: 5px; }
#gemini-content pre {
background: #1e1e1e;
color: #abb2bf;
padding: 38px 12px 12px 12px;
border-radius: 8px;
overflow-x: auto;
margin: 12px 0;
position: relative;
border: 1px solid var(--g-border);
}
.gemini-dark-mode #gemini-content pre {
background: #181a1f;
}
.code-header {
position: absolute;
top: 0; left: 0; right: 0;
height: 30px;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 12px;
font-size: 11px;
font-family: monospace;
color: #888;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.code-copy-btn-new {
background: transparent;
color: #abb2bf;
border: none;
font-size: 11px;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
transition: background 0.2s;
}
.code-copy-btn-new:hover {
background: rgba(255, 255, 255, 0.1);
}
#gemini-divider { height: 1px; border: 0; background: var(--g-border); margin: 8px 0 16px 0; }
/* Icons - Specific Animations */
.gemini-icon-btn {
width: 20px; height: 20px; cursor: pointer; opacity: 0.6; transition: 0.3s ease;
vertical-align: middle;
}
.gemini-icon-btn:hover { opacity: 1; }
#gemini-theme-toggle-btn:hover { transform: scale(1.2); }
#gemini-refresh-btn { transition: transform 0.6s ease-in-out; filter: var(--g-icon-filter); }
#gemini-refresh-btn:hover { transform: rotate(360deg); }
/* Toggle Switch */
.g-toggle { width: 44px; height: 24px; border-radius: 12px; position: relative; cursor: pointer; transition: background 0.2s; }
.g-knob { width: 22px; height: 22px; background: #fff; border-radius: 50%; position: absolute; top: 1px; left: 1px; transition: left 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.2); }
.g-toggle.on { background: #d1d5db; } .g-toggle.off { background: #3c4043; }
.g-toggle.on .g-knob { left: 21px; }
/* API Key Input */
#gemini-api-container { display: flex; flex-direction: column; gap: 10px; padding: 10px 0; }
#gemini-api-input { width: 95%; padding: 10px; border-radius: 8px; border: 1px solid var(--g-border); background: var(--g-bg); color: var(--g-text); }
#gemini-api-save-btn { align-self: flex-end; padding: 8px 16px; background: #4285f4; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; }
/* Device Specific */
.mobile-ua #gemini-box { border-radius: 16px; }
#b_context, .b_right { width: 432px !important; }
/* Skeleton Loading Animation */
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.skeleton-loader { display: flex; flex-direction: column; gap: 12px; padding: 4px 0; }
.skeleton-line {
height: 14px; width: 100%; border-radius: 4px;
background: linear-gradient(90deg, var(--g-bg) 25%, var(--g-border) 50%, var(--g-bg) 75%);
background-size: 200% 100%; animation: shimmer 1.5s infinite;
}
.skeleton-line.short { width: 60%; }
.skeleton-line.medium { width: 85%; }
/* RAG OFF diagonal slash */
#gemini-rag-toggle-btn { position: relative; }
#gemini-rag-toggle-btn.rag-off::after {
content: ''; position: absolute; inset: 0; pointer-events: none;
background: linear-gradient(
to bottom left,
transparent calc(50% - 1px),
var(--g-btn-border) calc(50% - 1px),
var(--g-btn-border) calc(50% + 1px),
transparent calc(50% + 1px)
);
}
`;
GM_addStyle(css);
document.documentElement.classList.add(Utils.Env.isMobile() ? 'mobile-ua' : 'desktop-ua');
}
};
/**
* ==========================================================================================
* 5. API & SERVICES
* ==========================================================================================
*/
const SearchScraper = {
getResults() {
if (!State.isRagEnabled()) return []; // RAG 비활성화 시 빈 결과 반환
const results = [];
const isGoogle = Utils.Env.isGoogle();
const isBing = Utils.Env.isBing();
if (isGoogle) {
// 구글 검색 결과 아이템 셀렉터 (div.g, div.MjjYud, .tF2Cxc 등 포함)
const items = document.querySelectorAll('div.g, div.MjjYud, .tF2Cxc');
items.forEach(item => {
const titleEl = item.querySelector('h3');
// 다양한 스니펫 셀렉터 대응 (새 레이아웃 대응용 line-clamp 스타일 div 및 추가 클래스 포함)
const snippetEl = item.querySelector('div.VwiC3b, span.aCOpbc, div.MUxGec, div[style*="-webkit-line-clamp"], .yXM1m, .kb0pb');
if (titleEl && snippetEl) {
const title = titleEl.textContent.trim();
const snippet = snippetEl.textContent.trim();
if (title && snippet) {
results.push({ title, snippet });
}
}
});
} else if (isBing) {
// 빙 검색 결과 아이템 셀렉터 (li.b_algo)
const items = document.querySelectorAll('li.b_algo');
items.forEach(item => {
const titleEl = item.querySelector('h2');
const snippetEl = item.querySelector('div.b_caption p, .b_algoSlug, div.b_attribution + p, p');
if (titleEl && snippetEl) {
const title = titleEl.textContent.trim();
const snippet = snippetEl.textContent.trim();
if (title && snippet) {
results.push({ title, snippet });
}
}
});
}
// 중복 제거 및 상위 5개 결과 추출
const unique = [];
const seen = new Set();
for (const res of results) {
const key = res.title + res.snippet;
if (!seen.has(key)) {
seen.add(key);
unique.push(res);
}
}
return unique.slice(0, 5);
}
};
const GeminiAPI = {
buildRagPrompt(query, results) {
const defaultPrompt = Utils.I18n.get('prompt', { query });
if (!results || results.length === 0) {
return defaultPrompt;
}
let context = "Below are the top search results from the search engine for this query:\n\n";
results.forEach((res, index) => {
context += `[Search Result ${index + 1}]\nTitle: ${res.title}\nSnippet: ${res.snippet}\n\n`;
});
const lang = navigator.language.includes('ko') ? 'ko' : 'default';
if (lang === 'ko') {
return `${context}
위 제공된 검색 결과 요약(Search Results)들을 신뢰성 있는 주요 정보원으로 사용하여, 다음 질문에 친절하고 상세하게 한국어로 답변해 주세요.
답변할 때 다음 지침을 지키십시오:
1. 제공된 검색 결과의 핵심 팩트를 우선적으로 정리하고 왜곡하지 마세요.
2. 필요하다면 본인의 일반 지식(우회 지식)을 결합하여 설명하되, 검색 결과와 모순되거나 잘못된 정보가 생기지 않도록 주의하세요.
3. 답변은 읽기 쉽도록 명확한 문단 구분과 마크다운(Markdown) 형식을 사용해 작성해 주세요.
질문: ${query}`;
} else {
return `${context}
Using the provided search results as the primary reliable sources of information, write a helpful and detailed response to the query: "${query}"
Please follow these guidelines:
1. Synthesize the core facts from the search results accurately without distortion.
2. If necessary, combine with your general knowledge, but ensure there are no contradictions with the search results.
3. Write the response in a structured and easy-to-read markdown format.`;
}
},
fetch(query, container, apiKey) {
container.innerHTML = `
<div class="skeleton-loader">
<div class="skeleton-line medium"></div>
<div class="skeleton-line"></div>
<div class="skeleton-line short"></div>
</div>
`;
// 시간 표시 초기화
const timeSpan = document.getElementById('gemini-time-info');
if (timeSpan) timeSpan.textContent = '(0.00s)';
const model = State.getModel();
const results = SearchScraper.getResults();
// RAG 뱃지 노출 상태 업데이트
const badge = document.getElementById('gemini-rag-badge');
if (badge) {
if (results.length > 0) {
badge.style.display = 'inline-block';
badge.title = `${results.length}개의 검색 결과를 반영하여 보강된 답변입니다.`;
} else {
badge.style.display = 'none';
}
}
const prompt = this.buildRagPrompt(query, results);
const startTime = performance.now();
let timerId = null;
// 스톱워치 시작
if (timeSpan) {
timerId = setInterval(() => {
const current = ((performance.now() - startTime) / 1000).toFixed(2);
timeSpan.textContent = `(${current}s)`;
}, 10);
}
GM_xmlhttpRequest({
method: 'POST',
url: `${Config.API.BASE_URL}${model}:generateContent?key=${apiKey}`,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }]
}),
onload: (res) => {
if (timerId) clearInterval(timerId); // 타이머 정지
try {
const endTime = performance.now();
const duration = ((endTime - startTime) / 1000).toFixed(2);
if (timeSpan) timeSpan.textContent = `(${duration}s)`;
const data = JSON.parse(res.responseText);
const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
if (text) {
sessionStorage.setItem(`${Config.STORAGE.PREFIX}${query}`, text);
sessionStorage.setItem(`${Config.STORAGE.PREFIX}${query}_time`, duration); // 시간 저장
sessionStorage.setItem(`${Config.STORAGE.PREFIX}${query}_rag_count`, results.length); // RAG 개수 저장
container.innerHTML = marked.parse(text);
UI.addCodeCopyButtons(container);
} else {
throw new Error(data.error?.message || 'Empty response');
}
} catch (e) {
container.textContent = `${Utils.I18n.get('error')} ${e.message}`;
}
},
onerror: () => {
if (timerId) clearInterval(timerId);
container.textContent = Utils.I18n.get('geminiEmpty');
}
});
}
};
const LinkCleaner = {
clean(root = document) {
root.querySelectorAll('a[href]').forEach(a => {
try {
const url = new URL(a.href);
// Google
if (url.hostname.includes('google.') && url.pathname === '/url') {
const q = url.searchParams.get('q') || url.searchParams.get('url');
if (q) a.href = decodeURIComponent(q);
}
// Bing
else if (url.hostname.includes('bing.com') && url.pathname.includes('/ck')) {
const u = url.searchParams.get('u');
if (u) {
try {
// Bing uses 'a1' prefix followed by base64 encoded URL
const target = u.replace(/^a1/, '');
const b64 = target.replace(/-/g, '+').replace(/_/g, '/');
// Add padding if needed
const pad = b64.length % 4;
const padded = pad ? b64 + '='.repeat(4 - pad) : b64;
const decoded = decodeURIComponent(atob(padded));
if (decoded.startsWith('http://') || decoded.startsWith('https://')) {
a.href = decoded;
}
} catch (e) {
// If base64 fails, fallback to standard link
}
}
}
} catch (e) { }
});
}
};
/**
* ==========================================================================================
* 6. UI COMPONENTS
* ==========================================================================================
*/
const UI = {
create(tag, { id, className, style, html, attributes, events } = {}) {
const el = document.createElement(tag);
if (id) el.id = id;
if (className) el.className = className;
if (style) Object.assign(el.style, style);
if (html) el.innerHTML = html;
if (attributes) Object.entries(attributes).forEach(([k, v]) => el.setAttribute(k, v));
if (events) Object.entries(events).forEach(([k, v]) => el.addEventListener(k, v));
return el;
},
Controls: {
createToggle() {
const enabled = State.isEnabled();
const wrap = UI.create('div', { style: { display: 'flex', alignItems: 'center', cursor: 'pointer', gap: '8px' } });
// Text Label
const label = UI.create('span', {
html: enabled ? 'ON' : 'OFF',
style: { fontSize: '13px', fontWeight: 'bold', color: enabled ? 'var(--g-title)' : '#888' }
});
// Switch
const switchEl = UI.create('div', { className: `g-toggle ${enabled ? 'on' : 'off'}` });
switchEl.appendChild(UI.create('div', { className: 'g-knob' }));
wrap.onclick = () => {
const next = !State.isEnabled();
State.setEnabled(next);
// Update UI
switchEl.className = `g-toggle ${next ? 'on' : 'off'}`;
label.textContent = next ? 'ON' : 'OFF';
label.style.color = next ? 'var(--g-title)' : '#888';
App.refresh();
};
wrap.append(label, switchEl);
return wrap;
}
},
createTopRow(query) {
const row = UI.create('div', { id: 'gemini-top-row' });
// 1. Search Switch Button
const isGoogle = Utils.Env.isGoogle();
const btn = UI.create('div', {
id: isGoogle ? 'bing-search-btn' : 'google-search-btn',
className: 'gemini-btn',
html: `<img src="${isGoogle ? Config.ASSETS.BING_LOGO : Config.ASSETS.GOOGLE_LOGO}"> <span>${Utils.I18n.get(isGoogle ? 'searchBing' : 'searchGoogle')}</span>`,
events: { click: () => window.open(`https://${isGoogle ? 'bing' : 'google'}.com/search?q=${encodeURIComponent(query)}`) },
style: { flex: '6' }
});
// 2. Model Selector
const select = UI.create('select', {
id: 'gemini-model-select-top',
className: 'gemini-select',
events: { change: (e) => { State.setModel(e.target.value); App.refresh(true); } },
style: { flex: '2' }
});
Config.MODELS.forEach(m => {
const opt = document.createElement('option');
opt.value = m.id; opt.textContent = m.name;
opt.selected = m.id === State.getModel();
select.appendChild(opt);
});
// 3. RAG Toggle Button (ON/OFF 상태를 극명하게 구별하는 디자인 적용)
const ragEnabled = State.isRagEnabled();
const ragBtn = UI.create('div', {
id: 'gemini-rag-toggle-btn',
className: ragEnabled ? 'gemini-btn' : 'gemini-btn rag-off',
html: '<span>RAG</span>',
style: {
flex: '2',
background: ragEnabled
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important'
: 'rgba(239, 68, 68, 0.08) !important',
color: ragEnabled
? '#fff !important'
: '#ef4444 !important',
border: '1px solid var(--g-btn-border)'
},
attributes: { title: ragEnabled ? 'RAG ON (실시간 검색 보강 사용 중)' : 'RAG OFF (실시간 검색 보강 사용 안 함)' },
events: {
click: () => {
const next = !State.isRagEnabled();
State.setRagEnabled(next);
ragBtn.classList.toggle('rag-off', !next);
ragBtn.style.background = next ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'rgba(239, 68, 68, 0.08)';
ragBtn.style.color = next ? '#fff' : '#ef4444';
ragBtn.style.border = '1px solid var(--g-btn-border)';
ragBtn.setAttribute('title', next ? 'RAG ON (실시간 검색 보강 사용 중)' : 'RAG OFF (실시간 검색 보강 사용 안 함)');
App.refresh(true);
}
}
});
row.appendChild(btn);
row.appendChild(select);
row.appendChild(ragBtn);
return row;
},
createApiKeyInput() {
const container = UI.create('div', { id: 'gemini-api-container' });
container.innerHTML = `<p style="margin:0; font-size:13px; font-weight:bold;">${Utils.I18n.get('enterApiKey')}</p>`;
const input = UI.create('input', { id: 'gemini-api-input', type: 'password', attributes: { placeholder: 'AIza...' } });
const saveKey = () => {
const val = input.value.trim();
if (val) { State.setApiKey(val); App.refresh(true); }
};
const btn = UI.create('button', {
id: 'gemini-api-save-btn', html: 'Save & Refresh', events: {
click: saveKey
}
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
saveKey();
}
});
container.appendChild(input);
container.appendChild(btn);
return container;
},
createMainBox(query) {
const box = UI.create('div', { id: 'gemini-box' });
// Header
const header = UI.create('div', { id: 'gemini-header' });
// Title Container (Wrapper)
const titleContainer = UI.create('div', { style: { display: 'flex', alignItems: 'center', gap: '0px' } });
// Link (Clickable)
const titleLink = UI.create('a', {
id: 'gemini-title-link',
attributes: { href: 'https://gemini.google.com/', target: '_blank' },
style: { display: 'flex', alignItems: 'center', gap: '8px', textDecoration: 'none', color: 'inherit' }
});
titleLink.innerHTML = `<img id="gemini-logo" src="${Config.ASSETS.GEMINI_LOGO}"> <h3 style="margin:0; font-size:16px; font-weight:bold; color:var(--g-title);">Gemini Results</h3>`;
// RAG Status Badge (Premium Styling)
const ragBadge = UI.create('span', {
id: 'gemini-rag-badge',
html: 'RAG',
style: {
fontSize: '10px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: '#fff',
padding: '2px 6px',
borderRadius: '4px',
fontWeight: 'bold',
marginLeft: '8px',
display: 'none',
verticalAlign: 'middle',
boxShadow: '0 1px 3px rgba(0,0,0,0.12)'
}
});
// Time Info (Non-clickable)
const timeSpan = UI.create('span', { id: 'gemini-time-info' });
titleContainer.append(titleLink, ragBadge, timeSpan);
const actions = UI.create('div', { id: 'gemini-actions' });
const toggle = this.Controls.createToggle();
const themeBtn = UI.create('img', {
id: 'gemini-theme-toggle-btn', className: 'gemini-icon-btn',
attributes: { src: State.getThemeIcon(), title: 'Toggle Theme' },
events: { click: () => { State.toggleTheme(); themeBtn.src = State.getThemeIcon(); } }
});
const refreshBtn = UI.create('img', {
id: 'gemini-refresh-btn', className: 'gemini-icon-btn',
attributes: { src: Config.ASSETS.REFRESH_ICON, title: 'Refresh' },
events: { click: () => App.refresh(true) }
});
actions.append(toggle, themeBtn, refreshBtn);
header.append(titleContainer, actions);
// Content
const content = UI.create('div', { id: 'gemini-content' });
const divider = UI.create('hr', { id: 'gemini-divider' });
box.append(header, divider, content);
return { box, content, refreshBtn };
},
addCodeCopyButtons(container) {
container.querySelectorAll('pre').forEach(pre => {
if (pre.querySelector('.code-header')) return;
// Extract language from class if marked added it, e.g. class="language-javascript"
const codeEl = pre.querySelector('code');
let lang = 'code';
if (codeEl) {
const match = [...codeEl.classList].find(c => c.startsWith('language-'));
if (match) {
lang = match.replace('language-', '');
}
}
const header = UI.create('div', {
className: 'code-header',
html: `<span class="code-lang">${lang.toUpperCase()}</span>`
});
const btn = UI.create('button', {
className: 'code-copy-btn-new',
html: 'Copy',
events: {
click: () => {
const code = codeEl?.innerText || pre.innerText;
const fallbackCopy = (text) => {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
btn.textContent = 'Copied!';
} catch (err) {
btn.textContent = 'Failed';
}
document.body.removeChild(textArea);
setTimeout(() => btn.textContent = 'Copy', 2000);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(code).then(() => {
btn.textContent = 'Copied!';
setTimeout(() => btn.textContent = 'Copy', 2000);
}).catch(() => fallbackCopy(code));
} else {
fallbackCopy(code);
}
}
}
});
header.appendChild(btn);
pre.prepend(header);
});
}
};
/**
* ==========================================================================================
* 7. MAIN APP CONTROLLER
* ==========================================================================================
*/
const App = {
elements: null,
init() {
State.init();
Styles.inject();
try { marked.setOptions({ breaks: true, gfm: true }); } catch (e) { } // 마크다운 옵션 설정
this.handleNavigation();
LinkCleaner.clean();
// Delayed init to wait for page load
const onReady = () => {
const target = Utils.Env.isGoogle() ? document.getElementById('rhs') : (document.getElementById('b_context') || document.querySelector('.b_right'));
// Mobile check first
if (Utils.Env.isMobile()) {
this.render(null); // Container not needed for mobile logic
} else if (target) {
this.render(target);
} else if (Utils.Env.isGoogle()) {
// Force rhs ...
// Force RHS creation if missing on Google
const rcnt = document.getElementById('rcnt');
if (rcnt) {
const rhs = UI.create('div', { id: 'rhs', style: { marginLeft: '16px', width: '432px' } });
rcnt.appendChild(rhs);
this.render(rhs);
}
}
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', onReady);
else onReady();
},
render(container) {
const query = Utils.Env.getQuery();
if (!query) return;
// 1. Mobile/Tablet Mode (Search Button Only)
if (Utils.Env.isMobile()) {
document.getElementById('mobile-search-btn-wrapper')?.remove();
const btn = UI.create('div', {
id: 'mobile-search-btn-wrapper',
style: { margin: '0', padding: '15px 16px', background: 'var(--g-bg)' }
});
// 모바일용 버튼 생성 (UI.createTopRow에서 버튼만 추출하여 사용하거나 새로 생성)
const isGoogle = Utils.Env.isGoogle();
const searchBtn = UI.create('div', {
id: isGoogle ? 'bing-search-btn' : 'google-search-btn',
className: 'gemini-btn',
html: `<img src="${isGoogle ? Config.ASSETS.BING_LOGO : Config.ASSETS.GOOGLE_LOGO}"> <span>${Utils.I18n.get(isGoogle ? 'searchBing' : 'searchGoogle')}</span>`,
events: { click: () => window.open(`https://${isGoogle ? 'bing' : 'google'}.com/search?q=${encodeURIComponent(query)}`) },
style: { width: '100%' } // 모바일은 꽉 차게
});
btn.appendChild(searchBtn);
// Insert Position Logic
if (isGoogle) {
const target = document.getElementById('main') || document.getElementById('rcnt');
if (target) target.parentNode.insertBefore(btn, target);
} else {
const target = document.getElementById('b_content');
if (target) target.prepend(btn);
}
return; // Gemini 로직 중단
}
// 2. Desktop Mode (Existing Logic)
if (!container) return;
// Google Width Enforcement
if (Utils.Env.isGoogle() && container.id === 'rhs') {
container.style.width = '432px';
container.style.minWidth = '432px';
}
// Remove existing
document.getElementById('gemini-wrapper')?.remove();
// Structure
const wrapper = UI.create('div', { id: 'gemini-wrapper' });
wrapper.appendChild(UI.createTopRow(query));
const { box, content, refreshBtn } = UI.createMainBox(query);
this.elements = { content, refreshBtn }; // Cache for updates
wrapper.appendChild(box);
container.prepend(wrapper);
this.loadContent(query);
},
loadContent(query, forceRefresh = false) {
if (!this.elements) return;
const { content, refreshBtn } = this.elements;
const apiKey = State.getApiKey();
if (!apiKey) {
content.innerHTML = '';
content.appendChild(UI.createApiKeyInput());
refreshBtn.style.display = 'none';
return;
}
refreshBtn.style.display = 'block';
if (!State.isEnabled()) {
content.textContent = Utils.I18n.get('geminiOff');
return;
}
const cached = !forceRefresh && sessionStorage.getItem(`${Config.STORAGE.PREFIX}${query}`);
if (cached) {
const cachedTime = sessionStorage.getItem(`${Config.STORAGE.PREFIX}${query}_time`);
const timeSpan = document.getElementById('gemini-time-info');
if (timeSpan && cachedTime) timeSpan.textContent = `(${cachedTime}s)`;
// 캐시된 RAG 뱃지 상태 복원
const cachedRagCount = sessionStorage.getItem(`${Config.STORAGE.PREFIX}${query}_rag_count`);
const badge = document.getElementById('gemini-rag-badge');
if (badge) {
if (cachedRagCount && parseInt(cachedRagCount) > 0) {
badge.style.display = 'inline-block';
badge.title = `${cachedRagCount}개의 검색 결과를 반영하여 보강된 답변입니다.`;
} else {
badge.style.display = 'none';
}
}
content.innerHTML = marked.parse(cached);
UI.addCodeCopyButtons(content);
} else {
GeminiAPI.fetch(query, content, apiKey);
}
},
refresh(force = false) {
if (force) {
// 모든 Gemini 관련 캐시(sessionStorage) 삭제
Object.keys(sessionStorage).forEach(key => {
if (key.startsWith(Config.STORAGE.PREFIX)) {
sessionStorage.removeItem(key);
}
});
}
const query = Utils.Env.getQuery();
if (query) this.loadContent(query, force);
},
handleNavigation() {
// SPA Navigation Watcher
let lastUrl = location.href;
const check = () => {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(() => {
const target = document.getElementById('rhs') || document.querySelector('#b_context') || document.querySelector('.b_right');
this.render(target);
LinkCleaner.clean();
}, 500);
}
};
// Listen to standard popstate and hashchange
window.addEventListener('popstate', check);
window.addEventListener('hashchange', check);
// Hook History API pushState/replaceState
const originalPush = history.pushState;
history.pushState = function (...args) {
originalPush.apply(this, args);
check();
};
const originalReplace = history.replaceState;
history.replaceState = function (...args) {
originalReplace.apply(this, args);
check();
};
}
};
// Run
App.init();
})();