Mathswatch Autosolver

Takes a screenshot of Mathswatch question images, sends it to Gemini, and displays the answer in a beautifully styled new tab.

  1. // ==UserScript==
  2. // @name Mathswatch Autosolver
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Takes a screenshot of Mathswatch question images, sends it to Gemini, and displays the answer in a beautifully styled new tab.
  6. // @author Your Name
  7. // @match https://vle.mathswatch.co.uk/*
  8. // @grant GM_openInTab
  9. // @grant GM_xmlhttpRequest
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. 'use strict';
  17.  
  18. const GEMINI_API_KEY_KEY = 'Your-Google-API-Key-Here';
  19. let geminiApiKey = GM_getValue(GEMINI_API_KEY_KEY, null);
  20. const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-latest:generateContent?key=';
  21. const DEFAULT_PROMPT = "Analyze the image and identify any questions. Answer the questions with as much detail as possible. Show your reasoning.";
  22. const ADDITIONAL_PROMPT_MESSAGE = "Enter any additional instructions or questions to send with the image (or leave blank for default prompt):";
  23.  
  24. async function checkApiKey() {
  25. if (!geminiApiKey) {
  26. geminiApiKey = prompt("Enter your Google AI Studio API Key:");
  27. if (geminiApiKey) {
  28. GM_setValue(GEMINI_API_KEY_KEY, geminiApiKey);
  29. alert("API key saved. Press Ctrl+X again to process the question.");
  30. } else {
  31. alert("API key required for the script to function.");
  32. }
  33. return false;
  34. }
  35. return true;
  36. }
  37.  
  38. async function captureScreenshot() {
  39. if (typeof html2canvas === "undefined") {
  40. await new Promise(resolve => {
  41. const script = document.createElement("script");
  42. script.src = "https://html2canvas.hertzen.com/dist/html2canvas.min.js";
  43. script.onload = resolve;
  44. document.head.appendChild(script);
  45. });
  46. }
  47.  
  48. // Capture only the images with the class 'img-responsive col-xs-12 img-question'
  49. const imageElements = document.querySelectorAll('img.img-responsive.col-xs-12.img-question');
  50. if (imageElements.length === 0) {
  51. alert("No image found with the specified class.");
  52. return;
  53. }
  54.  
  55. // Create a canvas of the first image (you can modify this for multiple images)
  56. const canvas = await html2canvas(imageElements[0], {
  57. useCORS: true,
  58. allowTaint: true,
  59. scrollX: 0,
  60. scrollY: 0,
  61. windowWidth: imageElements[0].naturalWidth,
  62. windowHeight: imageElements[0].naturalHeight,
  63. width: imageElements[0].naturalWidth,
  64. height: imageElements[0].naturalHeight
  65. });
  66.  
  67. return canvas;
  68. }
  69.  
  70. function convertCanvasToBlob(canvas) {
  71. return new Promise((resolve, reject) => {
  72. canvas.toBlob(blob => {
  73. blob ? resolve(blob) : reject(new Error('Failed to convert canvas to blob.'));
  74. }, 'image/png');
  75. });
  76. }
  77.  
  78. async function sendImageToGemini(imageBlob, additionalPrompt = "") {
  79. if (!await checkApiKey()) return;
  80.  
  81. const reader = new FileReader();
  82. reader.readAsDataURL(imageBlob);
  83.  
  84. return new Promise((resolve, reject) => {
  85. reader.onloadend = () => {
  86. const base64Image = reader.result.split(',')[1];
  87. const promptText = additionalPrompt.trim() !== "" ? additionalPrompt : DEFAULT_PROMPT;
  88.  
  89. const payload = {
  90. contents: [
  91. {
  92. parts: [
  93. { text: promptText },
  94. {
  95. inline_data: {
  96. mime_type: "image/png",
  97. data: base64Image
  98. }
  99. }
  100. ]
  101. }
  102. ]
  103. };
  104.  
  105. GM_xmlhttpRequest({
  106. method: "POST",
  107. url: GEMINI_API_URL + geminiApiKey,
  108. headers: { "Content-Type": "application/json" },
  109. data: JSON.stringify(payload),
  110. onload: function (response) {
  111. if (response.status >= 200 && response.status < 300) {
  112. try {
  113. const jsonResponse = JSON.parse(response.responseText);
  114. const answer = jsonResponse?.candidates?.[0]?.content?.parts?.[0]?.text || "No answer found.";
  115. displayAnswerInNewTab(answer);
  116. } catch (error) {
  117. reject("Error parsing response: " + error.message);
  118. }
  119. } else {
  120. reject(`API Error: ${response.status} - ${response.responseText}`);
  121. }
  122. },
  123. onerror: function (error) {
  124. reject("Request error: " + error);
  125. }
  126. });
  127. };
  128. reader.onerror = () => reject(new Error('Failed to read image.'));
  129. });
  130. }
  131.  
  132. function displayAnswerInNewTab(answer) {
  133. const newTabContent = `
  134. <!DOCTYPE html>
  135. <html lang="en">
  136. <head>
  137. <meta charset="UTF-8">
  138. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  139. <title>Gemini Answer</title>
  140. <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
  141. <style>
  142. @keyframes bgAnimation {
  143. 0% { background-position: 0% 50%; }
  144. 50% { background-position: 100% 50%; }
  145. 100% { background-position: 0% 50%; }
  146. }
  147.  
  148. body {
  149. font-family: 'Poppins', sans-serif;
  150. background: linear-gradient(135deg, #1E1E2F, #2D2D3F);
  151. background-size: 300% 300%;
  152. animation: bgAnimation 10s infinite alternate;
  153. color: #FFF;
  154. text-align: center;
  155. display: flex;
  156. justify-content: center;
  157. align-items: center;
  158. height: 100vh;
  159. margin: 0;
  160. }
  161. .container {
  162. background: rgba(255, 255, 255, 0.1);
  163. padding: 20px;
  164. border-radius: 12px;
  165. max-width: 600px;
  166. box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
  167. transition: transform 0.3s ease, box-shadow 0.3s ease;
  168. }
  169. .container:hover {
  170. transform: translateY(-5px);
  171. box-shadow: 0 10px 20px rgba(255, 255, 255, 0.4);
  172. }
  173. h1 {
  174. font-size: 22px;
  175. font-weight: 600;
  176. margin-bottom: 10px;
  177. }
  178. pre {
  179. white-space: pre-wrap;
  180. word-wrap: break-word;
  181. font-size: 14px;
  182. background: rgba(255, 255, 255, 0.1);
  183. padding: 10px;
  184. border-radius: 8px;
  185. text-align: left;
  186. max-height: 300px;
  187. overflow-y: auto;
  188. font-family: 'Poppins', sans-serif;
  189. }
  190. button {
  191. margin-top: 10px;
  192. padding: 10px 20px;
  193. font-size: 14px;
  194. border: none;
  195. border-radius: 8px;
  196. background: #FFC857;
  197. color: #222;
  198. cursor: pointer;
  199. transition: 0.3s;
  200. }
  201. button:hover {
  202. background: #FFA500;
  203. }
  204. </style>
  205. </head>
  206. <body>
  207. <div class="container">
  208. <h1>Gemini Answer</h1>
  209. <pre id="answer">${answer}</pre>
  210. <button onclick="copyToClipboard()">📋 Copy</button>
  211. </div>
  212.  
  213. <script>
  214. function copyToClipboard() {
  215. const answerText = document.getElementById("answer").textContent;
  216. navigator.clipboard.writeText(answerText).then(() => {
  217. alert("Copied to clipboard!");
  218. }).catch(err => console.error("Copy failed:", err));
  219. }
  220. </script>
  221. </body>
  222. </html>
  223. `;
  224.  
  225. GM_openInTab(`data:text/html;charset=utf-8,${encodeURIComponent(newTabContent)}`, { active: true });
  226. }
  227.  
  228. document.addEventListener('keydown', async function (event) {
  229. if (event.ctrlKey && event.key === 'x') {
  230. event.preventDefault();
  231. try {
  232. const canvas = await captureScreenshot();
  233. const imageBlob = await convertCanvasToBlob(canvas);
  234. const additionalPrompt = prompt(ADDITIONAL_PROMPT_MESSAGE);
  235. await sendImageToGemini(imageBlob, additionalPrompt);
  236. } catch (error) {
  237. alert("Error: " + error);
  238. }
  239. }
  240. });
  241.  
  242. console.log("Mathswatch Autosolver script loaded.");
  243. })();