Linux Do Translate

对回复进行翻译

// ==UserScript==
// @name         Linux Do Translate
// @namespace    linux-do-translate
// @version      0.2.4
// @author       delph1s
// @license      MIT
// @description  对回复进行翻译
// @match        https://linux.do/t/topic/*
// @connect      *
// @icon         https://cdn.linux.do/uploads/default/original/3X/9/d/9dd49731091ce8656e94433a26a3ef36062b3994.png
// @grant        unsafeWindow
// @grant        window.close
// @grant        window.focus
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// ==/UserScript==

(function () {
  'use strict';
  const REQUIRED_CHARS = 6;
  const SPACE_PRESS_COUNT = 3; // 连按次数
  const SPACE_PRESS_TIMEOUT = 1500; // 连续按键的最大时间间隔(毫秒)
  const TRANSLATE_PROVIDERS = [
    {
      text: 'LinuxDo Deeplx',
      value: 'deeplx-linuxdo',
    },
    {
      text: 'Deeplx',
      value: 'deeplx',
    },
    {
      text: 'Deepl',
      value: 'deepl',
    },
    {
      text: 'OpenAI',
      value: 'oai',
    },
    {
      text: 'OpenAI Proxy',
      value: 'oai-proxy',
    },
  ];
  const NOT_CUSTOM_URL_PROVIDERS = ['oai', 'deepl', 'deeplx-linuxdo'];
  const TRANSLATE_TARGET_LANG = {
    EN: { oai: 'English', deepl: 'EN' },
    ZH: { oai: 'Chinese', deepl: 'ZH' },
  };
  const TRANSLATE_TARGET_LANG_OPTIONS = [
    {
      text: 'English(英文)',
      value: 'EN',
    },
    {
      text: '中文(Chinese)',
      value: 'ZH',
    },
  ];
  const DEFAULT_CONFIG = {
    maxRetryTimes: 5,
    customUrl: '',
    authKey: '',
    enableTranslate: false,
    translateSourceLang: 'ZH',
    translateTargetLang: 'EN',
    translateProvider: 'deeplx-linuxdo',
    translateModel: 'gpt-4o',
    translateLayout: 'top',
    translateSize: 80,
    translateItalics: true,
    translateBold: false,
    translateReference: false,
    closeConfigAfterSave: true,
  };

  const uiIDs = {
    replyControl: 'reply-control',
    configButton: 'multi-lang-say-config-button',
    configPanel: 'multi-lang-say-config-panel',
    customUrlInput: 'custom-url-input',
    authKeyInput: 'auth-key-input',
    enableTranslateSwitch: 'enable-translate-switch',
    translateSourceLangSelect: 'translate-source-lang-select',
    translateTargetLangSelect: 'translate-target-lang-select',
    translateProviderSelect: 'translate-provider-select',
    translateModelInput: 'translate-model-input',
    translateLayoutSelect: 'translate-layout-select',
    translateSizeInput: 'translate-size-input',
    translateItalicsSwitch: 'translate-italics-switch',
    translateBoldSwitch: 'translate-bold-switch',
    translateReferenceSwitch: 'translate-reference-switch',
    closeConfigAfterSaveSwitch: 'close-after-save-switch',
  };

  let config = {
    maxRetryTimes: GM_getValue('maxRetryTimes', DEFAULT_CONFIG.maxRetryTimes),
    customUrl: GM_getValue('customUrl', DEFAULT_CONFIG.customUrl),
    authKey: GM_getValue('authKey', DEFAULT_CONFIG.authKey),
    enableTranslate: GM_getValue('enableTranslate', DEFAULT_CONFIG.enableTranslate),
    translateSourceLang: GM_getValue('translateSourceLang', DEFAULT_CONFIG.translateSourceLang),
    translateTargetLang: GM_getValue('translateTargetLang', DEFAULT_CONFIG.translateTargetLang),
    translateProvider: GM_getValue('translateProvider', DEFAULT_CONFIG.translateProvider),
    translateModel: GM_getValue('translateModel', DEFAULT_CONFIG.translateModel),
    translateLayout: GM_getValue('translateLayout', DEFAULT_CONFIG.translateLayout),
    translateSize: GM_getValue('translateSize', DEFAULT_CONFIG.translateSize),
    translateItalics: GM_getValue('translateItalics', DEFAULT_CONFIG.translateItalics),
    translateBold: GM_getValue('translateBold', DEFAULT_CONFIG.translateBold),
    translateReference: GM_getValue('translateReference', DEFAULT_CONFIG.translateReference),
    closeConfigAfterSave: GM_getValue('closeConfigAfterSave', DEFAULT_CONFIG.closeConfigAfterSave),
  };

  const genFormatDateTime = d => {
    return d.toLocaleString('zh-CN', {
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit',
      hour12: false,
    });
  };

  const genFormatNow = () => {
    return genFormatDateTime(new Date());
  };

  /**
   * 获取随机整数
   *
   * @param {number} start 范围开始
   * @param {number} end 范围结束
   * @returns
   */
  const randInt = (start, end) => {
    return Math.floor(Math.random() * (end - start + 1)) + start;
  };

  /**
   * 随机睡眠(毫秒)
   *
   * @param {number} start 范围开始
   * @param {number} end 范围结束
   */
  const randSleep = async (start = 2000, end = 3000) => {
    // 生成随机整数 randSleepTime,范围在 start 到 end 之间
    const randSleepTime = getRandomInt(start, end);
    // 睡眠时间
    return await new Promise(resolve => setTimeout(resolve, randSleepTime));
  };

  /**
   * 是否相同
   *
   * @param a
   * @param b
   * @returns
   */
  const isEqual = (a, b) => {
    if (a === null || a === undefined || b === null || b === undefined) {
      return a === b;
    }

    if (typeof a !== typeof b) {
      return false;
    }

    if (typeof a === 'string' || typeof a === 'number' || typeof a === 'boolean') {
      return a === b;
    }

    if (Array.isArray(a) && Array.isArray(b)) {
      if (a.length !== b.length) {
        return false;
      }

      return a.every((item, index) => isEqual(item, b[index]));
    }

    if (typeof a === 'object' && typeof b === 'object') {
      const keysA = Object.keys(a || {});
      const keysB = Object.keys(b || {});

      if (keysA.length !== keysB.length) {
        return false;
      }

      return keysA.every(key => isEqual(a[key], b[key]));
    }

    return false;
  };

  /**
   * 判断字符串中是否包含中文字符
   * @param {string} text
   * @returns {boolean}
   */
  const containsChinese = text => {
    return /[\u4e00-\u9fa5]/.test(text);
  };

  const getInvertColor = hex => {
    // 去掉前面的“#”字符
    hex = hex.replace('#', '');

    // 如果输入的是3位的hex值,转换为6位的
    if (hex.length === 3) {
      hex = hex
        .split('')
        .map(c => c + c)
        .join('');
    }

    // 计算相反的颜色
    const r = (255 - parseInt(hex.slice(0, 2), 16)).toString(16).padStart(2, '0');
    const g = (255 - parseInt(hex.slice(2, 4), 16)).toString(16).padStart(2, '0');
    const b = (255 - parseInt(hex.slice(4, 6), 16)).toString(16).padStart(2, '0');

    return `#${r}${g}${b}`;
  };

  const deeplxReq = text => {
    return {
      url: config.authKey ? `${config.customUrl}?token=${config.authKey}` : config.customUrl,
      headers: {
        'Content-Type': 'application/json',
      },
      data: JSON.stringify({
        text: text,
        target_lang: TRANSLATE_TARGET_LANG[config.translateTargetLang].deepl,
        source_lang: 'auto',
      }),
      responseType: 'json',
    };
  };

  const deeplxLinuxdoReq = text => {
    return {
      url: `https://api.deeplx.org/${config.authKey}/translate`,
      headers: {
        'Content-Type': 'application/json',
      },
      data: JSON.stringify({
        text: text,
        target_lang: TRANSLATE_TARGET_LANG[config.translateTargetLang].deepl,
        source_lang: 'auto',
      }),
      responseType: 'json',
    };
  };

  const deeplReq = text => {
    const authKey = config.authKey;
    const params = new URLSearchParams();
    params.append('text', text);
    params.append('target_lang', TRANSLATE_TARGET_LANG[config.translateTargetLang].deepl);
    params.append('source_lang', TRANSLATE_TARGET_LANG[config.translateSourceLang].deepl);
    return {
      url: 'https://api.deepl.com/v2/translate', // DeepL Pro API
      headers: {
        Authorization: `DeepL-Auth-Key ${authKey}`,
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      data: params.toString(),
      responseType: 'json',
    };
  };

  const deeplRes = res => {
    return res?.translations?.[0]?.text;
  };

  const oaiReq = (
    text,
    model = 'gpt-3.5-turbo',
    url = 'https://api.openai.com/v1/chat/completions',
    temperature = 0.5,
    maxTokens = 32000
  ) => {
    const authKey = config.authKey;
    return {
      url: url,
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${authKey}`,
      },
      data: JSON.stringify({
        model: model, // 或者您订阅的其他模型,例如 'gpt-4'
        messages: [
          {
            role: 'system',
            content:
              'You are a highly skilled translation engine. Your function is to translate texts accurately into the target {{to}}, maintaining the original format, technical terms, and abbreviations. Do not add any explanations or annotations to the translated text.',
          },
          {
            role: 'user',
            content: `Translate the following source text to ${
              TRANSLATE_TARGET_LANG[config.translateTargetLang].oai
            }, Output translation directly without any additional text.\nSource Text: ${text}\nTranslated Text:`,
          },
        ],
        temperature: temperature, // 控制生成内容的随机性,范围是 0 到 1
        max_tokens: maxTokens, // 响应的最大标记数
      }),
      responseType: 'json',
    };
  };

  const oaiRes = res => {
    return res.choices[0].message.content.trim();
  };

  const translateText = text => {
    const isDeepl = config.translateProvider === 'deepl';
    const isOAI = config.translateProvider === 'oai' || config.translateProvider === 'oai-proxy';

    let reqData;

    if (!config.authKey) {
      if (!config.customUrl) return '';
      if (config.translateProvider === 'deeplx') {
        reqData = deeplxReq(text);
      } else {
        return '';
      }
    } else if (isDeepl) {
      reqData = deeplReq(text);
    } else if (isOAI) {
      reqData = oaiReq(
        text,
        config.translateModel,
        NOT_CUSTOM_URL_PROVIDERS.includes(config.translateProvider)
          ? 'https://api.openai.com/v1/chat/completions'
          : config.customUrl,
        0.5,
        1600
      );
    } else {
      reqData = deeplxLinuxdoReq(text);
    }

    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: 'POST',
        url: reqData.url,
        headers: reqData.headers,
        data: reqData.data,
        responseType: reqData.responseType,
        onload: function (res) {
          console.log('Translation response:', res);
          console.log('Request details:', reqData);

          if (res.status === 200) {
            try {
              const response = typeof res.response === 'string' ? JSON.parse(res.response) : res.response;
              console.log('Parsed response:', response);

              let translation;

              if (isDeepl) {
                // Pro API 返回格式
                translation = deeplRes(response);
                console.log('DeepL translation:', translation);
              } else if (isOAI) {
                translation = oaiRes(response);
                console.log('OAI translation:', translation);
              } else {
                translation = response?.data;
                console.log('DeepLX translation:', translation);
              }

              resolve(translation || '');
            } catch (error) {
              console.error('Error parsing response:', error);
              resolve('');
            }
          } else {
            console.error('Translation failed:', {
              status: res.status,
              statusText: res.statusText,
              response: res.response,
              responseText: res.responseText,
              finalUrl: res.finalUrl,
              headers: res.responseHeaders,
            });
            resolve('');
          }
        },
        onerror: function (err) {
          console.error('Translation error details:', {
            error: err,
            errorText: err.toString(),
            status: err.status,
            statusText: err.statusText,
            responseText: err.responseText,
          });
          resolve('');
        },
      });
    });
  };

  const processTranslateText = async text => {
    // 定义需要保护的块的正则表达式
    const protectedBlocks = [
      // Markdown 代码块
      {
        regex: /```[\s\S]*?```/g,
        type: 'code',
      },
      // BBCode 标签块 (处理嵌套标签)
      {
        regex: /\[(size|spoiler|center|color|grid).*?\][\s\S]*?\[\/\1\]/g,
        type: 'bbcode',
      },
      // 已有的 ruby 标签
      {
        regex: /<ruby>[\s\S]*?<\/ruby>/g,
        type: 'ruby',
      },
      // HTML 标签块
      {
        regex: /<[^>]+>[\s\S]*?<\/[^>]+>/g,
        type: 'html',
      },
      // 图片标签
      {
        regex: /!\[image\]\(.*?\)/g,
        type: 'image',
      },
    ];

    // 创建占位符映射
    let placeholders = new Map();
    let placeholderCounter = 0;

    // 保护特殊块
    let processedText = text;
    for (const block of protectedBlocks) {
      processedText = processedText.replace(block.regex, match => {
        const placeholder = `__PROTECTED_${block.type}_${placeholderCounter++}__`;
        placeholders.set(placeholder, match);
        return placeholder;
      });
    }

    // 处理剩余文本
    const segments = processedText.split(/(\n)/);
    let translatedSegments = [];

    for (const segment of segments) {
      if (!segment.trim() || segment === '\n') {
        translatedSegments.push(segment);
        continue;
      }

      // 检查是否是占位符
      if (segment.startsWith('__PROTECTED_')) {
        translatedSegments.push(placeholders.get(segment));
        continue;
      }

      // 翻译普通文本
      let segmentTranslate = await translateText(segment);
      if (segmentTranslate === '') {
        return segmentTranslate;
      }

      if (config.translateItalics) {
        segmentTranslate = `[i]${segmentTranslate}[/i]`;
      }

      if (config.translateBold) {
        segmentTranslate = `[b]${segmentTranslate}[/b]`;
      }

      if (config.translateReference) {
        segmentTranslate = `> [size=${config.translateSize}]${segmentTranslate}[/size]`;
      } else {
        segmentTranslate = `[size=${config.translateSize}]${segmentTranslate}[/size]`;
      }

      if (config.translateLayout === 'bottom') {
        translatedSegments.push(`${segment}\n${config.translateReference ? "\n" : ""}${segmentTranslate}`);
      } else if (config.translateLayout === 'top') {
        translatedSegments.push(
          `${segmentTranslate}\n${config.translateReference ? "\n" : ""}${segment}`
        );
      }
    }

    // 合并结果
    return translatedSegments.join('');
  };

  const processTextArea = () => {
    let textarea = document.querySelector(`#${uiIDs.replyControl} textarea`);
    let text = textarea.value.trim();
    let originalLength = text.length;

    if (text.length !== 0 && originalLength >= REQUIRED_CHARS) {
      // 检查是否已存在拼音
      // const rubyRegex = /(<ruby>[\s\S]*?<\/ruby>)/g;

      // 为中文加入翻译
      if (config.enableTranslate) {
        textarea.value = '开始翻译...';

        processTranslateText(text).then(res => {
          textarea.value = res;

          // 创建并触发 input 事件
          const inputEvent = new Event('input', {
            bubbles: true,
            cancelable: true,
          });
          // 触发事件
          textarea.dispatchEvent(inputEvent);
        });
        return;
      }

      textarea.value = text;

      // 创建并触发 input 事件
      const inputEvent = new Event('input', {
        bubbles: true,
        cancelable: true,
      });
      // 触发事件
      textarea.dispatchEvent(inputEvent);
    }
  };

  const handleClick = event => {
    // 修复翻译两次的 BUG
    if (config.enableTranslate) {
      return;
    }

    if (event.target && event.target.closest('button.create')) {
      processTextArea();
    }
  };

  let spacePresses = 0;
  let lastKeyTime = 0;
  let timeoutHandle = null;

  const handleKeydown = event => {
    // console.log(`KeyboardEvent: key='${event.key}' | code='${event.code}'`);

    if (event.ctrlKey && event.key === 'Enter') {
      processTextArea();
      return;
    }

    // 使用 Alt+D 触发翻译
    if (event.altKey && event.keyCode === 68) {
      event.preventDefault(); // 阻止默认行为
      processTextArea();
      return;
    }

    const currentTime = Date.now();
    if (event.code === 'Space') {
      // 如果时间间隔太长,重置计数
      if (currentTime - lastKeyTime > SPACE_PRESS_TIMEOUT) {
        spacePresses = 1;
      } else {
        spacePresses += 1;
      }

      lastKeyTime = currentTime;

      // 清除之前的定时器
      if (timeoutHandle) {
        clearTimeout(timeoutHandle);
      }

      // 设置新的定时器,如果在 SPACE_PRESS_TIMEOUT 毫秒内没有新的按键,则重置计数
      timeoutHandle = setTimeout(() => {
        spacePresses = 0;
      }, SPACE_PRESS_TIMEOUT);

      // 检查是否达到了按键次数
      if (spacePresses === SPACE_PRESS_COUNT) {
        spacePresses = 0; // 重置计数

        // 执行翻译操作
        processTextArea();
      }
    } else {
      // 如果按下了其他键,重置计数
      spacePresses = 0;
      if (timeoutHandle) {
        clearTimeout(timeoutHandle);
        timeoutHandle = null;
      }
    }
  };

  const saveConfig = () => {
    const customUrlInput = document.getElementById(uiIDs.customUrlInput);
    config.customUrl = customUrlInput.value.trim();
    const authKeyInput = document.getElementById(uiIDs.authKeyInput);
    config.authKey = authKeyInput.value.trim();
    const translateModelInput = document.getElementById(uiIDs.translateModelInput);
    config.translateModel = translateModelInput.value;
    const transalteSizeInput = document.getElementById(uiIDs.translateSizeInput);
    config.translateSize = transalteSizeInput.value;
    console.log(config);

    GM_setValue('customUrl', config.customUrl);
    GM_setValue('authKey', config.authKey);
    GM_setValue('enableTranslate', config.enableTranslate);
    GM_setValue('translateModel', config.translateModel);
    GM_setValue('translateSize', config.translateSize);
    GM_setValue('translateItalics', config.translateItalics);
    GM_setValue('translateBold', config.translateBold);
    GM_setValue('translateReference', config.translateReference);
    GM_setValue('closeConfigAfterSave', config.closeConfigAfterSave);

    if (config.closeConfigAfterSave) {
      const panel = document.getElementById(uiIDs.configPanel);
      toggleConfigPanelAnimation(panel);
    }
  };

  const restoreDefaults = () => {
    if (confirm('确定要将所有设置恢复为默认值吗?')) {
      config = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
      GM_setValue('maxRetryTimes', config.maxRetryTimes);
      GM_setValue('customUrl', config.customUrl);
      GM_setValue('authKey', config.authKey);
      GM_setValue('enableTranslate', config.enableTranslate);
      GM_setValue('translateSourceLang', config.translateSourceLang);
      GM_setValue('translateTargetLang', config.translateTargetLang);
      GM_setValue('translateModel', config.translateModel);
      GM_setValue('translateLayout', config.translateLayout);
      GM_setValue('translateSize', config.translateSize);
      GM_setValue('translateItalics', config.translateItalics);
      GM_setValue('translateBold', config.translateBold);
      GM_setValue('translateReference', config.translateReference);
      GM_setValue('closeConfigAfterSave', config.closeConfigAfterSave);

      const panel = document.getElementById(uiIDs.configPanel);
      if (panel) {
        updateConfigPanelContent(panel);
      }
    }
  };

  const createFormGroup = (labelText, element) => {
    const group = document.createElement('div');
    group.className = 'form-group';

    const label = document.createElement('label');
    label.className = 'form-label';
    label.textContent = labelText;

    group.appendChild(label);
    group.appendChild(element);

    return group;
  };

  const createSelect = (eleId, configId, options, defaultValue, onChange = undefined) => {
    const select = document.createElement('select');
    select.className = 'modern-select';
    select.id = eleId;

    options.forEach(option => {
      const optionElement = document.createElement('option');
      optionElement.value = option.value;
      optionElement.textContent = option.text;
      select.appendChild(optionElement);
    });

    select.value = defaultValue;

    if (onChange !== undefined) {
      select.addEventListener('change', e => onChange(e));
    } else {
      select.addEventListener('change', e => {
        config[configId] = e.target.value;
        console.log(`[存储配置] ${configId}: ${config[configId]}`);
        GM_setValue(configId, config[configId]);
      });
    }

    return select;
  };

  const createInput = (eleId, value, type = 'text', placeholder = '') => {
    const input = document.createElement('input');
    input.className = 'modern-input';
    input.id = eleId;
    input.type = type;
    input.value = value;
    input.placeholder = placeholder;
    return input;
  };

  const createSwitch = (eleId, configId, checked, labelText) => {
    const container = document.createElement('div');
    container.className = 'switch-container';

    const label = document.createElement('span');
    label.className = 'form-label';
    label.style.margin = '0';
    label.textContent = labelText;

    const switchEl = document.createElement('div');
    switchEl.id = eleId;
    switchEl.className = `modern-switch${checked ? ' active' : ''}`;
    switchEl.addEventListener('click', () => {
      switchEl.classList.toggle('active');
      config[configId] = switchEl.classList.contains('active');
      console.log(`[存储配置] ${configId}: ${config[configId]}`);
      GM_setValue(configId, config[configId]);
    });

    container.appendChild(label);
    container.appendChild(switchEl);
    return container;
  };

  const createButton = (text, onClick, variant = 'secondary') => {
    const button = document.createElement('button');
    button.className = `modern-button ${variant}`;
    button.textContent = text;
    button.addEventListener('click', onClick);
    return button;
  };

  // const createTextArea = (id, value, labelText, placeholder) => {
  //   const container = document.createElement('div');
  //   container.style.marginBottom = '15px';

  //   const label = document.createElement('label');
  //   label.textContent = labelText;
  //   label.style.display = 'block';
  //   label.style.marginBottom = '5px';
  //   container.appendChild(label);

  //   const textarea = document.createElement('textarea');
  //   textarea.id = id;
  //   if (typeof value === 'string') {
  //     textarea.value = value;
  //   } else {
  //     textarea.value = JSON.stringify(value, null, 2);
  //   }
  //   textarea.placeholder = placeholder;
  //   textarea.rows = 5;
  //   textarea.style.width = '100%';
  //   textarea.style.padding = '5px';
  //   textarea.style.border = '1px solid var(--panel-border)';
  //   textarea.style.borderRadius = '4px';
  //   textarea.style.backgroundColor = 'var(--panel-bg)';
  //   textarea.style.color = 'var(--panel-text)';
  //   container.appendChild(textarea);

  //   return [container, textarea];
  // };

  const updateConfigPanelContent = (panel, panelContent) => {
    panelContent.innerHTML = '';

    // 添加表单元素
    const translateProviderSelect = createSelect(
      uiIDs.translateProviderSelect,
      'translateProvider',
      TRANSLATE_PROVIDERS,
      config.translateProvider,
      e => {
        config.translateProvider = e.target.value;

        const notCustomUrl = NOT_CUSTOM_URL_PROVIDERS.includes(config.translateProvider);
        const urlInput = document.getElementById(uiIDs.customUrlInput);
        if (notCustomUrl) {
          if (urlInput) {
            urlInput.disabled = true;
          }
        } else {
          if (urlInput) {
            urlInput.disabled = false;
          }
        }
        console.log(`[存储配置] translateProvider: ${config.translateProvider}`);

        GM_setValue('translateProvider', config.translateProvider);
      }
    );
    panelContent.appendChild(createFormGroup('翻译服务商(Provider)', translateProviderSelect));

    const customUrlInput = createInput(uiIDs.customUrlInput, config.customUrl, 'text', '填写自定义请求地址');
    const notCustomUrl = NOT_CUSTOM_URL_PROVIDERS.includes(config.translateProvider);
    if (notCustomUrl) {
      customUrlInput.disabled = true;
    }
    panelContent.appendChild(createFormGroup('自定义链接(Custom URL)', customUrlInput));

    const authKeyInput = createInput(uiIDs.authKeyInput, config.authKey, 'password', '输入认证密钥');
    panelContent.appendChild(createFormGroup('认证密钥(Auth Key)', authKeyInput));

    const modelInput = createInput(uiIDs.translateModelInput, config.translateModel, 'text', '输入翻译模型');
    panelContent.appendChild(createFormGroup('翻译模型(AI Model)', modelInput));

    const targetSourceSelect = createSelect(
      uiIDs.translateSourceLangSelect,
      'translateSourceLang',
      TRANSLATE_TARGET_LANG_OPTIONS,
      config.translateSourceLang
    );
    const targetLangSelect = createSelect(
      uiIDs.translateTargetLangSelect,
      'translateTargetLang',
      TRANSLATE_TARGET_LANG_OPTIONS,
      config.translateTargetLang
    );
    panelContent.appendChild(createFormGroup('源语言(Source Language)', targetSourceSelect));
    panelContent.appendChild(createFormGroup('目标语言(Target Language)', targetLangSelect));

    const sizeInput = createInput(
      uiIDs.translateSizeInput,
      config.translateSize,
      'number',
      '默认值为150(字体大小为原始的150%)'
    );
    panelContent.appendChild(createFormGroup('翻译字体大小(百分比)', sizeInput));

    const layoutSelect = createSelect(
      uiIDs.translateLayoutSelect,
      'translateLayout',
      [
        { text: '翻译在上(Translation On Top)', value: 'top' },
        { text: '翻译在下(Translation On Bottom)', value: 'bottom' },
      ],
      config.translateLayout
    );
    panelContent.appendChild(createFormGroup('翻译布局(Layout)', layoutSelect));
    TRANSLATE_TARGET_LANG_OPTIONS;
    // 添加开关
    panelContent.appendChild(
      createSwitch(uiIDs.enableTranslateSwitch, 'enableTranslate', config.enableTranslate, '启用翻译(Enable Translate)')
    );
    panelContent.appendChild(
      createSwitch(uiIDs.translateItalicsSwitch, 'translateItalics', config.translateItalics, '启用斜体(Enable Italic)')
    );
    panelContent.appendChild(
      createSwitch(uiIDs.translateBoldSwitch, 'translateBold', config.translateBold, '启用粗体(Enable Bold)')
    );
    panelContent.appendChild(
      createSwitch(uiIDs.translateReferenceSwitch, 'translateReference', config.translateReference, '转为引用(Convert to Quote)')
    );

    panelContent.appendChild(
      createSwitch(
        uiIDs.closeConfigAfterSaveSwitch,
        'closeConfigAfterSave',
        config.closeConfigAfterSave,
        '保存后自动关闭(Close Panel After Save)'
      )
    );

    // 创建按钮组
    const buttonGroup = document.createElement('div');
    buttonGroup.className = 'button-group';

    buttonGroup.appendChild(createButton('恢复默认', restoreDefaults));
    buttonGroup.appendChild(createButton('保存设置', saveConfig, 'primary'));
    buttonGroup.appendChild(
      createButton(
        '翻译(Translate)',
        processTextArea, // Call translate function directly
        'primary'
      )
    );
    buttonGroup.appendChild(
      createButton(
        '关闭',
        () => {
          toggleConfigPanelAnimation(panel);
        },
        'ghost'
      )
    );

    panelContent.appendChild(buttonGroup);
  };

  const createConfigPanel = () => {
    // 获取页面的 <meta name="theme-color"> 标签
    const themeColorMeta = document.querySelector('meta[name="theme-color"]');
    let themeColor = '#DDDDDD'; // 默认白色背景
    let invertedColor = '#222222'; // 默认黑色字体

    if (themeColorMeta) {
      themeColor = themeColorMeta.getAttribute('content');
      invertedColor = getInvertColor(themeColor); // 计算相反颜色
    }

    // 设置样式变量
    const style = document.createElement('style');
    style.textContent = `
      :root {
        --panel-bg: ${themeColor};
        --panel-text: ${invertedColor};
        --panel-border: ${invertedColor};
        --button-bg: ${invertedColor};
        --button-text: ${themeColor};
        --button-hover-bg: ${getInvertColor(invertedColor)};
        --button-hover-text: ${getInvertColor(themeColor)};
      }

      .modern-panel {
        position: fixed;
        top: 80px;
        right: 20px;
        width: 360px;
        background: color-mix(in srgb, var(--panel-bg) 85%, transparent);
        backdrop-filter: blur(12px);
        -webkit-backdrop-filter: blur(12px);
        border-radius: 16px;
        box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
        z-index: 10000;
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
        border: 1px solid color-mix(in srgb, var(--panel-border) 30%, transparent);
        overflow: hidden;
        opacity: 0;
        transform: translateY(-10px);
        transition: all 0.3s ease;
        color: var(--panel-text);
      }

      .modern-panel.show {
        opacity: 1;
        transform: translateY(0);
      }

      .modern-panel-header {
        padding: 20px 24px;
        border-bottom: 1px solid color-mix(in srgb, var(--panel-border) 10%, transparent);
        display: flex;
        justify-content: space-between;
        align-items: center;
      }

      .modern-panel-title {
        font-size: 18px;
        font-weight: 600;
        color: var(--panel-text);
        margin: 0;
      }

      .modern-panel-content {
        padding: 24px;
        max-height: calc(80vh - 140px);
        overflow-y: auto;
      }

      .modern-panel-content::-webkit-scrollbar {
        width: 6px;
      }

      .modern-panel-content::-webkit-scrollbar-thumb {
        background: color-mix(in srgb, var(--panel-border) 20%, transparent);
        border-radius: 3px;
      }

      .form-group {
        margin-bottom: 20px;
      }

      .form-label {
        display: block;
        font-size: 14px;
        font-weight: 500;
        color: var(--panel-text);
        margin-bottom: 8px;
      }

      .modern-select {
        width: 100%;
        padding: 10px 12px;
        border: 1px solid color-mix(in srgb, var(--panel-border) 20%, transparent);
        border-radius: 8px;
        background: color-mix(in srgb, var(--panel-bg) 90%, transparent);
        color: var(--panel-text);
        font-size: 14px;
        transition: all 0.2s ease;
        appearance: none;
        background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%23999' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
        background-repeat: no-repeat;
        background-position: right 12px center;
        cursor: pointer;
      }

      .modern-select:hover {
        border-color: color-mix(in srgb, var(--panel-border) 40%, transparent);
      }

      .modern-select:focus {
        outline: none;
        border-color: var(--button-bg);
        box-shadow: 0 0 0 3px color-mix(in srgb, var(--button-bg) 10%, transparent);
      }

      .modern-input {
        width: 100%;
        padding: 10px 12px;
        border: 1px solid color-mix(in srgb, var(--panel-border) 20%, transparent);
        border-radius: 8px;
        background: color-mix(in srgb, var(--panel-bg) 90%, transparent);
        color: var(--panel-text);
        font-size: 14px;
        transition: all 0.2s ease;
      }

      .modern-input:hover {
        border-color: color-mix(in srgb, var(--panel-border) 40%, transparent);
      }

      .modern-input:focus {
        outline: none;
        border-color: var(--button-bg);
        box-shadow: 0 0 0 3px color-mix(in srgb, var(--button-bg) 10%, transparent);
      }

      .switch-container {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 16px;
      }

      .modern-switch {
        position: relative;
        width: 44px;
        height: 24px;
        background: color-mix(in srgb, var(--panel-border) 20%, transparent);
        border-radius: 12px;
        padding: 2px;
        transition: background 0.3s ease;
        cursor: pointer;
      }

      .modern-switch.active {
        background: var(--button-bg);
      }

      .modern-switch::after {
        content: '';
        position: absolute;
        width: 20px;
        height: 20px;
        border-radius: 10px;
        background: var(--panel-bg);
        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
        transition: transform 0.3s ease;
        transform: translateY(1px) translateX(2px);
      }

      .modern-switch.active::after {
        transform: translateY(1px) translateX(22px);
      }

      .button-group {
        display: flex;
        gap: 12px;
        margin-top: 24px;
        padding-top: 20px;
        border-top: 1px solid color-mix(in srgb, var(--panel-border) 10%, transparent);
      }

      .modern-button {
        flex: 1;
        padding: 2px 8px;
        border-radius: 8px;
        font-size: 14px;
        font-weight: 500;
        cursor: pointer;
        transition: all 0.2s ease;
        border: none;
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 6px;
      }

      .modern-button.primary {
        background: var(--button-bg);
        color: var(--button-text);
      }

      .modern-button.primary:hover {
        background: var(--button-hover-bg);
        color: var(--button-hover-text);
      }

      .modern-button.secondary {
        background: color-mix(in srgb, var(--panel-border) 10%, transparent);
        color: var(--panel-text);
      }

      .modern-button.secondary:hover {
        background: color-mix(in srgb, var(--panel-border) 20%, transparent);
      }

      .modern-button.ghost {
        background: transparent;
        color: var(--panel-text);
      }

      .modern-button.ghost:hover {
        background: color-mix(in srgb, var(--panel-border) 10%, transparent);
      }
    }`;
    document.head.appendChild(style);

    const panel = document.createElement('div');
    panel.id = uiIDs.configPanel;
    panel.className = 'modern-panel';
    panel.style.display = 'none';

    // 创建头部
    const header = document.createElement('div');
    header.className = 'modern-panel-header';

    const title = document.createElement('h3');
    title.className = 'modern-panel-title';
    title.textContent = '设置';
    header.appendChild(title);

    // 创建内容区域
    const content = document.createElement('div');
    content.className = 'modern-panel-content';

    console.log();
    updateConfigPanelContent(panel, content);

    panel.appendChild(header);
    panel.appendChild(content);
    document.body.appendChild(panel);

    return panel;
  };

  const toggleConfigPanelAnimation = panel => {
    if (panel.style.display === 'none') {
      panel.classList.add('show');
      setTimeout(() => {
        panel.style.display = 'block';
      }, 10);
    } else {
      panel.classList.remove('show');
      setTimeout(() => {
        panel.style.display = 'none';
      }, 300);
    }
  };

  const toggleConfigPanel = () => {
    let panel = document.getElementById(uiIDs.configPanel);
    panel = panel || createConfigPanel();
    toggleConfigPanelAnimation(panel);
  };

  const createConfigButton = () => {
    const toolbar = document.querySelector('.d-editor-button-bar');
    if (!toolbar || document.getElementById(uiIDs.configButton)) return;

    const configButton = document.createElement('button');
    configButton.id = uiIDs.configButton;
    configButton.className = 'btn btn-flat btn-icon no-text user-menu-tab active';
    configButton.title = '配置';
    configButton.innerHTML =
      '<svg class="fa d-icon d-icon-discourse-other-tab svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#discourse-other-tab"></use></svg>';
    configButton.onclick = toggleConfigPanel;

    toolbar.appendChild(configButton);
  };

  const watchReplyControl = () => {
    const replyControl = document.getElementById(uiIDs.replyControl);
    if (!replyControl) return;

    const observer = new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
          if (replyControl.classList.contains('closed')) {
            const panel = document.getElementById(uiIDs.configPanel);
            if (panel) {
              panel.style.display = 'none';
            }
          } else {
            // 当 reply-control 重新打开时,尝试添加配置按钮
            setTimeout(createConfigButton, 500); // 给予一些时间让编辑器完全加载
          }
        }
      });
    });

    observer.observe(replyControl, { attributes: true });
  };

  const watchForEditor = () => {
    const observer = new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        if (mutation.type === 'childList') {
          const addedNodes = mutation.addedNodes;
          for (let node of addedNodes) {
            if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('d-editor')) {
              createConfigButton();
              return;
            }
          }
        }
      });
    });

    observer.observe(document.body, { childList: true, subtree: true });
  };

  const init = () => {
    const container = document.getElementById(uiIDs.replyControl);
    container.addEventListener('click', handleClick, true);
    document.addEventListener('keydown', handleKeydown, true);
    if (!document.getElementById(uiIDs.configButton)) {
      createConfigButton();
    }
    watchReplyControl();
    watchForEditor();
  };

  // 初始化
  setTimeout(() => {
    init();
  }, 1000);
})();