Bing Search with Gemini

Bing 검색 결과 우측에 Gemini 결과를 마크다운 스타일로 표시

  1. // ==UserScript==
  2. // @name Bing Search with Gemini
  3. // @version 1.4
  4. // @description Bing 검색 결과 우측에 Gemini 결과를 마크다운 스타일로 표시
  5. // @author lanpod
  6. // @match https://www.bing.com/search*
  7. // @grant GM_addStyle
  8. // @grant GM_xmlhttpRequest
  9. // @license MIT
  10. // @namespace http://tampermonkey.net/
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. GM_addStyle(`
  17. #gemini-box { width: 100%; background: #fff; border: 1px solid #e0e0e0; padding: 16px; margin-bottom: 20px; font-family: Arial, sans-serif; min-height: 100px; }
  18. #gemini-header { display: flex; align-items: center; margin-bottom: 8px; }
  19. #gemini-logo { width: 24px; height: 24px; margin-right: 8px; }
  20. #gemini-box h3 { margin: 0; font-size: 18px; color: #202124; }
  21. #gemini-divider { height: 1px; background: #e0e0e0; margin: 8px 0; }
  22. #gemini-content { font-size: 14px; line-height: 1.6; color: #333; }
  23. `);
  24.  
  25. const marked = { parse: text => text
  26. .replace(/^### (.*$)/gm, '<h3>$1</h3>')
  27. .replace(/^## (.*$)/gm, '<h2>$1</h2>')
  28. .replace(/^# (.*$)/gm, '<h1>$1</h1>')
  29. .replace(/^\* (.*$)/gm, '<li>$1</li>')
  30. .replace(/^- (.*$)/gm, '<li>$1</li>')
  31. .replace(/```(.*?)```/gs, '<pre><code>$1</code></pre>')
  32. .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
  33. .replace(/(?<!\*)\*(?!\*)(.*?)(?<!\*)\*(?!\*)/g, '<em>$1</em>')
  34. .replace(/\n/g, '<br>')
  35. };
  36.  
  37. let apiKey = localStorage.getItem('geminiApiKey') || prompt('Gemini API 키를 입력하세요:');
  38. if (apiKey) localStorage.setItem('geminiApiKey', apiKey);
  39.  
  40. function createGeminiBox() {
  41. const box = document.createElement('div');
  42. box.id = 'gemini-box';
  43. box.innerHTML = `
  44. <div id="gemini-header">
  45. <img id="gemini-logo" src="https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg" alt="Gemini Logo">
  46. <h3>Gemini 검색 결과</h3>
  47. </div>
  48. <hr id="gemini-divider">
  49. <div id="gemini-content">로딩 중...</div>
  50. `;
  51. return box;
  52. }
  53.  
  54. let currentQuery = null;
  55. function fetchGeminiResult(query) {
  56. const contentDiv = document.getElementById('gemini-content');
  57. contentDiv.innerText = '로딩 중...';
  58. GM_xmlhttpRequest({
  59. method: 'POST',
  60. url: `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`,
  61. headers: { 'Content-Type': 'application/json' },
  62. data: JSON.stringify({ contents: [{ parts: [{ text: `"${query}" 대한 정보를 마크다운 형식으로 작성해줘` }] }] }),
  63. onload: response => {
  64. if (query !== currentQuery) return;
  65. try {
  66. const result = JSON.parse(response.responseText);
  67. contentDiv.innerHTML = marked.parse(result.candidates?.[0]?.content?.parts?.[0]?.text || 'Gemini 응답 없음');
  68. } catch {
  69. contentDiv.innerText = '응답 처리 오류';
  70. }
  71. },
  72. onerror: () => { if (query === currentQuery) contentDiv.innerText = 'API 호출 실패'; }
  73. });
  74. }
  75.  
  76. function ensureGeminiBox() {
  77. const query = new URLSearchParams(window.location.search).get('q');
  78. if (!query || query === currentQuery) return;
  79. currentQuery = query;
  80.  
  81. const contextArea = document.getElementById('b_context');
  82. if (!contextArea) return;
  83.  
  84. let box = document.getElementById('gemini-box');
  85. if (!box) {
  86. box = createGeminiBox();
  87. contextArea.insertBefore(box, contextArea.firstChild);
  88. } else {
  89. document.getElementById('gemini-content').innerText = '로딩 중...';
  90. }
  91.  
  92. apiKey ? fetchGeminiResult(query) : document.getElementById('gemini-content').innerText = 'API 키 없음';
  93. }
  94.  
  95. const observer = new MutationObserver(ensureGeminiBox);
  96. observer.observe(document.body, { childList: true, subtree: true });
  97. ensureGeminiBox();
  98. })();