- // ==UserScript==
- // @name KhanHack
- // @namespace http://khan.dyntech.cc
- // @version 6.0
- // @description Sloppy lazybones anti-homework production
- // @author DynTech
- // @match *://*.khanacademy.org/*
- // @oicon https://dyntech.cc/favicon?q=khan.dyntech.cc
- // @icon https://cdn.dyntech.cc/r/khanhack.png
- // @grant none
- // @license MIT
- // ==/UserScript==
-
- (function() {
- 'use strict';
- // This FREE script was extracted from a $7.99/month extension. You're welcome.
- (function() {
- const originalFetch = window.fetch;
-
- // Check for an existing container and restore it if minimized
- let mainContainer = document.getElementById('questionDataContainer');
- if (mainContainer) {
- // If the container is minimized, restore it
- if (mainContainer.classList.contains('minimized')) {
- mainContainer.classList.remove('minimized');
- const restoreButton = document.getElementById('restoreButton');
- restoreButton.classList.remove('minimized');
- const minimizeButton = document.getElementById('minimizeButton');
- minimizeButton.style.display = 'block';
- }
- return; // Prevent adding a new container
- }
-
- // Create a new main container to hold the output data
- mainContainer = document.createElement('div');
- mainContainer.id = 'questionDataContainer';
-
- // Ensure the body is loaded before appending
- if (document.body) {
- document.body.appendChild(mainContainer);
- } else {
- window.addEventListener('DOMContentLoaded', () => {
- document.body.appendChild(mainContainer);
- });
- }
-
- // Style for the main container and question divs
- const style = document.createElement('style');
- style.innerHTML = `
- @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500&display=swap');
-
- #questionDataContainer {
- position: fixed;
- top: 10px;
- right: 10px;
- width: 300px;
- height: 500px;
- overflow-y: auto;
- background-color: #1a1a1a;
- border: 2px solid #131313;
- padding: 10px;
- z-index: 9999;
- font-family: Poppins, sans-serif;
- font-size: 12px;
- border-radius: .8vw;
- color: white;
- transition: all .3s ease;
- }
-
- #questionDataContainer::-webkit-scrollbar {
- width: .6vw;
- height: 12px;
- }
-
- #questionDataContainer::-webkit-scrollbar-track {
- background: #141414;
- }
-
- #questionDataContainer::-webkit-scrollbar-thumb {
- background-color: #24c39d;
- border-radius: 1vw;
- }
-
- #questionDataContainer.minimized {
- width: 67px;
- height: 38px;
- overflow: hidden;
- border-radius: 10px;
- padding: 0;
- }
-
- #minimizeButton {
- position: absolute;
- top: 5px;
- right: 5px;
- background-color: #333;
- color: white;
- border: none;
- border-radius: 5px;
- font-size: 12px;
- padding: 5px;
- cursor: pointer;
- font-family: Poppins, sans-serif;
- transition: all .3s ease;
- }
-
- #minimizeButton:hover {
- background-color: #444;
- transition: all .3s ease;
- }
-
- #questionDataContainer.minimized #khanHackHeader {
- opacity: 0;
- }
-
- #khanHackHeader {
- text-align: center;
- font-family: Poppins, sans-serif;
- margin-bottom: 0px;
- font-size: 28px;
- color: #fff;
- }
-
- #khanHackHeader span {
- color: #24c39d;
- }
-
- .question-div {
- margin-bottom: 10px;
- padding: 10px;
- border-radius: 5px;
- color: black;
- overflow: hidden;
- }
-
- .radio-div {
- background-color: #e0ffe0;
- }
-
- .expression-div {
- background-color: #fff0f0;
- }
-
- .dropdown-div {
- background-color: #f0f0ff;
- }
-
- .orderer-div {
- background-color: #f5f5dc;
- }
-
- .input-div {
- background-color: #ffefd5;
- }
-
- .plotter-div {
- background-color: #ffe4b5;
- }
-
- .no-support-div {
- background-color: #ffcccc;
- }
-
- #answerToggle, #refreshButton {
- display: block;
- margin: 10px auto;
- padding: 5px;
- font-size: 14px;
- background-color: #333;
- color: white;
- border: 1px solid #444;
- cursor: pointer;
- border-radius: 5px;
- font-family: Poppins, sans-serif;
- font-weight: 400;
- transition: all .3s ease;
- }
-
- #answerToggle:hover, #refreshButton:hover {
- background-color: #444;
- transition: all .3s ease;
- }
-
- #answerContainer {
- display: block;
- }
-
- #restoreButton {
- display: none;
- position: absolute;
- top: 5px;
- right: 5px;
- background-color: #333;
- color: white;
- border: none;
- border-radius: 5px;
- font-size: 12px;
- padding: 5px;
- cursor: pointer;
- font-family: Poppins, sans-serif;
- }
-
- #restoreButton.minimized {
- display: block;
- }
-
- img.img-ans {
- max-width: 100%;
- height: auto;
- border-radius: 5px;
- margin-top: 10px;
- }
- `;
- document.head.appendChild(style);
-
- // Add header, buttons, and answer container
- const header = document.createElement('h1');
- header.id = 'khanHackHeader';
- header.innerHTML = 'Khan<span>Hack</span>';
-
- const answerToggle = document.createElement('button');
- answerToggle.id = 'answerToggle';
- answerToggle.textContent = 'Hide Answers';
- let answersVisible = true;
-
- const refreshButton = document.createElement('button');
- refreshButton.id = 'refreshButton';
- refreshButton.textContent = 'Reset Answer List';
-
- const minimizeButton = document.createElement('button');
- minimizeButton.id = 'minimizeButton';
- minimizeButton.textContent = 'Minimize';
-
- const restoreButton = document.createElement('button');
- restoreButton.id = 'restoreButton';
- restoreButton.textContent = 'Restore';
-
- const answerContainer = document.createElement('div');
- answerContainer.id = 'answerContainer';
-
- mainContainer.appendChild(minimizeButton);
- mainContainer.appendChild(header);
- mainContainer.appendChild(answerToggle);
- mainContainer.appendChild(refreshButton);
- mainContainer.appendChild(answerContainer);
- mainContainer.appendChild(restoreButton);
-
- // Add functionality to hide/unhide the answers
- answerToggle.addEventListener('click', () => {
- answersVisible = !answersVisible;
- answerContainer.style.display = answersVisible ? 'block' : 'none';
- answerToggle.textContent = answersVisible ? 'Hide Answers' : 'Unhide Answers';
- });
-
- // Add functionality to refresh the page
- refreshButton.addEventListener('click', () => {
- location.reload();
- });
-
- // Add functionality to minimize the entire container
- minimizeButton.addEventListener('click', () => {
- mainContainer.classList.add('minimized');
- restoreButton.classList.add('minimized');
- minimizeButton.style.display = 'none';
- });
-
- // Add functionality to restore the container
- restoreButton.addEventListener('click', () => {
- mainContainer.classList.remove('minimized');
- restoreButton.classList.remove('minimized');
- minimizeButton.style.display = 'block';
- });
-
- // Utility function to convert decimal to simplified fraction
- function decimalToFraction(decimal) {
- if (decimal === 0) return "0";
-
- let negative = decimal < 0;
- decimal = Math.abs(decimal); // Make it positive to simplify calculations
-
- let whole = Math.trunc(decimal);
- let fraction = decimal - whole;
-
- const gcd = (a, b) => b ? gcd(b, a % b) : a;
- const precision = 1000000;
- let numerator = Math.round(fraction * precision);
- let denominator = precision;
-
- let commonDenominator = gcd(numerator, denominator);
- numerator = numerator / commonDenominator;
- denominator = denominator / commonDenominator;
-
- let fractionString = numerator === 0 ? '' : `${Math.abs(numerator)}/${denominator}`;
- let result = whole !== 0 ? `${whole} ${fractionString}`.trim() : fractionString;
-
- if (negative && result) {
- result = `-${result}`;
- }
-
- if (whole === 0 && numerator !== 0) {
- result = `${negative ? '-' : ''}${numerator}/${denominator}`;
- }
-
- return result;
- }
-
- // Function to handle image replacement
- function replaceGraphieImage(content) {
- const imageRegex = /!\[.*?\]\(web\+graphie:\/\/cdn.kastatic.org\/ka-perseus-graphie\/([a-f0-9]+)\)/;
- const match = content.match(imageRegex);
- if (match && match[1]) {
- const imageId = match[1];
- return `<img src="https://cdn.kastatic.org/ka-perseus-graphie/${imageId}.svg" class="img-ans" />`;
- }
- return content;
- }
-
- // Function to append content to the main container
- function appendToGUI(content, questionType) {
- try {
- const div = document.createElement('div');
- div.classList.add('question-div');
-
- // Add specific class based on question type
- if (questionType === 'radio') {
- div.classList.add('radio-div');
- } else if (questionType === 'expression') {
- div.classList.add('expression-div');
- } else if (questionType === 'dropdown') {
- div.classList.add('dropdown-div');
- } else if (questionType === 'orderer') {
- div.classList.add('orderer-div');
- } else if (questionType === 'input') {
- div.classList.add('input-div');
- } else if (questionType === 'plotter') {
- div.classList.add('plotter-div');
- } else if (questionType === 'no-support') {
- div.classList.add('no-support-div');
- }
-
- // Replace image markdown with <img> tag
- content = replaceGraphieImage(content);
-
- div.innerHTML = content; // Use innerHTML to allow image rendering
- answerContainer.appendChild(div); // Append answers to the answer container
- } catch (error) {
- console.error("Failed to append content to GUI:", error);
- }
- }
-
- // Function to clean up LaTeX-like expressions
- function cleanLatexExpression(expr) {
- if (typeof expr !== 'string') return expr; // Ensure it's a string before processing
- return expr.replace(/\\dfrac{(.+?)}{(.+?)}/g, '$1/$2') // Fractions
- .replace(/\\frac{(.+?)}{(.+?)}/g, '$1/$2') // Fractions
- .replace(/\\dfrac(\d+)(\d+)/g, '$1/$2') // Handle shorthand \\dfrac12 as 1/2
- .replace(/\\frac(\d+)(\d+)/g, '$1/$2') // Handle shorthand \\frac12 as 1/2
- .replace(/\\left\(/g, '(')
- .replace(/\\right\)/g, ')')
- .replace(/\\cdot/g, '*')
- .replace(/\\times/g, '*')
- .replace(/\\div/g, '/')
- .replace(/\\\\/g, '')
- .replace(/\\,/g, '')
- .replace(/\\sqrt{(.+?)}(.*?)/g, '√($1)')
- .replace(/\\sqrt/g, '√')
- .replace(/\\cos/g, 'cos') // Handle cosine function
- .replace(/\\sin/g, 'sin') // Handle sine function (in case you encounter it)
- .replace(/\\tan/g, 'tan') // Handle tangent function (in case you encounter it)
- .replace(/\\degree/g, '°') // Handle degree symbol
- .replace(/\\,/g, '')
- .replace(/\\\[/g, '[')
- .replace(/\\\]/g, ']')
- .replace(/\$/g, ''); // Remove dollar signs
- }
-
- // Override fetch to capture /getAssessmentItem response
- window.fetch = function() {
- return originalFetch.apply(this, arguments).then(async (response) => {
- try {
- // Only handle responses that contain assessment items (questions/answers)
- if (response.url.includes("/getAssessmentItem")) {
- const clonedResponse = response.clone();
- const jsonData = await clonedResponse.json();
- const itemData = jsonData.data.assessmentItem.item.itemData;
- const questionData = JSON.parse(itemData).question;
-
- // Log the full question data for debugging
- console.log('Full question data:', questionData);
-
- // Initialize a variable to hold the combined answers for dropdowns per question
- const combinedAnswersPerQuestion = {};
- let numericInputAnswers = [];
-
- // Iterate over each widget and handle the answer logic
- Object.keys(questionData.widgets).forEach(widgetKey => {
- const widget = questionData.widgets[widgetKey];
- let answer = "No answer available";
-
- // Log the entire widget for debugging
- console.log('Widget data:', widget);
-
- try {
- // Handle numeric-input type questions
- if (widget.type === "input-number" || widget.type === "numeric-input") {
- let answer;
-
- // Check different possible paths for the answer, ensuring 0 is treated as a valid answer
- if (widget.options?.value !== undefined && widget.options?.value !== null) {
- answer = widget.options.value;
- } else if (widget.options?.answers?.[0]?.value !== undefined && widget.options?.answers?.[0]?.value !== null) {
- answer = widget.options.answers[0].value;
- }
-
- // If the answer is found, push it to the numericInputAnswers array
- if (answer !== undefined && answer !== null) {
- numericInputAnswers.push(answer); // Add the answer to the array
- } else {
- console.error("Answer not found for widget:", widget);
- }
- }
-
- // Handle graphing type questions
- else if (widget.type === "grapher" || widget.type === "interactive-graph") {
- if (widget.options?.correct?.coords && widget.options.correct.coords.length > 0) {
- const coords = widget.options.correct.coords.map(coord => `(${coord.join(", ")})`);
- appendToGUI(`Graphing Question: Correct Coordinates: ${coords.join(" and ")}`, 'plotter');
- console.log('Graphing answers (coordinates):', coords);
- } else {
- appendToGUI(`Graphing Question: No valid coordinates found`, 'plotter');
- console.log('Graphing Question: No valid coordinates found');
- }
- }
-
- // Handle unsupported marker type questions
- else if (widget.type === "label-image") {
- appendToGUI("No hack support for this problem", 'no-support');
- }
-
- // Handle intercept type questions
- else if (widget.type === "numeric-input" && widget.options?.answers) {
- const yIntercept = `(0, ${widget.options.answers[0].value})`;
- const xIntercept = `(${widget.options.answers[1].value}, 0)`;
- appendToGUI(`y-intercept: ${yIntercept}, x-intercept: ${xIntercept}`, 'input');
- }
-
- // Handle dropdown type questions
- else if (widget.type === "dropdown") {
- if (widget.options?.choices) {
- answer = widget.options.choices.filter(c => c.correct).map(c => cleanLatexExpression(c.content));
- }
- const questionContent = questionData.content;
- if (!combinedAnswersPerQuestion[questionContent]) {
- combinedAnswersPerQuestion[questionContent] = [];
- }
- combinedAnswersPerQuestion[questionContent].push(...answer);
- }
-
- // Handle expression type questions
- else if (widget.type === "expression") {
- if (widget.options?.answerForms && widget.options.answerForms.length > 1) {
- answer = widget.options.answerForms.filter(af => af.considered === "correct").map(af => cleanLatexExpression(af.value));
- appendToGUI(`Any of the following: ${JSON.stringify(answer, null, 2)}`, 'expression');
- } else {
- answer = widget.options.answerForms.filter(af => af.considered === "correct").map(af => cleanLatexExpression(af.value));
- appendToGUI(`Question Type: expression, Answer: ${JSON.stringify(answer, null, 2)}`, 'expression');
- }
- console.log('Expression answers:', answer);
- }
-
- // Handle orderer type questions
- else if (widget.type === "orderer") {
- if (widget.options?.correctOptions) {
- const correctOrder = widget.options.correctOptions.map(option => option.content);
- appendToGUI(`Orderer Question: Correct Order: ${JSON.stringify(correctOrder, null, 2)}`, 'orderer');
- console.log('Orderer answers:', answer);
- }
- }
-
- // Handle radio type questions
- else if (widget.type === "radio") {
- if (widget.options?.choices) {
- const correctChoices = widget.options.choices.filter(c => c.correct).map(c => cleanLatexExpression(c.content || "None of the above"));
- answer = correctChoices;
- answer = answer.map(choice => replaceGraphieImage(choice)); // Replace image markdown
- }
- appendToGUI(`Question Type: radio, Answer: ${answer.join(', ')}`, 'radio');
- console.log('Radio answers:', answer);
- }
-
- // Handle plotter type questions
- else if (widget.type === "plotter") {
- const correctAnswers = widget.options?.correct || [];
- appendToGUI(`Data Plot Locations in Order: Answers: ${correctAnswers.join(", ")}`, 'plotter');
- console.log('Plotter correct answers:', correctAnswers);
- }
-
- } catch (innerError) {
- console.error("Error processing widget:", widget, innerError);
- }
- });
-
- if (numericInputAnswers.length > 0) {
- appendToGUI(`Numeric Input Question: Correct Answer(s): [${numericInputAnswers.join(', ')}]`, 'input');
- console.log(`Numeric Input Answers: [${numericInputAnswers.join(', ')}]`);
- }
-
- // Display combined dropdown answers at once after all widgets are processed
- Object.keys(combinedAnswersPerQuestion).forEach(questionContent => {
- const finalCombinedAnswers = combinedAnswersPerQuestion[questionContent];
- appendToGUI(`Combined Answers: ${JSON.stringify(finalCombinedAnswers, null, 2)}`, 'dropdown');
- console.log('Dropdown combined answers:', finalCombinedAnswers);
- });
-
- }
- return response;
- } catch (error) {
- console.error('Failed to fetch assessment data:', error);
- }
- }).catch((error) => {
- console.error("Network or fetch error:", error);
- });
- };
- })();
- })();