X translation

将 X 上的 Grok AI 翻译替换为谷歌翻译或 Gemini API。添加一键翻译悬浮按钮,支持滚动自动翻译与多模型选择。

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         X translation
// @name:zh-CN   X 一键翻译
// @namespace    https://x.com
// @author       Rove24
// @version      3.7
// @description  将 X 上的 Grok AI 翻译替换为谷歌翻译或 Gemini API。添加一键翻译悬浮按钮,支持滚动自动翻译与多模型选择。
// @match        https://x.com/*
// @match        https://twitter.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=x.com
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @connect      translate.googleapis.com
// @connect      generativelanguage.googleapis.com
// @connect      *
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

(function () {
  "use strict";

  const browserLang = navigator.language.split("-")[0];
  let isAllTranslated = GM_getValue("isAllTranslated", false);
  let lastUrl = location.href;

  const DEFAULT_GEMINI_MODEL = "gemini-flash-lite-latest";

  const GEMINI_MODELS = [
    { value: "gemini-flash-lite-latest", label: "Gemini Flash Lite Latest (默认)" },
    { value: "gemini-flash-latest", label: "Gemini Flash Latest" },
    { value: "gemini-3.5-flash-lite", label: "Gemini 3.5 Flash Lite" },
    { value: "gemini-3.1-flash-lite", label: "Gemini 3.1 Flash Lite" },
    { value: "gemini-3.7-flash", label: "Gemini 3.7 Flash" },
    { value: "gemini-3.6-flash", label: "Gemini 3.6 Flash" },
    { value: "gemini-3.5-flash", label: "Gemini 3.5 Flash" },
    { value: "gemini-3-flash", label: "Gemini 3 Flash" },
    { value: "custom", label: "自定义模型 (Custom Model)..." },
  ];

  // ✅ 修复1:使用正确的模型名格式
  const DEFAULT_AGNES_MODEL = "agnes-2.0-flash";

  const AGNES_MODELS = [
    { value: "agnes-2.0-flash", label: "Agnes 2.0 Flash" },
    { value: "agnes-2.5-flash", label: "Agnes 2.5 Flash (推荐)" },
    { value: "agnes-2.5-pro", label: "Agnes 2.5 Pro" },
    { value: "agnes-2.5-pro-alpha", label: "Agnes 2.5 Pro Alpha" },
    { value: "custom", label: "自定义模型 (Custom Model)..." },
  ];

  const DEFAULT_GEMINI_PROMPT =
    "请将以下内容完整翻译为 {targetLang}(简体中文)。\n" +
    "要求:\n" +
    "1. 忠实原文含义,保持自然的口语表达,网络梗与俚语需意译为贴切的对应语言;\n" +
    "2. 严格保留原有的换行、编号列表(如 1位、2位、排名)、标点符号和表情符号;\n" +
    "3. 保持类似 {{0}}、@用户名、#话题 占位标签原样不动;\n" +
    "4. 仅输出翻译后的结果,绝不要输出任何解释、前后缀或多余文字。\n\n" +
    "待翻译内容:\n{text}";

  function getTargetLang() {
    return GM_getValue("targetLang", browserLang);
  }

  const LANGUAGES = [
    ["af","南非语 (Afrikaans)"],["sq","阿尔巴尼亚语 (Albanian)"],["am","阿姆哈拉语 (Amharic)"],["ar","阿拉伯语 (Arabic)"],
    ["hy","亚美尼亚语 (Armenian)"],["az","阿塞拜疆语 (Azerbaijani)"],["eu","巴斯克语 (Basque)"],["be","白俄罗斯语 (Belarusian)"],
    ["bn","孟加拉语 (Bengali)"],["bs","波斯尼亚语 (Bosnian)"],["bg","保加利亚语 (Bulgarian)"],["my","缅甸语 (Burmese)"],
    ["ca","加泰罗尼亚语 (Catalan)"],["zh","中文 (Chinese)"],["hr","克罗地亚语 (Croatian)"],["cs","捷克语 (Czech)"],
    ["da","丹麦语 (Danish)"],["nl","荷兰语 (Dutch)"],["en","英语 (English)"],["et","爱沙尼亚语 (Estonian)"],
    ["fil","菲律宾语 (Filipino)"],["fi","芬兰语 (Finnish)"],["fr","法语 (French)"],["fy","弗里斯兰语 (Frisian)"],
    ["gl","加利西亚语 (Galician)"],["ka","格鲁吉亚语 (Georgian)"],["de","德语 (German)"],["el","希腊语 (Greek)"],
    ["gn","瓜拉尼语 (Guarani)"],["gu","古吉拉特语 (Gujarati)"],["he","希伯来语 (Hebrew)"],["hi","印地语 (Hindi)"],
    ["hu","匈牙利语 (Hungarian)"],["is","冰岛语 (Icelandic)"],["id","印尼语 (Indonesian)"],["ga","爱尔兰语 (Irish)"],
    ["it","意大利语 (Italian)"],["ja","日语 (Japanese)"],["kn","卡纳达语 (Kannada)"],["kk","哈萨克语 (Kazakh)"],
    ["km","高棉语 (Khmer)"],["ko","韩语 (Korean)"],["ky","吉尔吉斯语 (Kyrgyz)"],["lo","老挝语 (Lao)"],
    ["lv","拉脱维亚语 (Latvian)"],["ln","林加拉语 (Lingala)"],["lt","立陶宛语 (Lithuanian)"],["mk","马其顿语 (Macedonian)"],
    ["ms","马来语 (Malay)"],["ml","马拉雅拉姆语 (Malayalam)"],["mt","马耳他语 (Maltese)"],["mr","马拉地语 (Marathi)"],
    ["mn","蒙古语 (Mongolian)"],["ne","尼泊尔语 (Nepali)"],["no","挪威语 (Norwegian)"],["nb","挪威博克马尔语 (Norwegian Bokmål)"],
    ["fa","波斯语 (Persian)"],["pl","波兰语 (Polish)"],["pt","葡萄牙语 (Portuguese)"],["pa","旁遮普语 (Punjabi)"],
    ["ro","罗马尼亚语 (Romanian)"],["ru","俄语 (Russian)"],["sr","塞尔维亚语 (Serbian)"],["sk","斯洛伐克语 (Slovak)"],
    ["sl","斯洛文尼亚语 (Slovenian)"],["es","西班牙语 (Spanish)"],["sw","斯瓦希里语 (Swahili)"],["sv","瑞典语 (Swedish)"],
    ["ta","泰米尔语 (Tamil)"],["te","泰卢固语 (Telugu)"],["th","泰语 (Thai)"],["tr","土耳其语 (Turkish)"],
    ["uk","乌克兰语 (Ukrainian)"],["ur","乌尔都语 (Urdu)"],["uz","乌兹别克语 (Uzbek)"],["vi","越南语 (Vietnamese)"],
    ["cy","威尔士语 (Welsh)"],["zu","祖鲁语 (Zulu)"],
  ];

  // ✅ 新增:解析源语言代码的辅助函数
  const LANG_CODE_REGEX = /^([a-z]{2})(-[A-Z]{2})?$/;

  function parseSourceLang(fullText) {
    const lines = fullText.split("\n").map(l => l.trim()).filter(Boolean);
    if (lines.length >= 2 && LANG_CODE_REGEX.test(lines[0])) {
      return { sourceLang: lines[0].toLowerCase(), translatedText: lines.slice(1).join("\n").trim() };
    }
    return { sourceLang: "", translatedText: fullText.trim() };
  }

  function getThemeColors() {
    const bg = window.getComputedStyle(document.body).backgroundColor;
    const isDark = bg === "rgb(0, 0, 0)" || bg === "rgb(21, 32, 43)" || bg.includes("21, 32") || bg.includes("30, 41, 59");

    if (bg === "rgb(0, 0, 0)") {
      return {
        bg: "rgb(0, 0, 0)",
        border: "rgb(47, 51, 54)",
        icon: "#e7e9ea",
        secondaryText: "#71767b",
        hoverBg: "rgba(231, 233, 234, 0.1)",
        shadow: "0 0 10px rgba(255, 255, 255, 0.1)",
        activeBg: "rgb(29, 155, 240)",
        activeIcon: "#ffffff",
        activeHoverBg: "rgb(26, 140, 216)"
      };
    } else if (isDark) {
      return {
        bg: "rgb(21, 32, 43)",
        border: "rgb(56, 68, 77)",
        icon: "#8899a6",
        secondaryText: "#8899a6",
        hoverBg: "rgba(29, 155, 240, 0.1)",
        shadow: "0 0 12px rgba(0, 0, 0, 0.5)",
        activeBg: "rgb(29, 155, 240)",
        activeIcon: "#ffffff",
        activeHoverBg: "rgb(26, 140, 216)"
      };
    } else {
      return {
        bg: "#ffffff",
        border: "rgb(207, 217, 222)",
        icon: "#0f1419",
        secondaryText: "#536471",
        hoverBg: "rgba(15, 20, 25, 0.1)",
        shadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
        activeBg: "rgb(29, 155, 240)",
        activeIcon: "#ffffff",
        activeHoverBg: "rgb(26, 140, 216)"
      };
    }
  }

  function getLanguageName(code) {
    if (!code) return "";
    const baseCode = code.split("-")[0].toLowerCase();
    const lang = LANGUAGES.find(l => l[0] === baseCode || l[0] === code.toLowerCase());
    if (lang) {
      return lang[1].split(" (")[0];
    }
    if (baseCode === "zh") return "中文";
    if (baseCode === "iw") return "希伯来语";
    if (baseCode === "jw") return "爪哇语";
    return code;
  }

  function showLanguagePicker() {
    document.getElementById("gt-lang-picker")?.remove();
    const current = getTargetLang();

    const overlay = document.createElement("div");
    overlay.id = "gt-lang-picker";
    overlay.style.cssText =
      "position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:999999;" +
      "display:flex;align-items:center;justify-content:center;";

    const dialog = document.createElement("div");
    dialog.style.cssText =
      "background:#fff;border-radius:16px;padding:24px;width:340px;max-height:80vh;" +
      "display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,sans-serif;" +
      "color:#0f1419;box-shadow:0 8px 28px rgba(0,0,0,0.25);";

    const title = document.createElement("div");
    title.textContent = "选择翻译目标语言";
    title.style.cssText = "font-size:18px;font-weight:700;margin-bottom:16px;";

    const search = document.createElement("input");
    search.type = "text";
    search.placeholder = "搜索语言...";
    search.style.cssText =
      "width:100%;padding:10px 12px;border:1px solid #cfd9de;border-radius:8px;" +
      "font-size:14px;outline:none;box-sizing:border-box;margin-bottom:12px;";

    const list = document.createElement("div");
    list.style.cssText = "overflow-y:auto;flex:1;max-height:50vh;";

    function renderList(filter = "") {
      list.innerHTML = "";
      const lf = filter.toLowerCase();
      for (const [code, name] of LANGUAGES) {
        if (lf && !name.toLowerCase().includes(lf) && !code.includes(lf)) continue;

        const item = document.createElement("div");
        item.textContent = `${name} (${code})`;
        item.style.cssText =
          "padding:10px 12px;border-radius:8px;cursor:pointer;font-size:14px;" +
          (code === current ? "background:#e8f5fd;font-weight:600;color:#1d9bf0;" : "");
        item.addEventListener("mouseenter", () => {
          if (code !== current) item.style.background = "#f7f9f9";
        });
        item.addEventListener("mouseleave", () => {
          if (code !== current) item.style.background = "";
        });
        item.addEventListener("click", () => {
          GM_setValue("targetLang", code);
          overlay.remove();
          location.reload();
        });
        list.appendChild(item);
      }
    }

    search.addEventListener("input", () => renderList(search.value));
    overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });

    dialog.append(title, search, list);

    const resetBtn = document.createElement("button");
    resetBtn.textContent = `重置为浏览器默认语言 (${browserLang})`;
    resetBtn.style.cssText =
      "margin-top:12px;padding:8px;border:1px solid #cfd9de;border-radius:8px;" +
      "background:none;cursor:pointer;font-size:13px;color:#536471;width:100%;";
    resetBtn.addEventListener("click", () => {
      GM_setValue("targetLang", browserLang);
      overlay.remove();
      location.reload();
    });
    dialog.appendChild(resetBtn);

    overlay.appendChild(dialog);
    document.body.appendChild(overlay);
    search.focus();
    renderList();
  }

  function showSettingsModal() {
    document.getElementById("gt-settings-modal")?.remove();
    const colors = getThemeColors();
    const currentSource = GM_getValue("translateSource", "google");
    const currentApiKey = GM_getValue("geminiApiKey", "");
    const currentAgnesApiKey = GM_getValue("agnesApiKey", "");
    const currentModel = GM_getValue("geminiModel", DEFAULT_GEMINI_MODEL);
    const currentAgnesModel = GM_getValue("agnesModel", DEFAULT_AGNES_MODEL);
    const currentPrompt = GM_getValue("geminiPrompt", DEFAULT_GEMINI_PROMPT);
    const enableGrok = GM_getValue("enableGrok", false);

    const overlay = document.createElement("div");
    overlay.id = "gt-settings-modal";
    overlay.style.cssText =
      "position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:999999;" +
      "display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px);";

    const dialog = document.createElement("div");
    dialog.style.cssText =
      `background:${colors.bg};border:1px solid ${colors.border};border-radius:16px;padding:24px;width:400px;max-width:90vw;` +
      `display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,sans-serif;color:${colors.icon};` +
      `box-shadow:0 8px 30px rgba(0,0,0,0.35);box-sizing:border-box;`;

    // Gemini 模型选项
    const isKnownGeminiModel = GEMINI_MODELS.some(m => m.value === currentModel && m.value !== "custom");
    const selectedGeminiValue = isKnownGeminiModel ? currentModel : (currentModel ? "custom" : DEFAULT_GEMINI_MODEL);
    const geminiOptionsHtml = GEMINI_MODELS.map(m =>
      `<option value="${m.value}" ${m.value === selectedGeminiValue ? "selected" : ""} style="background:${colors.bg};color:${colors.icon};padding:8px;">${m.label}</option>`
    ).join("");

    // Agnes 模型选项
    const isKnownAgnesModel = AGNES_MODELS.some(m => m.value === currentAgnesModel && m.value !== "custom");
    const selectedAgnesValue = isKnownAgnesModel ? currentAgnesModel : (currentAgnesModel ? "custom" : DEFAULT_AGNES_MODEL);
    const agnesOptionsHtml = AGNES_MODELS.map(m =>
      `<option value="${m.value}" ${m.value === selectedAgnesValue ? "selected" : ""} style="background:${colors.bg};color:${colors.icon};padding:8px;">${m.label}</option>`
    ).join("");

    dialog.innerHTML = `
      <div style="font-size:18px;font-weight:700;margin-bottom:16px;display:flex;align-items:center;justify-content:space-between;">
        <div style="display:flex;align-items:center;gap:10px;">
          <span>翻译设置</span>
          <label style="display:inline-flex;align-items:center;gap:6px;cursor:pointer;user-select:none;" title="启用/关闭 X 自带的 Grok 翻译机制">
            <span style="font-size:12px;font-weight:600;color:${colors.secondaryText};">Grok翻译</span>
            <input id="gt-toggle-grok" type="checkbox" ${enableGrok ? "checked" : ""} style="display:none;">
            <span id="gt-switch-slider" style="width:34px;height:18px;background:${enableGrok ? "#1d9bf0" : colors.border};border-radius:10px;position:relative;display:inline-block;transition:background 0.2s;">
              <span id="gt-switch-knob" style="position:absolute;top:2px;left:${enableGrok ? "18px" : "2px"};width:14px;height:14px;border-radius:50%;background:#fff;transition:left 0.2s;box-shadow:0 1px 3px rgba(0,0,0,0.3);"></span>
            </span>
          </label>
        </div>
        <span id="gt-modal-close" style="cursor:pointer;font-size:18px;color:${colors.secondaryText};padding:4px;">✕</span>
      </div>

      <div style="margin-bottom:16px;">
        <label style="font-size:13px;font-weight:600;color:${colors.secondaryText};display:block;margin-bottom:8px;">翻译引擎 / 翻译源</label>
        <div style="display:flex;gap:14px;flex-wrap:wrap;">
          <label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:14px;">
            <input type="radio" name="gt-source" value="google" ${currentSource === "google" ? "checked" : ""}> Google 翻译
          </label>
          <label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:14px;">
            <input type="radio" name="gt-source" value="gemini" ${currentSource === "gemini" ? "checked" : ""}> Gemini API
          </label>
          <label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:14px;">
            <input type="radio" name="gt-source" value="agnes" ${currentSource === "agnes" ? "checked" : ""}> Agnes API
          </label>
        </div>
      </div>

      <!-- Gemini 配置区域 -->
      <div id="gt-gemini-config" style="display:${currentSource === "gemini" ? "flex" : "none"};flex-direction:column;gap:14px;margin-bottom:16px;">
        <div>
          <label style="font-size:13px;font-weight:600;color:${colors.secondaryText};display:block;margin-bottom:6px;">1. Gemini API Key</label>
          <input id="gt-input-api" type="password" placeholder="AIzaSy..." value="${escapeHtml(currentApiKey)}"
            style="width:100%;padding:9px 12px;border:1px solid ${colors.border};border-radius:8px;background:transparent;color:inherit;font-size:13px;outline:none;box-sizing:border-box;">
        </div>

        <div>
          <label style="font-size:13px;font-weight:600;color:${colors.secondaryText};display:block;margin-bottom:6px;">Gemini 模型版本</label>
          <div style="position:relative;width:100%;">
            <select id="gt-select-model"
              style="width:100%;padding:10px 32px 10px 12px;border:1px solid ${colors.border};border-radius:8px;background:${colors.bg};color:${colors.icon};font-size:13px;outline:none;box-sizing:border-box;cursor:pointer;appearance:none;-webkit-appearance:none;">
              ${geminiOptionsHtml}
            </select>
            <div style="position:absolute;right:12px;top:50%;transform:translateY(-50%);pointer-events:none;color:${colors.secondaryText};display:flex;align-items:center;">
              <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>
            </div>
          </div>
          <input id="gt-input-custom-model" type="text" placeholder="输入自定义模型名称,例如 gemini-2.0-flash" value="${!isKnownGeminiModel ? escapeHtml(currentModel) : ""}"
            style="width:100%;padding:8px 12px;border:1px solid ${colors.border};border-radius:8px;background:transparent;color:inherit;font-size:13px;outline:none;box-sizing:border-box;margin-top:8px;display:${selectedGeminiValue === "custom" ? "block" : "none"};">
        </div>
      </div>

      <!-- Agnes 配置区域 -->
      <div id="gt-agnes-config" style="display:${currentSource === "agnes" ? "flex" : "none"};flex-direction:column;gap:14px;margin-bottom:16px;">
        <div>
          <label style="font-size:13px;font-weight:600;color:${colors.secondaryText};display:block;margin-bottom:6px;">1. Agnes API Key</label>
          <input id="gt-input-agnes-api" type="password" placeholder="填写 Agnes API Key..." value="${escapeHtml(currentAgnesApiKey)}"
            style="width:100%;padding:9px 12px;border:1px solid ${colors.border};border-radius:8px;background:transparent;color:inherit;font-size:13px;outline:none;box-sizing:border-box;">
        </div>

        <div>
          <label style="font-size:13px;font-weight:600;color:${colors.secondaryText};display:block;margin-bottom:6px;">Agnes 模型版本</label>
          <div style="position:relative;width:100%;">
            <select id="gt-select-agnes-model"
              style="width:100%;padding:10px 32px 10px 12px;border:1px solid ${colors.border};border-radius:8px;background:${colors.bg};color:${colors.icon};font-size:13px;outline:none;box-sizing:border-box;cursor:pointer;appearance:none;-webkit-appearance:none;">
              ${agnesOptionsHtml}
            </select>
            <div style="position:absolute;right:12px;top:50%;transform:translateY(-50%);pointer-events:none;color:${colors.secondaryText};display:flex;align-items:center;">
              <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>
            </div>
          </div>
          <input id="gt-input-custom-agnes-model" type="text" placeholder="输入自定义模型名称" value="${!isKnownAgnesModel ? escapeHtml(currentAgnesModel) : ""}"
            style="width:100%;padding:8px 12px;border:1px solid ${colors.border};border-radius:8px;background:transparent;color:inherit;font-size:13px;outline:none;box-sizing:border-box;margin-top:8px;display:${selectedAgnesValue === "custom" ? "block" : "none"};">
        </div>
      </div>

      <!-- 公共提示词配置 -->
      <div id="gt-prompt-config" style="display:${currentSource !== "google" ? "block" : "none"};margin-bottom:16px;">
        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
          <label style="font-size:13px;font-weight:600;color:${colors.secondaryText};">提示词 (Prompt)</label>
          <span id="gt-btn-reset-prompt" style="font-size:12px;color:#1d9bf0;cursor:pointer;">重置默认</span>
        </div>
        <textarea id="gt-input-prompt" rows="4"
          style="width:100%;padding:8px 12px;border:1px solid ${colors.border};border-radius:8px;background:transparent;color:inherit;font-size:12px;outline:none;resize:vertical;box-sizing:border-box;">${escapeHtml(currentPrompt)}</textarea>
        <div style="font-size:11px;color:${colors.secondaryText};margin-top:4px;">支持占位符:<code>{targetLang}</code> 目标语言,<code>{text}</code> 待翻文本</div>
      </div>

      <div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px;">
        <span id="gt-btn-open-lang" style="font-size:13px;color:#1d9bf0;cursor:pointer;">目标语言 (${getTargetLang()})</span>
        <div style="display:flex;gap:8px;">
          <button id="gt-btn-cancel" style="padding:7px 14px;border:1px solid ${colors.border};border-radius:18px;background:transparent;color:${colors.icon};cursor:pointer;font-size:13px;font-weight:600;">取消</button>
          <button id="gt-btn-save" style="padding:7px 18px;border:none;border-radius:18px;background:#1d9bf0;color:#fff;cursor:pointer;font-size:13px;font-weight:600;">保存</button>
        </div>
      </div>
    `;

    const grokCheckbox = dialog.querySelector("#gt-toggle-grok");
    const grokSlider = dialog.querySelector("#gt-switch-slider");
    const grokKnob = dialog.querySelector("#gt-switch-knob");
    grokCheckbox.addEventListener("change", () => {
      if (grokCheckbox.checked) {
        grokSlider.style.background = "#1d9bf0";
        grokKnob.style.left = "18px";
      } else {
        grokSlider.style.background = colors.border;
        grokKnob.style.left = "2px";
      }
    });

    const radios = dialog.querySelectorAll('input[name="gt-source"]');
    const geminiBox = dialog.querySelector("#gt-gemini-config");
    const agnesBox = dialog.querySelector("#gt-agnes-config");
    const promptBox = dialog.querySelector("#gt-prompt-config");

    radios.forEach(r => {
      r.addEventListener("change", () => {
        geminiBox.style.display = r.value === "gemini" && r.checked ? "flex" : "none";
        agnesBox.style.display = r.value === "agnes" && r.checked ? "flex" : "none";
        promptBox.style.display = r.value !== "google" && r.checked ? "block" : "none";
      });
    });

    // Gemini 自定义模型切换
    const selectModel = dialog.querySelector("#gt-select-model");
    const customModelInput = dialog.querySelector("#gt-input-custom-model");
    selectModel.addEventListener("change", () => {
      customModelInput.style.display = selectModel.value === "custom" ? "block" : "none";
    });

    // Agnes 自定义模型切换
    const selectAgnesModel = dialog.querySelector("#gt-select-agnes-model");
    const customAgnesModelInput = dialog.querySelector("#gt-input-custom-agnes-model");
    selectAgnesModel.addEventListener("change", () => {
      customAgnesModelInput.style.display = selectAgnesModel.value === "custom" ? "block" : "none";
    });

    dialog.querySelector("#gt-modal-close").onclick = () => overlay.remove();
    dialog.querySelector("#gt-btn-cancel").onclick = () => overlay.remove();
    overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };

    dialog.querySelector("#gt-btn-open-lang").onclick = () => {
      overlay.remove();
      showLanguagePicker();
    };

    dialog.querySelector("#gt-btn-reset-prompt").onclick = () => {
      dialog.querySelector("#gt-input-prompt").value = DEFAULT_GEMINI_PROMPT;
    };

    dialog.querySelector("#gt-btn-save").onclick = () => {
      const selectedSource = dialog.querySelector('input[name="gt-source"]:checked').value;

      let finalGeminiModel = selectModel.value;
      if (finalGeminiModel === "custom") {
        finalGeminiModel = customModelInput.value.trim() || DEFAULT_GEMINI_MODEL;
      }

      let finalAgnesModel = selectAgnesModel.value;
      if (finalAgnesModel === "custom") {
        finalAgnesModel = customAgnesModelInput.value.trim() || DEFAULT_AGNES_MODEL;
      }

      GM_setValue("enableGrok", grokCheckbox.checked);
      GM_setValue("translateSource", selectedSource);
      GM_setValue("geminiApiKey", dialog.querySelector("#gt-input-api").value.trim());
      GM_setValue("geminiModel", finalGeminiModel);
      GM_setValue("agnesApiKey", dialog.querySelector("#gt-input-agnes-api").value.trim());
      GM_setValue("agnesModel", finalAgnesModel);
      GM_setValue("geminiPrompt", dialog.querySelector("#gt-input-prompt").value.trim() || DEFAULT_GEMINI_PROMPT);

      overlay.remove();
      patchAll();
    };

    overlay.appendChild(dialog);
    document.body.appendChild(overlay);
  }

  GM_registerMenuCommand("⚙️ 翻译与 API 设置", showSettingsModal);
  GM_registerMenuCommand("🌐 设置翻译语言", showLanguagePicker);

  function googleTranslate(text) {
    return new Promise((resolve, reject) => {
      const url =
        "https://translate.googleapis.com/translate_a/single?" +
        new URLSearchParams({
          client: "gtx",
          sl: "auto",
          tl: getTargetLang(),
          dt: "t",
          q: text,
        });

      GM_xmlhttpRequest({
        method: "GET",
        url,
        onload(res) {
          try {
            const data = JSON.parse(res.responseText);
            const translated = data[0].map((s) => s[0]).join("");
            const detectedLang = data[2];
            resolve({ text: translated, sourceLang: detectedLang });
          } catch (e) {
            reject(e);
          }
        },
        onerror: reject,
      });
    });
  }

  function geminiTranslate(text) {
    return new Promise((resolve, reject) => {
      const apiKey = GM_getValue("geminiApiKey", "").trim();
      if (!apiKey) {
        showSettingsModal();
        reject(new Error("请先在设置中填写 Gemini API Key!"));
        return;
      }

      const model = GM_getValue("geminiModel", DEFAULT_GEMINI_MODEL).trim() || DEFAULT_GEMINI_MODEL;
      const cleanModel = model.replace(/^models\//, "");
      const targetLangCode = getTargetLang();
      const targetLangName = getLanguageName(targetLangCode) || targetLangCode;

      const safeText = text.replace(/\uE000(\d+)\uE001/g, "{{$1}}");

      let customPrompt = GM_getValue("geminiPrompt", DEFAULT_GEMINI_PROMPT);
      let promptContent = customPrompt.replace("{targetLang}", targetLangName);
      if (promptContent.includes("{text}")) {
        promptContent = promptContent.replace("{text}", safeText);
      } else {
        promptContent = `${promptContent}\n\n${safeText}`;
      }

      const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(cleanModel)}:generateContent?key=${encodeURIComponent(apiKey)}`;

      // ✅ 修改系统提示词:要求第一行输出源语言代码
      const requestBody = {
        system_instruction: {
          parts: [{
            text: "你是一个专业的全语种翻译引擎。你的唯一任务是把用户输入的任何外语(英语、日语、西班牙语等)准确翻译为目标语言。严禁原样照抄输出原文,遇到 {{0}} 等占位符保持不变,仅输出最终翻译结果。**你必须先在回复的第一行单独输出源语言的 ISO 639-1 代码(如 en、ja、es、fr、de、ko 等),然后换行,再输出翻译内容。**"
          }]
        },
        contents: [
          {
            parts: [{ text: promptContent }]
          }
        ],
        generationConfig: {
          temperature: 0.3
        }
      };

      GM_xmlhttpRequest({
        method: "POST",
        url,
        headers: {
          "Content-Type": "application/json"
        },
        data: JSON.stringify(requestBody),
        async onload(res) {
          try {
            const data = JSON.parse(res.responseText);
            if (data.error) {
              console.error("Gemini API 错误:", data.error);
              reject(new Error(data.error.message || "Gemini API 返回错误"));
              return;
            }
            const candidate = data.candidates?.[0];
            const fullResponse = candidate?.content?.parts?.[0]?.text?.trim() || "";
            if (!fullResponse) {
              reject(new Error("Gemini 未返回翻译内容"));
              return;
            }

            // ✅ 解析源语言代码和翻译内容
            const { sourceLang, translatedText } = parseSourceLang(fullResponse);

            // 【防漏兜底】:若小模型原样照抄输出,自动切换 Google 翻译兜底
            if (translatedText.trim().toLowerCase() === safeText.trim().toLowerCase()) {
              const gResult = await googleTranslate(text);
              resolve(gResult);
              return;
            }

            resolve({ text: translatedText, sourceLang });
          } catch (e) {
            reject(e);
          }
        },
        onerror: reject,
      });
    });
  }

  function agnesTranslate(text) {
    return new Promise((resolve, reject) => {
      const apiKey = GM_getValue("agnesApiKey", "").trim();
      if (!apiKey) {
        showSettingsModal();
        reject(new Error("请先在设置中填写 Agnes API Key!"));
        return;
      }

      const model = GM_getValue("agnesModel", DEFAULT_AGNES_MODEL).trim() || DEFAULT_AGNES_MODEL;
      const targetLangCode = getTargetLang();
      const targetLangName = getLanguageName(targetLangCode) || targetLangCode;

      const safeText = text.replace(/\uE000(\d+)\uE001/g, "{{$1}}");

      let customPrompt = GM_getValue("geminiPrompt", DEFAULT_GEMINI_PROMPT);
      let promptContent = customPrompt.replace("{targetLang}", targetLangName);
      if (promptContent.includes("{text}")) {
        promptContent = promptContent.replace("{text}", safeText);
      } else {
        promptContent = `${promptContent}\n\n${safeText}`;
      }

      const url = "https://apihub.agnes-ai.com/v1/chat/completions";

      // ✅ 修改系统提示词:要求第一行输出源语言代码
      const requestBody = {
        model: model,
        messages: [
          {
            role: "system",
            content: "你是一个专业的全语种翻译引擎。你的唯一任务是把用户输入的任何外语(英语、日语、西班牙语等)准确翻译为目标语言。严禁原样照抄输出原文,遇到 {{0}} 等占位符保持不变,仅输出最终翻译结果。**你必须先在回复的第一行单独输出源语言的 ISO 639-1 代码(如 en、ja、es、fr、de、ko 等),然后换行,再输出翻译内容。**"
          },
          {
            role: "user",
            content: promptContent
          }
        ],
        temperature: 0.3
      };

      GM_xmlhttpRequest({
        method: "POST",
        url,
        headers: {
          "Content-Type": "application/json",
          "Authorization": `Bearer ${apiKey}`
        },
        data: JSON.stringify(requestBody),
        async onload(res) {
          try {
            const data = JSON.parse(res.responseText);
            if (data.error) {
              console.error("Agnes API 错误:", data.error);
              reject(new Error(data.error.message || "Agnes API 返回错误"));
              return;
            }
            const fullResponse = data.choices?.[0]?.message?.content?.trim() || "";
            if (!fullResponse) {
              reject(new Error("Agnes API 未返回翻译内容"));
              return;
            }

            // ✅ 解析源语言代码和翻译内容
            const { sourceLang, translatedText } = parseSourceLang(fullResponse);

            // 防漏兜底
            if (translatedText.trim().toLowerCase() === safeText.trim().toLowerCase()) {
              const gResult = await googleTranslate(text);
              resolve(gResult);
              return;
            }

            resolve({ text: translatedText, sourceLang });
          } catch (e) {
            reject(e);
          }
        },
        onerror: reject,
      });
    });
  }

  function translateText(text) {
    const source = GM_getValue("translateSource", "google");
    if (source === "gemini") {
      return geminiTranslate(text);
    } else if (source === "agnes") {
      return agnesTranslate(text);
    }
    return googleTranslate(text);
  }

  function getTweetText(button) {
    const birdwatch = button.closest('[data-testid="birdwatch-pivot"]');
    if (birdwatch) {
      let container = button.parentElement;
      while (container && container.parentElement !== birdwatch) {
        container = container.parentElement;
      }
      let sibling = container?.nextElementSibling;
      while (sibling) {
        if (sibling.tagName === "DIV") {
          return { element: sibling, text: sibling.innerText };
        }
        sibling = sibling.nextElementSibling;
      }
    }

    let node = button.parentElement;
    while (node) {
      const textEl = node.querySelector('[data-testid="tweetText"], [data-testid="UserDescription"]');
      if (textEl) return { element: textEl, text: textEl.innerText };
      node = node.parentElement;
    }
    return null;
  }

  const GT_PATH = "M12.87 15.07l-2.54-2.51.03-.03A17.52 17.52 0 0 0 14.07 6H17V4h-7V2H8v2H1v2h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z";
  const GEAR_PATH = "M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z";

  function escapeHtml(s) {
    return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  }

  function buildTranslationSource(root) {
    const placeholders = [];
    let text = "";
    const walk = (node) => {
      for (const child of node.childNodes) {
        if (child.nodeType === Node.TEXT_NODE) {
          text += child.textContent;
        } else if (child.nodeType === Node.ELEMENT_NODE) {
          const tag = child.tagName;
          if (tag === "A" || tag === "IMG") {
            const idx = placeholders.length;
            placeholders.push(child.outerHTML);
            text += `\uE000${idx}\uE001`;
          } else if (tag === "BR") {
            text += "\n";
          } else {
            walk(child);
          }
        }
      }
    };
    walk(root);
    return { text, placeholders };
  }

  function updateSourceLangText(langSpan, sourceLangCode, isVisible) {
    if (!langSpan) return;
    if (isVisible && sourceLangCode) {
      const langName = getLanguageName(sourceLangCode);
      if (langName) {
        langSpan.textContent = `翻译自 ${langName}`;
        langSpan.style.display = "inline";
      } else {
        langSpan.style.display = "none";
      }
    } else {
      langSpan.style.display = "none";
    }
  }

  async function doTranslate(tweetTextEl, btnSpan, langSpan, isManual = false) {
    if (tweetTextEl.dataset.gtTranslated === "true") {
      tweetTextEl.innerHTML = tweetTextEl.dataset.gtOriginalHtml;
      tweetTextEl.dataset.gtTranslated = "";
      if (btnSpan) btnSpan.textContent = "显示翻译";
      if (langSpan) updateSourceLangText(langSpan, "", false);

      if (isManual) {
        tweetTextEl.dataset.gtManualUntranslated = "true";
      }
      return;
    }

    if (tweetTextEl.dataset.gtTranslating === "true") return;
    tweetTextEl.dataset.gtTranslating = "true";

    if (isManual) {
      delete tweetTextEl.dataset.gtManualUntranslated;
    }

    const origBtnText = btnSpan?.textContent;
    if (btnSpan) btnSpan.textContent = "翻译中...";

    try {
      const { text: source, placeholders } = buildTranslationSource(tweetTextEl);
      const result = await translateText(source);

      const html = escapeHtml(result.text)
        .replace(/\n/g, "<br>")
        .replace(/\{\{\s*(\d+)\s*\}\}/g, (m, i) => placeholders[+i] ?? m)
        .replace(/\uE000\s*(\d+)\s*\uE001/g, (m, i) => placeholders[+i] ?? m)
        .replace(/\\uE000\s*(\d+)\s*\\uE001/gi, (m, i) => placeholders[+i] ?? m);

      tweetTextEl.dataset.gtOriginalHtml = tweetTextEl.innerHTML;
      tweetTextEl.dataset.gtTranslated = "true";
      tweetTextEl.dataset.gtSourceLang = result.sourceLang;
      tweetTextEl.innerHTML = html;

      if (btnSpan) btnSpan.textContent = "显示原文";
      if (langSpan) updateSourceLangText(langSpan, result.sourceLang, true);
    } catch (err) {
      console.error("翻译出错:", err);
      if (btnSpan) btnSpan.textContent = origBtnText || "显示翻译";
    } finally {
      tweetTextEl.dataset.gtTranslating = "";
    }
  }

  function revertGrokTranslation(article) {
    if (!article) return false;
    const spans = article.querySelectorAll("span");
    for (const span of spans) {
      if (span.closest('[data-gt-wrapper="true"]')) continue;
      const text = span.textContent.trim();
      if (text === "显示原文" || text === "Show original" || text === "元の言語で表示") {
        const btn = span.closest('[role="button"]') || span.closest("button") || span;
        if (btn && !btn.dataset.gtReverting) {
          btn.dataset.gtReverting = "true";
          btn.click();
          return true;
        }
      }
    }
    return false;
  }

  function findAndPatchButtons() {
    const enableGrok = GM_getValue("enableGrok", false);
    const allSpans = document.querySelectorAll("span");

    // 1. 处理官方/Grok "显示翻译" 按钮
    const targetTexts = ["Show translation", "Translate post", "Translate", "显示翻译", "翻译帖子", "翻译"];
    for (const span of allSpans) {
      if (span.closest('[data-gt-wrapper="true"]')) continue;

      const text = span.textContent.trim();
      if (targetTexts.includes(text)) {
        const btn = span.closest('[role="button"]') || span.closest("a") || span.parentElement;
        if (btn) {
          if (!enableGrok) {
            btn.dataset.googleTranslate = "patched";
            btn.style.setProperty("display", "none", "important");
            const tweet = getTweetText(btn);
            if (tweet && tweet.element) {
              delete tweet.element.dataset.gtFeedPatched;
            }
          } else {
            btn.style.removeProperty("display");
          }
        }
      }
    }

    // 2. 如果关闭了 Grok:彻底清除评价栏 与 [Grok图标 + 设置齿轮 ⚙] 这一整行
    if (!enableGrok) {
      // 屏蔽 "评价此翻译" 区域
      const rateTexts = ["评价此翻译", "Rate this translation", "Rate translation"];
      for (const span of allSpans) {
        const text = span.textContent.trim();
        if (rateTexts.some(t => text.startsWith(t))) {
          const container = span.closest('div[dir="ltr"]') || span.parentElement?.parentElement || span.parentElement;
          if (container) {
            container.style.setProperty("display", "none", "important");
          }
        }
      }

      // 精确锁定并隐藏 用户名 与 推文正文 之间的 Grok 属性行(包含 Grok 图标与齿轮)
      const tweetTexts = document.querySelectorAll('[data-testid="tweetText"]');
      for (const tEl of tweetTexts) {
        let prev = tEl.previousElementSibling;
        while (prev) {
          if (prev.dataset?.gtWrapper === "true") {
            prev = prev.previousElementSibling;
            continue;
          }
          if (prev.querySelector('[data-testid="User-Name"]') || prev.dataset?.testid === "User-Name") {
            break;
          }
          if (prev.querySelector('svg') || prev.querySelector('button') || prev.textContent.includes('翻译') || prev.textContent.includes('Translate') || prev.textContent.includes('原文')) {
            prev.style.setProperty("display", "none", "important");
          }
          prev = prev.previousElementSibling;
        }
      }
    }
  }

  function isOfficialTranslated(article) {
    if (!article) return false;
    const rawText = article.textContent;
    if (!rawText.includes("显示原文") && !rawText.includes("Show original") && !rawText.includes("元の言語で表示")) {
      return false;
    }
    const spans = article.querySelectorAll("span");
    for (const span of spans) {
      const text = span.textContent.trim();
      if (text === "显示原文" || text === "Show original" || text === "元の言語で表示") {
        if (!span.closest('[data-gt-wrapper="true"]')) {
          return true;
        }
      }
    }
    return false;
  }

  function injectFeedButtons() {
    const enableGrok = GM_getValue("enableGrok", false);
    const targetLang = getTargetLang();
    const tweetTexts = document.querySelectorAll('[data-testid="tweetText"]');

    for (const el of tweetTexts) {
      if (el.dataset.gtFeedPatched) continue;

      const article = el.closest("article");
      if (article && isOfficialTranslated(article)) {
        if (!enableGrok) {
          revertGrokTranslation(article);
        } else {
          el.dataset.gtFeedPatched = "true";
          continue;
        }
      }

      const lang = el.getAttribute("lang");
      if (!lang || lang === "und") continue;
      if (lang === targetLang) {
        el.dataset.gtFeedPatched = "true";
        continue;
      }

      let existingWrapper = el.previousElementSibling;
      if (existingWrapper && existingWrapper.dataset && existingWrapper.dataset.gtWrapper === "true") {
        el.dataset.gtFeedPatched = "true";
        continue;
      }

      const isTranslated = el.dataset.gtTranslated === "true";
      const sourceLang = el.dataset.gtSourceLang || "";

      const wrapper = document.createElement("div");
      wrapper.dataset.gtWrapper = "true";
      wrapper.style.cssText = "display:flex; align-items:center; margin-top:4px; margin-bottom:4px;";

      const btn = document.createElement("div");
      btn.dataset.googleTranslate = "patched";
      btn.style.cssText =
        "display:flex;align-items:center;gap:4px;cursor:pointer;padding:4px 0;padding-right:8px;" +
        "color:rgb(29,155,240);font-size:13px;width:fit-content;";
      btn.innerHTML =
        `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" ` +
        `width="1.1em" height="1.1em"><path d="${GT_PATH}"/></svg>` +
        `<span>${isTranslated ? "显示原文" : "显示翻译"}</span>`;

      const langSpan = document.createElement("span");
      langSpan.className = "gt-source-text";
      langSpan.style.cssText = `color:${getThemeColors().secondaryText}; font-size:13px; font-weight:normal; display:none; pointer-events:none;`;

      wrapper.appendChild(btn);
      wrapper.appendChild(langSpan);

      updateSourceLangText(langSpan, sourceLang, isTranslated);

      btn.addEventListener("click", (e) => {
        e.stopPropagation();
        e.preventDefault();
        const span = btn.querySelector("span");
        doTranslate(el, span, langSpan, true);
      });

      el.parentElement.insertBefore(wrapper, el);
      el.dataset.gtFeedPatched = "true";
    }
  }

  function getSpansForTweet(el) {
    let node = el.previousElementSibling;
    let limit = 3;
    while (node && limit > 0) {
      if (node.dataset && node.dataset.gtWrapper === "true") {
        return {
          btnSpan: node.querySelector("div > span"),
          langSpan: node.querySelector(".gt-source-text")
        };
      }
      node = node.previousElementSibling;
      limit--;
    }
    return { btnSpan: null, langSpan: null };
  }

  async function toggleAllTranslations(enable) {
    isAllTranslated = enable;
    GM_setValue("isAllTranslated", enable);
    updateFloatingButtonState();

    const targetLang = getTargetLang();
    const tweetTexts = document.querySelectorAll('[data-testid="tweetText"]');

    for (const el of tweetTexts) {
      if (!enable) {
        delete el.dataset.gtManualUntranslated;
      }

      const lang = el.getAttribute("lang");
      if (!lang || lang === targetLang || lang === "und") continue;

      const article = el.closest("article");
      if (article && isOfficialTranslated(article)) continue;

      const isTranslated = el.dataset.gtTranslated === "true";
      if (enable && !isTranslated) {
        const { btnSpan, langSpan } = getSpansForTweet(el);
        doTranslate(el, btnSpan, langSpan);
      } else if (!enable && isTranslated) {
        const { btnSpan, langSpan } = getSpansForTweet(el);
        doTranslate(el, btnSpan, langSpan);
      }
    }
  }

  function autoTranslateNewTweets() {
    const enableGrok = GM_getValue("enableGrok", false);
    const targetLang = getTargetLang();
    const tweetTexts = document.querySelectorAll('[data-testid="tweetText"]');
    for (const el of tweetTexts) {
      const lang = el.getAttribute("lang");
      if (!lang || lang === targetLang || lang === "und") continue;
      if (el.dataset.gtManualUntranslated === "true") continue;

      const article = el.closest("article");
      if (article && isOfficialTranslated(article)) {
        if (!enableGrok) {
          revertGrokTranslation(article);
        }
        continue;
      }

      if (el.dataset.gtTranslated !== "true" && el.dataset.gtTranslating !== "true") {
        const { btnSpan, langSpan } = getSpansForTweet(el);
        doTranslate(el, btnSpan, langSpan);
      }
    }
  }

  function checkUrlChange() {
    if (location.href !== lastUrl) {
      lastUrl = location.href;

      const elements = document.querySelectorAll('[data-testid="tweetText"]');
      for (const el of elements) {
        delete el.dataset.gtFeedPatched;
        delete el.dataset.gtTranslated;
        delete el.dataset.gtOriginalHtml;
        delete el.dataset.gtManualUntranslated;
        delete el.dataset.gtSourceLang;
      }
    }
  }

  function checkDrawerState() {
    const dmDrawer = document.querySelector('[data-testid="DMDrawerOuter"]') ||
                     document.querySelector('[data-testid="DMDrawer"]') ||
                     document.querySelector('[data-testid="SideSheet_Tab_chat"]');
    if (dmDrawer) {
      const rect = dmDrawer.getBoundingClientRect();
      if (rect.height > 100) return true;
    }

    const isGrokOpen = !!document.querySelector('[data-testid="SideSheet_Root"]') ||
                       !!document.querySelector('[data-testid="GrokSideSheet"]') ||
                       !!document.querySelector('[data-testid="GrokSideDrawer"]') ||
                       !!document.querySelector('div[role="dialog"] [aria-label*="Grok" i]') ||
                       !!document.querySelector('div[role="dialog"] [aria-label*="grok" i]');

    const isGrokPage = location.pathname === "/grok" || location.pathname.startsWith("/grok/");
    const isMessagePage = location.pathname.startsWith("/messages");

    if (isGrokOpen || isGrokPage || isMessagePage) return true;
    return false;
  }

  function findNativeFloatingButtons() {
    const buttons = [];
    const candidates = document.querySelectorAll('[role="button"], button, a, div[style*="cursor"]');
    const widthThreshold = window.innerWidth - 150;
    const heightThreshold = window.innerHeight - 250;

    for (const el of candidates) {
      if (el.id === "gt-global-toggle-btn" || el.id === "gt-settings-btn") continue;
      const rect = el.getBoundingClientRect();
      if (rect.width >= 35 && rect.width <= 65 && rect.height >= 35 && rect.height <= 65) {
        if (rect.right > widthThreshold && rect.bottom > heightThreshold) {
          const style = window.getComputedStyle(el);
          if (style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0") {
            const isDuplicate = buttons.some(b => b.el.contains(el) || el.contains(b.el));
            if (!isDuplicate) {
              buttons.push({ el, rect, top: rect.top });
            }
          }
        }
      }
    }
    buttons.sort((a, b) => b.top - a.top);
    return buttons;
  }

  function positionFloatingButton() {
    const btn = document.getElementById("gt-global-toggle-btn");
    const settingsBtn = document.getElementById("gt-settings-btn");
    if (!btn) return;

    const isDrawerOpen = checkDrawerState();
    if (isDrawerOpen) {
      btn.style.display = "none";
      if (settingsBtn) settingsBtn.style.display = "none";
      return;
    }

    const nativeButtons = findNativeFloatingButtons();

    if (nativeButtons.length > 0) {
      btn.style.display = "flex";
      if (settingsBtn) settingsBtn.style.display = "flex";
      const targetBtn = nativeButtons[nativeButtons.length - 1].el;
      const rect = targetBtn.getBoundingClientRect();
      const w = rect.width;
      const h = rect.height;
      btn.style.width = `${w}px`;
      btn.style.height = `${h}px`;
      btn.style.borderRadius = "16px";
      btn.style.borderWidth = "1px";
      btn.style.borderStyle = "solid";

      const bottomOffset = window.innerHeight - rect.top + 12;
      btn.style.bottom = `${bottomOffset}px`;

      const leftOffset = rect.left + rect.width / 2 - w / 2;
      btn.style.left = `${leftOffset}px`;
      btn.style.right = "auto";

      if (settingsBtn) {
        settingsBtn.style.bottom = `${bottomOffset + h - 10}px`;
        settingsBtn.style.left = `${leftOffset - 8}px`;
      }
    } else {
      btn.style.display = "flex";
      if (settingsBtn) settingsBtn.style.display = "flex";
      btn.style.width = "50px";
      btn.style.height = "50px";
      btn.style.borderRadius = "16px";
      btn.style.borderWidth = "1px";
      btn.style.borderStyle = "solid";
      btn.style.bottom = "20px";
      btn.style.right = "16px";
      btn.style.left = "auto";

      if (settingsBtn) {
        settingsBtn.style.bottom = "58px";
        settingsBtn.style.right = "54px";
        settingsBtn.style.left = "auto";
      }
    }
  }

  function updateFloatingButtonState() {
    const btn = document.getElementById("gt-global-toggle-btn");
    const settingsBtn = document.getElementById("gt-settings-btn");
    if (!btn) return;

    const colors = getThemeColors();
    const svg = btn.querySelector("svg");

    if (isAllTranslated) {
      btn.style.backgroundColor = colors.activeBg;
      btn.style.borderColor = colors.activeBg;
      if (svg) svg.style.color = colors.activeIcon;
    } else {
      btn.style.backgroundColor = colors.bg;
      btn.style.borderColor = colors.border;
      if (svg) svg.style.color = colors.icon;
    }

    if (settingsBtn) {
      settingsBtn.style.backgroundColor = colors.bg;
      settingsBtn.style.borderColor = colors.border;
      const sSvg = settingsBtn.querySelector("svg");
      if (sSvg) sSvg.style.color = colors.secondaryText;
    }
  }

  function injectFloatingButton() {
    if (document.getElementById("gt-global-toggle-btn")) {
      positionFloatingButton();
      updateFloatingButtonState();
      return;
    }

    const colors = getThemeColors();

    const btn = document.createElement("div");
    btn.id = "gt-global-toggle-btn";
    btn.title = "点击切换全局翻译\n右键点击打开设置";
    btn.style.cssText =
      "position:fixed;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:99999;" +
      "border:1px solid;border-radius:16px;" +
      "transition:background-color 0.2s, border-color 0.2s;box-shadow: " + colors.shadow + ";";

    btn.innerHTML =
      `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" ` +
      `width="24px" height="24px" style="transition: color 0.2s;"><path d="${GT_PATH}"/></svg>`;

    btn.addEventListener("mouseenter", () => {
      const currentColors = getThemeColors();
      if (isAllTranslated) {
        btn.style.backgroundColor = currentColors.activeHoverBg;
      } else {
        btn.style.backgroundColor = currentColors.hoverBg;
      }
    });

    btn.addEventListener("mouseleave", () => {
      const currentColors = getThemeColors();
      if (isAllTranslated) {
        btn.style.backgroundColor = currentColors.activeBg;
      } else {
        btn.style.backgroundColor = currentColors.bg;
      }
    });

    btn.addEventListener("click", (e) => {
      e.preventDefault();
      e.stopPropagation();
      toggleAllTranslations(!isAllTranslated);
    });

    btn.addEventListener("contextmenu", (e) => {
      e.preventDefault();
      e.stopPropagation();
      showSettingsModal();
    });

    const settingsBtn = document.createElement("div");
    settingsBtn.id = "gt-settings-btn";
    settingsBtn.title = "翻译源与 API 设置";
    settingsBtn.style.cssText =
      "position:fixed;width:24px;height:24px;display:flex;align-items:center;justify-content:center;" +
      "cursor:pointer;z-index:100000;border-radius:50%;border:1px solid;box-shadow:0 2px 6px rgba(0,0,0,0.2);" +
      "transition:transform 0.2s, background-color 0.2s;";

    settingsBtn.innerHTML =
      `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" ` +
      `width="14px" height="14px"><path d="${GEAR_PATH}"/></svg>`;

    settingsBtn.addEventListener("mouseenter", () => {
      settingsBtn.style.transform = "scale(1.15)";
      settingsBtn.style.backgroundColor = getThemeColors().hoverBg;
    });

    settingsBtn.addEventListener("mouseleave", () => {
      settingsBtn.style.transform = "scale(1)";
      settingsBtn.style.backgroundColor = getThemeColors().bg;
    });

    settingsBtn.addEventListener("click", (e) => {
      e.preventDefault();
      e.stopPropagation();
      showSettingsModal();
    });

    document.body.appendChild(btn);
    document.body.appendChild(settingsBtn);

    positionFloatingButton();
    updateFloatingButtonState();
    window.addEventListener("resize", triggerRecalibration);

    const themeObserver = new MutationObserver(() => {
      positionFloatingButton();
      updateFloatingButtonState();
    });
    themeObserver.observe(document.body, { attributes: true, attributeFilter: ["style", "class"] });
  }

  function triggerRecalibration() {
    const intervals = [50, 100, 200, 300, 500, 800];
    intervals.forEach(delay => {
      setTimeout(() => {
        positionFloatingButton();
        updateFloatingButtonState();
      }, delay);
    });
  }

  document.addEventListener("click", triggerRecalibration, true);

  function patchAll() {
    checkUrlChange();
    findAndPatchButtons();
    injectFeedButtons();
    injectFloatingButton();

    if (isAllTranslated) {
      autoTranslateNewTweets();
    }
  }

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

  patchAll();
})();