KhanHack

Sloppy lazybones anti-homework production

// ==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);
        });
    };
})();
})();