Gemini Hunter (UI Optimized)

Gemini Hunter 自动辅助工具(界面优化版):默认折叠配置项,支持自定义提问词、筛选关键词及图片开关。

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         Gemini Hunter (UI Optimized)
// @namespace    http://tampermonkey.net/
// @version      2.23.0
// @description  Gemini Hunter 自动辅助工具(界面优化版):默认折叠配置项,支持自定义提问词、筛选关键词及图片开关。
// @author       Mozi & Google Gemini
// @match        https://lmarena.ai/*
// @match        https://beta.lmarena.ai/*
// @match        https://chat.lmsys.org/*
// @icon         https://lmarena.ai/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_addStyle
// @grant        GM_notification
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // ============================================================
    // --- 【后台防休眠核心】 Web Worker 代理定时器 ---
    // ============================================================
    const workerBlob = new Blob([`
        var timers = {};
        self.onmessage = function(e) {
            var data = e.data;
            switch (data.type) {
                case 'SET_INTERVAL':
                    timers[data.id] = setInterval(function() {
                        self.postMessage({ type: 'TICK', id: data.id });
                    }, data.delay);
                    break;
                case 'CLEAR_INTERVAL':
                    clearInterval(timers[data.id]);
                    delete timers[data.id];
                    break;
                case 'SET_TIMEOUT':
                    timers[data.id] = setTimeout(function() {
                        self.postMessage({ type: 'TICK', id: data.id });
                        delete timers[data.id];
                    }, data.delay);
                    break;
                case 'CLEAR_TIMEOUT':
                    clearTimeout(timers[data.id]);
                    delete timers[data.id];
                    break;
            }
        };
    `], { type: 'application/javascript' });

    const workerUrl = URL.createObjectURL(workerBlob);
    const bgWorker = new Worker(workerUrl);
    const workerCallbacks = {};
    let workerTimerIdCounter = 0;

    bgWorker.onmessage = function(e) {
        const callback = workerCallbacks[e.data.id];
        if (callback && typeof callback === 'function') {
            callback();
        }
    };

    const wrapTimer = (type, callback, delay) => {
        const id = ++workerTimerIdCounter;
        workerCallbacks[id] = callback;
        bgWorker.postMessage({ type, id, delay });
        return id;
    };

    const clearWorkerTimer = (type, id) => {
        if (workerCallbacks[id]) {
            bgWorker.postMessage({ type, id });
            delete workerCallbacks[id];
        }
    };

    const setInterval = (cb, delay) => wrapTimer('SET_INTERVAL', cb, delay);
    const clearInterval = (id) => clearWorkerTimer('CLEAR_INTERVAL', id);
    const setTimeout = (cb, delay) => wrapTimer('SET_TIMEOUT', cb, delay);
    const clearTimeout = (id) => clearWorkerTimer('CLEAR_TIMEOUT', id);
    // ============================================================

    // --- 核心配置 ---
    const savedPrompt = GM_getValue('gh_custom_prompt', "你是谁");
    const savedKeywords = GM_getValue('gh_custom_keywords', "Gemini");

    const CONFIG = {
        placeholderText: "正在查找,请稍后...",
        resetUrl: window.location.origin + "/c/new",
        defaultIcon: "🧬",
        titlePrefix: "Gemini Hunter"
    };

    const getChatId = () => {
        const match = window.location.pathname.match(/\/c\/([a-z0-9-]+)/);
        return match ? match[1] : null;
    };

    const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);

    // --- 状态管理 ---
    let storedIsRunning = sessionStorage.getItem('gh_isRunning') === 'true';
    let isRunning = storedIsRunning;
    let attemptCount = parseInt(sessionStorage.getItem('gh_attempt_count') || '0');

    const currentChatId = getChatId();
    let savedLockedSide = sessionStorage.getItem('gh_locked_side');

    if (!savedLockedSide && currentChatId) {
        savedLockedSide = GM_getValue('gh_locked_side_' + currentChatId, null);
    }

    let lockedSide = savedLockedSide;

    // 设置项
    let isMinimized = GM_getValue('gh_minimized_state_v1', false);
    let isConfigOpen = GM_getValue('gh_config_open', false);
    let isAutoExpand = GM_getValue('gh_auto_expand', true);
    let isSoundEnabled = GM_getValue('gh_sound_enabled', true);
    let isAutoHideOther = GM_getValue('gh_auto_hide_other', false);
    let isRemoveVoteUI = GM_getValue('gh_remove_vote_ui', true);
    let isImageInjectionEnabled = GM_getValue('gh_inject_image', true);

    let timerInterval = null;
    let highlightInterval = null;
    let checkLoopInterval = null;
    let timerActive = false;
    let originalTitle = document.title;

    // --- UI 构建 (结构优化) ---
    const uiHtml = `
        <div id="gh-panel" style="${isMinimized ? 'display:none;' : ''}">
            <div class="gh-blur-bg"></div>
            <div class="gh-header" id="gh-header">
                <div class="gh-title">
                    <span class="gh-icon">🧬</span> GHunter
                </div>
                <div class="gh-win-ctrl">
                    <div id="gh-btn-cfg" title="设置" class="${isConfigOpen ? 'active' : ''}">⚙️</div>
                    <div id="gh-btn-min" title="收起"></div>
                </div>
            </div>
            <div class="gh-body">
                <!-- 核心信息区 (始终显示) -->
                <div class="gh-info-row">
                    <div class="gh-info-box">
                        <span class="gh-label">TIME</span>
                        <span id="gh-timer-display">0.00s</span>
                    </div>
                    <div class="gh-divider"></div>
                    <div class="gh-info-box">
                        <span class="gh-label">TRY</span>
                        <span id="gh-count-display">#${attemptCount}</span>
                    </div>
                </div>
                <div class="gh-status-bar">
                    <div class="gh-dot"></div>
                    <span id="gh-msg">就绪</span>
                </div>

                <!-- 控制按钮区 (始终显示) -->
                <div class="gh-controls">
                    <button id="gh-btn-start">
                        <span class="btn-icon">▶</span> 开始查找
                    </button>
                    <button id="gh-btn-stop" style="display:none;">
                        <span class="btn-icon">⏹</span> 停止运行
                    </button>
                </div>

                <!-- 配置面板 (可折叠) -->
                <div id="gh-config-body" style="${isConfigOpen ? '' : 'display:none;'}">
                    <div class="gh-line-divider"></div>
                    <div class="gh-input-area">
                        <div class="gh-input-group">
                            <span class="gh-input-label">提问词</span>
                            <input type="text" id="gh-inp-prompt" class="gh-text-input" value="${savedPrompt}">
                        </div>
                        <div class="gh-input-group">
                            <span class="gh-input-label">关键词 (逗号分隔)</span>
                            <input type="text" id="gh-inp-keywords" class="gh-text-input" value="${savedKeywords}" placeholder="如: Gemini, Google">
                        </div>
                    </div>

                    <div class="gh-settings-grid">
                        <label class="gh-toggle-box">
                            <span class="gh-lbl">注入图片</span>
                            <div class="gh-switch">
                                <input type="checkbox" id="gh-chk-image" ${isImageInjectionEnabled ? 'checked' : ''}>
                                <span class="gh-slider"></span>
                            </div>
                        </label>
                        <label class="gh-toggle-box">
                            <span class="gh-lbl">自动全屏</span>
                            <div class="gh-switch">
                                <input type="checkbox" id="gh-chk-expand" ${isAutoExpand ? 'checked' : ''}>
                                <span class="gh-slider"></span>
                            </div>
                        </label>
                        <label class="gh-toggle-box">
                            <span class="gh-lbl">专注模式</span>
                            <div class="gh-switch">
                                <input type="checkbox" id="gh-chk-hide" ${isAutoHideOther ? 'checked' : ''}>
                                <span class="gh-slider"></span>
                            </div>
                        </label>
                        <label class="gh-toggle-box">
                            <span class="gh-lbl">去投票框</span>
                            <div class="gh-switch">
                                <input type="checkbox" id="gh-chk-remove-vote" ${isRemoveVoteUI ? 'checked' : ''}>
                                <span class="gh-slider"></span>
                            </div>
                        </label>
                        <label class="gh-toggle-box" style="grid-column: span 2;">
                            <span class="gh-lbl">提示音效</span>
                            <div class="gh-switch">
                                <input type="checkbox" id="gh-chk-sound" ${isSoundEnabled ? 'checked' : ''}>
                                <span class="gh-slider"></span>
                            </div>
                        </label>
                    </div>
                </div>
            </div>
        </div>
        <div id="gh-ball" style="${!isMinimized ? 'display:none;' : ''}">
            <div class="gh-ball-ripple"></div>
            <div class="gh-ball-inner">
                <span class="gh-ball-text">${CONFIG.defaultIcon}</span>
            </div>
        </div>
    `;

    const container = document.createElement('div');
    container.innerHTML = uiHtml;
    document.body.appendChild(container);

    // --- CSS (样式优化) ---
    GM_addStyle(`
        :root { --gh-primary: #3b82f6; --gh-primary-dark: #2563eb; --gh-success: #10b981; --gh-danger: #ef4444; --gh-bg: rgba(255, 255, 255, 0.95); --gh-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); }
        #gh-panel { position: fixed; top: 12%; left: 5%; width: 90%; max-width: 250px; background: var(--gh-bg); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.8); border-radius: 16px; box-shadow: var(--gh-shadow); z-index: 100000; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; overflow: hidden; transition: opacity 0.2s, transform 0.2s; touch-action: none; }
        @media (min-width: 768px) { #gh-panel { width: 240px; top: 80px; right: 40px; left: auto; } }

        /* Header */
        .gh-header { padding: 10px 14px; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; border-bottom: 1px solid rgba(0,0,0,0.03); background: rgba(255,255,255,0.5); }
        .gh-title { font-weight: 700; font-size: 13px; color: #1f2937; display: flex; align-items: center; gap: 6px; }
        .gh-win-ctrl { display: flex; align-items: center; gap: 8px; }
        #gh-btn-min { width: 18px; height: 18px; border-radius: 50%; background: #e5e7eb; cursor: pointer; position: relative; transition: background 0.2s; }
        #gh-btn-min:hover { background: #d1d5db; }
        #gh-btn-min::before { content: ""; position: absolute; top: 8px; left: 4px; width: 10px; height: 2px; background: #6b7280; border-radius: 2px; }

        /* Config Button */
        #gh-btn-cfg { cursor: pointer; font-size: 14px; opacity: 0.6; transition: all 0.3s ease; line-height: 1; user-select: none; }
        #gh-btn-cfg:hover { opacity: 1; }
        #gh-btn-cfg.active { opacity: 1; transform: rotate(90deg); color: var(--gh-primary); }

        /* Body */
        .gh-body { padding: 12px 14px; display: flex; flex-direction: column; gap: 10px; }
        .gh-info-row { background: #f9fafb; border-radius: 10px; padding: 6px 10px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #f3f4f6; }
        .gh-info-box { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; }
        .gh-divider { width: 1px; height: 20px; background: #e5e7eb; margin: 0 8px; }
        .gh-label { font-size: 8px; font-weight: 800; color: #9ca3af; letter-spacing: 1px; margin-bottom: 1px; }
        #gh-timer-display { font-family: monospace; font-size: 14px; font-weight: 700; color: #374151; }
        #gh-count-display { font-family: monospace; font-size: 14px; font-weight: 700; color: #3b82f6; }

        .gh-status-bar { background: rgba(243, 244, 246, 0.6); padding: 6px 10px; border-radius: 8px; display: flex; align-items: center; gap: 8px; font-size: 12px; color: #4b5563; font-weight: 500; }
        .gh-dot { width: 6px; height: 6px; border-radius: 50%; background: #d1d5db; flex-shrink: 0;}
        .gh-dot.active { background: var(--gh-primary); box-shadow: 0 0 6px var(--gh-primary); animation: gh-pulse-dot 1.5s infinite; }
        .gh-dot.success { background: var(--gh-success); }
        .gh-dot.error { background: var(--gh-danger); }

        /* Controls */
        .gh-controls button { width: 100%; padding: 8px; border: none; border-radius: 10px; font-weight: 600; cursor: pointer; font-size: 13px; display: flex; align-items: center; justify-content: center; gap: 6px; color: white; transition: transform 0.1s; }
        .gh-controls button:active { transform: scale(0.97); }
        #gh-btn-start { background: linear-gradient(135deg, #2563eb, #3b82f6); box-shadow: 0 4px 10px rgba(37, 99, 235, 0.25); }
        #gh-btn-stop { background: linear-gradient(135deg, #dc2626, #ef4444); box-shadow: 0 4px 10px rgba(239, 68, 68, 0.25); }

        /* Config Panel (Collapsible) */
        #gh-config-body { display: flex; flex-direction: column; gap: 10px; animation: gh-slide-down 0.3s ease-out; transform-origin: top; }
        .gh-line-divider { height: 1px; background: #f3f4f6; margin: 2px 0; }

        /* Inputs */
        .gh-input-area { display: flex; flex-direction: column; gap: 8px; }
        .gh-input-group { display: flex; flex-direction: column; gap: 3px; }
        .gh-input-label { font-size: 10px; font-weight: 700; color: #6b7280; margin-left: 2px; }
        .gh-text-input { width: 100%; padding: 5px 8px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 11px; outline: none; background: rgba(255,255,255,0.6); color: #374151; transition: border-color 0.2s, background 0.2s; }
        .gh-text-input:focus { border-color: var(--gh-primary); background: #fff; }

        /* Settings Grid (2 Columns) */
        .gh-settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 10px; padding: 0 2px; }
        .gh-toggle-box { display: flex; justify-content: space-between; align-items: center; cursor: pointer; user-select: none; background: rgba(0,0,0,0.02); padding: 4px 6px; border-radius: 6px; border: 1px solid transparent; transition: background 0.2s; }
        .gh-toggle-box:hover { background: rgba(0,0,0,0.04); }
        .gh-lbl { font-size: 10px; font-weight: 600; color: #4b5563; }

        /* Switch */
        .gh-switch { position: relative; display: inline-block; width: 28px; height: 16px; flex-shrink: 0; margin-left: 4px; }
        .gh-switch input { opacity: 0; width: 0; height: 0; }
        .gh-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #e5e7eb; transition: .3s; border-radius: 34px; }
        .gh-slider:before { position: absolute; content: ""; height: 12px; width: 12px; left: 2px; bottom: 2px; background-color: white; transition: .3s; border-radius: 50%; box-shadow: 0 1px 3px rgba(0,0,0,0.15); }
        input:checked + .gh-slider { background-color: var(--gh-primary); }
        input:checked + .gh-slider:before { transform: translateX(12px); }

        /* Minimized Ball */
        #gh-ball { position: fixed; top: 70%; right: 15px; width: 48px; height: 48px; z-index: 100001; cursor: pointer; user-select: none; display: flex; align-items: center; justify-content: center; touch-action: none; }
        .gh-ball-inner { width: 40px; height: 40px; background: #ffffff; border-radius: 50%; color: #2563eb; display: flex; align-items: center; justify-content: center; position: relative; z-index: 2; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); }
        .gh-ball-text { font-weight: 800; font-size: 20px; transition: all 0.3s; }
        .gh-google-letter { background: linear-gradient(135deg, #4285F4 20%, #EA4335 40%, #FBBC05 60%, #34A853 80%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent; font-family: "Google Sans", "Product Sans", Roboto, Arial, sans-serif; font-weight: 900 !important; font-size: 25px !important; letter-spacing: -1px; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.1)); }
        .gh-ball-ripple { position: absolute; width: 52px; height: 52px; border-radius: 50%; border: 2px solid transparent; z-index: 1; transition: opacity 0.3s; pointer-events: none; opacity: 0; }
        #gh-ball.running .gh-ball-ripple { opacity: 1; border-top-color: #3b82f6; border-left-color: #a855f7; animation: gh-spin-smooth 1s linear infinite; }

        /* Utils */
        .gh-winner-glow { box-shadow: inset 0 0 0 3px #10b981, 0 0 25px rgba(16, 185, 129, 0.25) !important; border-radius: 12px !important; }
        .gh-hidden-bubble { display: none !important; }
        @keyframes gh-spin-smooth { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
        @keyframes gh-pulse-dot { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }
        @keyframes gh-slide-down { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
    `);

    // --- DOM 引用 ---
    const panel = document.getElementById('gh-panel');
    const ball = document.getElementById('gh-ball');
    const ballInner = ball.querySelector('.gh-ball-text');
    const btnMin = document.getElementById('gh-btn-min');
    const btnCfg = document.getElementById('gh-btn-cfg');
    const configBody = document.getElementById('gh-config-body');
    const header = document.getElementById('gh-header');
    const btnStart = document.getElementById('gh-btn-start');
    const btnStop = document.getElementById('gh-btn-stop');
    const txtMsg = document.getElementById('gh-msg');
    const dot = document.querySelector('.gh-dot');
    const chkExpand = document.getElementById('gh-chk-expand');
    const chkHide = document.getElementById('gh-chk-hide');
    const chkSound = document.getElementById('gh-chk-sound');
    const chkRemoveVote = document.getElementById('gh-chk-remove-vote');
    const chkImage = document.getElementById('gh-chk-image');
    const timerDisplay = document.getElementById('gh-timer-display');
    const countDisplay = document.getElementById('gh-count-display');
    const inpPrompt = document.getElementById('gh-inp-prompt');
    const inpKeywords = document.getElementById('gh-inp-keywords');

    // --- 辅助函数 ---
    function updateBallText(side) {
        if (side === 'A' || side === 'B' || side === 'BOTH') {
            ballInner.innerText = (side === 'BOTH') ? '双' : side;
            ballInner.className = 'gh-ball-text gh-google-letter';
        } else {
            ballInner.innerText = CONFIG.defaultIcon;
            ballInner.className = 'gh-ball-text';
        }
    }

    function updateCountDisplay() { countDisplay.innerText = "#" + attemptCount; }
    function getActiveKeywords() { return (inpKeywords.value || "Gemini").split(/[,,]/).map(k => k.trim()).filter(k => k.length > 0); }

    // --- 新增逻辑:查找并点击 New Chat 按钮 ---
    function clickNewChatBtn() {
        // 策略 1: 查找包含 "New Chat" 文本的链接 (桌面端,对应图3)
        const allAnchors = Array.from(document.querySelectorAll('a'));
        const textLink = allAnchors.find(a => a.innerText.trim() === "New Chat");
        if (textLink) {
            textLink.click();
            return true;
        }

        // 策略 2: 查找特定的 CSS 类名 (桌面端图标,对应图2)
        // "peer/menu-button" 需要转义为 "peer\/menu-button"
        const iconLink = document.querySelector('a.peer\\/menu-button');
        if (iconLink) {
            iconLink.click();
            return true;
        }

        // 策略 3: 移动端保底 (图1)
        // 移动端通常是一个链接到根目录的图标。尝试查找非logo的根链接,
        // 或者尝试查找带有 inline-flex 等类的特定元素 (不够稳健,最好用 href)
        // 这里作为保底,如果没有找到文本和特定class,尝试找 href="/" 的链接
        const homeLink = document.querySelector('a[href="/"]');
        if (homeLink) {
            // 排除可能是左上角Logo的情况(如果有img通常是logo,但这里New Chat也是按钮)
            // 大多数情况下,点击href="/"的按钮在SPA中就是新建对话
            homeLink.click();
            return true;
        }

        return false;
    }

    // --- 交互逻辑 ---
    panel.addEventListener('click', (e) => { e.stopPropagation(); });

    // 设置面板切换
    btnCfg.addEventListener('click', (e) => {
        e.stopPropagation();
        isConfigOpen = !isConfigOpen;
        configBody.style.display = isConfigOpen ? 'flex' : 'none';
        btnCfg.classList.toggle('active', isConfigOpen);
        GM_setValue('gh_config_open', isConfigOpen);
    });

    // 监听
    chkExpand.addEventListener('change', (e) => { isAutoExpand = e.target.checked; GM_setValue('gh_auto_expand', isAutoExpand); if (lockedSide && isAutoExpand) triggerExpand(lockedSide); });
    chkHide.addEventListener('change', (e) => { isAutoHideOther = e.target.checked; GM_setValue('gh_auto_hide_other', isAutoHideOther); });
    chkSound.addEventListener('change', (e) => { isSoundEnabled = e.target.checked; GM_setValue('gh_sound_enabled', isSoundEnabled); });
    chkRemoveVote.addEventListener('change', (e) => { isRemoveVoteUI = e.target.checked; GM_setValue('gh_remove_vote_ui', isRemoveVoteUI); removeVotingBar(); });
    chkImage.addEventListener('change', (e) => { isImageInjectionEnabled = e.target.checked; GM_setValue('gh_inject_image', isImageInjectionEnabled); });
    inpPrompt.addEventListener('input', (e) => { GM_setValue('gh_custom_prompt', e.target.value); });
    inpKeywords.addEventListener('input', (e) => { GM_setValue('gh_custom_keywords', e.target.value); });

    // --- 音频/核心/查找 逻辑 ---
    let _audioCtx = null;
    function playTone(freq, type, startDelay, duration) {
        if (!isSoundEnabled && freq > 0) return;
        if (!_audioCtx) { const AudioContext = window.AudioContext || window.webkitAudioContext; if (AudioContext) _audioCtx = new AudioContext(); }
        if (!_audioCtx) return;
        if (_audioCtx.state === 'suspended') _audioCtx.resume();
        const osc = _audioCtx.createOscillator(); const gain = _audioCtx.createGain();
        osc.type = type; osc.frequency.value = freq; osc.connect(gain); gain.connect(_audioCtx.destination);
        const now = _audioCtx.currentTime + startDelay;
        osc.start(now); gain.gain.setValueAtTime(0.05, now); gain.gain.exponentialRampToValueAtTime(0.001, now + duration); osc.stop(now + duration);
    }
    function keepAliveAudio() { playTone(0, 'sine', 0, 0.01); }
    function playVictoryTheme() {
        const melody = [[523.25, 0, 0.1, 'square'], [659.25, 0.1, 0.1, 'square'], [783.99, 0.2, 0.1, 'square'], [1046.50, 0.3, 0.4, 'square'], [523.25, 0.4, 0.05, 'sawtooth'], [783.99, 0.45, 0.05, 'sawtooth'], [1046.50, 0.5, 0.05, 'sawtooth'], [523.25, 0.6, 0.6, 'triangle']];
        melody.forEach(n => playTone(n[0], n[3], n[1], n[2]));
    }
    function isLoadingIndicatorVisible() { return !!document.querySelector('canvas[data-sentry-component="Loading"]'); }

    function startTimer() {
        if (timerInterval) clearInterval(timerInterval);
        timerActive = true; const startTime = Date.now();
        timerDisplay.innerText = "0.00s"; timerDisplay.style.color = "#374151";
        let hasSeenLoading = false;
        timerInterval = setInterval(() => {
            const elapsed = (Date.now() - startTime) / 1000;
            timerDisplay.innerText = elapsed.toFixed(2) + "s";
            document.title = `[${Math.floor(elapsed)}s] 查找中...`;
            const isLoading = isLoadingIndicatorVisible();
            if (isLoading) hasSeenLoading = true;
            if (hasSeenLoading && !isLoading) { stopTimer(); if (isSoundEnabled && !isRunning) playVictoryTheme(); }
        }, 50);
    }
    function stopTimer() {
        if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
        timerActive = false; timerDisplay.style.color = "#10b981"; timerDisplay.style.transform = "scale(1.2)";
        setTimeout(() => { timerDisplay.style.transform = "scale(1)"; }, 200);
        document.title = originalTitle;
    }

    function setupGlobalListeners() {
        document.addEventListener('keydown', (e) => { if (e.target.tagName === 'TEXTAREA' && e.key === 'Enter' && !e.shiftKey) { setTimeout(() => { startTimer(); }, 50); } }, true);
        document.addEventListener('click', (e) => { const btn = e.target.closest('button'); if (btn) { const label = btn.getAttribute('aria-label') || "", testid = btn.getAttribute('data-testid') || "", type = btn.getAttribute('type') || ""; if (label.includes("Send") || testid.includes("send") || type === "submit") { startTimer(); } } }, true);
    }
    setupGlobalListeners();

    function toggleMinimizeUI(e) {
        isMinimized = !isMinimized;
        GM_setValue('gh_minimized_state_v1', isMinimized);
        if (isMinimized) {
            const pRect = panel.getBoundingClientRect();
            const isRightSide = (pRect.left + pRect.width / 2) > (window.innerWidth / 2);
            let newTop = Math.max(10, Math.min(window.innerHeight - 60, pRect.top));
            let newLeft = isRightSide ? pRect.right - 48 : pRect.left;
            ball.style.top = newTop + 'px'; ball.style.left = newLeft + 'px';
            panel.style.display = 'none'; ball.style.display = 'flex';
        } else {
            const bRect = ball.getBoundingClientRect();
            const isRightSide = (bRect.left + bRect.width / 2) > (window.innerWidth / 2);
            panel.style.opacity = '0'; panel.style.display = 'block';
            const pWidth = panel.offsetWidth; const pHeight = panel.offsetHeight;
            let newLeft = isRightSide ? bRect.right - pWidth : bRect.left;
            newLeft = Math.max(5, Math.min(window.innerWidth - pWidth - 5, newLeft));
            let newTop = Math.max(5, Math.min(window.innerHeight - pHeight - 5, bRect.top));
            panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px';
            requestAnimationFrame(() => { panel.style.opacity = '1'; panel.style.transform = 'scale(1)'; });
            ball.style.display = 'none';
        }
    }
    btnMin.addEventListener('click', (e) => { e.stopPropagation(); toggleMinimizeUI(); });
    btnMin.addEventListener('touchend', (e) => { e.stopPropagation(); e.preventDefault(); toggleMinimizeUI(); });
    ball.addEventListener('click', (e) => { if(!ball.isDragging) { e.stopPropagation(); toggleMinimizeUI(); } });
    ball.addEventListener('touchend', (e) => { if(!ball.isDragging) { e.stopPropagation(); toggleMinimizeUI(); } });

    function makeDraggable(el, handle = el) {
        let startX, startY, initialLeft, initialTop;
        const getPos = (e) => e.touches ? e.touches[0] : e;
        const onMove = (e) => {
            e.preventDefault(); const { clientX, clientY } = getPos(e); const dx = clientX - startX, dy = clientY - startY;
            if (Math.abs(dx) > 5 || Math.abs(dy) > 5) el.isDragging = true;
            el.style.left = Math.max(0, Math.min(window.innerWidth - el.offsetWidth, initialLeft + dx)) + 'px';
            el.style.top = Math.max(0, Math.min(window.innerHeight - el.offsetHeight, initialTop + dy)) + 'px';
        };
        const onEnd = () => {
            document.body.style.userSelect = ''; handle.style.cursor = 'move'; el.style.transition = 'transform 0.3s, opacity 0.2s';
            document.removeEventListener('mousemove', onMove); document.removeEventListener('touchmove', onMove);
            document.removeEventListener('mouseup', onEnd); document.removeEventListener('touchend', onEnd);
        };
        const onStart = (e) => {
            if (e.target.closest('.gh-win-ctrl') || e.target.tagName === 'INPUT') return;
            e.stopPropagation(); const { clientX, clientY } = getPos(e); startX = clientX; startY = clientY;
            const rect = el.getBoundingClientRect(); initialLeft = rect.left; initialTop = rect.top;
            el.isDragging = false; el.style.position = 'fixed'; el.style.transition = 'none';
            if (el.id === 'gh-ball') el.style.animation = 'none';
            document.body.style.userSelect = 'none'; handle.style.cursor = 'grabbing';
            document.addEventListener('mousemove', onMove); document.addEventListener('touchmove', onMove, { passive: false });
            document.addEventListener('mouseup', onEnd); document.addEventListener('touchend', onEnd);
        };
        handle.addEventListener('mousedown', onStart); handle.addEventListener('touchstart', onStart, { passive: false });
    }
    makeDraggable(panel, header); makeDraggable(ball);

    function updateStatus(msg, type = 'normal') {
        txtMsg.innerText = msg; dot.className = 'gh-dot';
        if (type === 'active') dot.classList.add('active'); else if (type === 'success') dot.classList.add('success'); else if (type === 'error') dot.classList.add('error');
    }
    function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
    async function pasteGeneratedImage(element) {
        try {
            const blob = await new Promise(resolve => { const canvas = document.createElement('canvas'); canvas.width = canvas.height = 1; canvas.getContext('2d').fillStyle = '#FFFFFF'; canvas.getContext('2d').fillRect(0, 0, 1, 1); canvas.toBlob(resolve, 'image/png'); });
            const dataTransfer = new DataTransfer(); dataTransfer.items.add(new File([blob], "gen.png", { type: "image/png" }));
            element.focus(); element.dispatchEvent(new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData: dataTransfer }));
            return true;
        } catch (e) { return false; }
    }
    async function clickImageButton() { const btn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.trim() === "Image"); if (btn) { btn.click(); return true; } return false; }
    async function fillTextOnly(element, text) {
        element.focus(); const valueSetter = Object.getOwnPropertyDescriptor(element, 'value')?.set; const prototype = Object.getPrototypeOf(element); const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set;
        if (valueSetter && prototypeValueSetter && valueSetter !== prototypeValueSetter) prototypeValueSetter.call(element, text); else if (valueSetter) valueSetter.call(element, text); else element.value = text;
        element.dispatchEvent(new Event('input', { bubbles: true }));
    }
    async function clickSend() {
        startTimer(); const sendBtn = document.querySelector('button[data-testid="send-button"], button[aria-label="Send message"], button[type="submit"]');
        if (sendBtn && !sendBtn.disabled) { sendBtn.click(); return true; }
        const textarea = document.querySelector('textarea'); if (textarea) { textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); return true; } return false;
    }
    function removeVotingBar() {
        const targetDivs = document.querySelectorAll('.md\\:absolute.md\\:top-0.md\\:-translate-y-full');
        targetDivs.forEach(div => {
            const text = div.innerText;
            if (text.includes('Left is Better') || text.includes('A is better') || text.includes('tie') || text.includes('Both are bad')) {
                if (isRemoveVoteUI) { if (div.style.display !== 'none') div.style.display = 'none'; } else { if (div.style.display === 'none') div.style.display = ''; }
            }
        });
    }
    setInterval(removeVotingBar, 1000);

    function findTargetElement(text) {
        const xpath = `//*[normalize-space(text())='${text}']`; const snapshot = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
        for (let i = 0; i < snapshot.snapshotLength; i++) { const el = snapshot.snapshotItem(i); if (el.offsetParent !== null) return el; } return null;
    }
    function waitForResponse() {
        return new Promise(resolve => {
            if (checkLoopInterval) clearInterval(checkLoopInterval);
            const startTime = Date.now(); let seenLoading = false;
            checkLoopInterval = setInterval(() => {
                if (!isRunning) { clearInterval(checkLoopInterval); resolve("STOPPED"); return; }
                if (document.body.innerText.includes("Something went wrong") || document.body.innerText.includes("Failed to load")) { clearInterval(checkLoopInterval); resolve("ERROR"); }
                const hasLoading = isLoadingIndicatorVisible();
                if (hasLoading) seenLoading = true;
                if (seenLoading && !hasLoading) { clearInterval(checkLoopInterval); stopTimer(); resolve("FOUND"); return; }
                if ((Date.now() - startTime) > 10000 && !hasLoading) { const { A, B } = getModelResponses(); if (A.length > 10 && B.length > 10) { clearInterval(checkLoopInterval); stopTimer(); resolve("FOUND"); return; } }
            }, 100);
            setTimeout(() => { clearInterval(checkLoopInterval); resolve("TIMEOUT"); }, 120000);
        });
    }
    function getModelContainers() {
        const headerA = findTargetElement("Assistant A"); const headerB = findTargetElement("Assistant B");
        if (!headerA || !headerB) return { containerA: null, containerB: null };
        function findColumnWrapper(selfHeader, otherHeader) { let curr = selfHeader.parentElement; let bestContainer = null; for (let i = 0; i < 10 && curr && curr !== document.body; i++) { if (curr.contains(otherHeader)) break; bestContainer = curr; curr = curr.parentElement; } return bestContainer; }
        return { containerA: findColumnWrapper(headerA, headerB), containerB: findColumnWrapper(headerB, headerA) };
    }
    function getCurrentPrompt() { return inpPrompt.value || "你是谁"; }
    function getModelResponses() {
        const { containerA, containerB } = getModelContainers(); const bubbles = Array.from(document.querySelectorAll('.prose')); let lastBubbleA = "", lastBubbleB = ""; const currentPrompt = getCurrentPrompt();
        if (!containerA || !containerB) return { A: "", B: "" };
        bubbles.forEach(b => {
            const text = b.innerText; if (text.includes(currentPrompt) && text.length < (currentPrompt.length + 50)) return;
            if (text.includes("正在查找")) return;
            if (containerA.contains(b)) lastBubbleA = text; else if (containerB.contains(b)) lastBubbleB = text;
        });
        return { A: lastBubbleA || "", B: lastBubbleB || "" };
    }
    async function triggerExpand(side) {
        if (!side) return; if (side === 'BOTH') side = 'A';
        const headerEl = findTargetElement(side === 'A' ? "Assistant A" : "Assistant B");
        if (headerEl) { let parent = headerEl.parentElement; for(let i=0; i<5 && parent; i++) { const buttons = parent.querySelectorAll('button'); if (buttons.length >= 1) { buttons[buttons.length - 1].click(); return; } parent = parent.parentElement; } }
    }
    function startPersistentHighlight() {
        if (!lockedSide) return;
        updateStatus("锁定: " + (lockedSide==='BOTH'?'双侧':lockedSide), 'success'); updateBallText(lockedSide); document.title = `[${lockedSide}] 锁定成功 - GH`;
        if (highlightInterval) clearInterval(highlightInterval);
        let lastExpandCheck = 0; let lastBubbleCount = document.querySelectorAll('.prose').length; const currentPrompt = getCurrentPrompt();
        highlightInterval = setInterval(() => {
            const { containerA, containerB } = getModelContainers(); if (!containerA || !containerB) return;
            document.querySelectorAll('.prose').forEach(b => {
                const text = b.innerText; if (text.includes(currentPrompt) && text.length < 50) return;
                const isSideA = containerA.contains(b); const isSideB = containerB.contains(b); if (!isSideA && !isSideB) return;
                let isTarget = (lockedSide === 'BOTH') || (lockedSide === 'A' && isSideA) || (lockedSide === 'B' && isSideB);
                let container = b.closest('.bg-surface-primary') || b.closest('.border.rounded-xl') || b.closest('.border.rounded-lg') || b.closest('[data-testid="model-answer"]');
                if (!container && b.parentElement?.parentElement) container = b.parentElement.parentElement.parentElement;
                let targetEl = container || b;
                if (isTarget) { if (!targetEl.classList.contains('gh-winner-glow')) targetEl.classList.add('gh-winner-glow'); } else { if (targetEl.classList.contains('gh-winner-glow')) targetEl.classList.remove('gh-winner-glow'); }
                if (isAutoHideOther && lockedSide !== 'BOTH') { if ((lockedSide === 'A' && isSideB) || (lockedSide === 'B' && isSideA)) targetEl.classList.add('gh-hidden-bubble'); else targetEl.classList.remove('gh-hidden-bubble'); } else targetEl.classList.remove('gh-hidden-bubble');
            });
            const now = Date.now();
            if (isAutoExpand && (now - lastExpandCheck > 1000)) { lastExpandCheck = now; const currentCount = document.querySelectorAll('.prose').length; if (currentCount > lastBubbleCount) { lastBubbleCount = currentCount; setTimeout(() => triggerExpand(lockedSide), 500); } }
        }, 200);
    }

    async function runSequence() {
        if (sessionStorage.getItem('gh_isRunning') !== 'true') return;
        isRunning = true; sessionStorage.removeItem('gh_locked_side'); lockedSide = null;
        if (highlightInterval) clearInterval(highlightInterval);
        updateStatus("准备中...", 'normal');
        document.querySelectorAll('.gh-winner-glow').forEach(el => el.classList.remove('gh-winner-glow'));
        document.querySelectorAll('.gh-hidden-bubble').forEach(el => el.classList.remove('gh-hidden-bubble'));
        await sleep(1000);
        if (!isRunning) return;
        const textarea = document.querySelector('textarea, [contenteditable="true"]');
        if (!textarea) { updateStatus("输入框未就绪", 'error'); return; }
        if (isImageInjectionEnabled) { updateStatus("验证 (Gemini)...", 'active'); await pasteGeneratedImage(textarea); await sleep(2000); if (!isRunning) return; await clickImageButton(); await sleep(500); } else { updateStatus("跳过图片...", 'active'); await sleep(500); }
        const currentPrompt = inpPrompt.value || "你是谁";
        await fillTextOnly(textarea, currentPrompt);
        await sleep(300); await clickSend();
        updateStatus("等待回复...", 'active');
        const voteResult1 = await waitForResponse();
        if (voteResult1 === "STOPPED") return; if (voteResult1 !== "FOUND") return retry();
        updateStatus("判别中...", 'active'); await sleep(500);
        const resp1 = getModelResponses();
        const keywords = getActiveKeywords();
        const checkText = (text) => { if (!text) return false; return keywords.some(k => new RegExp(k, 'i').test(text)); };
        const isA = checkText(resp1.A); const isB = checkText(resp1.B);
        if (isA || isB) {
            lockedSide = (isA && isB) ? 'BOTH' : (isA ? 'A' : 'B');
            sessionStorage.setItem('gh_locked_side', lockedSide);
            const finalChatId = getChatId(); if (finalChatId) GM_setValue('gh_locked_side_' + finalChatId, lockedSide);
            updateBallText(lockedSide);
            const textareaDone = document.querySelector('textarea, [contenteditable="true"]'); if (textareaDone) await fillTextOnly(textareaDone, "");
            startPersistentHighlight();
            if(!isMinimized) toggleMinimizeUI();
            if (isSoundEnabled) { try { const u = new SpeechSynthesisUtterance(`锁定成功`); u.lang = 'zh-CN'; window.speechSynthesis.speak(u); } catch(e){} playVictoryTheme(); }
            GM_notification({ text: `成功锁定目标!位置:${lockedSide === 'BOTH' ? '双侧' : lockedSide + '侧'}`, title: 'Gemini Hunter', timeout: 5000 });
            sessionStorage.setItem('gh_isRunning', 'false'); isRunning = false; updateUIState(false);
        } else { updateStatus("未发现,重试...", 'error'); await sleep(1000); return retry(); }
    }

    function startHunt() {
        sessionStorage.removeItem('gh_locked_side'); lockedSide = null; keepAliveAudio();
        sessionStorage.setItem('gh_isRunning', 'true'); isRunning = true;
        attemptCount = 1; sessionStorage.setItem('gh_attempt_count', attemptCount);
        updateCountDisplay(); updateBallText(null); updateUIState(true);
        updateStatus("初始化...", 'active');

        // 逻辑更改:优先尝试点击按钮,如果成功则等待页面SPA跳转,否则回退到URL刷新
        setTimeout(async () => {
            if (clickNewChatBtn()) {
                // 如果点击成功,等待页面切换动画
                await sleep(1500);
                // 重新开始序列(因为SPA没有刷新页面,脚本状态未重置,需手动调用)
                runSequence();
            } else {
                // 找不到按钮,回退旧逻辑
                window.location.href = CONFIG.resetUrl;
            }
        }, 500);
    }

    function stopHunt() {
        sessionStorage.removeItem('gh_locked_side'); lockedSide = null;
        sessionStorage.setItem('gh_isRunning', 'false'); isRunning = false;
        if (timerInterval) clearInterval(timerInterval); if (highlightInterval) clearInterval(highlightInterval); if (checkLoopInterval) clearInterval(checkLoopInterval);
        timerActive = false; document.title = originalTitle;
        updateUIState(false); updateStatus("已停止", 'normal');
        document.querySelectorAll('.gh-winner-glow').forEach(el => el.classList.remove('gh-winner-glow'));
        document.querySelectorAll('.gh-hidden-bubble').forEach(el => el.classList.remove('gh-hidden-bubble'));
        updateBallText(null);
    }

    function retry() {
        if (sessionStorage.getItem('gh_isRunning') !== 'true' || !isRunning) { updateStatus("已停止", 'normal'); return; }
        attemptCount++; sessionStorage.setItem('gh_attempt_count', attemptCount);
        updateCountDisplay();

        // 逻辑更改:同 startHunt
        setTimeout(async () => {
            if (clickNewChatBtn()) {
                await sleep(1500);
                runSequence();
            } else {
                window.location.href = CONFIG.resetUrl;
            }
        }, 500);
    }

    function updateUIState(running) {
        if (running) { btnStart.style.display = 'none'; btnStop.style.display = 'flex'; ball.classList.add('running'); }
        else { btnStart.style.display = 'flex'; btnStop.style.display = 'none'; ball.classList.remove('running'); }
    }
    btnStart.addEventListener('click', startHunt); btnStart.addEventListener('touchend', (e) => { e.preventDefault(); startHunt(); });
    btnStop.addEventListener('click', stopHunt); btnStop.addEventListener('touchend', (e) => { e.preventDefault(); stopHunt(); });

    if (lockedSide) { if (!isMinimized) toggleMinimizeUI(); updateBallText(lockedSide); startPersistentHighlight(); }
    else { updateBallText(null); if (isMinimized) { panel.style.display = 'none'; ball.style.display = 'flex'; } else { panel.style.opacity = '0'; panel.style.transform = 'scale(0.9)'; setTimeout(() => { panel.style.opacity = '1'; panel.style.transform = 'scale(1)'; }, 100); } }

    const runState = sessionStorage.getItem('gh_isRunning');
    // 如果是页面刚刚加载(比如被旧逻辑刷新),且处于运行状态,则自动启动
    // 如果是点击按钮触发的SPA跳转,不会触发这里的 onload 逻辑,而是由 runSequence 递归处理
    if (isMobile && runState === null) startHunt();
    else if (isRunning && !lockedSide) { if (attemptCount === 0) attemptCount = 1; updateCountDisplay(); updateUIState(true); keepAliveAudio(); setTimeout(runSequence, 2500); }
})();