IXL Auto Answer (OpenAI API Required)

Sends HTML and canvas data to AI models for math problem-solving with enhanced accuracy, configurable API base, improved GUI with progress bar, auto-answer functionality, token usage display, rollback and detailed DOM change logging. API key is tested by direct server request.

// ==UserScript==
// @name         IXL Auto Answer (OpenAI API Required)
// @namespace    http://tampermonkey.net/
// @version      8.6
// @license      GPL-3.0
// @description  Sends HTML and canvas data to AI models for math problem-solving with enhanced accuracy, configurable API base, improved GUI with progress bar, auto-answer functionality, token usage display, rollback and detailed DOM change logging. API key is tested by direct server request.
// @match        https://*.ixl.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @require      https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js
// @require      https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
// ==/UserScript==

/*
  This script uses Marked (an MD rendering library). The above @require
  line imports marked for us to parse GPT’s output if it includes markdown.

  - We keep both “Auto Fill” (with code snippet insertion) and “Display Answer Only”.
  - If user picks "Auto Fill", we hide the display answer container and show the
    auto fill disclaimers. Conversely, "Display Answer Only" will show the final
    answer container but won't attempt code execution.
  - Keep the rentKey button and highlight it as it's crucial for monetization.
  - GPT answer's solution steps can be parsed using `marked.parse(...)` to display HTML output.
  - The rest of the logic is the same: we have multiple features:
    * Start Answer
    * Rollback
    * AutoSubmit
    * Refresh models
    * Rent Key button (emphasized)
    * The entire script is self-contained with your original userScript header.
*/

(function() {
  'use strict';

  // (1) MIGRATION/CONFIG STORAGE
  let oldStore1 = localStorage.getItem("gpt4o-modelConfigs");
  let oldStore2 = localStorage.getItem("ixlAutoAnswerConfigs");
  let newStore = localStorage.getItem("myNewIxLStorage");
  if (!newStore) {
    if (oldStore1) {
      localStorage.setItem("myNewIxLStorage", oldStore1);
      localStorage.removeItem("gpt4o-modelConfigs");
    } else if (oldStore2) {
      localStorage.setItem("myNewIxLStorage", oldStore2);
      localStorage.removeItem("ixlAutoAnswerConfigs");
    }
  }

  let modelConfigs = JSON.parse(localStorage.getItem("myNewIxLStorage") || "{}");
  if (!modelConfigs["gpt-4.1"]) {
    modelConfigs["gpt-4.1"] = {
      apiKey: "",
      apiBase: "https://api.openai.com/v1/chat/completions",
      discovered: false,
      modelList: []
    };
  }

  let config = {
    selectedModel: "gpt-4.1",
    language: localStorage.getItem("myIxLLang") || "en",
    mode: "displayOnly", // can be "autoFill" or "displayOnly"
    autoSubmit: false,
    totalTokens: 0,
    lastState: null
  };

  function saveConfig() {
    localStorage.setItem("myNewIxLStorage", JSON.stringify(modelConfigs));
    localStorage.setItem("myIxLLang", config.language);
  }

  // (2) MULTI-LANG TEXT
  const langText = {
    en: {
      panelTitle: "IXL Auto Answer (OpenAI API Required)",
      modeLabel: "Mode",
      modeAuto: "Auto Fill (Unstable)",
      modeDisp: "Display Answer Only",
      startButton: "Start Answering",
      rollbackButton: "Rollback",
      configAssistant: "Config Assistant",
      closeButton: "Close",
      logsButton: "Logs",
      logsHide: "Hide Logs",
      tokensLabel: "Tokens: ",
      statusIdle: "Status: Idle",
      statusWaiting: "Waiting for GPT...",
      statusDone: "Done.",
      requestError: "Request error: ",
      finalAnswerTitle: "Final Answer",
      stepsTitle: "Solution Steps",
      missingAnswerTag: "Missing <answer> tag",
      modelSelectLabel: "Model",
      modelDescLabel: "Model Description",
      customModelPlaceholder: "Custom model name",
      languageLabel: "Language",
      autoSubmitLabel: "Auto Submit",
      rentKeyButton: "Rent Key (Support Me!)",
      apiKeyLabel: "API Key",
      saveButton: "Save",
      testKeyButton: "Test Key",
      testKeyMsg: "Testing key...",
      keyOK: "API key valid.",
      keyBad: "API key invalid (missing 'test success').",
      placeKey: "Enter your API key",
      placeBase: "Enter your API base URL",
      apiBaseLabel: "API Base",
      refreshModels: "Refresh Models",
      getKeyLinkLabel: "Get API Key",
      disclaimAutoFill: "Warning: Auto Fill is unstable. Use carefully."
    },
    zh: {
      panelTitle: "IXL自动解题 (OpenAI)",
      modeLabel: "模式",
      modeAuto: "自动填入(不稳定)",
      modeDisp: "仅展示答案",
      startButton: "开始答题",
      rollbackButton: "撤回",
      configAssistant: "配置助手",
      closeButton: "关闭",
      logsButton: "日志",
      logsHide: "隐藏日志",
      tokensLabel: "用量: ",
      statusIdle: "状态:空闲",
      statusWaiting: "等待GPT...",
      statusDone: "完成。",
      requestError: "请求错误:",
      finalAnswerTitle: "最终答案",
      stepsTitle: "解题过程",
      missingAnswerTag: "缺少<answer>标签",
      modelSelectLabel: "模型",
      modelDescLabel: "模型介绍",
      customModelPlaceholder: "自定义模型名称",
      languageLabel: "语言",
      autoSubmitLabel: "自动提交",
      rentKeyButton: "租用Key (支持我!)",
      apiKeyLabel: "API密钥",
      saveButton: "保存",
      testKeyButton: "测试密钥",
      testKeyMsg: "正在测试...",
      keyOK: "API密钥有效。",
      keyBad: "API密钥无效(缺'test success')",
      placeKey: "输入API密钥",
      placeBase: "输入API基础地址",
      apiBaseLabel: "API基础地址",
      refreshModels: "刷新模型列表",
      getKeyLinkLabel: "获取API Key",
      disclaimAutoFill: "警告:自动填入模式可能不稳定,请慎用。"
    }
  };

  // (3) MODEL DESCRIPTIONS (Fixed English)
  const modelDescDB = {
    "gpt-4.1": "New Model, cheaper and a lot better than 4o",
    "gpt-4.1-mini": "New Model, cheaper and a little bit better than 4o",
    "gpt-4o": "Solves images, cost-effective.",
    "gpt-4o-mini": "Text-only, cheaper.",
    "o1": "Best for images but slow & expensive.",
    "o3-mini": "Text-only, cheaper than o1.",
    "deepseek-reasoner": "No images, cheaper than o1.",
    "deepseek-chat": "No images, cheap & fast as 4o.",
    "custom": "User-defined model",
    "o3": "Advanced multi-step reasoning model, optimized for deep inference and cost-effective over o1.",
    "o4-mini": "Compact variant of the o4 architecture, offering a balanced trade-off between speed, accuracy, and cost for text-only workloads.",
    "chatgpt-4o-least": "RLHF version, better than 4o, can be error-prone.",
  };

  // (4) BUILD UI
  const panel = document.createElement("div");
  panel.id = "ixl-auto-panel";
  panel.innerHTML = `
    <div class="ixl-header">
      <span id="panel-title">${langText[config.language].panelTitle}</span>
      <span id="token-count">${langText[config.language].tokensLabel}0</span>
      <button id="btn-logs">${langText[config.language].logsButton}</button>
      <button id="btn-close">${langText[config.language].closeButton}</button>
    </div>
    <div class="ixl-content">
      <div class="row">
        <label>${langText[config.language].modeLabel}:</label>
        <select id="sel-mode" style="width:100%;">
          <option value="autoFill">${langText[config.language].modeAuto}</option>
          <option value="displayOnly">${langText[config.language].modeDisp}</option>
        </select>
      </div>
      <div class="row" style="margin-top:8px; display:flex; gap:8px;">
        <button id="btn-start" class="btn-accent" style="flex:1;">${langText[config.language].startButton}</button>
        <button id="btn-rollback" class="btn-normal" style="flex:1;">${langText[config.language].rollbackButton}</button>
        <button id="btn-config-assist" class="btn-mini" style="flex:0;">${langText[config.language].configAssistant}</button>
      </div>
      <div id="answer-box" style="display:none; border:1px solid #999; padding:6px; background:#fff; margin-top:6px;">
        <h4 id="answer-title">${langText[config.language].finalAnswerTitle}</h4>
        <div id="answer-content" style="font-size:15px; font-weight:bold; color:#080;"></div>
        <hr/>
        <h5 id="steps-title">${langText[config.language].stepsTitle}</h5>
        <div id="steps-content" style="font-size:13px; color:#666;"></div>
      </div>
      <div id="progress-area" style="display:none; margin-top:8px;">
        <progress id="progress-bar" max="100" value="0" style="width:100%;"></progress>
        <span id="progress-label">${langText[config.language].statusWaiting}</span>
      </div>
      <p id="status-line" style="font-weight:bold; margin-top:6px;">${langText[config.language].statusIdle}</p>
      <div id="log-area" style="display:none; max-height:120px; overflow-y:auto; background:#fff; border:1px solid #888; margin-top:6px; padding:4px; font-family:monospace;"></div>
      <div class="row" style="margin-top:10px;">
        <label>${langText[config.language].modelSelectLabel}:</label>
        <select id="sel-model" style="width:100%;"></select>
        <p id="model-desc" style="font-size:12px; color:#666; margin:4px 0;"></p>
        <div id="custom-model-area" style="display:none;">
          <input type="text" id="custom-model-input" style="width:100%;" placeholder="${langText[config.language].customModelPlaceholder}" />
        </div>
      </div>
      <div class="row" style="margin-top:8px;">
        <label>${langText[config.language].languageLabel}:</label>
        <select id="sel-lang" style="width:100%;">
          <option value="en">English</option>
          <option value="zh">中文</option>
        </select>
      </div>
      <div id="auto-submit-row" style="margin-top:8px;">
        <label style="display:block;">${langText[config.language].autoSubmitLabel}:</label>
        <input type="checkbox" id="chk-auto-submit"/>
      </div>
      <button id="btn-rent" class="btn-normal" style="margin-top:10px; width:100%; font-weight:bold;">
        ${langText[config.language].rentKeyButton}
      </button>
      <div class="row" style="margin-top:10px;">
        <label>${langText[config.language].apiKeyLabel}:</label>
        <div style="display:flex; gap:4px; margin-top:4px;">
          <input type="password" id="txt-apikey" style="flex:1;" placeholder="${langText[config.language].placeKey}"/>
          <button id="btn-save-key">${langText[config.language].saveButton}</button>
          <button id="btn-test-key">${langText[config.language].testKeyButton}</button>
        </div>
      </div>
      <div class="row" style="margin-top:8px;">
        <label>${langText[config.language].apiBaseLabel}:</label>
        <div style="display:flex; gap:4px; margin-top:4px;">
          <input type="text" id="txt-apibase" style="flex:1;" placeholder="${langText[config.language].placeBase}"/>
          <button id="btn-save-base">${langText[config.language].saveButton}</button>
        </div>
      </div>
      <label style="margin-top:6px; display:block;">${langText[config.language].getKeyLinkLabel}:</label>
      <div style="display:flex; gap:4px; margin-top:4px;">
        <a id="link-getkey" href="#" target="_blank" class="link-btn" style="flex:1;">Link</a>
        <button id="btn-refresh" class="btn-normal" style="flex:1;">${langText[config.language].refreshModels}</button>
      </div>
    </div>
  `;
  document.body.appendChild(panel);

  // (5) CSS
  GM_addStyle(`
    #ixl-auto-panel {
      position: fixed;
      top:20px;
      right:20px;
      width:460px;
      background:#fff;
      border-radius:6px;
      box-shadow:0 2px 10px rgba(0,0,0,0.3);
      z-index:99999999;
      font-size:14px;
      font-family: "Segoe UI", Arial, sans-serif;
    }
    .ixl-header {
      background:#4caf50;
      color:#fff;
      padding:6px;
      display:flex;
      align-items:center;
      justify-content:flex-end;
      gap:6px;
    }
    #panel-title {
      font-weight:bold;
      margin-right:auto;
    }
    .ixl-content {
      padding:10px;
    }
    .row { margin-top:6px; }
    .btn-accent {
      background:#f0ad4e; color:#fff; border:none; border-radius:4px; font-weight:bold;
    }
    .btn-accent:hover { background:#ec971f; }
    .btn-normal {
      background:#ddd; color:#333; border:none; border-radius:4px;
    }
    .btn-normal:hover {
      background:#ccc;
    }
    .btn-mini {
      background:#bbb; color:#333; border:none; border-radius:4px;
      font-size:12px; padding:4px 6px;
    }
    .btn-mini:hover {
      background:#aaa;
    }
    .link-btn {
      background:#2f8ee0; color:#fff; border-radius:4px;
      text-decoration:none; text-align:center; padding:6px;
    }
    .link-btn:hover { opacity:0.8; }
  `);

  // (6) REFS
  const UI = {
    panel,
    logArea: document.getElementById("log-area"),
    logsBtn: document.getElementById("btn-logs"),
    closeBtn: document.getElementById("btn-close"),
    tokenCount: document.getElementById("token-count"),
    modeSelect: document.getElementById("sel-mode"),
    startBtn: document.getElementById("btn-start"),
    rollbackBtn: document.getElementById("btn-rollback"),
    confAssistBtn: document.getElementById("btn-config-assist"),
    answerBox: document.getElementById("answer-box"),
    answerContent: document.getElementById("answer-content"),
    stepsContent: document.getElementById("steps-content"),
    progressArea: document.getElementById("progress-area"),
    progressBar: document.getElementById("progress-bar"),
    progressLabel: document.getElementById("progress-label"),
    statusLine: document.getElementById("status-line"),
    modelSelect: document.getElementById("sel-model"),
    modelDesc: document.getElementById("model-desc"),
    customModelArea: document.getElementById("custom-model-area"),
    customModelInput: document.getElementById("custom-model-input"),
    langSelect: document.getElementById("sel-lang"),
    autoSubmitRow: document.getElementById("auto-submit-row"),
    autoSubmitToggle: document.getElementById("chk-auto-submit"),
    rentBtn: document.getElementById("btn-rent"),
    txtApiKey: document.getElementById("txt-apikey"),
    saveKeyBtn: document.getElementById("btn-save-key"),
    testKeyBtn: document.getElementById("btn-test-key"),
    txtApiBase: document.getElementById("txt-apibase"),
    saveBaseBtn: document.getElementById("btn-save-base"),
    linkGetKey: document.getElementById("link-getkey"),
    refreshBtn: document.getElementById("btn-refresh")
  };

  // (7) UTILS
  function logMsg(msg) {
    const time = new Date().toLocaleString();
    const div = document.createElement("div");
    div.textContent = `[${time}] ${msg}`;
    UI.logArea.appendChild(div);
    console.log("[Log]", msg);
  }
  function logDump(label, val) {
    let m = `[DUMP] ${label}: `;
    try { m += JSON.stringify(val); } catch(e){ m += String(val); }
    logMsg(m);
  }
  function updateLangText() {
    UI.logsBtn.textContent = (UI.logArea.style.display==="none") ? langText[config.language].logsButton : langText[config.language].logsHide;
    UI.closeBtn.textContent = langText[config.language].closeButton;
    UI.tokenCount.textContent = langText[config.language].tokensLabel + config.totalTokens;
    UI.statusLine.textContent = langText[config.language].statusIdle;
    UI.progressLabel.textContent = langText[config.language].statusWaiting;
    UI.modeSelect.options[0].text = langText[config.language].modeAuto;
    UI.modeSelect.options[1].text = langText[config.language].modeDisp;
    UI.startBtn.textContent = langText[config.language].startButton;
    UI.rollbackBtn.textContent = langText[config.language].rollbackButton;
    UI.confAssistBtn.textContent = langText[config.language].configAssistant;
    document.getElementById("answer-title").textContent = langText[config.language].finalAnswerTitle;
    document.getElementById("steps-title").textContent = langText[config.language].stepsTitle;
    UI.txtApiKey.placeholder = langText[config.language].placeKey;
    UI.saveKeyBtn.textContent = langText[config.language].saveButton;
    UI.testKeyBtn.textContent = langText[config.language].testKeyButton;
    UI.txtApiBase.placeholder = langText[config.language].placeBase;
    UI.saveBaseBtn.textContent = langText[config.language].saveButton;
    UI.linkGetKey.textContent = "Link";
    UI.refreshBtn.textContent = langText[config.language].refreshModels;
    UI.rentBtn.textContent = langText[config.language].rentKeyButton;
  }

  // (8) BUILD MODEL SELECT
  function buildModelSelect() {
    UI.modelSelect.innerHTML = "";
    const ogPre = document.createElement("optgroup");
    ogPre.label = "Predefined";
const builtins = ["gpt-4.1", "gpt-4o", "gpt-4.1-mini", "gpt-4.1-nano", "gpt-4o-mini", "o3", "o4-mini", "o1", "o3-mini", "deepseek-reasoner", "deepseek-chat", "chatgpt-4o-least"];
    for(const b of builtins){
      const opt = document.createElement("option");
      opt.value = b;
      opt.textContent = b;
      ogPre.appendChild(opt);
    }
    UI.modelSelect.appendChild(ogPre);

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

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

    if(UI.modelSelect.querySelector(`option[value="${config.selectedModel}"]`)){
      UI.modelSelect.value = config.selectedModel;
    } else {
      UI.modelSelect.value = "custom";
    }
    UI.modelDesc.textContent = modelDescDB[config.selectedModel] || "User-defined model";
    UI.customModelArea.style.display = (config.selectedModel==="custom")?"block":"none";
  }

  // (9) EVENT BIND
  UI.logsBtn.addEventListener("click",()=>{
    if(UI.logArea.style.display==="none"){
      UI.logArea.style.display="block";
      UI.logsBtn.textContent=langText[config.language].logsHide;
    } else {
      UI.logArea.style.display="none";
      UI.logsBtn.textContent=langText[config.language].logsButton;
    }
  });
  UI.closeBtn.addEventListener("click",()=>{
    panel.style.display="none";
    logMsg("User closed panel");
  });
  UI.modeSelect.addEventListener("change",()=>{
    config.mode = UI.modeSelect.value;
    if(config.mode==="autoFill"){
      UI.answerBox.style.display="none";
      UI.autoSubmitRow.style.display="block";
      alert(langText[config.language].disclaimAutoFill);
    } else {
      UI.answerBox.style.display="none";
      UI.autoSubmitRow.style.display="none";
    }
  });
  UI.startBtn.addEventListener("click",()=>{
    startAnswer();
  });
  UI.rollbackBtn.addEventListener("click",()=>{
    if(config.lastState){
      const div = getQuestionDiv();
      if(div){
        div.innerHTML = config.lastState;
        logMsg("Rolled back to previous question content");
      }
    } else {
      logMsg("No stored state for rollback");
    }
  });
  UI.confAssistBtn.addEventListener("click",()=>{
    openConfigAssistant();
  });
  UI.autoSubmitToggle.addEventListener("change",()=>{
    config.autoSubmit = UI.autoSubmitToggle.checked;
    logDump("AutoSubmit?", config.autoSubmit);
  });
  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",
        discovered:false,
        modelList:[]
      };
    }
    UI.customModelArea.style.display=(config.selectedModel==="custom")?"block":"none";
    UI.modelDesc.textContent=modelDescDB[config.selectedModel]||"User-defined model";
    UI.txtApiKey.value = modelConfigs[config.selectedModel].apiKey || "";
    UI.txtApiBase.value = modelConfigs[config.selectedModel].apiBase || "";
    // if user picks deepseek
    if(config.selectedModel.toLowerCase().includes("deepseek")){
      UI.txtApiBase.value="https://api.deepseek.com/v1/chat/completions";
      modelConfigs[config.selectedModel].apiBase="https://api.deepseek.com/v1/chat/completions";
    }
    updateManageLink();
  });
  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",
        discovered:false,
        modelList:[]
      };
    }
    buildModelSelect();
    UI.modelSelect.value="custom";
    UI.txtApiKey.value=modelConfigs[name].apiKey||"";
    UI.txtApiBase.value=modelConfigs[name].apiBase||"";
    updateManageLink();
  });
  UI.langSelect.addEventListener("change",()=>{
    config.language=UI.langSelect.value;
    saveConfig();
    updateLangText();
  });
  UI.rentBtn.addEventListener("click",()=>{
    openRentPopup();
  });
  UI.saveKeyBtn.addEventListener("click",()=>{
    const k=UI.txtApiKey.value.trim();
    modelConfigs[config.selectedModel].apiKey=k;
    saveConfig();
    logMsg("Saved new API key");
  });
  UI.testKeyBtn.addEventListener("click",()=>{
    testApiKey();
  });
  UI.saveBaseBtn.addEventListener("click",()=>{
    const nb=UI.txtApiBase.value.trim();
    modelConfigs[config.selectedModel].apiBase=nb;
    saveConfig();
    logMsg("Saved new API Base");
  });
  UI.refreshBtn.addEventListener("click",()=>{
    refreshModelList();
  });

  // (10) MISC FUNCS
  function updateManageLink(){
    let mod = config.selectedModel.toLowerCase();
    let link="#";
    if(mod.includes("deepseek")){
      link="https://platform.deepseek.com/api_keys";
    } else {
      link="https://platform.openai.com/api-keys";
    }
    modelConfigs[config.selectedModel].manageUrl=link;
    UI.linkGetKey.href=link;
    saveConfig();
  }
  function openRentPopup(){
    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.4)";
    overlay.style.zIndex="999999999";

    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.width="300px";
    box.style.backgroundColor="#fff";
    box.style.borderRadius="6px";
    box.style.padding="10px";
    box.innerHTML=`
      <h3 style="margin-top:0;">Rent Key</h3>
      <p>Contact me to rent an API key:</p>
      <ul>
        <li>[email protected]</li>
        <li>[email protected]</li>
      </ul>
      <p>Thanks for supporting!</p>
      <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);
    });
  }
  function testApiKey(){
    UI.statusLine.textContent=langText[config.language].testKeyMsg;
    let conf = modelConfigs[config.selectedModel];
    const payload={
      model: config.selectedModel,
      messages:[
        {role:"system", content:"Test key."},
        {role:"user", content:"Please ONLY respond with: test success"}
      ]
    };
    GM_xmlhttpRequest({
      method:"POST",
      url:conf.apiBase,
      headers:{
        "Content-Type":"application/json",
        "Authorization":"Bearer "+conf.apiKey
      },
      data:JSON.stringify(payload),
      onload:(resp)=>{
        UI.statusLine.textContent=langText[config.language].statusIdle;
        try{
          const data=JSON.parse(resp.responseText);
          const c = data.choices[0].message.content.toLowerCase();
          if(c.includes("test success")) alert(langText[config.language].keyOK);
          else alert(langText[config.language].keyBad);
        } catch(e){
          alert("Error parse test:"+e);
        }
      },
      onerror:(err)=>{
        UI.statusLine.textContent=langText[config.language].statusIdle;
        alert("Test key error:"+JSON.stringify(err));
      }
    });
  }
  function refreshModelList(){
    const c=modelConfigs[config.selectedModel];
    if(!c)return;
    const url=c.apiBase.replace("/chat/completions","/models");
    logMsg("refreshing from: "+url);
    GM_xmlhttpRequest({
      method:"GET",
      url,
      headers:{
        "Authorization":"Bearer "+c.apiKey
      },
      onload:(resp)=>{
        try{
          const d=JSON.parse(resp.responseText);
          logDump("Model Refresh", d);
          if(Array.isArray(d.data)){
            const arr=d.data.map(x=>x.id);
            c.modelList=arr;
            for(let m of arr){
              if(!modelConfigs[m]){
                modelConfigs[m]={
                  apiKey:c.apiKey,
                  apiBase:c.apiBase,
                  discovered:true,
                  modelList:[]
                };
              }
            }
            saveConfig();
            buildModelSelect();
            alert("Found models: "+arr.join(", "));
          }
        }catch(e){
          alert("Error parse model list:"+e);
        }
      },
      onerror:(err)=>{
        alert("Refresh error:"+JSON.stringify(err));
      }
    });
  }
  function openConfigAssistant(){
    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="999999999";
    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.width="320px";
    box.style.backgroundColor="#fff";
    box.style.borderRadius="6px";
    box.style.padding="10px";
    box.innerHTML=`
      <h3 style="margin-top:0;">${langText[config.language].configAssistant}</h3>
      <textarea id="assistant-inp" style="width:100%;height:80px;"></textarea>
      <button id="assistant-ask" style="margin-top:6px;">${langText[config.language].shortAI}</button>
      <button id="assistant-close" style="margin-top:6px;">${langText[config.language].closeButton}</button>
      <div id="assistant-out" style="margin-top:6px; border:1px solid #ccc; background:#fafafa; padding:6px; white-space:pre-wrap;"></div>
    `;
    overlay.appendChild(box);
    document.body.appendChild(overlay);
    const closeBtn=box.querySelector("#assistant-close");
    const askBtn=box.querySelector("#assistant-ask");
    const inp=box.querySelector("#assistant-inp");
    const out=box.querySelector("#assistant-out");
    closeBtn.addEventListener("click",()=>{
      document.body.removeChild(overlay);
    });
    askBtn.addEventListener("click",()=>{
      const q=inp.value.trim();
      if(!q)return;
      out.textContent="(waiting...)";
      askAssistant(q,(resp)=>{
        out.innerHTML=marked.parse(resp||"");
      },(err)=>{
        out.textContent="[Error] "+err;
      });
    });
  }
  function askAssistant(q,onSuccess,onError){
    const c=modelConfigs[config.selectedModel]||{};
    const pay={
      model:config.selectedModel,
      messages:[
        {role:"system", content:"You are the config assistant. Provide helpful info for user to reconfigure."},
        {role:"user", content:q}
      ]
    };
    GM_xmlhttpRequest({
      method:"POST",
      url:c.apiBase,
      headers:{
        "Content-Type":"application/json",
        "Authorization":"Bearer "+c.apiKey
      },
      data:JSON.stringify(pay),
      onload:(resp)=>{
        try{
          const d=JSON.parse(resp.responseText);
          const ans=d.choices[0].message.content;
          onSuccess(ans);
        }catch(e){
          onError("Parse error:"+e);
        }
      },
      onerror:(err)=>{
        onError(JSON.stringify(err));
      }
    });
  }

  function getQuestionDiv(){
    let d = document.evaluate(
      '/html/body/main/div/article/section/section/div/div[1]',
      document,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null
    ).singleNodeValue;
    if(!d)d=document.querySelector('main div.article, main>div, article');
    return d;
  }

  // progress
  let progressTimer=null;
  function startProgress(){
    UI.progressArea.style.display="block";
    UI.progressBar.value=0;
    progressTimer=setInterval(()=>{
      if(UI.progressBar.value<90) UI.progressBar.value+=2;
    },200);
  }
  function stopProgress(){
    if(progressTimer) clearInterval(progressTimer);
    UI.progressBar.value=100;
    setTimeout(()=>{
      UI.progressArea.style.display="none";
      UI.progressBar.value=0;
    },400);
  }

  // (11) MAIN LOGIC
  function startAnswer(){
    logMsg("User pressed StartAnswer");
    const dv=getQuestionDiv();
    if(!dv){
      logMsg("No question region found!");
      return;
    }
    config.lastState = dv.innerHTML;
    let userPrompt="HTML:\n"+dv.outerHTML+"\n";
    const latexCap = captureLatex(dv);
    if(latexCap) userPrompt+="LaTeX:\n"+latexCap+"\n";
    else {
      const c64=captureCanvas(dv);
      if(c64) userPrompt+="Canvas image base64 attached.\n";
    }

    UI.answerBox.style.display="none";
    let systemPrompt;
    if(config.mode==="autoFill"){
      systemPrompt = `
      You are an IXL math solver with automation support.
      
      Your task is to:
      1. Solve the math problem (HTML/LaTeX/canvas if provided),
      2. Output the solution using Markdown (LaTeX formulas in $...$),
      3. Provide final answer inside <answer>...</answer>,
      4. AND output a JavaScript snippet inside triple backticks to fill the answer automatically.
      
      Important rules:
      - DO NOT use LaTeX outside of Markdown or inside JavaScript.
      - DO NOT include LaTeX in the <answer> tag if you plan to auto-fill it via JS.
      - DO NOT output any math without $...$ wrapping.
      - DO NOT use (-$...$), always write $-\\frac{3}{10}$ instead.
      - Auto-fill code must be inside one single \`\`\`javascript code block.
      
      Sample structure:
      <answer>Final Answer (plain text if needed)</answer>
      
      Then the steps in Markdown + code block at the end:
      \`\`\`javascript
      // JS to fill input field
      document.querySelector("input").value = "-0.3";
      \`\`\`
      
      Avoid redundant explanations. Focus on clarity and automation.`;
    } else {
      systemPrompt = `
      You are an IXL math solver.
      
      Your task is to read the question (HTML and LaTeX/canvas if provided), analyze the math problem, and return a solution in Markdown format.
      
      - All mathematical expressions must be properly formatted using LaTeX syntax and enclosed in inline math: $...$, or block math: $$...$$.
      - Do NOT escape dollar signs. Output $...$ directly without backslashes.
      - For example, output $-\\frac{3}{10}$, NOT -$\\frac{3}{10}$ or (-$\\frac{3}{10}$).
      - Do not use backslashes outside math blocks.
      - The final numeric or symbolic answer MUST appear inside an <answer>...</answer> tag.
      - The answer tag must contain either plain text or LaTeX.
      
      You may use Markdown to present solution steps (headers, lists, etc.).
      
      Markdown output is required.`;
    }

    UI.statusLine.textContent=langText[config.language].statusWaiting;
    startProgress();

    let cConf = modelConfigs[config.selectedModel]||{};
    const pay={
      model:config.selectedModel,
      messages:[
        {role:"system", content:systemPrompt},
        {role:"user", content:userPrompt}
      ]
    };

    GM_xmlhttpRequest({
      method:"POST",
      url:cConf.apiBase,
      headers:{
        "Content-Type":"application/json",
        "Authorization":"Bearer "+cConf.apiKey
      },
      data:JSON.stringify(pay),
      onload:(resp)=>{
        stopProgress();
        try{
          const data=JSON.parse(resp.responseText);
          logDump("GPT raw", data);
          if(data.usage?.total_tokens){
            config.totalTokens+=data.usage.total_tokens;
            UI.tokenCount.textContent=langText[config.language].tokensLabel+config.totalTokens;
          }
          const fullOut=data.choices[0].message.content;
          // parse <answer> part
          const answerMatch=fullOut.match(/<answer>([\s\S]*?)<\/answer>/i);
          let finalAnswer="";
          let stepsText="";
          if(answerMatch){
            finalAnswer=answerMatch[1].trim();
            stepsText=fullOut.replace(/<answer>[\s\S]*?<\/answer>/i,"").trim();
          } else {
            finalAnswer=langText[config.language].missingAnswerTag;
            stepsText=fullOut;
          }

          // show container
          UI.answerBox.style.display=(config.mode==="displayOnly")?"block":"none";

          function wrapLatex(str) {
            // 修复常见错误:(-$frac...) → $-\frac...$
            str = str.replace(/\(-\$\\frac\{([^}]+)\}\{([^}]+)\}\$\)/g, (_, a, b) => `$-\\frac{${a}}{${b}}$`);
            // 正常包裹裸露 \frac
            str = str.replace(/\\frac\{[^}]+\}\{[^}]+\}/g, m => `$${m}$`);
            return str;
          }


          // parse steps as markdown
          function unescapeLatexDollar(str) {
            return str.replace(/\\\$/g, '$');  // 把 `\$` 转回 `$`
          }

          const stepsHtml = marked.parse(wrapLatex(unescapeLatexDollar(stepsText || "")));
          const finalHtml = marked.parse(wrapLatex(finalAnswer));


          UI.answerContent.innerHTML = finalHtml;
          UI.stepsContent.innerHTML = stepsHtml;

          if (window.MathJax) {
            MathJax.typesetPromise([UI.answerContent, UI.stepsContent])
              .then(() => logMsg("MathJax rendered LaTeX in answers."))
              .catch((e) => logMsg("MathJax render error: " + e));
          }

          if(config.mode==="autoFill"){
            // look for code
            const codeMatch=fullOut.match(/```javascript\s+([\s\S]*?)```/i);
            if(codeMatch && codeMatch[1]){
              runJsCode(codeMatch[1].trim());
              if(config.autoSubmit){
                doAutoSubmit();
              }
            } else {
              logMsg("No JS code found in GPT output for auto fill");
            }
          }
          UI.statusLine.textContent=langText[config.language].statusDone;
        } catch(e){
          UI.statusLine.textContent="Error parse GPT result";
          logDump("Parse GPT error", e);
        }
      },
      onerror:(err)=>{
        stopProgress();
        UI.statusLine.textContent=langText[config.language].requestError+JSON.stringify(err);
        logDump("Request error", err);
      }
    });
  }

  function runJsCode(codeStr){
    try{
      const sandbox={};
      (new Function("sandbox", "with(sandbox){"+codeStr+"}"))(sandbox);
    } catch(e){
      logDump("RunJS error", e);
    }
  }
  function doAutoSubmit(){
    let subBtn=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(!subBtn){
      subBtn=document.querySelector("button.submit, button[class*='submit']");
    }
    if(subBtn){
      logMsg("auto-submitting now");
      subBtn.click();
    } else {
      logMsg("no submit button found for autoSubmit");
    }
  }

  function captureLatex(div){
    const arr=div.querySelectorAll('script[type="math/tex"], .MathJax, .mjx-chtml');
    if(arr.length>0){
      let latex="";
      arr.forEach(e=>{ latex+=e.textContent+"\n"; });
      return latex;
    }
    return null;
  }
  function captureCanvas(div){
    const c=div.querySelector("canvas");
    if(c){
      const cv=document.createElement("canvas");
      cv.width=c.width;
      cv.height=c.height;
      cv.getContext("2d").drawImage(c,0,0);
      return cv.toDataURL("image/png").split(",")[1];
    }
    return null;
  }

  function getQuestionDiv(){
    let d=document.evaluate(
      '/html/body/main/div/article/section/section/div/div[1]',
      document,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null
    ).singleNodeValue;
    if(!d) d=document.querySelector('main div.article, main>div, article');
    return d;
  }

  function initAll(){
    buildModelSelect();
    let c=modelConfigs[config.selectedModel]||{};
    UI.txtApiKey.value=c.apiKey||"";
    UI.txtApiBase.value=c.apiBase||"";
    updateManageLink();
    UI.modeSelect.value=config.mode;
    if(config.mode==="displayOnly"){
      UI.answerBox.style.display="none";
      UI.autoSubmitRow.style.display="none";
    }
    UI.langSelect.value=config.language;
    updateLangText();
    logMsg("Script loaded. Marked version for MD rendering is required via @require. Full code included. Enjoy!");
  }
  window.MathJax = {
    tex: { inlineMath: [['$', '$'], ['\\(', '\\)']] },
    svg: { fontCache: 'global' }
  };
  initAll();
})();