Bing Plus

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

当前为 2025-04-23 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Bing Plus
  3. // @version 5.2
  4. // @description Display Gemini response results next to Bing search results and speed up searches by eliminating intermediate URLs.
  5. // @author lanpod
  6. // @match https://www.bing.com/search*
  7. // @grant GM_addStyle
  8. // @grant GM_xmlhttpRequest
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.7/marked.min.js
  10. // @license MIT
  11. // @namespace http://tampermonkey.net/
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. // 설정 모듈
  18. const Config = {
  19. API: {
  20. GEMINI_MODEL: 'gemini-2.0-flash',
  21. GEMINI_URL: 'https://generativelanguage.googleapis.com/v1beta/models/',
  22. MARKED_CDN_URL: 'https://api.cdnjs.com/libraries/marked'
  23. },
  24. VERSIONS: {
  25. MARKED_VERSION: '15.0.7'
  26. },
  27. CACHE: {
  28. PREFIX: 'gemini_cache_'
  29. },
  30. STORAGE_KEYS: {
  31. CURRENT_VERSION: 'markedCurrentVersion',
  32. LATEST_VERSION: 'markedLatestVersion',
  33. LAST_NOTIFIED: 'markedLastNotifiedVersion'
  34. },
  35. UI: {
  36. DEFAULT_MARGIN: 8,
  37. DEFAULT_PADDING: 16,
  38. Z_INDEX: 9999
  39. },
  40. STYLES: {
  41. COLORS: {
  42. BACKGROUND: '#fff',
  43. BORDER: '#e0e0e0',
  44. TEXT: '#000',
  45. TITLE: '#000',
  46. BUTTON_BG: '#f0f3ff',
  47. BUTTON_BORDER: '#ccc',
  48. DARK_BACKGROUND: '#202124',
  49. DARK_BORDER: '#5f6368',
  50. DARK_TEXT: '#fff',
  51. CODE_BLOCK_BG: '#f0f0f0', // 라이트 모드 코드 블록 배경색
  52. DARK_CODE_BLOCK_BG: '#555' // 다크 모드 코드 블록 배경색
  53. },
  54. BORDER: '1px solid #e0e0e0',
  55. BORDER_RADIUS: '4px',
  56. FONT_SIZE: {
  57. TEXT: '14px',
  58. TITLE: '18px'
  59. },
  60. ICON_SIZE: '20px',
  61. LOGO_SIZE: '24px',
  62. SMALL_ICON_SIZE: '16px'
  63. },
  64. ASSETS: {
  65. GOOGLE_LOGO: 'https://www.gstatic.com/marketing-cms/assets/images/bc/1a/a310779347afa1927672dc66a98d/g.png=s48-fcrop64=1,00000000ffffffff-rw',
  66. GEMINI_LOGO: 'https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg',
  67. REFRESH_ICON: 'https://www.svgrepo.com/show/533704/refresh-cw-alt-3.svg'
  68. },
  69. MESSAGE_KEYS: {
  70. PROMPT: 'prompt',
  71. ENTER_API_KEY: 'enterApiKey',
  72. GEMINI_EMPTY: 'geminiEmpty',
  73. PARSE_ERROR: 'parseError',
  74. NETWORK_ERROR: 'networkError',
  75. TIMEOUT: 'timeout',
  76. LOADING: 'loading',
  77. UPDATE_TITLE: 'updateTitle',
  78. UPDATE_NOW: 'updateNow',
  79. SEARCH_ON_GOOGLE: 'searchongoogle'
  80. }
  81. };
  82.  
  83. // 지역화 모듈
  84. const Localization = {
  85. MESSAGES: {
  86. [Config.MESSAGE_KEYS.PROMPT]: {
  87. ko: `"${'${query}'}" 대한 정보를 찾아줘`,
  88. zh: `请以标记格式填写有关\"${'${query}'}\"的信息。`,
  89. default: `Please write information about \"${'${query}'}\" in markdown format`
  90. },
  91. [Config.MESSAGE_KEYS.ENTER_API_KEY]: {
  92. ko: 'Gemini API 키를 입력하세요:',
  93. zh: '请输入 Gemini API 密钥:',
  94. default: 'Please enter your Gemini API key:'
  95. },
  96. [Config.MESSAGE_KEYS.GEMINI_EMPTY]: {
  97. ko: '⚠️ Gemini 응답이 비어있습니다.',
  98. zh: '⚠️ Gemini 返回为空。',
  99. default: '⚠️ Gemini response is empty.'
  100. },
  101. [Config.MESSAGE_KEYS.PARSE_ERROR]: {
  102. ko: '❌ 파싱 오류:',
  103. zh: '❌ 解析错误:',
  104. default: '❌ Parsing error:'
  105. },
  106. [Config.MESSAGE_KEYS.NETWORK_ERROR]: {
  107. ko: '❌ 네트워크 오류:',
  108. zh: '❌ 网络错误:',
  109. default: '❌ Network error:'
  110. },
  111. [Config.MESSAGE_KEYS.TIMEOUT]: {
  112. ko: '❌ 요청 시간이 초과되었습니다.',
  113. zh: '❌ 请求超时。',
  114. default: '❌ Request timeout'
  115. },
  116. [Config.MESSAGE_KEYS.LOADING]: {
  117. ko: '불러오는 중...',
  118. zh: '加载中...',
  119. default: 'Loading...'
  120. },
  121. [Config.MESSAGE_KEYS.UPDATE_TITLE]: {
  122. ko: 'marked.min.js 업데이트 필요',
  123. zh: '需要更新 marked.min.js',
  124. default: 'marked.min.js update required'
  125. },
  126. [Config.MESSAGE_KEYS.UPDATE_NOW]: {
  127. ko: '확인',
  128. zh: '确认',
  129. default: 'OK'
  130. },
  131. [Config.MESSAGE_KEYS.SEARCH_ON_GOOGLE]: {
  132. ko: 'Google 에서 검색하기',
  133. zh: '在 Google 上搜索',
  134. default: 'Search on Google'
  135. }
  136. },
  137. getMessage(key, vars = {}) {
  138. const lang = navigator.language;
  139. const langKey = lang.includes('ko') ? 'ko' : lang.includes('zh') ? 'zh' : 'default';
  140. const template = this.MESSAGES[key]?.[langKey] || this.MESSAGES[key]?.default || '';
  141. return template.replace(/\$\{(.*?)\}/g, (_, k) => vars[k] || '');
  142. }
  143. };
  144.  
  145. // 스타일 모듈
  146. const Styles = {
  147. inject() {
  148. console.log('Injecting styles...');
  149. const currentTheme = document.documentElement.getAttribute('data-theme') ||
  150. (document.documentElement.classList.contains('dark') ||
  151. document.documentElement.classList.contains('b_dark')) ? 'dark' :
  152. (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
  153. console.log(`Detected theme: ${currentTheme}`);
  154. GM_addStyle(`
  155. #b_results > li.b_ad a { color: green !important; }
  156.  
  157. /* 상위 요소 스타일 초기화 */
  158. #b_context, .b_context, .b_right {
  159. color: initial !important;
  160. border: none !important;
  161. border-width: 0 !important;
  162. border-style: none !important;
  163. border-collapse: separate !important;
  164. background: transparent !important;
  165. }
  166.  
  167. #b_context #gemini-box,
  168. .b_right #gemini-box {
  169. width: 100%;
  170. max-width: 100%;
  171. background: ${Config.STYLES.COLORS.BACKGROUND} !important;
  172. border: ${Config.STYLES.BORDER} !important;
  173. border-style: solid !important;
  174. border-width: 1px !important;
  175. border-radius: ${Config.STYLES.BORDER_RADIUS};
  176. padding: ${Config.UI.DEFAULT_PADDING}px;
  177. margin-bottom: ${Config.UI.DEFAULT_MARGIN * 2.5}px;
  178. font-family: sans-serif;
  179. overflow-x: auto;
  180. position: relative;
  181. box-sizing: border-box;
  182. color: initial !important;
  183. }
  184.  
  185. [data-theme="light"] #b_context #gemini-box,
  186. [data-theme="light"] .b_right #gemini-box,
  187. .light #b_context #gemini-box,
  188. .light .b_right #gemini-box {
  189. background: ${Config.STYLES.COLORS.BACKGROUND} !important;
  190. border: 1px solid ${Config.STYLES.COLORS.BORDER} !important;
  191. border-style: solid !important;
  192. border-width: 1px !important;
  193. }
  194.  
  195. [data-theme="light"] #b_context #gemini-box h3,
  196. [data-theme="light"] .b_right #gemini-box h3,
  197. .light #b_context #gemini-box h3,
  198. .light .b_right #gemini-box h3 {
  199. color: ${Config.STYLES.COLORS.TITLE} !important;
  200. }
  201.  
  202. [data-theme="light"] #b_context #gemini-content,
  203. [data-theme="light"] #b_context #gemini-content *,
  204. [data-theme="light"] .b_right #gemini-content,
  205. [data-theme="light"] .b_right #gemini-content *,
  206. .light #b_context #gemini-content,
  207. .light #b_context #gemini-content *,
  208. .light .b_right #gemini-content,
  209. .light .b_right #gemini-content * {
  210. color: ${Config.STYLES.COLORS.TEXT} !important;
  211. background: transparent !important;
  212. }
  213.  
  214. [data-theme="light"] #b_context #gemini-divider,
  215. [data-theme="light"] .b_right #gemini-divider,
  216. .light #b_context #gemini-divider,
  217. .light .b_right #gemini-divider {
  218. background: ${Config.STYLES.COLORS.BORDER} !important;
  219. }
  220.  
  221. [data-theme="dark"] #b_context #gemini-box,
  222. [data-theme="dark"] .b_right #gemini-box,
  223. .dark #b_context #gemini-box,
  224. .dark .b_right #gemini-box,
  225. .b_dark #b_context #gemini-box,
  226. .b_dark .b_right #gemini-box {
  227. background: ${Config.STYLES.COLORS.DARK_BACKGROUND} !important;
  228. border: 1px solid ${Config.STYLES.COLORS.DARK_BORDER} !important;
  229. border-style: solid !important;
  230. border-width: 1px !important;
  231. }
  232.  
  233. @media (prefers-color-scheme: dark) {
  234. #b_context #gemini-box,
  235. .b_right #gemini-box {
  236. background: ${Config.STYLES.COLORS.DARK_BACKGROUND} !important;
  237. border: 1px solid ${Config.STYLES.COLORS.DARK_BORDER} !important;
  238. border-style: solid !important;
  239. border-width: 1px !important;
  240. }
  241. }
  242.  
  243. [data-theme="dark"] #b_context #gemini-box h3,
  244. [data-theme="dark"] .b_right #gemini-box h3,
  245. .dark #b_context #gemini-box h3,
  246. .dark .b_right #gemini-box h3,
  247. .b_dark #b_context #gemini-box h3,
  248. .b_dark .b_right #gemini-box h3 {
  249. color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
  250. }
  251.  
  252. @media (prefers-color-scheme: dark) {
  253. #b_context #gemini-box h3,
  254. .b_right #gemini-box h3 {
  255. color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
  256. }
  257. }
  258.  
  259. [data-theme="dark"] #b_context #gemini-content,
  260. [data-theme="dark"] #b_context #gemini-content *,
  261. [data-theme="dark"] .b_right #gemini-content,
  262. [data-theme="dark"] .b_right #gemini-content *,
  263. .dark #b_context #gemini-content,
  264. .dark #b_context #gemini-content *,
  265. .dark .b_right #gemini-content,
  266. .dark .b_right #gemini-content *,
  267. .b_dark #b_context #gemini-content,
  268. .b_dark #b_context #gemini-content *,
  269. .b_dark .b_right #gemini-content,
  270. .b_dark .b_right #gemini-content * {
  271. color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
  272. background: transparent !important;
  273. }
  274.  
  275. @media (prefers-color-scheme: dark) {
  276. #b_context #gemini-content,
  277. #b_context #gemini-content *,
  278. .b_right #gemini-content,
  279. .b_right #gemini-content * {
  280. color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
  281. background: transparent !important;
  282. }
  283. }
  284.  
  285. /* 코드 블록 스타일 */
  286. #gemini-content pre {
  287. background: ${Config.STYLES.COLORS.CODE_BLOCK_BG} !important;
  288. padding: ${Config.UI.DEFAULT_MARGIN + 2}px;
  289. border-radius: ${Config.STYLES.BORDER_RADIUS};
  290. overflow-x: auto;
  291. }
  292.  
  293. /* 다크 모드에서 코드 블록 배경색 */
  294. [data-theme="dark"] #gemini-content pre,
  295. .dark #gemini-content pre,
  296. .b_dark #gemini-content pre,
  297. [data-theme="dark"] #b_context #gemini-content pre,
  298. [data-theme="dark"] .b_right #gemini-content pre,
  299. .dark #b_context #gemini-content pre,
  300. .dark .b_right #gemini-content pre,
  301. .b_dark #b_context #gemini-content pre,
  302. .b_dark .b_right #gemini-content pre {
  303. background: ${Config.STYLES.COLORS.DARK_CODE_BLOCK_BG} !important;
  304. }
  305.  
  306. @media (prefers-color-scheme: dark) {
  307. #gemini-content pre,
  308. #b_context #gemini-content pre,
  309. .b_right #gemini-content pre {
  310. background: ${Config.STYLES.COLORS.DARK_CODE_BLOCK_BG} !important;
  311. }
  312. }
  313.  
  314. [data-theme="dark"] #b_context #gemini-divider,
  315. [data-theme="dark"] .b_right #gemini-divider,
  316. .dark #b_context #gemini-divider,
  317. .dark .b_right #gemini-divider,
  318. .b_dark #b_context #gemini-divider,
  319. .b_dark .b_right #gemini-divider {
  320. background: ${Config.STYLES.COLORS.DARK_BORDER} !important;
  321. }
  322.  
  323. @media (prefers-color-scheme: dark) {
  324. #b_context #gemini-divider,
  325. .b_right #gemini-divider {
  326. background: ${Config.STYLES.COLORS.DARK_BORDER} !important;
  327. }
  328. }
  329.  
  330. #gemini-header {
  331. display: flex;
  332. align-items: center;
  333. justify-content: space-between;
  334. margin-bottom: ${Config.UI.DEFAULT_MARGIN}px;
  335. }
  336.  
  337. #gemini-title-wrap {
  338. display: flex;
  339. align-items: center;
  340. }
  341.  
  342. #gemini-logo {
  343. width: ${Config.STYLES.LOGO_SIZE};
  344. height: ${Config.STYLES.LOGO_SIZE};
  345. margin-right: ${Config.UI.DEFAULT_MARGIN}px;
  346. }
  347.  
  348. #gemini-box h3 {
  349. margin: 0;
  350. font-size: ${Config.STYLES.FONT_SIZE.TITLE};
  351. font-weight: bold;
  352. }
  353.  
  354. #gemini-refresh-btn {
  355. width: ${Config.STYLES.ICON_SIZE};
  356. height: ${Config.STYLES.ICON_SIZE};
  357. cursor: pointer;
  358. opacity: 0.6;
  359. transition: transform 0.5s ease;
  360. }
  361.  
  362. #gemini-refresh-btn:hover {
  363. opacity: 1;
  364. transform: rotate(360deg);
  365. }
  366.  
  367. #gemini-divider {
  368. height: 1px;
  369. margin: ${Config.UI.DEFAULT_MARGIN}px 0;
  370. }
  371.  
  372. #gemini-content {
  373. font-size: ${Config.STYLES.FONT_SIZE.TEXT};
  374. line-height: 1.6;
  375. white-space: pre-wrap;
  376. word-wrap: break-word;
  377. background: transparent !important;
  378. }
  379.  
  380. #google-search-btn {
  381. width: 100%;
  382. max-width: 100%;
  383. font-size: ${Config.STYLES.FONT_SIZE.TEXT};
  384. padding: ${Config.UI.DEFAULT_MARGIN}px;
  385. margin-bottom: ${Config.UI.DEFAULT_MARGIN * 1.25}px;
  386. cursor: pointer;
  387. border: 1px solid ${Config.STYLES.COLORS.BUTTON_BORDER};
  388. border-radius: ${Config.STYLES.BORDER_RADIUS};
  389. background-color: ${Config.STYLES.COLORS.BUTTON_BG};
  390. color: ${Config.STYLES.COLORS.TITLE};
  391. font-family: sans-serif;
  392. display: flex;
  393. align-items: center;
  394. justify-content: center;
  395. gap: ${Config.UI.DEFAULT_MARGIN}px;
  396. transition: transform 0.2s ease; /* 확대 애니메이션 부드럽게 */
  397. }
  398.  
  399. #google-search-btn img {
  400. width: ${Config.STYLES.SMALL_ICON_SIZE};
  401. height: ${Config.STYLES.SMALL_ICON_SIZE};
  402. vertical-align: middle;
  403. transition: transform 0.2s ease; /* 아이콘 확대 애니메이션 */
  404. }
  405.  
  406. /* PC에서 버튼 호버 시 확대 효과 */
  407. @media (min-width: 769px) {
  408. #google-search-btn:hover {
  409. transform: scale(1.1); /* 버튼 전체 확대 */
  410. }
  411. #google-search-btn:hover img {
  412. transform: scale(1.1); /* 아이콘 추가 확대 */
  413. }
  414. }
  415.  
  416. #marked-update-popup {
  417. position: fixed;
  418. top: 30%;
  419. left: 50%;
  420. transform: translate(-50%, -50%);
  421. background: ${Config.STYLES.COLORS.BACKGROUND};
  422. padding: ${Config.UI.DEFAULT_PADDING * 1.25}px;
  423. z-index: ${Config.UI.Z_INDEX};
  424. border: 1px solid ${Config.STYLES.COLORS.BUTTON_BORDER};
  425. box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  426. text-align: center;
  427. }
  428.  
  429. [data-theme="dark"] #marked-update-popup,
  430. .dark #marked-update-popup,
  431. .b_dark #marked-update-popup {
  432. background: ${Config.STYLES.COLORS.DARK_BACKGROUND} !important;
  433. color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
  434. }
  435.  
  436. @media (prefers-color-scheme: dark) {
  437. #marked-update-popup {
  438. background: ${Config.STYLES.COLORS.DARK_BACKGROUND} !important;
  439. color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
  440. }
  441. }
  442.  
  443. #marked-update-popup button {
  444. margin-top: ${Config.UI.DEFAULT_MARGIN * 1.25}px;
  445. padding: ${Config.UI.DEFAULT_MARGIN}px ${Config.UI.DEFAULT_PADDING}px;
  446. cursor: pointer;
  447. border: 1px solid ${Config.STYLES.COLORS.BUTTON_BORDER};
  448. border-radius: ${Config.STYLES.BORDER_RADIUS};
  449. background-color: ${Config.STYLES.COLORS.BUTTON_BG};
  450. color: ${Config.STYLES.COLORS.TITLE};
  451. font-family: sans-serif;
  452. }
  453.  
  454. @media (max-width: 768px) {
  455. #google-search-btn {
  456. max-width: 96%;
  457. margin: ${Config.UI.DEFAULT_MARGIN}px auto;
  458. padding: ${Config.UI.DEFAULT_PADDING * 0.75}px;
  459. border-radius: 16px;
  460. }
  461. #gemini-box {
  462. padding: ${Config.UI.DEFAULT_PADDING * 0.75}px;
  463. border-radius: 16px;
  464. }
  465. }
  466. `);
  467. console.log('Styles injected', {
  468. light: {
  469. background: Config.STYLES.COLORS.BACKGROUND,
  470. text: Config.STYLES.COLORS.TEXT,
  471. title: Config.STYLES.COLORS.TITLE,
  472. border: Config.STYLES.COLORS.BORDER,
  473. codeBlockBg: Config.STYLES.COLORS.CODE_BLOCK_BG
  474. },
  475. dark: {
  476. background: Config.STYLES.COLORS.DARK_BACKGROUND,
  477. text: Config.STYLES.COLORS.DARK_TEXT,
  478. border: Config.STYLES.COLORS.DARK_BORDER,
  479. codeBlockBg: Config.STYLES.COLORS.DARK_CODE_BLOCK_BG
  480. }
  481. });
  482.  
  483. // 계산된 스타일 디버깅
  484. setTimeout(() => {
  485. const geminiBox = document.querySelector('#b_context #gemini-box') ||
  486. document.querySelector('.b_right #gemini-box');
  487. const content = document.querySelector('#b_context #gemini-content') ||
  488. document.querySelector('.b_right #gemini-content');
  489. const codeBlock = document.querySelector('#gemini-content pre');
  490. const bContext = document.querySelector('#b_context');
  491. const bContextParent = document.querySelector('.b_context');
  492. const bRight = document.querySelector('.b_right');
  493. if (geminiBox && content) {
  494. const computedBoxStyle = window.getComputedStyle(geminiBox);
  495. const computedContentStyle = window.getComputedStyle(content);
  496. const computedCodeBlockStyle = codeBlock ? window.getComputedStyle(codeBlock) : null;
  497. const computedBContextStyle = bContext ? window.getComputedStyle(bContext) : null;
  498. const computedBContextParentStyle = bContextParent ? window.getComputedStyle(bContextParent) : null;
  499. const computedBRightStyle = bRight ? window.getComputedStyle(bRight) : null;
  500. console.log('Computed styles:', {
  501. geminiBox: {
  502. background: computedBoxStyle.backgroundColor,
  503. border: computedBoxStyle.border,
  504. borderStyle: computedBoxStyle.borderStyle,
  505. borderWidth: computedBoxStyle.borderWidth,
  506. borderColor: computedBoxStyle.borderColor
  507. },
  508. geminiContent: {
  509. background: computedContentStyle.backgroundColor,
  510. color: computedContentStyle.color,
  511. children: Array.from(content.children).map(child => ({
  512. tag: child.tagName,
  513. color: window.getComputedStyle(child).color,
  514. background: child.tagName === 'PRE' ? window.getComputedStyle(child).backgroundColor : undefined
  515. }))
  516. },
  517. codeBlock: codeBlock ? {
  518. background: computedCodeBlockStyle.backgroundColor,
  519. padding: computedCodeBlockStyle.padding,
  520. borderRadius: computedCodeBlockStyle.borderRadius
  521. } : 'No code block found',
  522. bContext: bContext ? {
  523. background: computedBContextStyle.backgroundColor,
  524. color: computedBContextStyle.color,
  525. border: computedBContextStyle.border
  526. } : null,
  527. bContextParent: bContextParent ? {
  528. background: computedBContextParentStyle.backgroundColor,
  529. color: computedBContextParentStyle.color,
  530. border: computedBContextParentStyle.border
  531. } : null,
  532. bRight: bRight ? {
  533. background: computedBRightStyle.backgroundColor,
  534. color: computedBRightStyle.color,
  535. border: computedBRightStyle.border
  536. } : null
  537. });
  538. } else {
  539. console.log('Elements not found for computed style check', {
  540. geminiBox: !!geminiBox,
  541. content: !!content,
  542. codeBlock: !!codeBlock,
  543. bContext: !!bContext,
  544. bContextParent: !!bContextParent,
  545. bRight: !!bRight
  546. });
  547. }
  548. }, 2000); // 2초 지연으로 DOM 로드 대기
  549. }
  550. };
  551.  
  552. // 유틸리티 모듈
  553. const Utils = {
  554. isDesktop() {
  555. const isDesktop = window.innerWidth > 768 && !/Mobi|Android/i.test(navigator.userAgent);
  556. console.log('isDesktop:', { width: window.innerWidth, userAgent: navigator.userAgent, result: isDesktop });
  557. return isDesktop;
  558. },
  559. isGeminiAvailable() {
  560. const hasBContext = !!document.getElementById('b_context');
  561. const hasBRight = !!document.querySelector('.b_right');
  562. console.log('Bing isGeminiAvailable:', { isDesktop: this.isDesktop(), hasBContext, hasBRight });
  563. return this.isDesktop() && (hasBContext || hasBRight);
  564. },
  565. getQuery() {
  566. const query = new URLSearchParams(location.search).get('q');
  567. console.log('getQuery:', { query, search: location.search });
  568. return query;
  569. },
  570. getApiKey() {
  571. let key = localStorage.getItem('geminiApiKey');
  572. if (!key) {
  573. key = prompt(Localization.getMessage(Config.MESSAGE_KEYS.ENTER_API_KEY));
  574. if (key) localStorage.setItem('geminiApiKey', key);
  575. console.log('API key:', key ? 'stored' : 'prompt failed');
  576. } else {
  577. console.log('API key retrieved');
  578. }
  579. return key;
  580. }
  581. };
  582.  
  583. // UI 모듈
  584. const UI = {
  585. createGoogleButton(query) {
  586. const btn = document.createElement('button');
  587. btn.id = 'google-search-btn';
  588. btn.innerHTML = `
  589. <img src="${Config.ASSETS.GOOGLE_LOGO}" alt="Google Logo">
  590. ${Localization.getMessage(Config.MESSAGE_KEYS.SEARCH_ON_GOOGLE)}
  591. `;
  592. btn.onclick = () => window.open(`https://www.google.com/search?q=${encodeURIComponent(query)}`, '_blank');
  593. return btn;
  594. },
  595. createGeminiBox(query, apiKey) {
  596. const box = document.createElement('div');
  597. box.id = 'gemini-box';
  598. box.innerHTML = `
  599. <div id="gemini-header">
  600. <div id="gemini-title-wrap">
  601. <img id="gemini-logo" src="${Config.ASSETS.GEMINI_LOGO}" alt="Gemini Logo">
  602. <h3>Gemini Search Results</h3>
  603. </div>
  604. <img id="gemini-refresh-btn" title="Refresh" src="${Config.ASSETS.REFRESH_ICON}" />
  605. </div>
  606. <hr id="gemini-divider">
  607. <div id="gemini-content">${Localization.getMessage(Config.MESSAGE_KEYS.LOADING)}</div>
  608. `;
  609. box.querySelector('#gemini-refresh-btn').onclick = () => GeminiAPI.fetch(query, box.querySelector('#gemini-content'), apiKey, true);
  610. return box;
  611. },
  612. createGeminiUI(query, apiKey) {
  613. const wrapper = document.createElement('div');
  614. wrapper.appendChild(this.createGoogleButton(query));
  615. wrapper.appendChild(this.createGeminiBox(query, apiKey));
  616. console.log('Gemini UI created:', { query, hasApiKey: !!apiKey });
  617. return wrapper;
  618. }
  619. };
  620.  
  621. // Gemini API 모듈
  622. const GeminiAPI = {
  623. fetch(query, container, apiKey, force = false) {
  624. console.log('Fetching Gemini API:', { query, force });
  625. VersionChecker.checkMarkedJsVersion();
  626.  
  627. const cacheKey = `${Config.CACHE.PREFIX}${query}`;
  628. if (!force) {
  629. const cached = sessionStorage.getItem(cacheKey);
  630. if (cached) {
  631. container.innerHTML = marked.parse(cached);
  632. console.log('Loaded from cache:', { query });
  633. return;
  634. }
  635. }
  636.  
  637. container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.LOADING);
  638.  
  639. GM_xmlhttpRequest({
  640. method: 'POST',
  641. url: `${Config.API.GEMINI_URL}${Config.API.GEMINI_MODEL}:generateContent?key=${apiKey}`,
  642. headers: { 'Content-Type': 'application/json' },
  643. data: JSON.stringify({
  644. contents: [{
  645. parts: [{ text: Localization.getMessage(Config.MESSAGE_KEYS.PROMPT, { query }) }]
  646. }]
  647. }),
  648. onload({ responseText }) {
  649. try {
  650. const text = JSON.parse(responseText)?.candidates?.[0]?.content?.parts?.[0]?.text;
  651. if (text) {
  652. sessionStorage.setItem(cacheKey, text);
  653. container.innerHTML = marked.parse(text);
  654. console.log('Gemini API success:', { query });
  655. } else {
  656. container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.GEMINI_EMPTY);
  657. console.log('Gemini API empty response');
  658. }
  659. } catch (e) {
  660. container.textContent = `${Localization.getMessage(Config.MESSAGE_KEYS.PARSE_ERROR)} ${e.message}`;
  661. console.error('Gemini API parse error:', e.message);
  662. }
  663. },
  664. onerror: err => {
  665. container.textContent = `${Localization.getMessage(Config.MESSAGE_KEYS.NETWORK_ERROR)} ${err.finalUrl}`;
  666. console.error('Gemini API network error:', err);
  667. },
  668. ontimeout: () => {
  669. container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.TIMEOUT);
  670. console.error('Gemini API timeout');
  671. }
  672. });
  673. }
  674. };
  675.  
  676. // 링크 정리 모듈
  677. const LinkCleaner = {
  678. decodeRealUrl(url, key) {
  679. const param = new URL(url).searchParams.get(key)?.replace(/^a1/, '');
  680. if (!param) return null;
  681. try {
  682. const decoded = decodeURIComponent(atob(param.replace(/_/g, '/').replace(/-/g, '+')));
  683. return decoded.startsWith('/') ? location.origin + decoded : decoded;
  684. } catch {
  685. return null;
  686. }
  687. },
  688. resolveRealUrl(url) {
  689. const rules = [
  690. { pattern: /bing\.com\/(ck\/a|aclick)/, key: 'u' },
  691. { pattern: /so\.com\/search\/eclk/, key: 'aurl' }
  692. ];
  693. for (const { pattern, key } of rules) {
  694. if (pattern.test(url)) {
  695. const real = this.decodeRealUrl(url, key);
  696. if (real && real !== url) return real;
  697. }
  698. }
  699. return url;
  700. },
  701. convertLinksToReal(root) {
  702. root.querySelectorAll('a[href]').forEach(a => {
  703. const realUrl = this.resolveRealUrl(a.href);
  704. if (realUrl && realUrl !== a.href) a.href = realUrl;
  705. });
  706. console.log('Links converted');
  707. }
  708. };
  709.  
  710. // 버전 확인 모듈
  711. const VersionChecker = {
  712. compareVersions(current, latest) {
  713. const currentParts = current.split('.').map(Number);
  714. const latestParts = latest.split('.').map(Number);
  715. for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
  716. const c = currentParts[i] || 0;
  717. const l = latestParts[i] || 0;
  718. if (c < l) return -1;
  719. if (c > l) return 1;
  720. }
  721. return 0;
  722. },
  723. checkMarkedJsVersion() {
  724. localStorage.setItem(Config.STORAGE_KEYS.CURRENT_VERSION, Config.VERSIONS.MARKED_VERSION);
  725.  
  726. GM_xmlhttpRequest({
  727. method: 'GET',
  728. url: Config.API.MARKED_CDN_URL,
  729. onload({ responseText }) {
  730. try {
  731. const latest = JSON.parse(responseText).version;
  732. console.log(`marked.js version: current=${Config.VERSIONS.MARKED_VERSION}, latest=${latest}`);
  733.  
  734. localStorage.setItem(Config.STORAGE_KEYS.LATEST_VERSION, latest);
  735.  
  736. const lastNotified = localStorage.getItem(Config.STORAGE_KEYS.LAST_NOTIFIED);
  737. console.log(`Last notified version: ${lastNotified || 'none'}`);
  738.  
  739. if (this.compareVersions(Config.VERSIONS.MARKED_VERSION, latest) < 0 &&
  740. (!lastNotified || this.compareVersions(lastNotified, latest) < 0)) {
  741. console.log('Popup display condition met');
  742.  
  743. const existingPopup = document.getElementById('marked-update-popup');
  744. if (existingPopup) {
  745. existingPopup.remove();
  746. console.log('Existing popup removed');
  747. }
  748.  
  749. const popup = document.createElement('div');
  750. popup.id = 'marked-update-popup';
  751. popup.innerHTML = `
  752. <p><b>${Localization.getMessage(Config.MESSAGE_KEYS.UPDATE_TITLE)}</b></p>
  753. <p>Current: ${Config.VERSIONS.MARKED_VERSION}<br>Latest: ${latest}</p>
  754. <button>${Localization.getMessage(Config.MESSAGE_KEYS.UPDATE_NOW)}</button>
  755. `;
  756. popup.querySelector('button').onclick = () => {
  757. localStorage.setItem(Config.STORAGE_KEYS.LAST_NOTIFIED, latest);
  758. console.log(`Notified version recorded: ${latest}`);
  759. popup.remove();
  760. };
  761. document.body.appendChild(popup);
  762. console.log('New popup displayed');
  763. } else {
  764. console.log('Popup display condition not met');
  765. }
  766. } catch (e) {
  767. console.warn('marked.min.js version check error:', e.message);
  768. }
  769. },
  770. onerror: () => console.warn('marked.min.js version check request failed')
  771. });
  772. }
  773. };
  774.  
  775. // 메인 모듈
  776. const Main = {
  777. renderGemini() {
  778. console.log('renderGemini called');
  779.  
  780. const query = Utils.getQuery();
  781. if (!query || document.getElementById('google-search-btn')) {
  782. console.log('Skipped:', { queryExists: !!query, googleBtnExists: !!document.getElementById('google-search-btn') });
  783. return;
  784. }
  785.  
  786. if (Utils.isDesktop()) {
  787. if (!Utils.isGeminiAvailable()) {
  788. console.log('Skipped PC: isGeminiAvailable false');
  789. return;
  790. }
  791.  
  792. const apiKey = Utils.getApiKey();
  793. if (!apiKey) {
  794. console.log('Skipped PC: No API key');
  795. return;
  796. }
  797.  
  798. const contextTarget = document.getElementById('b_context') ||
  799. document.querySelector('.b_right');
  800. if (!contextTarget) {
  801. console.error('Target element (#b_context or .b_right) not found for PC UI insertion');
  802. return;
  803. }
  804.  
  805. const ui = UI.createGeminiUI(query, apiKey);
  806. contextTarget.prepend(ui);
  807. console.log('PC: Gemini UI (with Google button) inserted into target element');
  808.  
  809. const content = ui.querySelector('#gemini-content');
  810. const cache = sessionStorage.getItem(`${Config.CACHE.PREFIX}${query}`);
  811. content.innerHTML = cache ? marked.parse(cache) : Localization.getMessage(Config.MESSAGE_KEYS.LOADING);
  812. if (!cache) GeminiAPI.fetch(query, content, apiKey);
  813.  
  814. // Gemini 박스 삽입 여부 확인
  815. const geminiBox = document.querySelector('#gemini-box');
  816. console.log('Gemini box inserted:', !!geminiBox);
  817. } else {
  818. const contentTarget = document.getElementById('b_content');
  819. if (!contentTarget) {
  820. console.error('b_content not found for mobile Google button insertion');
  821. return;
  822. }
  823.  
  824. const googleBtn = UI.createGoogleButton(query);
  825. contentTarget.parentNode.insertBefore(googleBtn, contentTarget);
  826. console.log('Mobile: Google search button inserted before b_content');
  827. }
  828. },
  829. observeUrlChange() {
  830. let lastUrl = location.href;
  831. const observer = new MutationObserver(() => {
  832. if (location.href !== lastUrl) {
  833. lastUrl = location.href;
  834. console.log('MutationObserver triggered: URL changed');
  835. this.renderGemini();
  836. LinkCleaner.convertLinksToReal(document);
  837. }
  838. });
  839. observer.observe(document.body, { childList: true, subtree: true });
  840. console.log('Observing URL changes on document.body');
  841. },
  842. observeThemeChange() {
  843. const themeObserver = new MutationObserver(() => {
  844. const newTheme = document.documentElement.getAttribute('data-theme') ||
  845. (document.documentElement.classList.contains('dark') ||
  846. document.documentElement.classList.contains('b_dark')) ? 'dark' :
  847. (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
  848. console.log(`Theme changed: ${newTheme}`);
  849. Styles.inject();
  850. });
  851. themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });
  852.  
  853. // 시스템 테마 변경 감지
  854. window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
  855. const newTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  856. console.log(`System theme changed: ${newTheme}`);
  857. Styles.inject();
  858. });
  859.  
  860. // 타겟 요소 스타일 변경 감지
  861. const contextObserver = new MutationObserver(() => {
  862. console.log('Target element style changed, reapplying styles');
  863. Styles.inject();
  864. });
  865. const targetElement = document.querySelector('#b_context') ||
  866. document.querySelector('.b_right');
  867. if (targetElement) {
  868. contextObserver.observe(targetElement, { attributes: true, attributeFilter: ['style', 'class'] });
  869. }
  870. console.log('Observing theme and style changes');
  871. },
  872. waitForElement(selector, callback, maxAttempts = 20, interval = 500) {
  873. let attempts = 0;
  874. const checkElement = () => {
  875. const element = document.querySelector(selector);
  876. if (element) {
  877. console.log(`Element found: ${selector}`);
  878. callback(element);
  879. } else if (attempts < maxAttempts) {
  880. attempts++;
  881. console.log(`Waiting for element: ${selector}, attempt ${attempts}/${maxAttempts}`);
  882. setTimeout(checkElement, interval);
  883. } else {
  884. console.error(`Element not found after ${maxAttempts} attempts: ${selector}`);
  885. }
  886. };
  887. checkElement();
  888. },
  889. init() {
  890. console.log('Bing Plus init:', { hostname: location.hostname, url: location.href });
  891. try {
  892. // 페이지 로드 완료 후 타겟 요소 대기
  893. this.waitForElement('#b_context, .b_right, #b_content', () => {
  894. Styles.inject();
  895. LinkCleaner.convertLinksToReal(document);
  896. this.renderGemini();
  897. this.observeUrlChange();
  898. this.observeThemeChange();
  899.  
  900. // DOM 구조 디버깅
  901. const bContext = document.getElementById('b_context');
  902. const bContextParent = document.querySelector('.b_context');
  903. const bRight = document.querySelector('.b_right');
  904. const bContent = document.getElementById('b_content');
  905. console.log('DOM structure debugging:', {
  906. bContextExists: !!bContext,
  907. bContextParentExists: !!bContextParent,
  908. bRightExists: !!bRight,
  909. bContentExists: !!bContent
  910. });
  911. });
  912. } catch (e) {
  913. console.error('Init error:', e.message);
  914. }
  915. }
  916. };
  917.  
  918. console.log('Bing Plus script loaded');
  919. Main.init();
  920. })();