KhanHack

Khan Academy Answer Hack

目前為 2024-10-31 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name KhanHack
  3. // @namespace https://greasyfork.org/users/783447
  4. // @version 5.7.1
  5. // @description Khan Academy Answer Hack
  6. // @author Logzilla6 - IlyTobias - Illusions
  7. // @match https://*.khanacademy.org/*
  8. // @icon https://i.ibb.co/K5g1KMq/Untitled-drawing-3.png
  9. // ==/UserScript==
  10.  
  11. //ALL FOLLOWING CODE IS UNDER THE KHANHACK TRADEMARK. UNAUTHORIZED DISTRIBUTION CAN/WILL RESULT IN LEGAL ACTION
  12.  
  13. //Note that KhanHack™ is an independent initiative and is not affiliated with or endorsed by Khan Academy. We respect the work of Khan Academy and its mission to provide free education, but KhanHack™ operates separately with its own unique goals.
  14.  
  15. let mainMenu = document.createElement('div');
  16. mainMenu.id = 'mainMenu';
  17. mainMenu.style.position = 'fixed';
  18. mainMenu.style.bottom = '.5vw';
  19. mainMenu.style.left = '19vw';
  20. mainMenu.style.width = '300px';
  21. mainMenu.style.height = '400px';
  22. mainMenu.style.backgroundColor = '#123576';
  23. mainMenu.style.border = '3px solid #07152e';
  24. mainMenu.style.borderRadius = '20px';
  25. mainMenu.style.padding = '10px';
  26. mainMenu.style.color = "white";
  27. mainMenu.style.fontFamily = "Noto sans";
  28. mainMenu.style.fontWeight = "500";
  29. mainMenu.style.transition = "all 0.3s ease";
  30. mainMenu.style.zIndex = '1000';
  31. mainMenu.style.display = 'flex';
  32. mainMenu.style.flexDirection = 'column';
  33.  
  34. let copied = document.createElement('div');
  35. copied.id = 'copyText';
  36. copied.style.position = 'fixed';
  37. copied.style.bottom = '17.5vw';
  38. copied.style.left = '22.2vw';
  39. copied.style.width = '150px';
  40. copied.style.height = 'auto';
  41. copied.style.backgroundColor = '#123576';
  42. copied.style.border = '3px solid #07152e';
  43. copied.style.borderRadius = '13px';
  44. copied.style.color = "white";
  45. copied.style.fontFamily = "Noto sans";
  46. copied.style.fontWeight = "500";
  47. copied.style.transition = "all 0.15s ease";
  48. copied.style.padding = "5px";
  49. copied.style.opacity = "0";
  50.  
  51. let answerBlocks = [];
  52. let currentCombinedAnswer = '';
  53. let isGhostModeEnabled = false;
  54.  
  55. const setCopiedContent = () => {
  56. copied.innerHTML = `
  57. <div style="display: flex; flex-direction: column; align-items: center; text-align: center; position: relative; gap: 10px; opacity: 1; transition: opacity 0.5s ease;">
  58. <a>Successfully Copied!</a>
  59. </div>
  60.  
  61. <style>
  62. @import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap');
  63. </style>
  64. `;
  65. }
  66.  
  67. const copyFlash = () => {
  68. copied.style.opacity = "1";
  69. setTimeout(() => {
  70. copied.style.opacity = "0";
  71. }, 1300);
  72. };
  73.  
  74. const setMainMenuContent = () => {
  75. mainMenu.innerHTML =`
  76. <div id="menuContent" style="display: flex; flex-direction: column; align-items: center; gap: 10px; opacity: 1; transition: opacity 0.5s ease; height: 100%;">
  77. <head>
  78. <img id="discordIcon" src="https://i.ibb.co/grF973h/discord.png" alt="Discord" style="position: absolute; left: 15px; top: 15px; width: 24px; height: 24px; opacity: 1; transition: opacity 0.5s ease; cursor: pointer;" />
  79. <img id="headerImage" src="https://i.ibb.co/h2GFJ5f/khanhack.png" style="width: 130px; opacity: 1; transition: opacity 0.5s ease;" />
  80. <img id="gearIcon" src="https://i.ibb.co/q0QVKGG/gearicon.png" alt="Settings" style="position: absolute; right: 15px; top: 15px; width: 24px; height: 24px; opacity: 1; transition: opacity 0.5s ease; cursor: pointer;" />
  81. </head>
  82. <div id="answerList" style="width: 100%; display: flex; flex-direction: column; gap: 10px; flex-grow: 1; max-height: calc(100% - 100px); overflow-y: scroll; padding-bottom: 10px;"></div>
  83. </div>
  84.  
  85. <img id="toggleButton" src="https://i.ibb.co/RpqPcR1/hamburger.png" class="toggleButton">
  86. <img id="clearButton" src="https://i.ibb.co/VYs1BPQ/can.png" style="width: 20px; height: 20px; bottom: 12px; right: 8px; position: absolute; cursor: pointer;">
  87.  
  88. <style>
  89. @import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap');
  90.  
  91. .toggleButton {
  92. position: absolute;
  93. bottom: 7px;
  94. left: 7px;
  95. height: 20px;
  96. width: 20px;
  97. cursor: pointer;
  98. }
  99.  
  100. .block {
  101. width: 280px;
  102. min-height: 40px;
  103. height: auto;
  104. background-color: #f0f0f0;
  105. padding: 10px;
  106. border-radius: 10px;
  107. opacity: 1;
  108. display: flex;
  109. justify-content: center;
  110. transition: opacity 0.5s ease;
  111. align-items: center;
  112. margin-left: auto;
  113. margin-right: auto;
  114. transition: 0.2s ease;
  115. word-wrap: break-word;
  116. }
  117.  
  118. .block:hover {
  119. background-color: #d9d7d7;
  120. }
  121.  
  122. .answer {
  123. margin: 0;
  124. text-align: center;
  125. color: black;
  126. font-family: "Noto Sans";
  127. font-weight: 500;
  128. }
  129.  
  130. .imgBlock img {
  131. width: 250px;
  132. border-radius: 10px;
  133. }
  134.  
  135. .copied {
  136. margin-top: -200px;
  137. }
  138.  
  139. #answerList::-webkit-scrollbar {
  140. display: none;
  141. }
  142.  
  143. #answerList {
  144. -ms-overflow-style: none;
  145. scrollbar-width: none;
  146. }
  147. </style>
  148. `;
  149.  
  150. addToggle();
  151. addSettings();
  152. addDiscord();
  153. addClear();
  154.  
  155.  
  156.  
  157. const answerList = document.getElementById('answerList');
  158. answerList.innerHTML = '';
  159.  
  160. answerBlocks.forEach(block => {
  161. if (block.type === 'text') {
  162. addAnswerBlock(block.content, false);
  163. } else if (block.type === 'image') {
  164. addImgAnswerBlock(block.content, false);
  165. }
  166. });
  167.  
  168. if (isGhostModeEnabled) {
  169. enableGhostMode();
  170. }
  171. };
  172.  
  173. const addImgAnswerBlock = (imgSrc, save = true) => {
  174. const answerList = document.getElementById('answerList');
  175. const block = document.createElement('div');
  176. block.className = 'block imgBlock';
  177.  
  178. const img = document.createElement('img');
  179. img.src = imgSrc;
  180.  
  181. block.appendChild(img);
  182. answerList.appendChild(block);
  183.  
  184. if (save) {
  185. answerBlocks.push({ type: 'image', content: imgSrc });
  186. }
  187. };
  188.  
  189. let isMenuVisible = true;
  190. const addToggle = () => {
  191. document.getElementById('toggleButton').addEventListener('click', function() {
  192. const clearButton = document.getElementById('clearButton');
  193. if (isMenuVisible) {
  194. mainMenu.style.height = '15px';
  195. mainMenu.style.width = '15px';
  196. document.getElementById('menuContent').style.opacity = '0';
  197. clearButton.style.opacity = '0';
  198. setTimeout(() => {
  199. document.getElementById('menuContent').style.display = 'none';
  200. clearButton.style.display = 'none';
  201. }, 50);
  202. } else {
  203. mainMenu.style.height = '400px';
  204. mainMenu.style.width = '300px';
  205. document.getElementById('menuContent').style.display = 'flex';
  206. clearButton.style.display = 'block';
  207. setTimeout(() => {
  208. document.getElementById('menuContent').style.opacity = '1';
  209. clearButton.style.opacity = '1';
  210. }, 100);
  211. }
  212. isMenuVisible = !isMenuVisible;
  213. });
  214. };
  215.  
  216. const addSettings = () => {
  217. document.getElementById('gearIcon').addEventListener('click', function() {
  218. mainMenu.innerHTML = `
  219. <div id="settingsContent" style="display: flex; flex-direction: column; align-items: center; position: relative; opacity: 1; transition: opacity 0.5s ease;">
  220. <img id="backArrow" src="https://i.ibb.co/Jt4qrD7/pngwing-com-1.png" alt="Back" style="position: absolute; left: 7px; top: 3px; width: 24px; height: 24px; opacity: 1; transition: opacity 0.5s ease; cursor: pointer;" />
  221.  
  222. <h3 style="margin: 0; text-align: center; color: white; font-family: Noto sans; font-weight: 500;">Settings Menu</h3>
  223. <p style="text-align: center; color: white; font-family: Noto sans; margin-top: 15px;">
  224. Ghost Mode: <input type="checkbox" id="ghostModeToggle" class="ghostToggle" ${isGhostModeEnabled ? 'checked' : ''}>
  225. </p>
  226. <p style="text-align: center; color: white; font-family: Noto sans; margin-top: 15px;">Auto Answer: BETA</p>
  227. <p style="text-align: center; color: white; font-family: Noto sans; margin-top: 15px;">Point Farmer: (Coming Soon)</p>
  228. <p style="text-align: center; color: white; font-family: Noto sans; margin-top: 30px; font-size: 25px;">Beta Access In Discord</p>
  229. <p style="text-align: center; color: white; font-family: Noto sans; margin-top: 80px;">KhanHack | 5.7</p>
  230.  
  231. <style>
  232. @import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap');
  233.  
  234. .ghostToggle {
  235. width: 20px;
  236. height: 20px;
  237. background-color: white;
  238. border-radius: 50%;
  239. vertical-align: middle;
  240. border: 2px solid #07152e;
  241. appearance: none;
  242. -webkit-appearance: none;
  243. outline: none;
  244. cursor: pointer;
  245. transition: 0.2s ease;
  246. }
  247.  
  248. .ghostToggle:checked {
  249. background-color: #2967d9;
  250. }
  251. </style>
  252. </div>
  253. `;
  254. document.getElementById('backArrow').addEventListener('click', setMainMenuContent);
  255. document.getElementById('ghostModeToggle').addEventListener('change', function() {
  256. isGhostModeEnabled = this.checked;
  257. if (isGhostModeEnabled) {
  258. enableGhostMode();
  259. } else {
  260. disableGhostMode();
  261. }
  262. });
  263. });
  264. };
  265.  
  266. const enableGhostMode = () => {
  267. mainMenu.style.opacity = '0';
  268. mainMenu.addEventListener('mouseenter', handleMouseEnter);
  269. mainMenu.addEventListener('mouseleave', handleMouseLeave);
  270. };
  271.  
  272. const disableGhostMode = () => {
  273. mainMenu.style.opacity = '1';
  274. mainMenu.removeEventListener('mouseenter', handleMouseEnter);
  275. mainMenu.removeEventListener('mouseleave', handleMouseLeave);
  276. };
  277.  
  278. const handleMouseEnter = () => {
  279. mainMenu.style.opacity = '1';
  280. };
  281.  
  282. const handleMouseLeave = () => {
  283. mainMenu.style.opacity = '0';
  284. };
  285.  
  286. const addDiscord = () => {
  287. document.getElementById('discordIcon').addEventListener('click', function() {
  288. window.open('https://discord.gg/khanhack', '_blank');
  289. });
  290. };
  291.  
  292. const addClear = () => {
  293. document.getElementById('clearButton').addEventListener('click', function() {
  294. location.reload();
  295. });
  296. };
  297.  
  298. const script = document.createElement("script");
  299. script.src = "https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js";
  300. document.head.appendChild(script);
  301.  
  302. const katexStyle = document.createElement("link");
  303. katexStyle.rel = "stylesheet";
  304. katexStyle.href = "https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css";
  305. document.head.appendChild(katexStyle);
  306.  
  307. const addAnswerBlock = (answer, save = true) => {
  308. const answerList = document.getElementById('answerList');
  309. const block = document.createElement('div');
  310.  
  311. block.className = 'block no-select';
  312. block.style.cursor = "pointer";
  313. block.addEventListener("click", () => {
  314. navigator.clipboard.writeText(answer);
  315. copyFlash();
  316. });
  317.  
  318. const tc = document.createElement('p');
  319. tc.className = 'answer';
  320.  
  321. const latexPattern = /\\frac|\\sqrt|\\times|\\cdot|\\degree|\\left|\\right|\^|\$|\{|\}/;
  322. if (latexPattern.test(answer)) {
  323. tc.innerHTML = '';
  324. katex.render(answer, tc)
  325. } else {
  326. tc.innerHTML = `${answer}`;
  327. }
  328.  
  329. tc.style.fontFamily = '"Noto Sans", sans-serif';
  330. tc.style.fontWeight = "500";
  331. tc.style.fontStyle = "normal";
  332. tc.style.fontSize = "16px"
  333.  
  334. block.appendChild(tc);
  335. answerList.appendChild(block);
  336.  
  337. if (save) {
  338. answerBlocks.push({ type: 'text', content: answer });
  339. }
  340. };
  341.  
  342. document.body.appendChild(mainMenu);
  343. document.body.appendChild(copied);
  344. setMainMenuContent();
  345. setCopiedContent();
  346.  
  347. let originalJson = JSON.parse;
  348. JSON.parse = function (jsonString) {
  349. let parsedData = originalJson(jsonString);
  350.  
  351. try {
  352. if (parsedData.data && parsedData.data.assessmentItem && parsedData.data.assessmentItem.item) {
  353.  
  354. let itemData = JSON.parse(parsedData.data.assessmentItem.item.itemData);
  355. let hasGradedWidget = Object.values(itemData.question.widgets).some(widget => widget.graded === true);
  356. if (hasGradedWidget) {
  357.  
  358.  
  359. for (let widgetKey in itemData.question.widgets) {
  360.  
  361.  
  362.  
  363. let widget = itemData.question.widgets[widgetKey];
  364.  
  365. switch (widget.type) {
  366. case "numeric-input":
  367. handleNumeric(widget);
  368. break;
  369. case "radio":
  370. handleRadio(widget);
  371. break;
  372. case "expression":
  373. handleExpression(widget);
  374. break;
  375. case "dropdown":
  376. handleDropdown(widget);
  377. break;
  378. case "interactive-graph":
  379. handleIntGraph(widget);
  380. break;
  381. case "grapher":
  382. handleGrapher(widget);
  383. break;
  384. case "input-number":
  385. handleInputNum(widget);
  386. break;
  387. case "matcher":
  388. handleMatcher(widget);
  389. break;
  390. case "categorizer":
  391. handleCateg(widget);
  392. break;
  393. case "label-image":
  394. handleLabel(widget);
  395. break;
  396. case "matrix":
  397. handleMatrix(widget);
  398. break;
  399. default:
  400. console.log("Unknown widget: " + widget.type);
  401. break;
  402. }
  403. }
  404.  
  405. if (currentCombinedAnswer.trim() !== '') {
  406. addAnswerBlock(currentCombinedAnswer);
  407. currentCombinedAnswer = '';
  408. }
  409. }
  410. }
  411. } catch (error) {
  412. console.log("Error parsing JSON:", error);
  413. }
  414.  
  415. return parsedData;
  416. };
  417.  
  418. function cleanLatexExpression(answer) {
  419. return answer
  420. //.replace(/\\times/g, '×')
  421. // .replace(/\\frac{([^}]*)}{([^}]*)}/g, '($1/$2)')
  422. // .replace(/\\cdot/g, '⋅')
  423. // .replace(/\\left|\\right/g, '')
  424. // .replace(/\^/g, '^')
  425. // .replace(/\\(?:[a-zA-Z]+)/g, '')
  426. // .replace(/(?<!\\){|}/g, '')
  427. // .replace(/\\sqrt/g, '')
  428. //.replace(/\degree/g, '')
  429. .replace(/\$/g, '');
  430. }
  431.  
  432. function handleRadio(widget) {
  433. let corAns = widget.options.choices.filter(item => item.correct === true).map(item => item.content);
  434. let ansArr = [];
  435. let isNone = widget.options.choices.filter(item => item.isNoneOfTheAbove === true && item.correct === true)
  436.  
  437. if (isNone.length > 0) {
  438. currentCombinedAnswer += "None of the above";
  439. return;
  440. }
  441.  
  442. corAns.forEach(answer => {
  443. const hasGraphie = answer.includes('web+graphie')
  444. const hasNotGraphie = answer.includes('![]')
  445.  
  446. if(hasGraphie || hasNotGraphie == true) {
  447. if(hasGraphie == true) {
  448. const split = answer.split('web+graphie');
  449. const midUrl = split[1].slice(0, -1);
  450. const finalUrl = 'https' + midUrl + '.svg';
  451. addImgAnswerBlock(finalUrl);
  452. } else if(hasNotGraphie == true) {
  453. const finalUrl = answer.slice(4, -1)
  454. addImgAnswerBlock(finalUrl);
  455. }
  456. } else {
  457. let cleaned = cleanLatexExpression(answer)
  458. ansArr.push(cleaned)
  459. }
  460.  
  461.  
  462. })
  463.  
  464. if(ansArr.length) {
  465. currentCombinedAnswer += ansArr.join("|")
  466. }
  467.  
  468. }
  469.  
  470. function handleLabel(widget) {
  471. let corAns = widget.options.markers.filter(item => item.answers).map(item => item.answers)
  472. let labels = widget.options.markers.filter(item => item.label).map(item => item.label)
  473. let ansArr = []
  474.  
  475. corAns.forEach((answer, index) => {
  476. if(labels == 0) {
  477. let cleaned = cleanLatexExpression(answer.toString());
  478. ansArr.push(cleaned)
  479.  
  480. } else {
  481. let cleaned = cleanLatexExpression(answer.toString());
  482. let finLabel = labels[index].replace('Point ', '').replace(/[.]/g, '').trim() || "";
  483. let labeledAnswer = `${finLabel}: ${cleaned}`;
  484. ansArr.push(labeledAnswer)
  485. }
  486.  
  487. })
  488.  
  489. if(ansArr.length) {
  490. currentCombinedAnswer += ansArr.join("|")
  491. }
  492.  
  493.  
  494. }
  495.  
  496. function handleNumeric(widget) {
  497. console.log(widget)
  498. const numericAnswer = widget.options.answers[0].value;
  499. currentCombinedAnswer += ` ${numericAnswer} `;
  500. }
  501.  
  502. function handleExpression(widget) {
  503. let expressionAnswer = widget.options.answerForms[0].value;
  504. let cleaned = cleanLatexExpression(expressionAnswer)
  505. currentCombinedAnswer += ` ${cleaned} `;
  506. }
  507.  
  508. function handleDropdown(widget) {
  509. let content = widget.options.choices.filter(item => item.correct === true).map(item => item.content);
  510. currentCombinedAnswer += ` ${content[0]} `;
  511. }
  512.  
  513. function handleIntGraph(widget) {
  514. let coords = widget.options.correct.coords;
  515. let validCoords = coords.filter(coord => coord !== undefined);
  516. currentCombinedAnswer += ` ${validCoords.join(' | ')} `;
  517. }
  518.  
  519. function handleInputNum(widget) {
  520. let inputNumAnswer = widget.options.value;
  521. currentCombinedAnswer += ` ${inputNumAnswer} `;
  522. }
  523.  
  524. function handleMatcher(widget) {
  525. let matchAnswer = widget.options.right;
  526. currentCombinedAnswer += ` ${matchAnswer} `;
  527. }
  528.  
  529. function handleGrapher(widget) {
  530. let coords = widget.options.correct.coords;
  531. currentCombinedAnswer += ` ${coords.join(' | ')} `;
  532. }
  533.  
  534. function handleCateg(widget) {
  535. let values = widget.options.values;
  536. let categories = widget.options.categories;
  537. let labeledValues = values.map(value => categories[value]);
  538.  
  539. currentCombinedAnswer += ` ${labeledValues} `
  540. }
  541.  
  542. function handleMatrix(widget) {
  543. let arrs = widget.options.answers;
  544. currentCombinedAnswer += ` ${arrs.join(' | ')} `
  545. }