Gemini Hunter

锁定Gemini:UI已等比例缩小。针对手机端优化。后台运行增强版。位置记忆同步优化。可配置隐藏投票框。

目前為 2025-11-16 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Gemini Hunter
// @namespace    http://tampermonkey.net/
// @version      2.13.3
// @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
// @description 锁定Gemini:UI已等比例缩小。针对手机端优化。后台运行增强版。位置记忆同步优化。可配置隐藏投票框。
// ==/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 _setInterval = function(callback, delay) {
        const id = ++workerTimerIdCounter;
        workerCallbacks[id] = callback;
        bgWorker.postMessage({ type: 'SET_INTERVAL', id: id, delay: delay });
        return id;
    };

    const _clearInterval = function(id) {
        if (workerCallbacks[id]) {
            bgWorker.postMessage({ type: 'CLEAR_INTERVAL', id: id });
            delete workerCallbacks[id];
        }
    };

    const _setTimeout = function(callback, delay) {
        const id = ++workerTimerIdCounter;
        workerCallbacks[id] = callback;
        bgWorker.postMessage({ type: 'SET_TIMEOUT', id: id, delay: delay });
        return id;
    };

    const _clearTimeout = function(id) {
        if (workerCallbacks[id]) {
            bgWorker.postMessage({ type: 'CLEAR_TIMEOUT', id: id });
            delete workerCallbacks[id];
        }
    };

    const setInterval = _setInterval;
    const clearInterval = _clearInterval;
    const setTimeout = _setTimeout;
    const clearTimeout = _clearTimeout;
    // ============================================================

    // --- 核心配置 ---
    const CONFIG = {
        prompt1: "你是谁",
        keyword1: "Gemini",
        placeholderText: "正在查找,请稍后...",
        resetUrl: window.location.origin + "/c/new",
        defaultIcon: "🧬",
        titlePrefix: "Gemini Hunter"
    };

    // --- 状态管理 ---
    const sessionActive = sessionStorage.getItem('gh_session_active');
    let storedIsRunning = GM_getValue('isRunning', false);

    if (storedIsRunning && !sessionActive) {
        storedIsRunning = false;
        GM_setValue('isRunning', false);
    }

    let isRunning = storedIsRunning;
    let savedLockedSide = GM_getValue('gh_locked_side', null);
    let lockedSide = savedLockedSide;

    let isMinimized = GM_getValue('gh_minimized_state_v1', 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 chatMonitorInterval = null;
    let timerInterval = null;
    let highlightInterval = null;
    let checkLoopInterval = null;
    let timerActive = false;
    let originalTitle = document.title;

    // --- UI 构建 ---
    // 注意:这里的 ball-text 初始值暂时留空或默认,后续由 JS 初始化更新
    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> Gemini Hunter
                </div>
                <div class="gh-win-ctrl">
                    <div id="gh-btn-min" title="收起"></div>
                </div>
            </div>
            <div class="gh-body">
                <div class="gh-timer-box">
                    <span class="gh-timer-label">TIME</span>
                    <span id="gh-timer-display">0.00 s</span>
                </div>
                <div class="gh-status-bar">
                    <div class="gh-dot"></div>
                    <span id="gh-msg">就绪</span>
                </div>
                <div class="gh-settings">
                    <label class="gh-toggle-row">
                        <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-row">
                        <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-row">
                        <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-row">
                        <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 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>
        </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: #6366f1; --gh-primary-dark: #4f46e5; --gh-success: #10b981; --gh-danger: #ef4444; --gh-bg: rgba(255, 255, 255, 0.92); --gh-shadow: 0 8px 20px -5px rgba(0, 0, 0, 0.15); }
        #gh-panel { position: fixed; top: 12%; left: 5%; width: 90%; max-width: 270px; background: var(--gh-bg); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); border: 1px solid rgba(255,255,255,0.6); border-radius: 18px; box-shadow: var(--gh-shadow); z-index: 100000; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; overflow: hidden; transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s; touch-action: none; }
        @media (min-width: 768px) { #gh-panel { width: 220px; top: 80px; right: 40px; left: auto; } }
        .gh-header { padding: 12px 15px 8px; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; }
        .gh-title { font-weight: 700; font-size: 13px; color: #1f2937; display: flex; align-items: center; gap: 6px; }
        .gh-ver { font-size: 9px; font-weight: 600; color: #9ca3af; background: rgba(0,0,0,0.05); padding: 2px 6px; border-radius: 8px; text-transform: uppercase;}
        .gh-win-ctrl { display: flex; padding: 4px; margin: -4px; }
        #gh-btn-min { width: 22px; height: 22px; border-radius: 50%; background: rgba(0,0,0,0.06); cursor: pointer; position: relative; }
        #gh-btn-min::before { content: ""; position: absolute; top: 10px; left: 6px; width: 10px; height: 2px; background: #6b7280; border-radius: 2px; }
        .gh-body { padding: 0 15px 18px; display: flex; flex-direction: column; gap: 12px; }
        .gh-timer-box { background: #f9fafb; border-radius: 12px; padding: 8px 12px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb; }
        .gh-timer-label { font-size: 9px; font-weight: 800; color: #9ca3af; letter-spacing: 1px; }
        #gh-timer-display { font-family: monospace; font-size: 17px; font-weight: 700; color: #374151; }
        .gh-status-bar { background: rgba(255,255,255,0.6); border: 1px solid rgba(0,0,0,0.05); padding: 8px 10px; border-radius: 10px; display: flex; align-items: center; gap: 8px; font-size: 12px; color: #4b5563; font-weight: 500; }
        .gh-dot { width: 8px; height: 8px; 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); }
        .gh-settings { padding: 0 2px; display: flex; flex-direction: column; gap: 8px; }
        .gh-toggle-row { display: flex; justify-content: space-between; align-items: center; cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; }
        .gh-lbl { font-size: 12px; font-weight: 600; color: #4b5563; }
        .gh-switch { position: relative; display: inline-block; width: 34px; height: 20px; flex-shrink: 0;}
        .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: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; transition: .3s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        input:checked + .gh-slider { background-color: var(--gh-primary); }
        input:checked + .gh-slider:before { transform: translateX(14px); }
        .gh-controls button { width: 100%; padding: 10px; border: none; border-radius: 12px; font-weight: 600; cursor: pointer; font-size: 13px; display: flex; align-items: center; justify-content: center; gap: 6px; color: white; touch-action: manipulation; }
        #gh-btn-start { background: linear-gradient(135deg, #4f46e5, #6366f1); box-shadow: 0 3px 10px rgba(99, 102, 241, 0.3); }
        #gh-btn-stop { background: linear-gradient(135deg, #dc2626, #ef4444); box-shadow: 0 3px 10px rgba(239, 68, 68, 0.3); }
        .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; }
        #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: #4f46e5; 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; }

        /* Google Style Letters */
        .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; /* Fallback */
            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; }
        @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; } }
    `);

    // --- 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 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 timerDisplay = document.getElementById('gh-timer-display');

    // --- 辅助:更新悬浮球文字样式 ---
    function updateBallText(side) {
        if (side === 'A' || side === 'B' || side === 'BOTH') {
            ballInner.innerText = (side === 'BOTH') ? '双' : side;
            ballInner.classList.add('gh-google-letter');
            ballInner.classList.remove('gh-ball-text'); // 移除默认的蓝色样式类,防止冲突(其实主要是字号颜色的覆盖)
            // 为了稳妥,直接操作classList添加google类即可,因为CSS权重
            ballInner.className = 'gh-ball-text gh-google-letter';
        } else {
            ballInner.innerText = CONFIG.defaultIcon;
            ballInner.className = 'gh-ball-text';
        }
    }

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

    if (window.innerWidth < 768 && !isMinimized && !panel.style.left) {
        panel.style.left = '5%'; panel.style.top = '20%';
    }

    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();
    });

    // --- 音频管理 ---
    function keepAliveAudio() {
        const AudioContext = window.AudioContext || window.webkitAudioContext;
        if (!AudioContext) return;
        const ctx = new AudioContext();
        const osc = ctx.createOscillator();
        const gain = ctx.createGain();
        gain.gain.value = 0.01;
        osc.connect(gain);
        gain.connect(ctx.destination);
        osc.start();
        osc.stop(ctx.currentTime + 0.1);
    }

    function playVictoryTheme() {
        if (!isSoundEnabled) return;
        const AudioContext = window.AudioContext || window.webkitAudioContext;
        if (!AudioContext) return;
        const ctx = new AudioContext();
        const now = ctx.currentTime;
        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 => {
            const o = ctx.createOscillator(); const g = ctx.createGain();
            o.type = n[3]; o.frequency.value = n[0]; o.connect(g); g.connect(ctx.destination);
            o.start(now+n[1]); g.gain.setValueAtTime(0.05, now+n[1]);
            g.gain.exponentialRampToValueAtTime(0.001, now+n[1]+n[2]); o.stop(now+n[1]+n[2]);
        });
    }

    // --- 计时器逻辑 ---
    function startTimer() {
        if (timerInterval) clearInterval(timerInterval);
        timerActive = true;
        const startTime = Date.now();
        timerDisplay.innerText = "0.00 s";
        timerDisplay.style.color = "#374151";

        timerInterval = setInterval(() => {
            const elapsed = (Date.now() - startTime) / 1000;
            const timeStr = elapsed.toFixed(2);
            timerDisplay.innerText = timeStr + " s";
            document.title = `[${Math.floor(elapsed)}s] 查找中...`;
        }, 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 isLoadingIndicatorVisible() {
        const el = document.querySelector('canvas[data-sentry-component="Loading"]');
        return !!el;
    }

    let uiTimerSeenLoading = false;
    setInterval(() => {
        if (timerActive) {
            const isLoading = isLoadingIndicatorVisible();
            if (isLoading) {
                uiTimerSeenLoading = true;
            }
            if (uiTimerSeenLoading && !isLoading) {
                stopTimer();
                uiTimerSeenLoading = false;
                // --- 新增:手动提问完成后的提示音 ---
                if (isSoundEnabled && !isRunning) {
                    playVictoryTheme();
                }
            }
        } else {
            uiTimerSeenLoading = false;
        }
    }, 100);

    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') || "";
                const testid = btn.getAttribute('data-testid') || "";
                const 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 screenMid = window.innerWidth / 2;
            const isRightSide = (pRect.left + pRect.width / 2) > screenMid;
            let newTop = pRect.top;
            let newLeft;
            if (isRightSide) newLeft = pRect.right - 48;
            else newLeft = pRect.left;
            if (newTop < 0) newTop = 10;
            if (newTop > window.innerHeight - 60) newTop = window.innerHeight - 60;
            ball.style.top = newTop + 'px';
            ball.style.left = newLeft + 'px';
            panel.style.display = 'none';
            ball.style.display = 'flex';
        } else {
            const bRect = ball.getBoundingClientRect();
            const screenMid = window.innerWidth / 2;
            const isRightSide = (bRect.left + bRect.width / 2) > screenMid;
            panel.style.opacity = '0';
            panel.style.display = 'block';
            const pWidth = panel.offsetWidth;
            const pHeight = panel.offsetHeight;
            let newTop = bRect.top;
            let newLeft;
            if (isRightSide) newLeft = bRect.right - pWidth;
            else newLeft = bRect.left;
            if (newLeft < 5) newLeft = 5;
            if (newLeft + pWidth > window.innerWidth) newLeft = window.innerWidth - pWidth - 5;
            if (newTop < 5) newTop = 5;
            if (newTop + pHeight > window.innerHeight) newTop = window.innerHeight - pHeight - 5;
            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(element, handle = element) {
        let isDragging = false;
        let startX, startY, initialLeft, initialTop;
        const onStart = (clientX, clientY) => {
            isDragging = true;
            element.isDragging = false;
            startX = clientX;
            startY = clientY;
            const rect = element.getBoundingClientRect();
            initialLeft = rect.left;
            initialTop = rect.top;
            element.style.position = 'fixed';
            element.style.right = 'auto';
            element.style.bottom = 'auto';
            element.style.left = initialLeft + 'px';
            element.style.top = initialTop + 'px';
            element.style.transition = 'none';
            if(element === ball) element.style.animation = 'none';
        };
        const onMove = (clientX, clientY) => {
            if (!isDragging) return;
            if (Math.abs(clientX - startX) > 5) element.isDragging = true;
            let newLeft = initialLeft + (clientX - startX);
            let newTop = initialTop + (clientY - startY);
            const maxW = window.innerWidth - element.offsetWidth;
            const maxH = window.innerHeight - element.offsetHeight;
            if (newLeft < 0) newLeft = 0;
            if (newLeft > maxW) newLeft = maxW;
            if (newTop < 0) newTop = 0;
            if (newTop > maxH) newTop = maxH;
            element.style.left = newLeft + 'px';
            element.style.top = newTop + 'px';
        };
        const onEnd = () => {
            if(isDragging) {
                isDragging = false;
                document.body.style.userSelect = '';
                handle.style.cursor = 'move';
                element.style.transition = 'transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s';
            }
        };
        handle.addEventListener('mousedown', (e) => {
            if(e.target === btnMin || e.target.closest('.gh-win-ctrl')) return;
            e.stopPropagation();
            document.body.style.userSelect = 'none';
            handle.style.cursor = 'grabbing';
            onStart(e.clientX, e.clientY);
        });
        document.addEventListener('mousemove', (e) => { onMove(e.clientX, e.clientY); });
        document.addEventListener('mouseup', onEnd);
        handle.addEventListener('touchstart', (e) => {
            if(e.target === btnMin || e.target.closest('.gh-win-ctrl')) return;
            const touch = e.touches[0];
            onStart(touch.clientX, touch.clientY);
        }, { passive: false });
        document.addEventListener('touchmove', (e) => {
            if (isDragging) {
                e.preventDefault();
                const touch = e.touches[0];
                onMove(touch.clientX, touch.clientY);
            }
        }, { passive: false });
        document.addEventListener('touchend', onEnd);
    }

    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)); }
    function generateWhitePngBlob() {
        return new Promise((resolve) => {
            const canvas = document.createElement('canvas'); canvas.width = 100; canvas.height = 100;
            const ctx = canvas.getContext('2d'); ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, 100, 100);
            canvas.toBlob((blob) => { resolve(blob); }, 'image/png');
        });
    }
    async function pasteGeneratedImage(element) {
        try {
            const blob = await generateWhitePngBlob();
            const file = new File([blob], "gen.png", { type: "image/png" });
            const dataTransfer = new DataTransfer(); dataTransfer.items.add(file);
            const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData: dataTransfer });
            element.focus(); element.dispatchEvent(pasteEvent); return true;
        } catch (e) { return false; }
    }
    async function clickImageButton() {
        const allButtons = document.querySelectorAll('button');
        for (let btn of allButtons) { if (btn.innerText && btn.innerText.trim() === "Image") { btn.click(); return true; } }
        return false;
    }
    function getSendButton() {
        return document.querySelector('button[data-testid="send-button"]') || document.querySelector('button[aria-label="Send message"]') || document.querySelector('button[type="submit"]');
    }
    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 = getSendButton();
        if (sendBtn && !sendBtn.disabled) { sendBtn.click(); return true; }
        else {
            const textarea = document.querySelector('textarea');
            if(textarea) textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
            return true;
        }
    }

    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 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;
                }
                const timePassed = Date.now() - startTime;
                if (timePassed > 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 findHeaderElement(text) {
        const candidates = Array.from(document.querySelectorAll('span, h2, div, h3'));
        return candidates.find(el => el.innerText && el.innerText.trim() === text && el.offsetParent !== null);
    }

    function getModelContainers() {
        const headerA = findHeaderElement("Assistant A");
        const headerB = findHeaderElement("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 getModelResponses() {
        const { containerA, containerB } = getModelContainers();
        const bubbles = Array.from(document.querySelectorAll('.prose'));
        let lastBubbleA = "";
        let lastBubbleB = "";
        if (!containerA || !containerB) return { A: "", B: "" };
        bubbles.forEach(b => {
            const text = b.innerText;
            if (text.includes(CONFIG.prompt1) && text.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 targetText = side === 'A' ? "Assistant A" : "Assistant B";
        const headerEl = findHeaderElement(targetText);
        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);
        // 频率提高到 200ms,确保光圈一直显示
        highlightInterval = setInterval(() => {
            const { containerA, containerB } = getModelContainers();
            if (!containerA || !containerB) return;
            const bubbles = Array.from(document.querySelectorAll('.prose'));
            bubbles.forEach(b => {
                const text = b.innerText;
                if (text.includes(CONFIG.prompt1) && text.length < 50) return;

                const isSideA = containerA.contains(b);
                const isSideB = containerB.contains(b);
                if (!isSideA && !isSideB) return;

                let isTarget = false;
                if (lockedSide === 'BOTH') isTarget = true;
                else if (lockedSide === 'A' && isSideA) isTarget = true;
                else if (lockedSide === 'B' && isSideB) isTarget = true;

                // 增强的容器查找逻辑,确保找到可视卡片
                let container = b.closest('.bg-surface-primary') ||
                                b.closest('.border.rounded-xl') ||
                                b.closest('.border.rounded-lg') ||
                                b.closest('[data-testid="model-answer"]');

                // 如果找不到常见容器,向上查找3层作为保底
                if (!container && b.parentElement && 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');
            });
        }, 200); // 极速刷新模式

        if (chatMonitorInterval) clearInterval(chatMonitorInterval);
        let lastBubbleCount = document.querySelectorAll('.prose').length;
        chatMonitorInterval = setInterval(async () => {
            if (!isAutoExpand) return;
            const currentCount = document.querySelectorAll('.prose').length;
            if (currentCount > lastBubbleCount) {
                lastBubbleCount = currentCount;
                await sleep(800);
                triggerExpand(lockedSide);
            }
        }, 1000);
    }

    async function runSequence() {
        if (!GM_getValue('isRunning', false)) return;
        isRunning = true;
        GM_deleteValue('gh_locked_side');
        lockedSide = null;

        if (chatMonitorInterval) clearInterval(chatMonitorInterval);
        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; }

        updateStatus("验证 (Gemini)...", 'active');
        await pasteGeneratedImage(textarea);
        await sleep(2000);
        if (!isRunning) return;
        await clickImageButton();
        await sleep(500);
        await fillTextOnly(textarea, CONFIG.prompt1);
        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 regex = new RegExp(CONFIG.keyword1, 'i');
        const isA = regex.test(resp1.A);
        const isB = regex.test(resp1.B);
        let finalFound = false;
        if (isA || isB) {
            finalFound = true;
            if (isA && isB) lockedSide = 'BOTH';
            else if (isA) lockedSide = 'A';
            else lockedSide = 'B';
        }

        if (finalFound) {
            GM_setValue('gh_locked_side', 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 });

            GM_setValue('isRunning', false);
            sessionStorage.removeItem('gh_session_active');
            isRunning = false;
            updateUIState(false);
        } else {
            updateStatus("未发现,重试...", 'error');
            await sleep(1000);
            return retry();
        }
    }

    function startHunt() {
        GM_deleteValue('gh_locked_side');
        lockedSide = null;
        keepAliveAudio();
        sessionStorage.setItem('gh_session_active', 'true');
        GM_setValue('isRunning', true);
        isRunning = true;
        updateBallText(null); // 重置为图标
        updateUIState(true);
        updateStatus("初始化...", 'active');
        window.location.href = CONFIG.resetUrl;
    }

    function stopHunt() {
        GM_deleteValue('gh_locked_side');
        lockedSide = null;
        GM_setValue('isRunning', false);
        sessionStorage.removeItem('gh_session_active');
        isRunning = false;
        if (timerInterval) clearInterval(timerInterval);
        if (chatMonitorInterval) clearInterval(chatMonitorInterval);
        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 (!GM_getValue('isRunning', false) || !isRunning) {
            updateStatus("已停止", 'normal');
            return;
        }
        window.location.href = CONFIG.resetUrl;
    }

    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);
        }
    }

    if (isRunning && !lockedSide && sessionActive) {
        updateUIState(true);
        keepAliveAudio();
        setTimeout(runSequence, 2000);
    } else if (!sessionActive && isRunning) {
        updateUIState(false);
        GM_setValue('isRunning', false);
    }

})();