ReadTheory

Automatically answers ReadTheory quiz questions using AI

// ==UserScript==
// @name         ReadTheory
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Automatically answers ReadTheory quiz questions using AI
// @author       theroyalwhale
// @match        https://readtheoryapp.com/app/student/quiz
// @grant        GM_xmlhttpRequest
// @connect      openrouter.ai
// @icon         https://dyntech.cc/favicon?q=https://readtheory.org
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const AI_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
    const AI_API_KEY = 'Grab your own key from OpenRouter';
    const AI_MODEL = 'moonshotai/kimi-k2-0905';

    let isProcessing = false;
    let currentUrl = window.location.href;
    let lastProcessedQuestion = null;
    let noSubmitButtonCount = 0;

    // Check if question has already been processed
    function isQuestionAlreadyProcessed(question) {
        return lastProcessedQuestion === question;
    }

    // Check if an answer is already selected
    function isAnswerAlreadySelected() {
        const selectedAnswer = document.querySelector('.student-quiz-page__answer.answer-card-wrapper.selected') ||
                             document.querySelector('.student-quiz-page__answer.answer-card-wrapper[aria-checked="true"]') ||
                             document.querySelector('.answer-card-wrapper.selected');
        return selectedAnswer !== null;
    }

    // Check if submit/next button exists
    function submitButtonExists() {
        const nextButton = document.querySelector('.primary-button.student-quiz-page__question-next.next-btn.quiz-tab-item');
        const submitButton = document.querySelector('.primary-button.student-quiz-page__question-submit.quiz-tab-item');
        return nextButton || submitButton;
    }
    function isOnQuizPage() {
        return window.location.href.includes('/app/student/quiz') &&
               window.location.href === currentUrl;
    }

    // Extract passage text from description wrapper
    function extractPassageText() {
        const descriptionWrapper = document.querySelector('.description-wrapper');
        if (!descriptionWrapper) return null;

        const paragraphs = descriptionWrapper.querySelectorAll('p');
        let passageText = '';
        paragraphs.forEach(p => {
            passageText += p.textContent.trim() + '\n';
        });
        return passageText.trim();
    }

    // Extract question text
    function extractQuestion() {
        const questionElement = document.querySelector('.student-quiz-page__question[role="title"]');
        return questionElement ? questionElement.textContent.trim() : null;
    }

    // Extract answer choices
    function extractAnswerChoices() {
        const answersContainer = document.querySelector('.student-quiz-page__answers.quiz-tab-item[role="main"]');
        if (!answersContainer) return null;

        const choices = [];
        const choiceElements = answersContainer.querySelectorAll('.student-quiz-page__answer.answer-card-wrapper[role="radio"]');

        choiceElements.forEach((element, index) => {
            const alphaElement = element.querySelector('.answer-card__alpha');
            const bodyElement = element.querySelector('.answer-card__body');

            if (alphaElement && bodyElement) {
                const letter = alphaElement.textContent.trim();
                const text = bodyElement.textContent.trim();
                choices.push({
                    letter: letter,
                    text: text,
                    element: element,
                    index: index
                });
            }
        });

        return choices;
    }

    // Call AI API to get the answer
    async function getAIAnswer(passageText, question, choices) {
        const choicesText = choices.map(choice => `${choice.letter}. ${choice.text}`).join('\n');

        const prompt = `Please read the following passage and answer the multiple choice question.
You MUST respond with ONLY the letter of the correct answer (A, B, C, D, or E).
Do not include any explanation or additional text - just the single letter.

PASSAGE:
${passageText}

QUESTION:
${question}

CHOICES:
${choicesText}

Answer (single letter only):`;

        try {
            // Generic API call structure - modify based on your specific AI service
            const response = await fetch(AI_API_URL, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${AI_API_KEY}`
                },
                body: JSON.stringify({
                    model: AI_MODEL,
                    messages: [
                        {
                            role: 'user',
                            content: prompt
                        }
                    ],
                    max_tokens: 10,
                    temperature: 0.1
                })
            });

            if (!response.ok) {
                throw new Error(`API request failed: ${response.status}`);
            }

            const data = await response.json();

            // Extract answer based on common API response formats
            let answer;
            if (data.choices && data.choices[0] && data.choices[0].message) {
                // OpenAI format
                answer = data.choices[0].message.content.trim().toUpperCase();
            } else if (data.content && data.content[0] && data.content[0].text) {
                // Anthropic format
                answer = data.content[0].text.trim().toUpperCase();
            } else if (typeof data === 'string') {
                // Simple string response
                answer = data.trim().toUpperCase();
            } else {
                throw new Error('Unexpected API response format');
            }

            // Extract just the letter (remove any extra characters)
            const letterMatch = answer.match(/[A-Z]/);
            return letterMatch ? letterMatch[0] : null;

        } catch (error) {
            console.error('AI API Error:', error);
            return null;
        }
    }

    // Click the answer choice
    function selectAnswer(choices, answerLetter) {
        const selectedChoice = choices.find(choice => choice.letter === answerLetter);
        if (selectedChoice) {
            console.log(`Selecting answer: ${answerLetter} - ${selectedChoice.text}`);
            selectedChoice.element.click();
            return true;
        }
        return false;
    }

    // Click the submit button
    function submitAnswer() {
        // Try to find Next button first (after answer is selected)
        let submitButton = document.querySelector('.primary-button.student-quiz-page__question-next.next-btn.quiz-tab-item');

        // If Next button not found, try the Submit button
        if (!submitButton) {
            submitButton = document.querySelector('.primary-button.student-quiz-page__question-submit.quiz-tab-item');
        }

        if (submitButton && !submitButton.classList.contains('disabled')) {
            console.log('Clicking button:', submitButton.textContent.trim());
            submitButton.click();
            return true;
        }
        return false;
    }

    // Main processing function
    async function processQuestion() {
        if (isProcessing || !isOnQuizPage()) return;

        // Check if submit button exists, if not for multiple checks, refresh
        if (!submitButtonExists()) {
            noSubmitButtonCount++;
            if (noSubmitButtonCount >= 3) {
                console.log('Submit button missing for 3+ checks, refreshing page...');
                location.reload();
                return;
            }
            return;
        } else {
            noSubmitButtonCount = 0; // Reset counter if button exists
        }

        // Extract question to check if it's already processed
        const question = extractQuestion();
        if (!question) {
            console.log('No question found, waiting...');
            return;
        }

        // Skip if already processed this question
        if (isQuestionAlreadyProcessed(question)) {
            console.log('Question already processed, skipping AI call...');

            // Check if we need to submit an already selected answer
            if (isAnswerAlreadySelected()) {
                setTimeout(() => {
                    if (submitAnswer()) {
                        console.log('Previously selected answer submitted');
                        setTimeout(() => {
                            isProcessing = false;
                            currentUrl = window.location.href;
                            lastProcessedQuestion = null; // Reset for new question
                        }, 3000);
                    }
                }, 5000);
            }
            return;
        }

        // Skip if answer is already selected (but question is new)
        if (isAnswerAlreadySelected()) {
            console.log('Answer already selected for this question, just submitting...');
            lastProcessedQuestion = question;
            setTimeout(() => {
                if (submitAnswer()) {
                    console.log('Already selected answer submitted');
                    setTimeout(() => {
                        isProcessing = false;
                        currentUrl = window.location.href;
                        lastProcessedQuestion = null;
                    }, 3000);
                }
            }, 5000);
            return;
        }

        isProcessing = true;
        console.log('Processing new question...');

        try {
            // Extract all required elements
            const passageText = extractPassageText();
            const choices = extractAnswerChoices();

            if (!passageText || !question || !choices || choices.length === 0) {
                console.log('Missing required elements, waiting...');
                isProcessing = false;
                return;
            }

            console.log('Passage extracted:', passageText.substring(0, 100) + '...');
            console.log('Question:', question);
            console.log('Choices:', choices.map(c => `${c.letter}: ${c.text}`));

            // Mark this question as processed
            lastProcessedQuestion = question;

            // Get AI answer
            const aiAnswer = await getAIAnswer(passageText, question, choices);

            if (!aiAnswer) {
                console.error('Failed to get AI answer');
                isProcessing = false;
                lastProcessedQuestion = null; // Reset if failed
                return;
            }

            console.log('AI Answer:', aiAnswer);

            // Select the answer
            if (selectAnswer(choices, aiAnswer)) {
                // Wait 5 seconds before submitting
                setTimeout(() => {
                    if (submitAnswer()) {
                        console.log('Answer submitted successfully');
                        // Wait for page to refresh/change
                        setTimeout(() => {
                            isProcessing = false;
                            currentUrl = window.location.href;
                            lastProcessedQuestion = null; // Reset for new question
                        }, 3000);
                    } else {
                        console.log('Submit button not ready, will retry...');
                        isProcessing = false;
                    }
                }, 5000);
            } else {
                console.error('Failed to select answer:', aiAnswer);
                isProcessing = false;
                lastProcessedQuestion = null; // Reset if failed
            }

        } catch (error) {
            console.error('Error processing question:', error);
            isProcessing = false;
            lastProcessedQuestion = null; // Reset if error
        }
    }

    // Start the automation
    function startAutomation() {
        console.log('ReadTheory Auto Answer script started');
        console.log('Make sure to configure your AI API credentials in the script!');

        // Initial attempt
        setTimeout(processQuestion, 2000);

        // Set up interval to check for new questions
        setInterval(() => {
            if (isOnQuizPage() && !isProcessing) {
                processQuestion();
            } else if (!isOnQuizPage()) {
                console.log('Quiz completed or navigated away from quiz page');
            }
        }, 3000);
    }

    // Wait for page to load completely
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', startAutomation);
    } else {
        startAutomation();
    }

})();