Prysaic Tool BigIdeasMath Pear-assistant

Support for AI dialogue answers and the environment for hiding tools.

// ==UserScript==
// @name         Prysaic Tool BigIdeasMath Pear-assistant
// @namespace    http://prysaic.com/
// @version      0.1.22-beta
// @description  Support for AI dialogue answers and the environment for hiding tools.
// @match        https://app.edulastic.com/student/assessment/*
// @match        https://*.bigideasmath.com/BIM/student/*
// @icon         https://prysaic.com/wp-content/uploads/2025/04/e69caae591bde5908d-e589afe69cac-e589afe69cac.png
// @grant        none
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/html2canvas.min.js
// @license MIT
// @compatible         firefox
// @compatible         chrome
// @compatible         opera
// @compatible         safari
// @compatible         edge
// ==/UserScript==

  // ===========================================================================================================================================================================
  //           _____                    _____                _____                    _____                    _____                    _____                    _____
  //          /\    \                  /\    \              |\    \                  /\    \                  /\    \                  /\    \                  /\    \
  //         /::\    \                /::\    \             |:\____\                /::\    \                /::\    \                /::\    \                /::\    \
  //        /::::\    \              /::::\    \            |::|   |               /::::\    \              /::::\    \               \:::\    \              /::::\    \
  //       /::::::\    \            /::::::\    \           |::|   |              /::::::\    \            /::::::\    \               \:::\    \            /::::::\    \
  //      /:::/\:::\    \          /:::/\:::\    \          |::|   |             /:::/\:::\    \          /:::/\:::\    \               \:::\    \          /:::/\:::\    \
  //     /:::/__\:::\    \        /:::/__\:::\    \         |::|   |            /:::/__\:::\    \        /:::/__\:::\    \               \:::\    \        /:::/  \:::\    \
  //    /::::\   \:::\    \      /::::\   \:::\    \        |::|   |            \:::\   \:::\    \      /::::\   \:::\    \              /::::\    \      /:::/    \:::\    \
  //   /::::::\   \:::\    \    /::::::\   \:::\    \       |::|___|______    ___\:::\   \:::\    \    /::::::\   \:::\    \    ____    /::::::\    \    /:::/    / \:::\    \
  //  /:::/\:::\   \:::\____\  /:::/\:::\   \:::\____\      /::::::::\    \  /\   \:::\   \:::\    \  /:::/\:::\   \:::\    \  /\   \  /:::/\:::\    \  /:::/    /   \:::\    \
  // /:::/  \:::\   \:::|    |/:::/  \:::\   \:::|    |    /::::::::::\____\/::\   \:::\   \:::\____\/:::/  \:::\   \:::\____\/::\   \/:::/  \:::\____\/:::/____/     \:::\____\
  // \::/    \:::\  /:::|____|\::/   |::::\  /:::|____|   /:::/~~~~/~~      \:::\   \:::\   \::/    /\::/    \:::\  /:::/    /\:::\  /:::/    \::/    /\:::\    \      \::/    /
  //  \/_____/\:::\/:::/    /  \/____|:::::\/:::/    /   /:::/    /          \:::\   \:::\   \/____/  \/____/ \:::\/:::/    /  \:::\/:::/    / \/____/  \:::\    \      \/____/
  //           \::::::/    /         |:::::::::/    /   /:::/    /            \:::\   \:::\    \               \::::::/    /    \::::::/    /            \:::\    \
  //            \::::/    /          |::|\::::/    /   /:::/    /              \:::\   \:::\____\               \::::/    /      \::::/____/              \:::\    \
  //             \::/____/           |::| \::/____/    \::/    /                \:::\  /:::/    /               /:::/    /        \:::\    \               \:::\    \
  //              ~~                 |::|  ~|           \/____/                  \:::\/:::/    /               /:::/    /          \:::\    \               \:::\    \
  //                                 |::|   |                                     \::::::/    /               /:::/    /            \:::\    \               \:::\    \
  //                                 \::|   |                                      \::::/    /               /:::/    /              \:::\____\               \:::\____\
  //                                  \:|   |                                       \::/    /                \::/    /                \::/    /                \::/    /
  //                                   \|___|                                        \/____/                  \/____/                  \/____/                  \/____/
  // ===========================================================================================================================================================================

  // ————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

  // >Support email: [email protected] 【For support with more website adaptations and other features, please contact us via email】

  // This Plugin ("Plugin") is provided "as is" and "with all faults" without any warranty, express or implied, including, without limitation, any warranties of merchantability, fitness for a particular purpose, non-infringement, or that the use of the Plugin will be uninterrupted or error-free. By using this Plugin, you expressly acknowledge and agree that:

  // 1. Use of the Plugin is entirely at your own risk. You assume full responsibility for any and all losses, damages, or adverse consequences, whether direct, indirect, incidental, special, consequential, or punitive, that may result from your use or inability to use the Plugin, including, without limitation, any loss of data, loss of profits, or any other damages or losses.

  // 2. The developer, its affiliates, licensors, or any other parties involved in the creation, development, distribution, or maintenance of the Plugin shall not be liable for any claims, demands, actions, or causes of action, including without limitation, damages for loss of use, data, or profits, arising out of or in connection with your use of or reliance on the Plugin, even if advised of the possibility of such damages.

  // 3. The Plugin is provided solely for personal, non-commercial use for learning and assistance purposes. You agree that you will not use the Plugin for any illegal, unauthorized, or unethical purposes and that you are solely responsible for ensuring compliance with all applicable laws and regulations in your jurisdiction.

  // 4. Any reliance on the information provided by the Plugin is at your own risk. The Plugin is intended solely as an aid for educational purposes and does not guarantee any specific outcomes, accuracy, or completeness of the content generated. The developer makes no representations or warranties regarding the reliability, accuracy, or timeliness of the information or results obtained through the use of the Plugin.

  // 5. By installing, accessing, or using the Plugin, you agree to indemnify, defend, and hold harmless the developer and its affiliates from and against any and all claims, damages, obligations, losses, liabilities, costs, or debt, and expenses (including but not limited to attorney's fees) arising from your use of the Plugin or any violation of these terms.

  // 6. In no event shall the developer, its affiliates, or any other parties involved be liable for any direct, indirect, incidental, special, consequential, or punitive damages, including but not limited to loss of profits, revenue, data, or use, incurred by you or any third party, whether in an action in contract, tort (including negligence), or otherwise, arising from your access to, use of, or inability to use the Plugin, even if advised of the possibility of such damages.

  // 7. This disclaimer is intended to limit the liability of the developer to the fullest extent permitted by applicable law. Some jurisdictions do not allow the exclusion or limitation of liability for incidental or consequential damages, so the above limitations may not apply to you.

  // By installing, accessing, or using the Plugin, you acknowledge that you have read, understood, and agree to be bound by the terms of this disclaimer. If you do not agree with any part of this disclaimer, you must not use the Plugin.

(function () {
  'use strict';

  // ================================================
  // 配置区
  // ================================================
  const OPENAI_API_KEY = "API"; // 请替换为你的真实 API Key
  let currentModel = localStorage.getItem("prysaic_model") || "gpt-4o-mini";
  const LANGUAGES = ["zh", "en"];
  let currentLang = localStorage.getItem("prysaic_lang") || "zh";
  let lastSwitchTime = 0;
  let hasAgreed = false; // 用户必须输入 "yes" 同意协议后才能使用
  let prevID = null;
  // 防止重复声明对话历史(用于问答模式)
  if (typeof window.__prysaicConversationHistory === "undefined") {
    window.__prysaicConversationHistory = [];
  }
  let conversationHistory = window.__prysaicConversationHistory;

  // ================================================
  // 中/英文
  // ================================================
  const i18n = {
    zh: {
      agreement: "用户协议与不可传播声明",
      confirmText: "请输入 'yes' 以同意协议,否则无法使用插件。",
      start: "确认",
      placeholder: "请输入 'yes'",
      dragHint: "📥 拖动截图图片到此开始识别(PNG / JPG)",
      thinking: "🧠 正在思考,请稍候...",
      error: "❌ 错误:",
      toastModel: "✅ 模型切换为 ",
      toastLang: "✅ 语言切换为 ",
      showPlugin: "📘 打开助手",
      verifySuccess: "验证成功!",
      qaMode: "问答模式",
      example1: "代数的常用公式",
      example2: "微积分的常用公式",
      examplePlaceholder: "请输入问题..."
    },
    en: {
      agreement: "User Agreement & Non-Distribution Policy",
      confirmText: "Type 'yes' to agree, otherwise you cannot use this plugin.",
      start: "Confirm",
      placeholder: "Enter 'yes'",
      dragHint: "📥 Drag image (PNG / JPG) here to start",
      thinking: "🧠 Thinking, please wait...",
      error: "❌ Error: ",
      toastModel: "✅ Model switched to ",
      toastLang: "✅ Language switched to ",
      showPlugin: "📘 Show Assistant",
      verifySuccess: "Verification Success!",
      qaMode: "Q&A Mode",
      example1: "Common algebra formulas",
      example2: "Common calculus formulas",
      examplePlaceholder: "Enter your question..."
    }
  };

  function getText(key) {
    return i18n[currentLang][key] || key;
  }

  // ================================================
  // 弹幕提示
  // ================================================
  function showToast(message) {
    const toast = document.createElement("div");
    toast.className = "pry-toast";
    toast.innerText = message;
    document.body.appendChild(toast);
    setTimeout(() => {
      toast.style.opacity = "0";
      setTimeout(() => toast.remove(), 300);
    }, 2000);
  }

  // ================================================
  // 引入 MathJax 与 Google 字体
  // ================================================
  const mathjax = document.createElement('script');
  mathjax.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
  document.head.appendChild(mathjax);

  const fontLink = document.createElement('link');
  fontLink.href = "https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap";
  fontLink.rel = "stylesheet";
  document.head.appendChild(fontLink);

  // ================================================
  // 全局 CSS 与 UI 美化
  // ================================================
  const styleVars = document.createElement("style");
  styleVars.innerHTML = `
    :root {
      --pry-bg: rgba(255,255,255,0.65);
      --pry-color: #003366;
      --pry-border: #a0c4e4;
      --pry-header: rgba(208,233,255,0.85);
      --pry-accent: #4da6ff;
      --pry-accent-soft: #cce7ff;
      --pry-danger: #ff5e5e;
      --pry-ok: #24c88b;
      --pry-border-radius: 10px;
    }
    body.dark-theme {
      --pry-bg: rgba(30,30,30,0.85);
      --pry-color: #f0f0f0;
      --pry-border: #444;
      --pry-header: rgba(50,50,50,0.85);
    }
    #prysaicWindow {
      backdrop-filter: blur(8px);
      display: flex;
      flex-direction: column;
      box-sizing: border-box;
      min-width: 300px;
      min-height: 200px;
      max-width: 90vw;
      max-height: 90vh;
    }
    #gptAnswerBox, #dropZone, #qaPanel {
      flex: 1;
      width: 100%;
      overflow: auto;
    }
    .pry-toast {
      position: fixed;
      bottom: 30px;
      right: 30px;
      background: var(--pry-ok);
      color: white;
      padding: 8px 16px;
      border-radius: 6px;
      font-weight: 600;
      z-index: 99999;
      box-shadow: 0 2px 8px rgba(0,0,0,0.2);
      transition: opacity 0.3s;
    }
    .qaExamples {
      margin-bottom: 8px;
      display: flex;
      gap: 6px;
      flex-wrap: wrap;
    }
    .qaExamples button {
      background: var(--pry-accent);
      color: white;
      border: none;
      border-radius: 6px;
      padding: 4px 8px;
      cursor: pointer;
    }
  `;
  document.head.appendChild(styleVars);

  // ================================================
  // 全局变量
  // ================================================
  let answerBox = null, dropArea = null, qaPanel = null;

  // ================================================
  // 检测页面(BigIdeasMath 或 Edulastic)并加载插件
  // ================================================
  function pluginLoader() {
    if (
      (typeof LearnosityAssess !== "undefined" && LearnosityAssess.getCurrentItem) ||
      window.location.href.includes("app.edulastic.com/student/assessment/")
    ) {
      initPrysaic();
    } else {
      setTimeout(pluginLoader, 1000);
    }
  }
  pluginLoader();

  // ================================================
  // 构建 UI 浮窗
  // ================================================
  function createUI() {
    const box = document.createElement("div");
    box.id = "prysaicWindow";
    box.style = `
      position: fixed;
      top: 180px;
      right: 40px;
      width: 360px;
      max-height: 480px;
      display: flex;
      flex-direction: column;
      background: var(--pry-bg);
      color: var(--pry-color);
      font-family: 'Inter', sans-serif;
      border: 1px solid var(--pry-border);
      border-radius: var(--pry-border-radius);
      box-shadow: 0 4px 10px rgba(0,0,0,0.15);
      z-index: 9999;
      resize: both;
      overflow: hidden;
      transition: background 0.3s, color 0.3s, border 0.3s;
      backdrop-filter: blur(8px);
    `;
    box.innerHTML = `
      <!-- 头部区域 -->
      <div id="pryHeader" style="padding:10px; background:var(--pry-header); display:flex; justify-content:space-between; align-items:center; cursor:move; transition: background 0.3s;">
        <span><b>Prysaic Tool v1.1.0</b></span>
        <div>
          <button id="themeBtn" title="切换暗/亮模式" style="margin-right:4px;">🌓</button>
          <button id="qaBtn" title="${getText("qaMode")}" style="margin-right:4px;">💬</button>
          <button id="hideBtn" title="隐藏窗口">❌</button>
        </div>
      </div>
      <!-- 用户协议区域 -->
      <div id="agreementBox" style="padding:12px; font-size:14px; line-height:1.6; border-bottom:1px solid var(--pry-border); background:var(--pry-accent-soft); transition: all 0.3s;">
        <h3 style="margin:0;">📜 ${getText("agreement")}</h3>
        <ul style="margin:8px 0;">
          <li>禁止传播、倒卖或商业使用本插件</li>
          <li>仅供个人学习辅助,使用风险自担</li>
          <li>Prohibit the dissemination, resale, or commercial use of this plugin</li>
          <li>For personal learning assistance only, use at your own risk</li>
          <li><a href="https://prysaic.com/tools/" target="_blank">User Agreement</a></li>
          <li>--------------------------------</li>
          <li>Support Email:[email protected]</li>
        </ul>
        <p style="color:red; font-weight:bold;">${getText("confirmText")}</p>
        <div style="margin-top:10px; text-align:center;">
          <input id="yesInput" placeholder="${getText("placeholder")}" style="width:60%; padding:6px; border:1px solid #aaa; border-radius:6px; transition: border-color 0.3s;">
          <button id="yesConfirm" style="margin-left:6px; padding:6px 12px; background:var(--pry-accent); color:white; border:none; border-radius:6px; transition: background 0.3s;">
            ${getText("start")}
          </button>
        </div>
      </div>
      <!-- 图片识别答案区域 -->
      <div id="gptAnswerBox" style="padding:10px; font-size:14px; line-height:1.6; display:none; flex:1; overflow:auto; transition: opacity 0.3s;">
        <p>欢迎使用Prysaic数学助手</p>
        <p>--------------------</p>
        <p>Welcome to use Prysaic Math Assistant</p>
      </div>
      <!-- 图片上传区域 -->
      <div id="dropZone" style="display:none; width:100%; height:100%; flex:1; display:flex; align-items:center; justify-content:center; border:2px dashed #bbb; font-size:14px; color:#666; transition: background 0.3s;">
        ${getText("dragHint")}
      </div>
      <!-- 问答功能区域 -->
      <div id="qaPanel" style="display:none; width:100%; height:100%; flex:1; flex-direction:column; overflow:auto; border:1px solid var(--pry-border); padding:10px; box-sizing:border-box; background:var(--pry-bg);">
        <div id="qaExamples" class="qaExamples"></div>
        <div id="qaHistory" style="flex:1; overflow:auto; margin-bottom:8px;"></div>
        <div id="qaInputArea" style="display:flex;">
          <input id="qaInput" type="text" placeholder="${getText("examplePlaceholder")}" style="flex:1; padding:6px; border:1px solid #aaa; border-radius:6px;">
          <button id="qaSend" style="margin-left:4px; padding:6px 12px; background:var(--pry-accent); color:white; border:none; border-radius:6px;">${currentLang==='zh'?'发送':'Send'}</button>
        </div>
      </div>
      <!-- 设置区域,新增主页按钮 -->
      <div id="settingsPanel" style="display:none; flex:none; gap:8px; padding:10px; border-top:1px solid var(--pry-border); background:var(--pry-accent-soft); transition: background 0.3s;">
        <select id="modelSelect">
          <option value="gpt-4o-mini" ${currentModel==="gpt-4o-mini"?"selected":""}>GPT-4o-mini</option>
          <option value="gpt-3.5-turbo" ${currentModel==="gpt-3.5-turbo"?"selected":""}>GPT-3.5 Turbo</option>
        </select>
        <select id="langSelect">
          <option value="zh" ${currentLang==="zh"?"selected":""}>中文</option>
          <option value="en" ${currentLang==="en"?"selected":""}>English</option>
        </select>
        <button id="retryBtn" style="padding:4px 8px;">↻</button>
        <button id="uploadBtn" style="padding:4px 8px;">📤</button>
        <button id="homeBtn" style="padding:4px 8px;">🏠</button>
      </div>
    `;
    document.body.appendChild(box);
    return box;
  }
  // ================================================
  // 绑定 UI 事件及拖拽功能
  // ================================================
  function bindUIActions(box) {
    answerBox = box.querySelector("#gptAnswerBox");
    dropArea = box.querySelector("#dropZone");
    qaPanel = box.querySelector("#qaPanel");

    const themeBtn = box.querySelector("#themeBtn");
    const hideBtn = box.querySelector("#hideBtn");
    const yesInput = box.querySelector("#yesInput");
    const yesConfirm = box.querySelector("#yesConfirm");
    const agreementBox = box.querySelector("#agreementBox");
    const retryBtn = box.querySelector("#retryBtn");
    const uploadBtn = box.querySelector("#uploadBtn");
    const homeBtn = box.querySelector("#homeBtn");
    const qaBtnEl = box.querySelector("#qaBtn");
    const modelSel = box.querySelector("#modelSelect");
    const langSel = box.querySelector("#langSelect");
    const settingsPanel = box.querySelector("#settingsPanel");

    // 用户协议验证
    yesConfirm.onclick = () => {
      if (yesInput.value.trim().toLowerCase() !== "yes") {
        alert("未同意协议,无法使用插件");
        return;
      }
      hasAgreed = true;
      agreementBox.style.display = "none";
      answerBox.style.display = "block";
      settingsPanel.style.display = "flex";
      showToast(getText("verifySuccess"));
    };

    // 为用户协议输入框增加快捷键:Enter 键触发确认;Delete 键删除当前光标处的一个字符
    yesInput.addEventListener("keydown", (e) => {
      if (e.key === "Enter") {
        e.preventDefault();
        yesConfirm.click();
      } else if (e.key === "Delete" || e.keyCode === 46) {
        e.preventDefault();
        let start = yesInput.selectionStart;
        let end = yesInput.selectionEnd;
        if (start === end) {
          yesInput.value = yesInput.value.substring(0, start) + yesInput.value.substring(start + 1);
          yesInput.selectionStart = yesInput.selectionEnd = start;
        } else {
          yesInput.value = yesInput.value.substring(0, start) + yesInput.value.substring(end);
          yesInput.selectionStart = yesInput.selectionEnd = start;
        }
      }
    });

    // 主题切换
    themeBtn.onclick = () => {
      document.body.classList.toggle("dark-theme");
    };

    // 隐藏窗口
    hideBtn.onclick = () => {
      box.style.display = "none";
    };

    // 重试按钮
    retryBtn.onclick = () => {
      if (!hasAgreed) return alert("请先同意协议");
      alert("请重新拖拽上传图片以刷新答案");
    };

    // 上传按钮:切换上传区域与答案区域
    uploadBtn.onclick = () => {
      if (!hasAgreed) return alert("请先同意协议");
      if (dropArea.style.display === "flex") {
        dropArea.style.display = "none";
        answerBox.style.display = "block";
      } else {
        dropArea.style.display = "flex";
        answerBox.style.display = "none";
      }
    };

    // 主页按钮:返回欢迎页面
    homeBtn.onclick = () => {
      dropArea.style.display = "none";
      qaPanel.style.display = "none";
      answerBox.innerHTML = `<p>欢迎使用Prysaic数学助手</p>
                              <p>--------------------</p>
                              <p>Welcome to use Prysaic Math Assistant</p>`;
      answerBox.style.display = "block";
    };

    // 问答按钮:切换到问答模式
    qaBtnEl.onclick = () => {
      if (!hasAgreed) return alert("请先同意协议");
      answerBox.style.display = "none";
      dropArea.style.display = "none";
      qaPanel.style.display = "flex";
    };

    // 绑定问答发送及快捷键
    const qaSend = box.querySelector("#qaSend");
    const qaInput = box.querySelector("#qaInput");
    const qaHistory = box.querySelector("#qaHistory");

    qaSend.onclick = async () => {
      const userMsg = qaInput.value.trim();
      if (!userMsg) return;
      appendToQAHistory("user", userMsg);
      qaInput.value = "";
      appendToQAHistory("assistant", getText("thinking"));
      try {
        const reply = await sendQAMessage(userMsg);
        removeLastQAEntry();
        appendToQAHistory("assistant", reply);
      } catch (err) {
        removeLastQAEntry();
        appendToQAHistory("assistant", getText("error") + err.message);
      }
    };

    // 为问答输入框增加快捷键:Enter 键发送;Delete 键删除当前光标处的一个字符
    qaInput.addEventListener("keydown", (e) => {
      if (e.key === "Enter") {
        e.preventDefault();
        qaSend.click();
      } else if (e.key === "Delete" || e.keyCode === 46) {
        e.preventDefault();
        let start = qaInput.selectionStart;
        let end = qaInput.selectionEnd;
        if (start === end) {
          qaInput.value = qaInput.value.substring(0, start) + qaInput.value.substring(start + 1);
          qaInput.selectionStart = qaInput.selectionEnd = start;
        } else {
          qaInput.value = qaInput.value.substring(0, start) + qaInput.value.substring(end);
          qaInput.selectionStart = qaInput.selectionEnd = start;
        }
      }
    });

    // 模型切换
    modelSel.onchange = (e) => {
      const now = Date.now();
      if (now - lastSwitchTime < 2000) {
        alert("请等待2秒后再切换模型");
        modelSel.value = currentModel;
        return;
      }
      currentModel = e.target.value;
      localStorage.setItem("prysaic_model", currentModel);
      lastSwitchTime = now;
      answerBox.innerHTML = `<span style="color:green;">${getText("toastModel")}${currentModel}</span>`;
    };

    // 语言切换
    langSel.onchange = (e) => {
      localStorage.setItem("prysaic_lang", e.target.value);
      answerBox.innerHTML = `<span style="color:green;">${getText("toastLang")}${e.target.value}</span>`;
      setTimeout(() => location.reload(), 800);
    };

    enableHighFPSDrag(box, box.querySelector("#pryHeader"));
    hookCalculator();
  }

  // ================================================
  // 高帧率拖拽逻辑
  // ================================================
  function enableHighFPSDrag(el, handle) {
    let dragging = false, offsetX = 0, offsetY = 0;
    handle.addEventListener("mousedown", (e) => {
      dragging = true;
      offsetX = e.clientX - el.offsetLeft;
      offsetY = e.clientY - el.offsetTop;
    });
    document.addEventListener("mouseup", () => dragging = false);
    function dragStep(e) {
      if (dragging) {
        el.style.left = `${e.clientX - offsetX}px`;
        el.style.top = `${e.clientY - offsetY}px`;
        el.style.right = "auto";
      }
      requestAnimationFrame(() => dragStep(e));
    }
    document.addEventListener("mousemove", (e) => {
      requestAnimationFrame(() => dragStep(e));
    });
  }

  // ================================================
  // 钩子:Calculator 按钮唤出插件
  // ================================================
  function hookCalculator() {
    const calcBtn = document.querySelector('button.lrn-calc-toggle[data-type="calculator"]');
    if (calcBtn) {
      calcBtn.onclick = (e) => {
        e.preventDefault();
        const box = document.querySelector("#prysaicWindow");
        if (box) box.style.display = "flex";
      };
    } else {
      setTimeout(hookCalculator, 1000);
    }
  }

  // ================================================
  // GPT 图像识别调用
  // ================================================
  async function askGPT(base64img) {
    if (!hasAgreed) throw new Error("尚未同意协议");
    if (!OPENAI_API_KEY) throw new Error("未设置 API Key");
    const sysPromptZh = "你是一个严谨的数学助教。请仔细分析图像中的题目,并产出正确答案、详细解题步骤及严谨的逻辑说明,全部用中文返回。格式:<br><b>Problem:</b> 问题描述<br>---<br><b>Answer:</b> $...$<br>---<br><b>Steps:</b> 详细解题步骤与逻辑说明。";
    const sysPromptEn = "You are a rigorous math tutor. Carefully analyze the image to produce the correct answer, detailed solution steps, and a rigorous logical explanation, all in English. Format:<br><b>Problem:</b> Problem description<br>---<br><b>Answer:</b> $...$<br>---<br><b>Steps:</b> Detailed solution steps and logical explanation.";
    const sysPrompt = currentLang === "zh" ? sysPromptZh : sysPromptEn;
    const res = await fetch("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${OPENAI_API_KEY}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        model: currentModel,
        messages: [
          { role: "system", content: sysPrompt },
          {
            role: "user",
            content: [{
              type: "image_url",
              image_url: { detail: "low", url: `data:image/jpeg;base64,${base64img}` }
            }]
          }
        ],
        max_tokens: 600,
        temperature: 0.2
      })
    });
    const data = await res.json();
    console.log("GPT API 返回数据:", data);
    if (data.error) throw new Error(data.error.message);
    return data.choices?.[0]?.message?.content?.trim() || "⚠️ 无返回内容";
  }

  // ================================================
  // GPT 问答模式调用
  // ================================================
  async function sendQAMessage(userMsg) {
    const sysPromptQA = currentLang === "zh" ?
      "你是一位美国大学教授,学识渊博、严谨且富有文化。请以严谨、详细且富有逻辑的方式回答问题。" :
      "You are an American university professor, highly knowledgeable, rigorous, and cultured. Please provide accurate, detailed, and logically sound answers.";
    const messages = [
      { role: "system", content: sysPromptQA }
    ].concat(conversationHistory);
    messages.push({ role: "user", content: userMsg });
    const res = await fetch("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${OPENAI_API_KEY}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        model: currentModel,
        messages: messages,
        max_tokens: 600,
        temperature: 0.2
      })
    });
    const data = await res.json();
    console.log("GPT QA API 返回数据:", data);
    if (data.error) throw new Error(data.error.message);
    const reply = data.choices?.[0]?.message?.content?.trim() || "⚠️ 无返回内容";
    conversationHistory.push({ role: "user", content: userMsg });
    conversationHistory.push({ role: "assistant", content: reply });
    return reply;
  }

  // ================================================
  // 渲染数学公式(MathJax 支持)
  // ================================================
  function renderAnswer(rawText) {
    rawText = rawText.replace(/\$(.*?)\$/g, "\\($1\\)");
    rawText = rawText.replace(/\*\*(.*?)\*\*/g, "<b>$1</b>").replace(/\n/g, "<br>");
    if (answerBox) {
      answerBox.innerHTML = rawText;
      if (window.MathJax?.typesetPromise) {
        MathJax.typesetPromise([answerBox]);
      }
    }
  }

  async function processQuestion(force = false) {
    if (!hasAgreed) return;
    answerBox.innerHTML = `<i>${getText("thinking")}</i>`;
  }

  // ================================================
  // 拖图上传区域绑定
  // ================================================
  function bindDropArea() {
    dropArea.ondragover = (e) => {
      e.preventDefault();
      dropArea.style.background = "#ffeeee";
    };
    dropArea.ondragleave = (e) => {
      e.preventDefault();
      dropArea.style.background = "#fff8f8";
    };
    dropArea.ondrop = async (e) => {
      e.preventDefault();
      dropArea.style.background = "#fff8f8";
      if (!hasAgreed) return alert("尚未同意协议,无法识别图片");
      const file = e.dataTransfer.files?.[0];
      if (!file || !file.type.startsWith("image/")) {
        dropArea.innerText = "❌ 请上传 PNG 或 JPG 图片";
        return;
      }
      dropArea.innerText = getText("thinking");
      const reader = new FileReader();
      reader.onload = async (evt) => {
        const base64 = evt.target.result.split(",")[1];
        console.log("手动上传图片 base64 长度:", base64?.length);
        try {
          const raw = await askGPT(base64);
          renderAnswer(raw);
          dropArea.style.display = "none";
          answerBox.style.display = "block";
        } catch (err) {
          dropArea.innerHTML = `<span style="color:red;">${getText("error")}${err.message}</span>`;
        }
      };
      reader.readAsDataURL(file);
    };
  }

  // ================================================
  // 插件初始化
  // ================================================
  function initPrysaic() {
    if (document.querySelector("#prysaicWindow")) return;
    const box = createUI();
    bindUIActions(box);
    bindDropArea();
    hookCalculator();
    initQA();
    if (window.location.href.includes("app.edulastic.com/student/assessment/")) {
      hookEdulasticButton();
    }
  }

  // ================================================
  // 钩子:Edulastic Magnify 按钮
  // ================================================
  function hookEdulasticButton() {
    function tryHook() {
      const edulasticBtn = document.querySelector('button[aria-label="Magnify"]');
      if (edulasticBtn) {
        edulasticBtn.addEventListener("click", (e) => {
          e.preventDefault();
          const box = document.querySelector("#prysaicWindow");
          if (box) box.style.display = "flex";
        });
      } else {
        setTimeout(tryHook, 1000);
      }
    }
    tryHook();
  }

  // ================================================
  // 监测页面加载后启动插件
  // ================================================
  function waitForPlugin() {
    const check = setInterval(() => {
      if (
        (typeof LearnosityAssess !== "undefined" && LearnosityAssess.getCurrentItem) ||
        window.location.href.includes("app.edulastic.com/student/assessment/")
      ) {
        clearInterval(check);
        initPrysaic();
      }
    }, 1000);
  }
  waitForPlugin();

  // ================================================
  // 初始化 Q&A 模式(绑定快捷示例及问答发送)
  // ================================================
  function initQA() {
    conversationHistory.length = 0; // 清空历史
    const qaExamples = document.querySelector("#qaExamples");
    const examples = currentLang === "zh" ? [getText("example1"), getText("example2")] : [getText("example1"), getText("example2")];
    examples.forEach(text => {
      const btn = document.createElement("button");
      btn.innerText = text;
      btn.onclick = async () => {
        appendToQAHistory("user", text);
        appendToQAHistory("assistant", getText("thinking"));
        try {
          const reply = await sendQAMessage(text);
          removeLastQAEntry();
          appendToQAHistory("assistant", reply);
        } catch (err) {
          removeLastQAEntry();
          appendToQAHistory("assistant", getText("error") + err.message);
        }
      };
      qaExamples.appendChild(btn);
    });
    const qaSend = document.querySelector("#qaSend");
    const qaInput = document.querySelector("#qaInput");
    qaSend.onclick = async () => {
      const userMsg = qaInput.value.trim();
      if (!userMsg) return;
      appendToQAHistory("user", userMsg);
      qaInput.value = "";
      appendToQAHistory("assistant", getText("thinking"));
      try {
        const reply = await sendQAMessage(userMsg);
        removeLastQAEntry();
        appendToQAHistory("assistant", reply);
      } catch (err) {
        removeLastQAEntry();
        appendToQAHistory("assistant", getText("error") + err.message);
      }
    };
    // 为问答输入框增加快捷键:Enter 键发送;Delete 键删除当前光标处的一个字符(兼容 e.key 和 e.keyCode)
    qaInput.addEventListener("keydown", (e) => {
      if (e.key === "Enter") {
        e.preventDefault();
        qaSend.click();
      } else if (e.key === "Delete" || e.keyCode === 46) {
        e.preventDefault();
        let start = qaInput.selectionStart;
        let end = qaInput.selectionEnd;
        if (start === end) {
          qaInput.value = qaInput.value.substring(0, start) + qaInput.value.substring(start + 1);
          qaInput.selectionStart = qaInput.selectionEnd = start;
        } else {
          qaInput.value = qaInput.value.substring(0, start) + qaInput.value.substring(end);
          qaInput.selectionStart = qaInput.selectionEnd = start;
        }
      }
    });
  }

  function appendToQAHistory(role, message) {
    const qaHistory = document.querySelector("#qaHistory");
    const entry = document.createElement("div");
    entry.style.marginBottom = "8px";
    entry.innerHTML = `<strong>${role === "user" ? (currentLang==="zh"?"我":"Me") : (currentLang==="zh"?"教授":"Professor")}:</strong> ${message}`;
    qaHistory.appendChild(entry);
    qaHistory.scrollTop = qaHistory.scrollHeight;
    if (window.MathJax?.typesetPromise) {
      MathJax.typesetPromise([qaHistory]);
    }
  }

  function removeLastQAEntry() {
    const qaHistory = document.querySelector("#qaHistory");
    if (qaHistory.lastChild) {
      qaHistory.lastChild.remove();
    }
  }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址