Greasy Fork 支持简体中文。

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.

  1. // ==UserScript==
  2. // @name IXL Auto Answer (OpenAI API Required)
  3. // @namespace http://tampermonkey.net/
  4. // @version 8.6
  5. // @license GPL-3.0
  6. // @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.
  7. // @match https://*.ixl.com/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_addStyle
  10. // @require https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js
  11. // @require https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
  12. // ==/UserScript==
  13.  
  14. /*
  15. This script uses Marked (an MD rendering library). The above @require
  16. line imports marked for us to parse GPT’s output if it includes markdown.
  17.  
  18. - We keep both “Auto Fill” (with code snippet insertion) and “Display Answer Only”.
  19. - If user picks "Auto Fill", we hide the display answer container and show the
  20. auto fill disclaimers. Conversely, "Display Answer Only" will show the final
  21. answer container but won't attempt code execution.
  22. - Keep the rentKey button and highlight it as it's crucial for monetization.
  23. - GPT answer's solution steps can be parsed using `marked.parse(...)` to display HTML output.
  24. - The rest of the logic is the same: we have multiple features:
  25. * Start Answer
  26. * Rollback
  27. * AutoSubmit
  28. * Refresh models
  29. * Rent Key button (emphasized)
  30. * The entire script is self-contained with your original userScript header.
  31. */
  32.  
  33. (function() {
  34. 'use strict';
  35.  
  36. // (1) MIGRATION/CONFIG STORAGE
  37. let oldStore1 = localStorage.getItem("gpt4o-modelConfigs");
  38. let oldStore2 = localStorage.getItem("ixlAutoAnswerConfigs");
  39. let newStore = localStorage.getItem("myNewIxLStorage");
  40. if (!newStore) {
  41. if (oldStore1) {
  42. localStorage.setItem("myNewIxLStorage", oldStore1);
  43. localStorage.removeItem("gpt4o-modelConfigs");
  44. } else if (oldStore2) {
  45. localStorage.setItem("myNewIxLStorage", oldStore2);
  46. localStorage.removeItem("ixlAutoAnswerConfigs");
  47. }
  48. }
  49.  
  50. let modelConfigs = JSON.parse(localStorage.getItem("myNewIxLStorage") || "{}");
  51. if (!modelConfigs["gpt-4.1"]) {
  52. modelConfigs["gpt-4.1"] = {
  53. apiKey: "",
  54. apiBase: "https://api.openai.com/v1/chat/completions",
  55. discovered: false,
  56. modelList: []
  57. };
  58. }
  59.  
  60. let config = {
  61. selectedModel: "gpt-4.1",
  62. language: localStorage.getItem("myIxLLang") || "en",
  63. mode: "displayOnly", // can be "autoFill" or "displayOnly"
  64. autoSubmit: false,
  65. totalTokens: 0,
  66. lastState: null
  67. };
  68.  
  69. function saveConfig() {
  70. localStorage.setItem("myNewIxLStorage", JSON.stringify(modelConfigs));
  71. localStorage.setItem("myIxLLang", config.language);
  72. }
  73.  
  74. // (2) MULTI-LANG TEXT
  75. const langText = {
  76. en: {
  77. panelTitle: "IXL Auto Answer (OpenAI API Required)",
  78. modeLabel: "Mode",
  79. modeAuto: "Auto Fill (Unstable)",
  80. modeDisp: "Display Answer Only",
  81. startButton: "Start Answering",
  82. rollbackButton: "Rollback",
  83. configAssistant: "Config Assistant",
  84. closeButton: "Close",
  85. logsButton: "Logs",
  86. logsHide: "Hide Logs",
  87. tokensLabel: "Tokens: ",
  88. statusIdle: "Status: Idle",
  89. statusWaiting: "Waiting for GPT...",
  90. statusDone: "Done.",
  91. requestError: "Request error: ",
  92. finalAnswerTitle: "Final Answer",
  93. stepsTitle: "Solution Steps",
  94. missingAnswerTag: "Missing <answer> tag",
  95. modelSelectLabel: "Model",
  96. modelDescLabel: "Model Description",
  97. customModelPlaceholder: "Custom model name",
  98. languageLabel: "Language",
  99. autoSubmitLabel: "Auto Submit",
  100. rentKeyButton: "Rent Key (Support Me!)",
  101. apiKeyLabel: "API Key",
  102. saveButton: "Save",
  103. testKeyButton: "Test Key",
  104. testKeyMsg: "Testing key...",
  105. keyOK: "API key valid.",
  106. keyBad: "API key invalid (missing 'test success').",
  107. placeKey: "Enter your API key",
  108. placeBase: "Enter your API base URL",
  109. apiBaseLabel: "API Base",
  110. refreshModels: "Refresh Models",
  111. getKeyLinkLabel: "Get API Key",
  112. disclaimAutoFill: "Warning: Auto Fill is unstable. Use carefully."
  113. },
  114. zh: {
  115. panelTitle: "IXL自动解题 (OpenAI)",
  116. modeLabel: "模式",
  117. modeAuto: "自动填入(不稳定)",
  118. modeDisp: "仅展示答案",
  119. startButton: "开始答题",
  120. rollbackButton: "撤回",
  121. configAssistant: "配置助手",
  122. closeButton: "关闭",
  123. logsButton: "日志",
  124. logsHide: "隐藏日志",
  125. tokensLabel: "用量: ",
  126. statusIdle: "状态:空闲",
  127. statusWaiting: "等待GPT...",
  128. statusDone: "完成。",
  129. requestError: "请求错误:",
  130. finalAnswerTitle: "最终答案",
  131. stepsTitle: "解题过程",
  132. missingAnswerTag: "缺少<answer>标签",
  133. modelSelectLabel: "模型",
  134. modelDescLabel: "模型介绍",
  135. customModelPlaceholder: "自定义模型名称",
  136. languageLabel: "语言",
  137. autoSubmitLabel: "自动提交",
  138. rentKeyButton: "租用Key (支持我!)",
  139. apiKeyLabel: "API密钥",
  140. saveButton: "保存",
  141. testKeyButton: "测试密钥",
  142. testKeyMsg: "正在测试...",
  143. keyOK: "API密钥有效。",
  144. keyBad: "API密钥无效(缺'test success')",
  145. placeKey: "输入API密钥",
  146. placeBase: "输入API基础地址",
  147. apiBaseLabel: "API基础地址",
  148. refreshModels: "刷新模型列表",
  149. getKeyLinkLabel: "获取API Key",
  150. disclaimAutoFill: "警告:自动填入模式可能不稳定,请慎用。"
  151. }
  152. };
  153.  
  154. // (3) MODEL DESCRIPTIONS (Fixed English)
  155. const modelDescDB = {
  156. "gpt-4.1": "New Model, cheaper and a lot better than 4o",
  157. "gpt-4.1-mini": "New Model, cheaper and a little bit better than 4o",
  158. "gpt-4o": "Solves images, cost-effective.",
  159. "gpt-4o-mini": "Text-only, cheaper.",
  160. "o1": "Best for images but slow & expensive.",
  161. "o3-mini": "Text-only, cheaper than o1.",
  162. "deepseek-reasoner": "No images, cheaper than o1.",
  163. "deepseek-chat": "No images, cheap & fast as 4o.",
  164. "custom": "User-defined model",
  165. "o3": "Advanced multi-step reasoning model, optimized for deep inference and cost-effective over o1.",
  166. "o4-mini": "Compact variant of the o4 architecture, offering a balanced trade-off between speed, accuracy, and cost for text-only workloads.",
  167. "chatgpt-4o-least": "RLHF version, better than 4o, can be error-prone.",
  168. };
  169.  
  170. // (4) BUILD UI
  171. const panel = document.createElement("div");
  172. panel.id = "ixl-auto-panel";
  173. panel.innerHTML = `
  174. <div class="ixl-header">
  175. <span id="panel-title">${langText[config.language].panelTitle}</span>
  176. <span id="token-count">${langText[config.language].tokensLabel}0</span>
  177. <button id="btn-logs">${langText[config.language].logsButton}</button>
  178. <button id="btn-close">${langText[config.language].closeButton}</button>
  179. </div>
  180. <div class="ixl-content">
  181. <div class="row">
  182. <label>${langText[config.language].modeLabel}:</label>
  183. <select id="sel-mode" style="width:100%;">
  184. <option value="autoFill">${langText[config.language].modeAuto}</option>
  185. <option value="displayOnly">${langText[config.language].modeDisp}</option>
  186. </select>
  187. </div>
  188. <div class="row" style="margin-top:8px; display:flex; gap:8px;">
  189. <button id="btn-start" class="btn-accent" style="flex:1;">${langText[config.language].startButton}</button>
  190. <button id="btn-rollback" class="btn-normal" style="flex:1;">${langText[config.language].rollbackButton}</button>
  191. <button id="btn-config-assist" class="btn-mini" style="flex:0;">${langText[config.language].configAssistant}</button>
  192. </div>
  193. <div id="answer-box" style="display:none; border:1px solid #999; padding:6px; background:#fff; margin-top:6px;">
  194. <h4 id="answer-title">${langText[config.language].finalAnswerTitle}</h4>
  195. <div id="answer-content" style="font-size:15px; font-weight:bold; color:#080;"></div>
  196. <hr/>
  197. <h5 id="steps-title">${langText[config.language].stepsTitle}</h5>
  198. <div id="steps-content" style="font-size:13px; color:#666;"></div>
  199. </div>
  200. <div id="progress-area" style="display:none; margin-top:8px;">
  201. <progress id="progress-bar" max="100" value="0" style="width:100%;"></progress>
  202. <span id="progress-label">${langText[config.language].statusWaiting}</span>
  203. </div>
  204. <p id="status-line" style="font-weight:bold; margin-top:6px;">${langText[config.language].statusIdle}</p>
  205. <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>
  206. <div class="row" style="margin-top:10px;">
  207. <label>${langText[config.language].modelSelectLabel}:</label>
  208. <select id="sel-model" style="width:100%;"></select>
  209. <p id="model-desc" style="font-size:12px; color:#666; margin:4px 0;"></p>
  210. <div id="custom-model-area" style="display:none;">
  211. <input type="text" id="custom-model-input" style="width:100%;" placeholder="${langText[config.language].customModelPlaceholder}" />
  212. </div>
  213. </div>
  214. <div class="row" style="margin-top:8px;">
  215. <label>${langText[config.language].languageLabel}:</label>
  216. <select id="sel-lang" style="width:100%;">
  217. <option value="en">English</option>
  218. <option value="zh">中文</option>
  219. </select>
  220. </div>
  221. <div id="auto-submit-row" style="margin-top:8px;">
  222. <label style="display:block;">${langText[config.language].autoSubmitLabel}:</label>
  223. <input type="checkbox" id="chk-auto-submit"/>
  224. </div>
  225. <button id="btn-rent" class="btn-normal" style="margin-top:10px; width:100%; font-weight:bold;">
  226. ${langText[config.language].rentKeyButton}
  227. </button>
  228. <div class="row" style="margin-top:10px;">
  229. <label>${langText[config.language].apiKeyLabel}:</label>
  230. <div style="display:flex; gap:4px; margin-top:4px;">
  231. <input type="password" id="txt-apikey" style="flex:1;" placeholder="${langText[config.language].placeKey}"/>
  232. <button id="btn-save-key">${langText[config.language].saveButton}</button>
  233. <button id="btn-test-key">${langText[config.language].testKeyButton}</button>
  234. </div>
  235. </div>
  236. <div class="row" style="margin-top:8px;">
  237. <label>${langText[config.language].apiBaseLabel}:</label>
  238. <div style="display:flex; gap:4px; margin-top:4px;">
  239. <input type="text" id="txt-apibase" style="flex:1;" placeholder="${langText[config.language].placeBase}"/>
  240. <button id="btn-save-base">${langText[config.language].saveButton}</button>
  241. </div>
  242. </div>
  243. <label style="margin-top:6px; display:block;">${langText[config.language].getKeyLinkLabel}:</label>
  244. <div style="display:flex; gap:4px; margin-top:4px;">
  245. <a id="link-getkey" href="#" target="_blank" class="link-btn" style="flex:1;">Link</a>
  246. <button id="btn-refresh" class="btn-normal" style="flex:1;">${langText[config.language].refreshModels}</button>
  247. </div>
  248. </div>
  249. `;
  250. document.body.appendChild(panel);
  251.  
  252. // (5) CSS
  253. GM_addStyle(`
  254. #ixl-auto-panel {
  255. position: fixed;
  256. top:20px;
  257. right:20px;
  258. width:460px;
  259. background:#fff;
  260. border-radius:6px;
  261. box-shadow:0 2px 10px rgba(0,0,0,0.3);
  262. z-index:99999999;
  263. font-size:14px;
  264. font-family: "Segoe UI", Arial, sans-serif;
  265. }
  266. .ixl-header {
  267. background:#4caf50;
  268. color:#fff;
  269. padding:6px;
  270. display:flex;
  271. align-items:center;
  272. justify-content:flex-end;
  273. gap:6px;
  274. }
  275. #panel-title {
  276. font-weight:bold;
  277. margin-right:auto;
  278. }
  279. .ixl-content {
  280. padding:10px;
  281. }
  282. .row { margin-top:6px; }
  283. .btn-accent {
  284. background:#f0ad4e; color:#fff; border:none; border-radius:4px; font-weight:bold;
  285. }
  286. .btn-accent:hover { background:#ec971f; }
  287. .btn-normal {
  288. background:#ddd; color:#333; border:none; border-radius:4px;
  289. }
  290. .btn-normal:hover {
  291. background:#ccc;
  292. }
  293. .btn-mini {
  294. background:#bbb; color:#333; border:none; border-radius:4px;
  295. font-size:12px; padding:4px 6px;
  296. }
  297. .btn-mini:hover {
  298. background:#aaa;
  299. }
  300. .link-btn {
  301. background:#2f8ee0; color:#fff; border-radius:4px;
  302. text-decoration:none; text-align:center; padding:6px;
  303. }
  304. .link-btn:hover { opacity:0.8; }
  305. `);
  306.  
  307. // (6) REFS
  308. const UI = {
  309. panel,
  310. logArea: document.getElementById("log-area"),
  311. logsBtn: document.getElementById("btn-logs"),
  312. closeBtn: document.getElementById("btn-close"),
  313. tokenCount: document.getElementById("token-count"),
  314. modeSelect: document.getElementById("sel-mode"),
  315. startBtn: document.getElementById("btn-start"),
  316. rollbackBtn: document.getElementById("btn-rollback"),
  317. confAssistBtn: document.getElementById("btn-config-assist"),
  318. answerBox: document.getElementById("answer-box"),
  319. answerContent: document.getElementById("answer-content"),
  320. stepsContent: document.getElementById("steps-content"),
  321. progressArea: document.getElementById("progress-area"),
  322. progressBar: document.getElementById("progress-bar"),
  323. progressLabel: document.getElementById("progress-label"),
  324. statusLine: document.getElementById("status-line"),
  325. modelSelect: document.getElementById("sel-model"),
  326. modelDesc: document.getElementById("model-desc"),
  327. customModelArea: document.getElementById("custom-model-area"),
  328. customModelInput: document.getElementById("custom-model-input"),
  329. langSelect: document.getElementById("sel-lang"),
  330. autoSubmitRow: document.getElementById("auto-submit-row"),
  331. autoSubmitToggle: document.getElementById("chk-auto-submit"),
  332. rentBtn: document.getElementById("btn-rent"),
  333. txtApiKey: document.getElementById("txt-apikey"),
  334. saveKeyBtn: document.getElementById("btn-save-key"),
  335. testKeyBtn: document.getElementById("btn-test-key"),
  336. txtApiBase: document.getElementById("txt-apibase"),
  337. saveBaseBtn: document.getElementById("btn-save-base"),
  338. linkGetKey: document.getElementById("link-getkey"),
  339. refreshBtn: document.getElementById("btn-refresh")
  340. };
  341.  
  342. // (7) UTILS
  343. function logMsg(msg) {
  344. const time = new Date().toLocaleString();
  345. const div = document.createElement("div");
  346. div.textContent = `[${time}] ${msg}`;
  347. UI.logArea.appendChild(div);
  348. console.log("[Log]", msg);
  349. }
  350. function logDump(label, val) {
  351. let m = `[DUMP] ${label}: `;
  352. try { m += JSON.stringify(val); } catch(e){ m += String(val); }
  353. logMsg(m);
  354. }
  355. function updateLangText() {
  356. UI.logsBtn.textContent = (UI.logArea.style.display==="none") ? langText[config.language].logsButton : langText[config.language].logsHide;
  357. UI.closeBtn.textContent = langText[config.language].closeButton;
  358. UI.tokenCount.textContent = langText[config.language].tokensLabel + config.totalTokens;
  359. UI.statusLine.textContent = langText[config.language].statusIdle;
  360. UI.progressLabel.textContent = langText[config.language].statusWaiting;
  361. UI.modeSelect.options[0].text = langText[config.language].modeAuto;
  362. UI.modeSelect.options[1].text = langText[config.language].modeDisp;
  363. UI.startBtn.textContent = langText[config.language].startButton;
  364. UI.rollbackBtn.textContent = langText[config.language].rollbackButton;
  365. UI.confAssistBtn.textContent = langText[config.language].configAssistant;
  366. document.getElementById("answer-title").textContent = langText[config.language].finalAnswerTitle;
  367. document.getElementById("steps-title").textContent = langText[config.language].stepsTitle;
  368. UI.txtApiKey.placeholder = langText[config.language].placeKey;
  369. UI.saveKeyBtn.textContent = langText[config.language].saveButton;
  370. UI.testKeyBtn.textContent = langText[config.language].testKeyButton;
  371. UI.txtApiBase.placeholder = langText[config.language].placeBase;
  372. UI.saveBaseBtn.textContent = langText[config.language].saveButton;
  373. UI.linkGetKey.textContent = "Link";
  374. UI.refreshBtn.textContent = langText[config.language].refreshModels;
  375. UI.rentBtn.textContent = langText[config.language].rentKeyButton;
  376. }
  377.  
  378. // (8) BUILD MODEL SELECT
  379. function buildModelSelect() {
  380. UI.modelSelect.innerHTML = "";
  381. const ogPre = document.createElement("optgroup");
  382. ogPre.label = "Predefined";
  383. 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"];
  384. for(const b of builtins){
  385. const opt = document.createElement("option");
  386. opt.value = b;
  387. opt.textContent = b;
  388. ogPre.appendChild(opt);
  389. }
  390. UI.modelSelect.appendChild(ogPre);
  391.  
  392. const discovered = Object.keys(modelConfigs).filter(k=>modelConfigs[k].discovered);
  393. if(discovered.length>0){
  394. const ogDisc = document.createElement("optgroup");
  395. ogDisc.label = "Discovered";
  396. discovered.forEach(m=>{
  397. const opt = document.createElement("option");
  398. opt.value = m;
  399. opt.textContent = m;
  400. ogDisc.appendChild(opt);
  401. });
  402. UI.modelSelect.appendChild(ogDisc);
  403. }
  404.  
  405. const optCust = document.createElement("option");
  406. optCust.value = "custom";
  407. optCust.textContent = "custom";
  408. UI.modelSelect.appendChild(optCust);
  409.  
  410. if(UI.modelSelect.querySelector(`option[value="${config.selectedModel}"]`)){
  411. UI.modelSelect.value = config.selectedModel;
  412. } else {
  413. UI.modelSelect.value = "custom";
  414. }
  415. UI.modelDesc.textContent = modelDescDB[config.selectedModel] || "User-defined model";
  416. UI.customModelArea.style.display = (config.selectedModel==="custom")?"block":"none";
  417. }
  418.  
  419. // (9) EVENT BIND
  420. UI.logsBtn.addEventListener("click",()=>{
  421. if(UI.logArea.style.display==="none"){
  422. UI.logArea.style.display="block";
  423. UI.logsBtn.textContent=langText[config.language].logsHide;
  424. } else {
  425. UI.logArea.style.display="none";
  426. UI.logsBtn.textContent=langText[config.language].logsButton;
  427. }
  428. });
  429. UI.closeBtn.addEventListener("click",()=>{
  430. panel.style.display="none";
  431. logMsg("User closed panel");
  432. });
  433. UI.modeSelect.addEventListener("change",()=>{
  434. config.mode = UI.modeSelect.value;
  435. if(config.mode==="autoFill"){
  436. UI.answerBox.style.display="none";
  437. UI.autoSubmitRow.style.display="block";
  438. alert(langText[config.language].disclaimAutoFill);
  439. } else {
  440. UI.answerBox.style.display="none";
  441. UI.autoSubmitRow.style.display="none";
  442. }
  443. });
  444. UI.startBtn.addEventListener("click",()=>{
  445. startAnswer();
  446. });
  447. UI.rollbackBtn.addEventListener("click",()=>{
  448. if(config.lastState){
  449. const div = getQuestionDiv();
  450. if(div){
  451. div.innerHTML = config.lastState;
  452. logMsg("Rolled back to previous question content");
  453. }
  454. } else {
  455. logMsg("No stored state for rollback");
  456. }
  457. });
  458. UI.confAssistBtn.addEventListener("click",()=>{
  459. openConfigAssistant();
  460. });
  461. UI.autoSubmitToggle.addEventListener("change",()=>{
  462. config.autoSubmit = UI.autoSubmitToggle.checked;
  463. logDump("AutoSubmit?", config.autoSubmit);
  464. });
  465. UI.modelSelect.addEventListener("change",()=>{
  466. config.selectedModel = UI.modelSelect.value;
  467. if(!modelConfigs[config.selectedModel]){
  468. modelConfigs[config.selectedModel] = {
  469. apiKey:"",
  470. apiBase:"https://api.openai.com/v1/chat/completions",
  471. discovered:false,
  472. modelList:[]
  473. };
  474. }
  475. UI.customModelArea.style.display=(config.selectedModel==="custom")?"block":"none";
  476. UI.modelDesc.textContent=modelDescDB[config.selectedModel]||"User-defined model";
  477. UI.txtApiKey.value = modelConfigs[config.selectedModel].apiKey || "";
  478. UI.txtApiBase.value = modelConfigs[config.selectedModel].apiBase || "";
  479. // if user picks deepseek
  480. if(config.selectedModel.toLowerCase().includes("deepseek")){
  481. UI.txtApiBase.value="https://api.deepseek.com/v1/chat/completions";
  482. modelConfigs[config.selectedModel].apiBase="https://api.deepseek.com/v1/chat/completions";
  483. }
  484. updateManageLink();
  485. });
  486. UI.customModelInput.addEventListener("change",()=>{
  487. const name = UI.customModelInput.value.trim();
  488. if(!name)return;
  489. config.selectedModel=name;
  490. if(!modelConfigs[name]){
  491. modelConfigs[name]={
  492. apiKey:"",
  493. apiBase:"https://api.openai.com/v1/chat/completions",
  494. discovered:false,
  495. modelList:[]
  496. };
  497. }
  498. buildModelSelect();
  499. UI.modelSelect.value="custom";
  500. UI.txtApiKey.value=modelConfigs[name].apiKey||"";
  501. UI.txtApiBase.value=modelConfigs[name].apiBase||"";
  502. updateManageLink();
  503. });
  504. UI.langSelect.addEventListener("change",()=>{
  505. config.language=UI.langSelect.value;
  506. saveConfig();
  507. updateLangText();
  508. });
  509. UI.rentBtn.addEventListener("click",()=>{
  510. openRentPopup();
  511. });
  512. UI.saveKeyBtn.addEventListener("click",()=>{
  513. const k=UI.txtApiKey.value.trim();
  514. modelConfigs[config.selectedModel].apiKey=k;
  515. saveConfig();
  516. logMsg("Saved new API key");
  517. });
  518. UI.testKeyBtn.addEventListener("click",()=>{
  519. testApiKey();
  520. });
  521. UI.saveBaseBtn.addEventListener("click",()=>{
  522. const nb=UI.txtApiBase.value.trim();
  523. modelConfigs[config.selectedModel].apiBase=nb;
  524. saveConfig();
  525. logMsg("Saved new API Base");
  526. });
  527. UI.refreshBtn.addEventListener("click",()=>{
  528. refreshModelList();
  529. });
  530.  
  531. // (10) MISC FUNCS
  532. function updateManageLink(){
  533. let mod = config.selectedModel.toLowerCase();
  534. let link="#";
  535. if(mod.includes("deepseek")){
  536. link="https://platform.deepseek.com/api_keys";
  537. } else {
  538. link="https://platform.openai.com/api-keys";
  539. }
  540. modelConfigs[config.selectedModel].manageUrl=link;
  541. UI.linkGetKey.href=link;
  542. saveConfig();
  543. }
  544. function openRentPopup(){
  545. const overlay=document.createElement("div");
  546. overlay.style.position="fixed";
  547. overlay.style.top="0"; overlay.style.left="0";
  548. overlay.style.width="100%"; overlay.style.height="100%";
  549. overlay.style.backgroundColor="rgba(0,0,0,0.4)";
  550. overlay.style.zIndex="999999999";
  551.  
  552. const box=document.createElement("div");
  553. box.style.position="absolute";
  554. box.style.top="50%"; box.style.left="50%";
  555. box.style.transform="translate(-50%,-50%)";
  556. box.style.width="300px";
  557. box.style.backgroundColor="#fff";
  558. box.style.borderRadius="6px";
  559. box.style.padding="10px";
  560. box.innerHTML=`
  561. <h3 style="margin-top:0;">Rent Key</h3>
  562. <p>Contact me to rent an API key:</p>
  563. <ul>
  564. <li>felixliujy@Gmail.com</li>
  565. <li>admin@obanarchy.org</li>
  566. </ul>
  567. <p>Thanks for supporting!</p>
  568. <button id="rent-close-btn">${langText[config.language].closeButton}</button>
  569. `;
  570. overlay.appendChild(box);
  571. document.body.appendChild(overlay);
  572. box.querySelector("#rent-close-btn").addEventListener("click",()=>{
  573. document.body.removeChild(overlay);
  574. });
  575. }
  576. function testApiKey(){
  577. UI.statusLine.textContent=langText[config.language].testKeyMsg;
  578. let conf = modelConfigs[config.selectedModel];
  579. const payload={
  580. model: config.selectedModel,
  581. messages:[
  582. {role:"system", content:"Test key."},
  583. {role:"user", content:"Please ONLY respond with: test success"}
  584. ]
  585. };
  586. GM_xmlhttpRequest({
  587. method:"POST",
  588. url:conf.apiBase,
  589. headers:{
  590. "Content-Type":"application/json",
  591. "Authorization":"Bearer "+conf.apiKey
  592. },
  593. data:JSON.stringify(payload),
  594. onload:(resp)=>{
  595. UI.statusLine.textContent=langText[config.language].statusIdle;
  596. try{
  597. const data=JSON.parse(resp.responseText);
  598. const c = data.choices[0].message.content.toLowerCase();
  599. if(c.includes("test success")) alert(langText[config.language].keyOK);
  600. else alert(langText[config.language].keyBad);
  601. } catch(e){
  602. alert("Error parse test:"+e);
  603. }
  604. },
  605. onerror:(err)=>{
  606. UI.statusLine.textContent=langText[config.language].statusIdle;
  607. alert("Test key error:"+JSON.stringify(err));
  608. }
  609. });
  610. }
  611. function refreshModelList(){
  612. const c=modelConfigs[config.selectedModel];
  613. if(!c)return;
  614. const url=c.apiBase.replace("/chat/completions","/models");
  615. logMsg("refreshing from: "+url);
  616. GM_xmlhttpRequest({
  617. method:"GET",
  618. url,
  619. headers:{
  620. "Authorization":"Bearer "+c.apiKey
  621. },
  622. onload:(resp)=>{
  623. try{
  624. const d=JSON.parse(resp.responseText);
  625. logDump("Model Refresh", d);
  626. if(Array.isArray(d.data)){
  627. const arr=d.data.map(x=>x.id);
  628. c.modelList=arr;
  629. for(let m of arr){
  630. if(!modelConfigs[m]){
  631. modelConfigs[m]={
  632. apiKey:c.apiKey,
  633. apiBase:c.apiBase,
  634. discovered:true,
  635. modelList:[]
  636. };
  637. }
  638. }
  639. saveConfig();
  640. buildModelSelect();
  641. alert("Found models: "+arr.join(", "));
  642. }
  643. }catch(e){
  644. alert("Error parse model list:"+e);
  645. }
  646. },
  647. onerror:(err)=>{
  648. alert("Refresh error:"+JSON.stringify(err));
  649. }
  650. });
  651. }
  652. function openConfigAssistant(){
  653. const overlay=document.createElement("div");
  654. overlay.style.position="fixed";
  655. overlay.style.top="0"; overlay.style.left="0";
  656. overlay.style.width="100%"; overlay.style.height="100%";
  657. overlay.style.backgroundColor="rgba(0,0,0,0.5)";
  658. overlay.style.zIndex="999999999";
  659. const box=document.createElement("div");
  660. box.style.position="absolute";
  661. box.style.top="50%"; box.style.left="50%";
  662. box.style.transform="translate(-50%,-50%)";
  663. box.style.width="320px";
  664. box.style.backgroundColor="#fff";
  665. box.style.borderRadius="6px";
  666. box.style.padding="10px";
  667. box.innerHTML=`
  668. <h3 style="margin-top:0;">${langText[config.language].configAssistant}</h3>
  669. <textarea id="assistant-inp" style="width:100%;height:80px;"></textarea>
  670. <button id="assistant-ask" style="margin-top:6px;">${langText[config.language].shortAI}</button>
  671. <button id="assistant-close" style="margin-top:6px;">${langText[config.language].closeButton}</button>
  672. <div id="assistant-out" style="margin-top:6px; border:1px solid #ccc; background:#fafafa; padding:6px; white-space:pre-wrap;"></div>
  673. `;
  674. overlay.appendChild(box);
  675. document.body.appendChild(overlay);
  676. const closeBtn=box.querySelector("#assistant-close");
  677. const askBtn=box.querySelector("#assistant-ask");
  678. const inp=box.querySelector("#assistant-inp");
  679. const out=box.querySelector("#assistant-out");
  680. closeBtn.addEventListener("click",()=>{
  681. document.body.removeChild(overlay);
  682. });
  683. askBtn.addEventListener("click",()=>{
  684. const q=inp.value.trim();
  685. if(!q)return;
  686. out.textContent="(waiting...)";
  687. askAssistant(q,(resp)=>{
  688. out.innerHTML=marked.parse(resp||"");
  689. },(err)=>{
  690. out.textContent="[Error] "+err;
  691. });
  692. });
  693. }
  694. function askAssistant(q,onSuccess,onError){
  695. const c=modelConfigs[config.selectedModel]||{};
  696. const pay={
  697. model:config.selectedModel,
  698. messages:[
  699. {role:"system", content:"You are the config assistant. Provide helpful info for user to reconfigure."},
  700. {role:"user", content:q}
  701. ]
  702. };
  703. GM_xmlhttpRequest({
  704. method:"POST",
  705. url:c.apiBase,
  706. headers:{
  707. "Content-Type":"application/json",
  708. "Authorization":"Bearer "+c.apiKey
  709. },
  710. data:JSON.stringify(pay),
  711. onload:(resp)=>{
  712. try{
  713. const d=JSON.parse(resp.responseText);
  714. const ans=d.choices[0].message.content;
  715. onSuccess(ans);
  716. }catch(e){
  717. onError("Parse error:"+e);
  718. }
  719. },
  720. onerror:(err)=>{
  721. onError(JSON.stringify(err));
  722. }
  723. });
  724. }
  725.  
  726. function getQuestionDiv(){
  727. let d = document.evaluate(
  728. '/html/body/main/div/article/section/section/div/div[1]',
  729. document,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null
  730. ).singleNodeValue;
  731. if(!d)d=document.querySelector('main div.article, main>div, article');
  732. return d;
  733. }
  734.  
  735. // progress
  736. let progressTimer=null;
  737. function startProgress(){
  738. UI.progressArea.style.display="block";
  739. UI.progressBar.value=0;
  740. progressTimer=setInterval(()=>{
  741. if(UI.progressBar.value<90) UI.progressBar.value+=2;
  742. },200);
  743. }
  744. function stopProgress(){
  745. if(progressTimer) clearInterval(progressTimer);
  746. UI.progressBar.value=100;
  747. setTimeout(()=>{
  748. UI.progressArea.style.display="none";
  749. UI.progressBar.value=0;
  750. },400);
  751. }
  752.  
  753. // (11) MAIN LOGIC
  754. function startAnswer(){
  755. logMsg("User pressed StartAnswer");
  756. const dv=getQuestionDiv();
  757. if(!dv){
  758. logMsg("No question region found!");
  759. return;
  760. }
  761. config.lastState = dv.innerHTML;
  762. let userPrompt="HTML:\n"+dv.outerHTML+"\n";
  763. const latexCap = captureLatex(dv);
  764. if(latexCap) userPrompt+="LaTeX:\n"+latexCap+"\n";
  765. else {
  766. const c64=captureCanvas(dv);
  767. if(c64) userPrompt+="Canvas image base64 attached.\n";
  768. }
  769.  
  770. UI.answerBox.style.display="none";
  771. let systemPrompt;
  772. if(config.mode==="autoFill"){
  773. systemPrompt = `
  774. You are an IXL math solver with automation support.
  775. Your task is to:
  776. 1. Solve the math problem (HTML/LaTeX/canvas if provided),
  777. 2. Output the solution using Markdown (LaTeX formulas in $...$),
  778. 3. Provide final answer inside <answer>...</answer>,
  779. 4. AND output a JavaScript snippet inside triple backticks to fill the answer automatically.
  780. Important rules:
  781. - DO NOT use LaTeX outside of Markdown or inside JavaScript.
  782. - DO NOT include LaTeX in the <answer> tag if you plan to auto-fill it via JS.
  783. - DO NOT output any math without $...$ wrapping.
  784. - DO NOT use (-$...$), always write $-\\frac{3}{10}$ instead.
  785. - Auto-fill code must be inside one single \`\`\`javascript code block.
  786. Sample structure:
  787. <answer>Final Answer (plain text if needed)</answer>
  788. Then the steps in Markdown + code block at the end:
  789. \`\`\`javascript
  790. // JS to fill input field
  791. document.querySelector("input").value = "-0.3";
  792. \`\`\`
  793. Avoid redundant explanations. Focus on clarity and automation.`;
  794. } else {
  795. systemPrompt = `
  796. You are an IXL math solver.
  797. Your task is to read the question (HTML and LaTeX/canvas if provided), analyze the math problem, and return a solution in Markdown format.
  798. - All mathematical expressions must be properly formatted using LaTeX syntax and enclosed in inline math: $...$, or block math: $$...$$.
  799. - Do NOT escape dollar signs. Output $...$ directly without backslashes.
  800. - For example, output $-\\frac{3}{10}$, NOT -$\\frac{3}{10}$ or (-$\\frac{3}{10}$).
  801. - Do not use backslashes outside math blocks.
  802. - The final numeric or symbolic answer MUST appear inside an <answer>...</answer> tag.
  803. - The answer tag must contain either plain text or LaTeX.
  804. You may use Markdown to present solution steps (headers, lists, etc.).
  805. Markdown output is required.`;
  806. }
  807.  
  808. UI.statusLine.textContent=langText[config.language].statusWaiting;
  809. startProgress();
  810.  
  811. let cConf = modelConfigs[config.selectedModel]||{};
  812. const pay={
  813. model:config.selectedModel,
  814. messages:[
  815. {role:"system", content:systemPrompt},
  816. {role:"user", content:userPrompt}
  817. ]
  818. };
  819.  
  820. GM_xmlhttpRequest({
  821. method:"POST",
  822. url:cConf.apiBase,
  823. headers:{
  824. "Content-Type":"application/json",
  825. "Authorization":"Bearer "+cConf.apiKey
  826. },
  827. data:JSON.stringify(pay),
  828. onload:(resp)=>{
  829. stopProgress();
  830. try{
  831. const data=JSON.parse(resp.responseText);
  832. logDump("GPT raw", data);
  833. if(data.usage?.total_tokens){
  834. config.totalTokens+=data.usage.total_tokens;
  835. UI.tokenCount.textContent=langText[config.language].tokensLabel+config.totalTokens;
  836. }
  837. const fullOut=data.choices[0].message.content;
  838. // parse <answer> part
  839. const answerMatch=fullOut.match(/<answer>([\s\S]*?)<\/answer>/i);
  840. let finalAnswer="";
  841. let stepsText="";
  842. if(answerMatch){
  843. finalAnswer=answerMatch[1].trim();
  844. stepsText=fullOut.replace(/<answer>[\s\S]*?<\/answer>/i,"").trim();
  845. } else {
  846. finalAnswer=langText[config.language].missingAnswerTag;
  847. stepsText=fullOut;
  848. }
  849.  
  850. // show container
  851. UI.answerBox.style.display=(config.mode==="displayOnly")?"block":"none";
  852.  
  853. function wrapLatex(str) {
  854. // 修复常见错误:(-$frac...) → $-\frac...$
  855. str = str.replace(/\(-\$\\frac\{([^}]+)\}\{([^}]+)\}\$\)/g, (_, a, b) => `$-\\frac{${a}}{${b}}$`);
  856. // 正常包裹裸露 \frac
  857. str = str.replace(/\\frac\{[^}]+\}\{[^}]+\}/g, m => `$${m}$`);
  858. return str;
  859. }
  860.  
  861.  
  862. // parse steps as markdown
  863. function unescapeLatexDollar(str) {
  864. return str.replace(/\\\$/g, '$'); // 把 `\$` 转回 `$`
  865. }
  866.  
  867. const stepsHtml = marked.parse(wrapLatex(unescapeLatexDollar(stepsText || "")));
  868. const finalHtml = marked.parse(wrapLatex(finalAnswer));
  869.  
  870.  
  871. UI.answerContent.innerHTML = finalHtml;
  872. UI.stepsContent.innerHTML = stepsHtml;
  873.  
  874. if (window.MathJax) {
  875. MathJax.typesetPromise([UI.answerContent, UI.stepsContent])
  876. .then(() => logMsg("MathJax rendered LaTeX in answers."))
  877. .catch((e) => logMsg("MathJax render error: " + e));
  878. }
  879.  
  880. if(config.mode==="autoFill"){
  881. // look for code
  882. const codeMatch=fullOut.match(/```javascript\s+([\s\S]*?)```/i);
  883. if(codeMatch && codeMatch[1]){
  884. runJsCode(codeMatch[1].trim());
  885. if(config.autoSubmit){
  886. doAutoSubmit();
  887. }
  888. } else {
  889. logMsg("No JS code found in GPT output for auto fill");
  890. }
  891. }
  892. UI.statusLine.textContent=langText[config.language].statusDone;
  893. } catch(e){
  894. UI.statusLine.textContent="Error parse GPT result";
  895. logDump("Parse GPT error", e);
  896. }
  897. },
  898. onerror:(err)=>{
  899. stopProgress();
  900. UI.statusLine.textContent=langText[config.language].requestError+JSON.stringify(err);
  901. logDump("Request error", err);
  902. }
  903. });
  904. }
  905.  
  906. function runJsCode(codeStr){
  907. try{
  908. const sandbox={};
  909. (new Function("sandbox", "with(sandbox){"+codeStr+"}"))(sandbox);
  910. } catch(e){
  911. logDump("RunJS error", e);
  912. }
  913. }
  914. function doAutoSubmit(){
  915. let subBtn=document.evaluate(
  916. '/html/body/main/div/article/section/section/div/div[1]/section/div/section/div/button',
  917. document,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null
  918. ).singleNodeValue;
  919. if(!subBtn){
  920. subBtn=document.querySelector("button.submit, button[class*='submit']");
  921. }
  922. if(subBtn){
  923. logMsg("auto-submitting now");
  924. subBtn.click();
  925. } else {
  926. logMsg("no submit button found for autoSubmit");
  927. }
  928. }
  929.  
  930. function captureLatex(div){
  931. const arr=div.querySelectorAll('script[type="math/tex"], .MathJax, .mjx-chtml');
  932. if(arr.length>0){
  933. let latex="";
  934. arr.forEach(e=>{ latex+=e.textContent+"\n"; });
  935. return latex;
  936. }
  937. return null;
  938. }
  939. function captureCanvas(div){
  940. const c=div.querySelector("canvas");
  941. if(c){
  942. const cv=document.createElement("canvas");
  943. cv.width=c.width;
  944. cv.height=c.height;
  945. cv.getContext("2d").drawImage(c,0,0);
  946. return cv.toDataURL("image/png").split(",")[1];
  947. }
  948. return null;
  949. }
  950.  
  951. function getQuestionDiv(){
  952. let d=document.evaluate(
  953. '/html/body/main/div/article/section/section/div/div[1]',
  954. document,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null
  955. ).singleNodeValue;
  956. if(!d) d=document.querySelector('main div.article, main>div, article');
  957. return d;
  958. }
  959.  
  960. function initAll(){
  961. buildModelSelect();
  962. let c=modelConfigs[config.selectedModel]||{};
  963. UI.txtApiKey.value=c.apiKey||"";
  964. UI.txtApiBase.value=c.apiBase||"";
  965. updateManageLink();
  966. UI.modeSelect.value=config.mode;
  967. if(config.mode==="displayOnly"){
  968. UI.answerBox.style.display="none";
  969. UI.autoSubmitRow.style.display="none";
  970. }
  971. UI.langSelect.value=config.language;
  972. updateLangText();
  973. logMsg("Script loaded. Marked version for MD rendering is required via @require. Full code included. Enjoy!");
  974. }
  975. window.MathJax = {
  976. tex: { inlineMath: [['$', '$'], ['\\(', '\\)']] },
  977. svg: { fontCache: 'global' }
  978. };
  979. initAll();
  980. })();