IXL Auto Answer (Display by Default, AI Helper, IXL-styled)

Default to Display Answer Only; AI helper can configure script; new IXL-style layout; manage URL logic; rent API key link

目前為 2025-04-13 提交的版本,檢視 最新版本

// ==UserScript==
// @name         IXL Auto Answer (Display by Default, AI Helper, IXL-styled)
// @namespace    http://tampermonkey.net/
// @version      13.0
// @license      GPL-3.0
// @description  Default to Display Answer Only; AI helper can configure script; new IXL-style layout; manage URL logic; rent API key link
// @match        https://*.ixl.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    //----------------------------------------------------------------------
    // 1) 说明 & 配置
    //----------------------------------------------------------------------
    // 脚本概述:提供自动答题(Display/AutoFill)、多模型管理、AI助手对话等
    // 下面这个描述会在 askAiQuestion() 时作为 system 内容的一部分
    const scriptDescription = `
This script can:
1) Solve IXL questions in two modes:
   - Display-only: GPT returns plain text answers (Unicode only, no LaTeX/Markdown).
   - Auto-fill: GPT returns JavaScript code to fill in answers automatically (unstable).
2) Supports multiple models, each with own apiKey/apiBase.
3) Has an AI Helper to discuss or reconfigure the script. The function window.AI_setScriptConfig(...) can be used to apply config changes.
4) 'Get API Key' link points to:
   - openai: https://platform.openai.com/api-keys
   - deepseek: https://platform.deepseek.com/api_keys
   - else: '#'
5) There's also a 'Rent API Key' button to pop up your contact info.
You are an assistant that helps the user understand or configure the script.
Allowed config fields: selectedModel, fillMode, autoSubmitEnabled, language, etc.
Return JSON if you want the script to apply changes: e.g. { "fillMode": "autoFill" } 
    `;

    // modelConfigs:各模型对应的 key/baseURL。 discovered=true 表示是通过 /models 动态发现
    let modelConfigs = JSON.parse(localStorage.getItem("gpt4o-modelConfigs") || "{}");

    // 预置模型列表
    const predefinedModels = [
        "gpt-4o", "gpt-4o-mini", "o1", "o3-mini",
        "deepseek-reasoner", "deepseek-chat", "chatgpt-4o-least"
    ];

    // 如果没有 gpt-4o,就给它一个默认
    if (!modelConfigs["gpt-4o"]) {
        modelConfigs["gpt-4o"] = {
            apiKey: localStorage.getItem("gpt4o-api-key") || "",
            apiBase: localStorage.getItem("gpt4o-api-base") || "https://api.openai.com/v1/chat/completions",
            manageUrl: "",
            modelList: [],
            discovered: false
        };
    }

    // 全局配置:默认 fillMode = displayOnly
    const config = {
        selectedModel: "gpt-4o",
        language: localStorage.getItem("gpt4o-language") || "en",
        tokenUsage: 0,
        lastTargetState: null,
        retryCount: 0,
        maxRetry: 2,
        fillMode: "displayOnly",
        autoSubmitEnabled: false
    };

    function saveModelConfigs() {
        localStorage.setItem("gpt4o-modelConfigs", JSON.stringify(modelConfigs));
    }

    //----------------------------------------------------------------------
    // 2) 多语言文本
    //----------------------------------------------------------------------
    const langText = {
        en: {
            fillModeLabel: "Answer Mode",
            fillMode_auto: "Auto Fill (unstable)",
            fillMode_display: "Display Only (default)",
            startAnswering: "Start Answering",
            rollback: "Rollback Last Answer",
            language: "Language",
            modelSelection: "Select Model",
            modelDescription: "Model Description",
            setApiKey: "Set API Key",
            saveApiKey: "Save API Key",
            apiKeyPlaceholder: "Enter your API key",
            setApiBase: "Set API Base",
            saveApiBase: "Save API Base",
            apiBasePlaceholder: "Enter your API base URL",
            statusWaiting: "Status: Waiting for input",
            analyzingHtml: "Analyzing HTML structure...",
            extractingData: "Extracting question data...",
            constructingApi: "Constructing API request...",
            waitingGpt: "Waiting for GPT response...",
            parsingResponse: "Parsing GPT response...",
            executingCode: "Executing code...",
            submissionComplete: "Submission complete.",
            requestError: "Request error: ",
            showLog: "Show Logs",
            hideLog: "Hide Logs",
            customModelPlaceholder: "Enter your custom model name",
            checkApiKey: "Test API Key",
            checkingApiKey: "Testing API key...",
            apiKeyValid: "API key seems valid (test success).",
            apiKeyInvalid: "API key is invalid (did not see test success).",
            progressText: "Processing...",
            tokenUsage: "Tokens: ",
            closeButton: "Close",
            manageModelLink: "Get API Key",
            refreshModelList: "Refresh Model List",
            modelListLabel: "Fetched Model Names",
            askAi: "Ask AI",
            askAiTitle: "AI Helper",
            rentApiKey: "Rent API Key"
        },
        zh: {
            fillModeLabel: "答题模式",
            fillMode_auto: "自动填写(不稳定)",
            fillMode_display: "仅显示(默认)",
            startAnswering: "开始答题",
            rollback: "撤回上一次",
            language: "语言",
            modelSelection: "选择模型",
            modelDescription: "模型介绍",
            setApiKey: "设置 API 密钥",
            saveApiKey: "保存 API 密钥",
            apiKeyPlaceholder: "请输入您的 API 密钥",
            setApiBase: "设置 API 基础地址",
            saveApiBase: "保存 API 基础地址",
            apiBasePlaceholder: "请输入您的 API 基础地址",
            statusWaiting: "状态:等待输入",
            analyzingHtml: "分析 HTML 结构...",
            extractingData: "提取问题数据...",
            constructingApi: "构造 API 请求...",
            waitingGpt: "等待 GPT 响应...",
            parsingResponse: "解析 GPT 响应...",
            executingCode: "执行代码...",
            submissionComplete: "完成提交。",
            requestError: "请求错误:",
            showLog: "显示日志",
            hideLog: "隐藏日志",
            customModelPlaceholder: "输入自定义模型名称",
            checkApiKey: "测试 API 密钥",
            checkingApiKey: "正在测试 API 密钥...",
            apiKeyValid: "API 密钥有效(收到 test success)。",
            apiKeyInvalid: "API 密钥无效(没有收到 test success)。",
            progressText: "处理中...",
            tokenUsage: "使用量: ",
            closeButton: "关闭",
            manageModelLink: "获取 API Key",
            refreshModelList: "刷新模型列表",
            modelListLabel: "已获取模型名称",
            askAi: "问AI",
            askAiTitle: "AI 助手",
            rentApiKey: "租用 API Key"
        }
    };

    //----------------------------------------------------------------------
    // 3) 模型介绍
    //----------------------------------------------------------------------
    const modelDescriptions = {
        "gpt-4o": "Can solve problems with images, cost-effective.",
        "gpt-4o-mini": "Handles text-only questions, cheap option.",
        "o1": "Solves image problems with highest accuracy, but is slow and expensive.",
        "o3-mini": "Handles text-only questions, fast and cost-effective, but accuracy is not as high as o1.",
        "deepseek-reasoner": "Similar speed to o1, lower accuracy. No image recognition, cheaper than o1.",
        "deepseek-chat": "Similar speed to 4o, similar accuracy, no image recognition, cheapest.",
        "chatgpt-4o-least": "Unstable RLHF version. More human-like but prone to mistakes/hallucinations.",
        "custom": "User-defined model. Please enter your model name below."
    };

    //----------------------------------------------------------------------
    // 4) 构建主面板(模仿 IXL 风格布局)
    //----------------------------------------------------------------------
    const panel = document.createElement('div');
    panel.id = "gpt4o-panel";
    panel.innerHTML = `
      <div class="ixl-header-bar">
          <span class="ixl-header-title">GPT Answer Assistant</span>
          <div class="ixl-header-right">
              <span id="token-usage-display" class="ixl-token-usage">${langText[config.language].tokenUsage}0</span>
              <button id="toggle-log-btn">${langText[config.language].showLog}</button>
              <button id="close-button">${langText[config.language].closeButton}</button>
          </div>
      </div>
      <div class="ixl-content-area">

          <button id="start-answering" class="ixl-btn-emphasized">${langText[config.language].startAnswering}</button>

          <div class="ixl-row">
              <div class="ixl-col">
                  <label id="label-fill-mode">${langText[config.language].fillModeLabel}:</label>
                  <select id="fill-mode-select">
                      <option value="autoFill">${langText[config.language].fillMode_auto}</option>
                      <option value="displayOnly">${langText[config.language].fillMode_display}</option>
                  </select>
              </div>
              <div class="ixl-col">
                  <button id="rollback-answer">${langText[config.language].rollback}</button>
              </div>
          </div>

          <div class="ixl-row">
              <div class="ixl-col">
                  <label id="label-model-selection">${langText[config.language].modelSelection}:</label>
                  <select id="model-select"></select>
                  <p id="model-description"></p>
              </div>
              <!-- 自定义模型 -->
              <div class="ixl-col" id="custom-model-group" style="display: none;">
                  <label id="label-custom-model">${langText[config.language].modelSelection} (Custom):</label>
                  <input type="text" id="custom-model-input" placeholder="${langText[config.language].customModelPlaceholder}">
              </div>
          </div>

          <div class="ixl-row">
              <div class="ixl-col">
                  <label id="label-api-key">${langText[config.language].setApiKey}:</label>
                  <input type="password" id="api-key-input" placeholder="${langText[config.language].apiKeyPlaceholder}">
                  <button id="save-api-key">${langText[config.language].saveApiKey}</button>
                  <button id="check-api-key-btn">${langText[config.language].checkApiKey}</button>
              </div>
              <div class="ixl-col">
                  <label id="label-api-base">${langText[config.language].setApiBase}:</label>
                  <input type="text" id="api-base-input" placeholder="${langText[config.language].apiBasePlaceholder}">
                  <button id="save-api-base">${langText[config.language].saveApiBase}</button>
              </div>
          </div>

          <div class="ixl-row">
              <div class="ixl-col">
                  <label>${langText[config.language].manageModelLink}:</label>
                  <div style="display:flex; gap:10px;">
                      <a id="manage-model-link" href="#" target="_blank" class="ixl-link">Open Link</a>
                      <button id="rent-api-btn" style="flex-shrink:0;">${langText[config.language].rentApiKey}</button>
                  </div>
              </div>
              <div class="ixl-col">
                  <button id="refresh-model-list-btn">${langText[config.language].refreshModelList}</button>
              </div>
          </div>

          <!-- auto submit -->
          <div class="ixl-row" id="auto-submit-group">
              <div class="ixl-col">
                  <label id="label-auto-submit">
                      <input type="checkbox" id="auto-submit-toggle">
                      <span id="span-auto-submit">Enable Auto Submit</span>
                  </label>
              </div>
          </div>

          <div class="ixl-row">
              <div class="ixl-col">
                  <label id="label-language">${langText[config.language].language}:</label>
                  <select id="language-select">
                      <option value="en" ${config.language === "en" ? "selected" : ""}>English</option>
                      <option value="zh" ${config.language === "zh" ? "selected" : ""}>中文</option>
                  </select>
              </div>
          </div>

          <div id="progress-container" style="display:none; margin-top:10px;">
              <progress id="progress-bar" max="100" value="0"></progress>
              <span id="progress-text">${langText[config.language].progressText}</span>
          </div>

          <p id="status" style="margin-top:10px;font-weight:bold;">
            ${langText[config.language].statusWaiting}
          </p>

          <!-- log -->
          <div id="log-container" style="display: none; max-height: 180px; overflow-y: auto; border: 1px solid #ccc; margin-top: 10px; padding: 5px; background-color: #fff;font-family:monospace;"></div>

          <!-- 如果 fillMode = displayOnly 这里显示答案 -->
          <div id="answer-display" style="display: none; margin-top: 10px; padding: 8px; border: 1px solid #ccc; background-color: #fff;">
              <h4>GPT Answer:</h4>
              <div id="answer-content" style="white-space: pre-wrap;"></div>
          </div>

          <!-- 底部:问 AI -->
          <button id="ask-ai-btn" class="ixl-btn-secondary" style="margin-top: 10px;">
              ${langText[config.language].askAi}
          </button>
      </div>
    `;
    document.body.appendChild(panel);

    // 常用的 UI 句柄
    const UI = {
        panel,
        logContainer: panel.querySelector("#log-container"),
        status: panel.querySelector("#status"),
        tokenUsageDisplay: panel.querySelector("#token-usage-display"),
        closeButton: panel.querySelector("#close-button"),
        toggleLogBtn: panel.querySelector("#toggle-log-btn"),
        progressContainer: panel.querySelector("#progress-container"),
        progressBar: panel.querySelector("#progress-bar"),
        startAnswering: panel.querySelector("#start-answering"),
        rollbackAnswer: panel.querySelector("#rollback-answer"),
        fillModeSelect: panel.querySelector("#fill-mode-select"),
        answerDisplay: panel.querySelector("#answer-display"),
        answerContent: panel.querySelector("#answer-content"),
        autoSubmitGroup: panel.querySelector("#auto-submit-group"),
        autoSubmitToggle: panel.querySelector("#auto-submit-toggle"),
        languageSelect: panel.querySelector("#language-select"),
        apiKeyInput: panel.querySelector("#api-key-input"),
        apiBaseInput: panel.querySelector("#api-base-input"),
        modelSelect: panel.querySelector("#model-select"),
        modelDescription: panel.querySelector("#model-description"),
        customModelGroup: panel.querySelector("#custom-model-group"),
        customModelInput: panel.querySelector("#custom-model-input"),
        manageModelLink: panel.querySelector("#manage-model-link"),
        refreshModelListBtn: panel.querySelector("#refresh-model-list-btn"),
        rentApiBtn: panel.querySelector("#rent-api-btn")
    };

    //----------------------------------------------------------------------
    // 5) 样式 (IXL-like)
    //----------------------------------------------------------------------
    GM_addStyle(`
      /* 全局面板外观 */
      #gpt4o-panel {
          position: fixed;
          top: 80px;
          right: 20px;
          width: 600px;
          z-index: 999999;
          border-radius: 6px;
          box-shadow: 0 3px 12px rgba(0,0,0,0.3);
          overflow: hidden;
          font-family: "Arial", sans-serif;
      }
      .ixl-header-bar {
          background-color: #003b5c;
          color: #fff;
          padding: 8px 15px;
          display: flex;
          justify-content: space-between;
          align-items: center;
      }
      .ixl-header-title {
          font-size: 16px;
          font-weight: bold;
      }
      .ixl-header-right button {
          background-color: #d9534f;
          border: none;
          color: #fff;
          padding: 4px 8px;
          border-radius: 3px;
          margin-left: 5px;
          cursor: pointer;
      }
      .ixl-header-right button:hover {
          opacity: 0.8;
      }
      .ixl-header-right .ixl-token-usage {
          margin-right: 10px;
          font-weight: bold;
      }
      .ixl-content-area {
          background-color: #f0f4f5;
          padding: 15px;
      }

      /* 行列布局 */
      .ixl-row {
          display: flex;
          flex-wrap: wrap;
          gap: 10px;
          margin-top: 10px;
      }
      .ixl-col {
          flex: 1;
          min-width: 0;
      }

      /* 按钮 */
      button {
          cursor: pointer;
      }
      .ixl-btn-emphasized {
          display: block;
          width: 100%;
          background-color: #f0ad4e; /* 橘黄色 */
          color: #fff;
          padding: 10px 0;
          font-size: 15px;
          font-weight: bold;
          border: none;
          border-radius: 4px;
          text-align: center;
      }
      .ixl-btn-emphasized:hover {
          background-color: #ec971f;
      }
      .ixl-btn-secondary {
          width: 100%;
          background-color: #bbb;
          color: #333;
          padding: 10px 0;
          border: none;
          border-radius: 4px;
          font-size: 14px;
      }
      .ixl-btn-secondary:hover {
          background-color: #aaa;
      }

      input, select, button {
          font-size: 14px;
          padding: 6px;
          box-sizing: border-box;
          width: 100%;
      }

      .ixl-link {
          display: inline-block;
          padding: 6px;
          background-color: #2f8ee0;
          color: #fff;
          border-radius: 4px;
          text-decoration: none;
          text-align: center;
      }
      .ixl-link:hover {
          opacity: 0.8;
      }
    `);

    //----------------------------------------------------------------------
    // 6) 日志函数
    //----------------------------------------------------------------------
    function logMessage(msg) {
        const now = new Date().toLocaleString();
        const div = document.createElement('div');
        div.textContent = `[${now}] ${msg}`;
        UI.logContainer.appendChild(div);
        console.log(`[Log] ${msg}`);
    }
    function logDump(label, val) {
        let msg = `[DUMP] ${label}: `;
        if (typeof val === "object") {
            try { msg += JSON.stringify(val); } catch(e){ msg += String(val); }
        } else {
            msg += String(val);
        }
        logMessage(msg);
    }

    //----------------------------------------------------------------------
    // 7) 填充语言文本
    //----------------------------------------------------------------------
    function updateLanguageText() {
        UI.startAnswering.textContent = langText[config.language].startAnswering;
        UI.rollbackAnswer.textContent = langText[config.language].rollback;

        panel.querySelector("#label-fill-mode").textContent = langText[config.language].fillModeLabel + ":";
        UI.fillModeSelect.options[0].text = langText[config.language].fillMode_auto;
        UI.fillModeSelect.options[1].text = langText[config.language].fillMode_display;

        panel.querySelector("#close-button").textContent = langText[config.language].closeButton;

        panel.querySelector("#label-model-selection").textContent = langText[config.language].modelSelection + ":";
        panel.querySelector("#label-custom-model").textContent = langText[config.language].modelSelection + " (Custom):";
        UI.customModelInput.placeholder = langText[config.language].customModelPlaceholder;

        panel.querySelector("#label-api-key").textContent = langText[config.language].setApiKey + ":";
        UI.apiKeyInput.placeholder = langText[config.language].apiKeyPlaceholder;
        panel.querySelector("#save-api-key").textContent = langText[config.language].saveApiKey;
        panel.querySelector("#check-api-key-btn").textContent = langText[config.language].checkApiKey;

        panel.querySelector("#label-api-base").textContent = langText[config.language].setApiBase + ":";
        UI.apiBaseInput.placeholder = langText[config.language].apiBasePlaceholder;
        panel.querySelector("#save-api-base").textContent = langText[config.language].saveApiBase;

        panel.querySelector("#refresh-model-list-btn").textContent = langText[config.language].refreshModelList;

        panel.querySelector("#span-auto-submit").textContent = "Enable Auto Submit"; // 仅英语写死了,也可多语言化
        panel.querySelector("#label-language").textContent = langText[config.language].language + ":";

        panel.querySelector("#progress-text").textContent = langText[config.language].progressText;
        UI.status.textContent = langText[config.language].statusWaiting;
        UI.toggleLogBtn.textContent = (UI.logContainer.style.display === "none") ? langText[config.language].showLog : langText[config.language].hideLog;
        UI.tokenUsageDisplay.textContent = langText[config.language].tokenUsage + config.tokenUsage;

        panel.querySelector("#ask-ai-btn").textContent = langText[config.language].askAi;
        panel.querySelector("#manage-model-link").textContent = langText[config.language].manageModelLink;
        UI.rentApiBtn.textContent = langText[config.language].rentApiKey;
    }

    //----------------------------------------------------------------------
    // 8) 构建下拉框
    //----------------------------------------------------------------------
    function rebuildModelSelect() {
        UI.modelSelect.innerHTML = "";
        // 预置
        const ogPre = document.createElement("optgroup");
        ogPre.label = "Predefined";
        predefinedModels.forEach(m => {
            const opt = document.createElement("option");
            opt.value = m;
            opt.textContent = m;
            ogPre.appendChild(opt);
        });
        UI.modelSelect.appendChild(ogPre);

        // 动态发现
        const ogDisc = document.createElement("optgroup");
        ogDisc.label = "Discovered";
        const discoveredKeys = Object.keys(modelConfigs).filter(k => modelConfigs[k].discovered);
        if (discoveredKeys.length > 0) {
            discoveredKeys.forEach(m => {
                const opt = document.createElement("option");
                opt.value = m;
                opt.textContent = m;
                ogDisc.appendChild(opt);
            });
            UI.modelSelect.appendChild(ogDisc);
        }

        // custom
        const optCustom = document.createElement("option");
        optCustom.value = "custom";
        optCustom.textContent = "custom";
        UI.modelSelect.appendChild(optCustom);

        // set selected
        if (UI.modelSelect.querySelector(`option[value="${config.selectedModel}"]`)) {
            UI.modelSelect.value = config.selectedModel;
        } else {
            UI.modelSelect.value = "custom";
        }

        UI.modelDescription.textContent = modelDescriptions[config.selectedModel] || "Custom or discovered model.";
        UI.customModelGroup.style.display = (config.selectedModel === "custom") ? "block" : "none";
    }

    //----------------------------------------------------------------------
    // 9) 事件绑定
    //----------------------------------------------------------------------
    // 关闭面板
    UI.closeButton.addEventListener("click", () => {
        panel.style.display = "none";
        logMessage("Panel closed by user.");
    });

    // 显示/隐藏日志
    UI.toggleLogBtn.addEventListener("click", () => {
        if (UI.logContainer.style.display === "none") {
            UI.logContainer.style.display = "block";
            UI.toggleLogBtn.textContent = langText[config.language].hideLog;
            logMessage("Log panel shown.");
        } else {
            UI.logContainer.style.display = "none";
            UI.toggleLogBtn.textContent = langText[config.language].showLog;
            logMessage("Log panel hidden.");
        }
    });

    // 语言切换
    UI.languageSelect.addEventListener("change", () => {
        config.language = UI.languageSelect.value;
        localStorage.setItem("gpt4o-language", config.language);
        updateLanguageText();
    });

    // 答题模式切换
    UI.fillModeSelect.addEventListener("change", () => {
        config.fillMode = UI.fillModeSelect.value;
        if (config.fillMode === "displayOnly") {
            UI.answerDisplay.style.display = "block";
            UI.answerContent.textContent = "";
            UI.autoSubmitGroup.style.display = "none";
        } else {
            // 当用户选择 autoFill 时,提示不稳定
            alert("Warning: Auto Fill mode is unstable. Recommended only if you need automatic filling.");
            UI.answerDisplay.style.display = "none";
            UI.autoSubmitGroup.style.display = "block";
        }
    });

    // 开始答题
    UI.startAnswering.addEventListener("click", () => {
        answerQuestion();
    });

    // 撤回
    UI.rollbackAnswer.addEventListener("click", () => {
        if (config.lastTargetState) {
            const tgt = getTargetDiv();
            if (tgt) {
                tgt.innerHTML = config.lastTargetState;
                logMessage("Rolled back to previous state.");
            } else {
                logMessage("Rollback failed: no target found.");
            }
        } else {
            logMessage("No previous state available for rollback.");
        }
    });

    // 模型选择
    UI.modelSelect.addEventListener("change", () => {
        config.selectedModel = UI.modelSelect.value;
        if (!modelConfigs[config.selectedModel]) {
            modelConfigs[config.selectedModel] = {
                apiKey: "",
                apiBase: "https://api.openai.com/v1/chat/completions",
                manageUrl: "",
                modelList: [],
                discovered: false
            };
        }
        UI.modelDescription.textContent = modelDescriptions[config.selectedModel] || "Custom or discovered model.";
        UI.customModelGroup.style.display = (config.selectedModel === "custom") ? "block" : "none";
        UI.apiKeyInput.value = modelConfigs[config.selectedModel].apiKey || "";
        UI.apiBaseInput.value = modelConfigs[config.selectedModel].apiBase || "";
        updateManageUrl(); // 更新“Get API Key”链接
    });

    // 自定义模型
    UI.customModelInput.addEventListener("change", () => {
        const name = UI.customModelInput.value.trim();
        if (!name) return;
        config.selectedModel = name;
        if (!modelConfigs[name]) {
            modelConfigs[name] = {
                apiKey: "",
                apiBase: "https://api.openai.com/v1/chat/completions",
                manageUrl: "",
                modelList: [],
                discovered: false
            };
        }
        rebuildModelSelect();
        UI.modelSelect.value = "custom";
        UI.apiKeyInput.value = modelConfigs[name].apiKey;
        UI.apiBaseInput.value = modelConfigs[name].apiBase;
        updateManageUrl();
    });

    // 保存 apiKey
    panel.querySelector("#save-api-key").addEventListener("click", () => {
        const newKey = UI.apiKeyInput.value.trim();
        modelConfigs[config.selectedModel].apiKey = newKey;
        saveModelConfigs();
        logDump("API Key saved", newKey);
    });

    // 测试 apiKey
    panel.querySelector("#check-api-key-btn").addEventListener("click", () => {
        UI.status.textContent = langText[config.language].checkingApiKey;
        checkApiKey();
    });

    // 保存 apiBase
    panel.querySelector("#save-api-base").addEventListener("click", () => {
        const newBase = UI.apiBaseInput.value.trim();
        modelConfigs[config.selectedModel].apiBase = newBase;
        saveModelConfigs();
        logDump("API Base saved", newBase);
    });

    // 刷新模型列表
    UI.refreshModelListBtn.addEventListener("click", () => {
        refreshModelList();
    });

    // Auto Submit
    UI.autoSubmitToggle.addEventListener("change", () => {
        config.autoSubmitEnabled = UI.autoSubmitToggle.checked;
        logDump("autoSubmitEnabled", config.autoSubmitEnabled);
    });

    // 租用APIkey
    UI.rentApiBtn.addEventListener("click", () => {
        showRentApiPopup();
    });

    // 问AI
    panel.querySelector("#ask-ai-btn").addEventListener("click", () => {
        openAiHelperDialog();
    });

    //----------------------------------------------------------------------
    // 10) ManageUrl / RentKey 弹窗
    //----------------------------------------------------------------------
    function updateManageUrl() {
        // 如果包含 "deepseek" 则指向 deepseek api;若不是则 openai;如都不匹配则 "#"
        let modelName = config.selectedModel.toLowerCase();
        let link = "#";
        if (modelName.includes("deepseek")) {
            link = "https://platform.deepseek.com/api_keys";
        } else {
            // 默认当成 openai
            link = "https://platform.openai.com/api-keys";
        }
        modelConfigs[config.selectedModel].manageUrl = link;
        UI.manageModelLink.href = link;
        saveModelConfigs();
    }

    function showRentApiPopup() {
        const overlay = document.createElement("div");
        overlay.style.position = "fixed";
        overlay.style.top = "0";
        overlay.style.left = "0";
        overlay.style.width = "100%";
        overlay.style.height = "100%";
        overlay.style.zIndex = "100000";
        overlay.style.backgroundColor = "rgba(0,0,0,0.5)";
        const box = document.createElement("div");
        box.style.position = "absolute";
        box.style.top = "50%";
        box.style.left = "50%";
        box.style.transform = "translate(-50%,-50%)";
        box.style.backgroundColor = "#fff";
        box.style.padding = "20px";
        box.style.borderRadius = "6px";
        box.style.width = "400px";
        box.innerHTML = `
            <h3>Rent an API Key</h3>
            <p>Please contact me at:</p>
            <ul>
                <li>[email protected]</li>
                <li>[email protected]</li>
            </ul>
            <button id="rent-close-btn">${langText[config.language].closeButton}</button>
        `;
        overlay.appendChild(box);
        document.body.appendChild(overlay);
        box.querySelector("#rent-close-btn").addEventListener("click", () => {
            document.body.removeChild(overlay);
        });
    }

    //----------------------------------------------------------------------
    // 11) AI Helper:能够配置脚本
    //----------------------------------------------------------------------
    function openAiHelperDialog() {
        const overlay = document.createElement("div");
        overlay.style.position = "fixed";
        overlay.style.top = "0";
        overlay.style.left = "0";
        overlay.style.width = "100%";
        overlay.style.height = "100%";
        overlay.style.backgroundColor = "rgba(0,0,0,0.5)";
        overlay.style.zIndex = "200001";

        const box = document.createElement("div");
        box.style.position = "absolute";
        box.style.top = "50%";
        box.style.left = "50%";
        box.style.transform = "translate(-50%, -50%)";
        box.style.backgroundColor = "#fff";
        box.style.padding = "20px";
        box.style.borderRadius = "6px";
        box.style.width = "480px";
        box.style.maxHeight = "80%";
        box.style.overflowY = "auto";
        box.style.textAlign = "left";

        box.innerHTML = `
            <h3>${langText[config.language].askAiTitle}</h3>
            <textarea id="ask-ai-question" style="width:100%;height:80px;" placeholder="Type your question..."></textarea>
            <div style="margin-top:10px;">
                <button id="ask-ai-submit">Submit</button>
                <button id="ask-ai-close">${langText[config.language].closeButton}</button>
            </div>
            <pre id="ask-ai-answer" style="margin-top:10px;white-space:pre-wrap;background:#f7f7f7;padding:10px;border-radius:4px;max-height:300px;overflow:auto;"></pre>
        `;
        overlay.appendChild(box);
        document.body.appendChild(overlay);

        const txtQuestion = box.querySelector("#ask-ai-question");
        const divAnswer = box.querySelector("#ask-ai-answer");
        box.querySelector("#ask-ai-close").addEventListener("click", () => {
            document.body.removeChild(overlay);
        });
        box.querySelector("#ask-ai-submit").addEventListener("click", () => {
            const question = txtQuestion.value.trim();
            if (!question) return;
            divAnswer.textContent = "... loading ...";
            askAiQuestion(question, (answer) => {
                divAnswer.textContent = answer;
            });
        });
    }

    function askAiQuestion(userQuery, callback) {
        const modelConf = modelConfigs[config.selectedModel] || {};
        const payload = {
            model: config.selectedModel,
            messages: [
                { role: "system", content: scriptDescription },
                { role: "user", content: userQuery }
            ]
        };
        GM_xmlhttpRequest({
            method: "POST",
            url: modelConf.apiBase || "https://api.openai.com/v1/chat/completions",
            headers: {
                "Content-Type": "application/json",
                "Authorization": `Bearer ${modelConf.apiKey}`
            },
            data: JSON.stringify(payload),
            onload: function(resp) {
                try {
                    const data = JSON.parse(resp.responseText);
                    const text = data.choices[0].message.content.trim();
                    callback(text);
                } catch(e) {
                    callback("[Error parsing AI response]");
                    logDump("askAiQuestion Parse Error", e);
                }
            },
            onerror: function(err) {
                callback("[AI request failed]");
                logDump("askAiQuestion Error", err);
            }
        });
    }

    // 提供给外部AI一个配置函数
    window.AI_setScriptConfig = function(newCfg) {
        // newCfg 可能形如 { fillMode: "autoFill", language: "en", autoSubmitEnabled: true }
        // 这里我们有选择地应用
        if (typeof newCfg.language === "string") {
            config.language = newCfg.language;
            localStorage.setItem("gpt4o-language", config.language);
            updateLanguageText();
        }
        if (typeof newCfg.fillMode === "string") {
            config.fillMode = newCfg.fillMode;
            UI.fillModeSelect.value = config.fillMode;
            if (config.fillMode === "displayOnly") {
                UI.answerDisplay.style.display = "block";
                UI.answerContent.textContent = "";
                UI.autoSubmitGroup.style.display = "none";
            } else {
                UI.answerDisplay.style.display = "none";
                UI.autoSubmitGroup.style.display = "block";
            }
        }
        if (typeof newCfg.autoSubmitEnabled === "boolean") {
            config.autoSubmitEnabled = newCfg.autoSubmitEnabled;
            UI.autoSubmitToggle.checked = newCfg.autoSubmitEnabled;
        }
        // 其他更多字段...
        logMessage("AI_setScriptConfig invoked with: " + JSON.stringify(newCfg));
    };

    //----------------------------------------------------------------------
    // 12) 测试 API Key
    //----------------------------------------------------------------------
    function checkApiKey() {
        const modelConf = modelConfigs[config.selectedModel];
        if (!modelConf) return;
        const testPayload = {
            model: config.selectedModel,
            messages: [
                {role: "system", content: "You are a quick test assistant."},
                {role: "user", content: "Please ONLY respond with: test success"}
            ]
        };
        GM_xmlhttpRequest({
            method: "POST",
            url: modelConf.apiBase || "https://api.openai.com/v1/chat/completions",
            headers: {
                "Content-Type": "application/json",
                "Authorization": `Bearer ${modelConf.apiKey}`
            },
            data: JSON.stringify(testPayload),
            onload: function(resp) {
                UI.status.textContent = langText[config.language].statusWaiting;
                try {
                    logDump("checkApiKey Response", resp.responseText);
                    const data = JSON.parse(resp.responseText);
                    const ans = data.choices[0].message.content.trim().toLowerCase();
                    if (ans.includes("test success")) {
                        alert(langText[config.language].apiKeyValid);
                    } else {
                        alert(langText[config.language].apiKeyInvalid);
                    }
                } catch(e) {
                    alert("Error while testing key: " + e);
                }
            },
            onerror: function(err) {
                logDump("API Key Test Error", err);
                UI.status.textContent = langText[config.language].statusWaiting;
                alert("Test failed: " + JSON.stringify(err));
            }
        });
    }

    //----------------------------------------------------------------------
    // 13) 获取模型列表
    //----------------------------------------------------------------------
    function refreshModelList() {
        const modelConf = modelConfigs[config.selectedModel];
        if (!modelConf) return;
        const url = modelConf.apiBase.replace("/chat/completions","/models");
        logMessage("Fetching model list from: " + url);
        GM_xmlhttpRequest({
            method: "GET",
            url: url,
            headers: {
                "Authorization": `Bearer ${modelConf.apiKey}`
            },
            onload: function(resp) {
                logDump("FetchModelListResponse", resp.responseText);
                try {
                    const data = JSON.parse(resp.responseText);
                    if (data.data && Array.isArray(data.data)) {
                        const newList = data.data.map(x => x.id);
                        modelConf.modelList = newList;
                        // 把新发现的模型注册
                        newList.forEach(m => {
                            if (!modelConfigs[m]) {
                                modelConfigs[m] = {
                                    apiKey: modelConf.apiKey,
                                    apiBase: modelConf.apiBase,
                                    manageUrl: "",
                                    modelList: [],
                                    discovered: true
                                };
                            }
                        });
                        saveModelConfigs();
                        rebuildModelSelect();
                        alert("Model list refreshed. Found: " + newList.join(", "));
                    } else {
                        alert("Unexpected model list response. Check console for details.");
                    }
                } catch(e) {
                    alert("Failed to parse model list: " + e);
                }
            },
            onerror: function(err) {
                alert("Error refreshing model list: " + JSON.stringify(err));
            }
        });
    }

    //----------------------------------------------------------------------
    // 14) 题目区域 + GPT 交互
    //----------------------------------------------------------------------
    function getTargetDiv() {
        let targ = document.evaluate(
            '/html/body/main/div/article/section/section/div/div[1]',
            document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null
        ).singleNodeValue;
        if (!targ) {
            targ = document.querySelector('main div.article, main > div, article');
        }
        return targ;
    }

    function monitorDOMChanges(el) {
        if (!el) return;
        const obs = new MutationObserver((mutations) => {
            mutations.forEach(m => {
                logDump("DOM mutation", {
                    type: m.type,
                    added: m.addedNodes.length,
                    removed: m.removedNodes.length
                });
            });
        });
        obs.observe(el, {childList:true, subtree:true});
        logMessage("Monitoring DOM changes on target element.");
    }

    function captureMathContent(el) {
        let mathEls = el.querySelectorAll('script[type="math/tex"], .MathJax, .mjx-chtml');
        if (mathEls.length > 0) {
            let latex = "";
            mathEls.forEach(n => latex += n.textContent + "\n");
            logDump("Captured latex", latex);
            return latex;
        }
        return null;
    }

    function captureCanvasImage(el) {
        let can = el.querySelector('canvas');
        if (can) {
            logMessage("Canvas found, capturing as base64...");
            const offC = document.createElement('canvas');
            offC.width = can.width;
            offC.height = can.height;
            offC.getContext('2d').drawImage(can, 0,0);
            return offC.toDataURL("image/png").split(",")[1];
        }
        return null;
    }

    // 发送 GPT 请求
    function answerQuestion() {
        logMessage("AnswerQuestion triggered.");
        const targetDiv = getTargetDiv();
        if (!targetDiv) {
            UI.status.textContent = "Error: can't find question region.";
            logMessage("No targetDiv found.");
            return;
        }
        config.lastTargetState = targetDiv.innerHTML;
        monitorDOMChanges(targetDiv);

        const htmlContent = targetDiv.outerHTML;
        const latex = captureMathContent(targetDiv);
        const canvasData = latex ? null : captureCanvasImage(targetDiv);

        // 要求 GPT 仅使用 unicode、不使用markdown、latex。如果 autoFill,就允许 triple-backtick code
        let systemPrompt = "";
        let userContent = "";

        if (config.fillMode === "displayOnly") {
            // 仅文本回答(unicode数学符号)
            systemPrompt = "You are a math assistant specialized in solving IXL math problems. Output the final numeric/textual answer in plain text with only unicode math. No Markdown or LaTeX. No code blocks.";
            userContent = `HTML: ${htmlContent}\n`;
            if (latex) userContent += `MathLaTeX:\n${latex}\n`;
            if (canvasData) userContent += "Canvas base64 attached (pretend you can interpret it).";
        } else {
            // autoFill,需要三重反引号JS
            systemPrompt = "You are a math assistant for IXL. Output a JavaScript code snippet with triple backticks ```javascript ...``` that fills all required answer fields. Use only unicode for any math symbols, no latex or markdown outside code. The code must be the entire message, no extra text.";
            userContent = `Given HTML:\n${htmlContent}\n`;
            if (latex) userContent += `MathLaTeX:\n${latex}\n`;
            if (canvasData) userContent += "Canvas base64 attached (pretend you can interpret it).";
        }

        const messages = [
            { role: "system", content: systemPrompt },
            { role: "user", content: userContent }
        ];
        const reqPayload = {
            model: config.selectedModel,
            messages: messages
        };

        UI.status.textContent = langText[config.language].waitingGpt;
        startFakeProgress();

        const mc = modelConfigs[config.selectedModel] || {};
        GM_xmlhttpRequest({
            method: "POST",
            url: mc.apiBase || "https://api.openai.com/v1/chat/completions",
            headers: {
                "Content-Type": "application/json",
                "Authorization": `Bearer ${mc.apiKey}`
            },
            data: JSON.stringify(reqPayload),
            onload: function(resp) {
                finishProgress();
                try {
                    const data = JSON.parse(resp.responseText);
                    if (data.usage && data.usage.total_tokens) {
                        config.tokenUsage = data.usage.total_tokens;
                        UI.tokenUsageDisplay.textContent = langText[config.language].tokenUsage + config.tokenUsage;
                    }
                    const output = data.choices[0].message.content.trim();
                    if (config.fillMode === "displayOnly") {
                        UI.answerDisplay.style.display = "block";
                        UI.answerContent.textContent = output;
                    } else {
                        const code = extractJSCode(output);
                        if (!code) {
                            logMessage("No JavaScript code found in GPT output.");
                            UI.status.textContent = "Error: No code found in GPT answer.";
                            return;
                        }
                        runInSandbox(code);
                        if (config.autoSubmitEnabled) {
                            submitAnswer();
                        }
                    }
                    UI.status.textContent = langText[config.language].submissionComplete;
                } catch(e) {
                    UI.status.textContent = "Error handling GPT answer.";
                    logDump("AnswerError", e);
                }
            },
            onerror: function(err) {
                finishProgress();
                UI.status.textContent = langText[config.language].requestError + JSON.stringify(err);
                logDump("RequestError", err);
            }
        });
    }

    function extractJSCode(content) {
        const re = /```javascript\s+([\s\S]*?)\s+```/i;
        const match = content.match(re);
        return match && match[1] ? match[1].trim() : null;
    }

    function runInSandbox(code) {
        try {
            const sandbox = {};
            (new Function("sandbox", "with(sandbox){"+code+"}"))(sandbox);
        } catch(e) {
            logDump("SandboxError", e);
        }
    }

    function submitAnswer() {
        let btn = document.evaluate(
            '/html/body/main/div/article/section/section/div/div[1]/section/div/section/div/button',
            document,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null
        ).singleNodeValue;
        if (!btn) {
            btn = document.querySelector('button.submit, button[class*="submit"]');
        }
        if (btn) {
            logMessage("Auto-submitting the answer...");
            btn.click();
        } else {
            logMessage("No submit button found.");
        }
    }

    //----------------------------------------------------------------------
    // 15) 初始化
    //----------------------------------------------------------------------
    function initSelectedModelUI() {
        // 如果指定模型不存在就默认 gpt-4o
        if (!modelConfigs[config.selectedModel]) {
            config.selectedModel = "gpt-4o";
        }
        rebuildModelSelect();

        const mconf = modelConfigs[config.selectedModel];
        UI.apiKeyInput.value = mconf.apiKey || "";
        UI.apiBaseInput.value = mconf.apiBase || "";
        updateManageUrl();

        UI.fillModeSelect.value = config.fillMode;
        if (config.fillMode === "displayOnly") {
            UI.answerDisplay.style.display = "block";
            UI.answerContent.textContent = "";
            UI.autoSubmitGroup.style.display = "none";
        } else {
            UI.answerDisplay.style.display = "none";
            UI.autoSubmitGroup.style.display = "block";
        }
    }

    initSelectedModelUI();
    updateLanguageText();
    logMessage("Script loaded with 'Display Only' default, AI helper, IXL-style UI, etc.");
})();