Bing Search with Gemini

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

// ==UserScript==
// @name         Bing Search with Gemini
// @version      1.4
// @description  Bing 검색 결과 우측에 Gemini 결과를 마크다운 스타일로 표시
// @author       lanpod
// @match        https://www.bing.com/search*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @license      MIT
// @namespace http://tampermonkey.net/
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
        #gemini-box { width: 100%; background: #fff; border: 1px solid #e0e0e0; padding: 16px; margin-bottom: 20px; font-family: Arial, sans-serif; min-height: 100px; }
        #gemini-header { display: flex; align-items: center; margin-bottom: 8px; }
        #gemini-logo { width: 24px; height: 24px; margin-right: 8px; }
        #gemini-box h3 { margin: 0; font-size: 18px; color: #202124; }
        #gemini-divider { height: 1px; background: #e0e0e0; margin: 8px 0; }
        #gemini-content { font-size: 14px; line-height: 1.6; color: #333; }
    `);

    const marked = { parse: text => text
        .replace(/^### (.*$)/gm, '<h3>$1</h3>')
        .replace(/^## (.*$)/gm, '<h2>$1</h2>')
        .replace(/^# (.*$)/gm, '<h1>$1</h1>')
        .replace(/^\* (.*$)/gm, '<li>$1</li>')
        .replace(/^- (.*$)/gm, '<li>$1</li>')
        .replace(/```(.*?)```/gs, '<pre><code>$1</code></pre>')
        .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
        .replace(/(?<!\*)\*(?!\*)(.*?)(?<!\*)\*(?!\*)/g, '<em>$1</em>')
        .replace(/\n/g, '<br>')
    };

    let apiKey = localStorage.getItem('geminiApiKey') || prompt('Gemini API 키를 입력하세요:');
    if (apiKey) localStorage.setItem('geminiApiKey', apiKey);

    function createGeminiBox() {
        const box = document.createElement('div');
        box.id = 'gemini-box';
        box.innerHTML = `
            <div id="gemini-header">
                <img id="gemini-logo" src="https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg" alt="Gemini Logo">
                <h3>Gemini 검색 결과</h3>
            </div>
            <hr id="gemini-divider">
            <div id="gemini-content">로딩 중...</div>
        `;
        return box;
    }

    let currentQuery = null;
    function fetchGeminiResult(query) {
        const contentDiv = document.getElementById('gemini-content');
        contentDiv.innerText = '로딩 중...';
        GM_xmlhttpRequest({
            method: 'POST',
            url: `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`,
            headers: { 'Content-Type': 'application/json' },
            data: JSON.stringify({ contents: [{ parts: [{ text: `"${query}"에 대한 정보를 마크다운 형식으로 작성해줘` }] }] }),
            onload: response => {
                if (query !== currentQuery) return;
                try {
                    const result = JSON.parse(response.responseText);
                    contentDiv.innerHTML = marked.parse(result.candidates?.[0]?.content?.parts?.[0]?.text || 'Gemini 응답 없음');
                } catch {
                    contentDiv.innerText = '응답 처리 오류';
                }
            },
            onerror: () => { if (query === currentQuery) contentDiv.innerText = 'API 호출 실패'; }
        });
    }

    function ensureGeminiBox() {
        const query = new URLSearchParams(window.location.search).get('q');
        if (!query || query === currentQuery) return;
        currentQuery = query;

        const contextArea = document.getElementById('b_context');
        if (!contextArea) return;

        let box = document.getElementById('gemini-box');
        if (!box) {
            box = createGeminiBox();
            contextArea.insertBefore(box, contextArea.firstChild);
        } else {
            document.getElementById('gemini-content').innerText = '로딩 중...';
        }

        apiKey ? fetchGeminiResult(query) : document.getElementById('gemini-content').innerText = 'API 키 없음';
    }

    const observer = new MutationObserver(ensureGeminiBox);
    observer.observe(document.body, { childList: true, subtree: true });
    ensureGeminiBox();
})();