ReadTheory

Automatically answers ReadTheory quiz questions using AI

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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

(function () {
    'use strict';

    const API_URL = 'https://openrouter.ai/api/v1/chat/completions';
    const API_KEY = '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';
    const MODEL = 'openrouter/free';

    const DELAYS = {
        SELECT_TO_SUBMIT: 800,
        POLL_INTERVAL: 1500,
        INIT: 2000,
        NO_BUTTON_RELOAD: 3,
    };

    let processing = false;
    let lastQuestion = null;
    let noButtonStreak = 0;

    function getPassage() {
        const wrapper = document.querySelector('.description-wrapper');
        if (!wrapper) return null;
        return [...wrapper.querySelectorAll('p')]
            .map(p => p.textContent.trim())
            .filter(Boolean)
            .join('\n');
    }

    function getQuestion() {
        const el = document.querySelector('.student-quiz-page__question');
        return el ? el.textContent.trim() : null;
    }

    function getChoices() {
        const answers = document.querySelector('.student-quiz-page__answers[role="radiogroup"]');
        if (!answers) return null;
        return [...answers.querySelectorAll('.student-quiz-page__answer.answer-card-wrapper[role="radio"]')]
            .map((el) => {
                const letter = el.querySelector('.answer-card__alpha')?.textContent.trim();
                const text = el.querySelector('.answer-card__body')?.textContent.trim();
                if (!letter || !text) return null;
                return { letter, text, el };
            })
            .filter(Boolean);
    }

    function isAnswerSelected() {
        return !!document.querySelector(
            '.student-quiz-page__answer.answer-card-wrapper.selected, ' +
            '.student-quiz-page__answer.answer-card-wrapper[aria-checked="true"]'
        );
    }

    function getActionButton() {
        return (
            document.querySelector('.primary-button.student-quiz-page__question-next.next-btn.quiz-tab-item') ||
            document.querySelector('.primary-button.student-quiz-page__question-submit.quiz-tab-item')
        );
    }

    function clickSubmit() {
        const btn = getActionButton();
        if (btn && !btn.classList.contains('disabled')) {
            btn.click();
            return true;
        }
        return false;
    }

    function sleep(ms) {
        return new Promise(r => setTimeout(r, ms));
    }

    function waitForNewQuestion(oldQuestion, timeout = 8000) {
        return new Promise((resolve) => {
            const deadline = Date.now() + timeout;
            const iv = setInterval(() => {
                const q = getQuestion();
                if ((q && q !== oldQuestion) || Date.now() > deadline) {
                    clearInterval(iv);
                    resolve();
                }
            }, 200);
        });
    }

    async function askAI(passage, question, choices) {
        const choiceLines = choices.map(c => `${c.letter}. ${c.text}`).join('\n');
        const prompt = `Read the passage and answer the multiple choice question.
Respond with ONLY a single letter (A, B, C, D, or E). No explanation.

PASSAGE:
${passage}

QUESTION:
${question}

CHOICES:
${choiceLines}

Answer:`;

        const res = await fetch(API_URL, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${API_KEY}`,
            },
            body: JSON.stringify({
                model: MODEL,
                max_tokens: 200,
                temperature: 0.1,
                reasoning: { effort: 'none' },
                messages: [{ role: 'user', content: prompt }],
            }),
        });

        if (!res.ok) throw new Error(`API ${res.status}`);

        const data = await res.json();
        const raw = data?.choices?.[0]?.message?.content?.trim().toUpperCase() ?? '';
        const match = raw.match(/[A-E]/);
        return match ? match[0] : null;
    }

    function selectAnswer(choices, letter) {
        const target = choices.find(c => c.letter === letter);
        if (!target) return false;
        target.el.click();
        return true;
    }

    async function tick() {
        if (processing) return;

        const btn = getActionButton();
        if (!btn) {
            noButtonStreak++;
            if (noButtonStreak >= DELAYS.NO_BUTTON_RELOAD) {
                console.log('[RT] No button for 3 ticks, reloading');
                location.reload();
            }
            return;
        }
        noButtonStreak = 0;

        const question = getQuestion();
        if (!question) return;

        if (question === lastQuestion) return;

        if (isAnswerSelected()) {
            lastQuestion = question;
            processing = true;
            await sleep(DELAYS.SELECT_TO_SUBMIT);
            if (clickSubmit()) await waitForNewQuestion(question);
            processing = false;
            lastQuestion = null;
            return;
        }

        const passage = getPassage();
        const choices = getChoices();
        if (!passage || !choices?.length) return;

        processing = true;
        lastQuestion = question;

        console.log('[RT] Question:', question);

        try {
            const answer = await askAI(passage, question, choices);
            if (!answer) throw new Error('No answer from AI');

            console.log('[RT] AI picked:', answer);

            if (!selectAnswer(choices, answer)) throw new Error(`Couldn't click choice ${answer}`);

            await sleep(DELAYS.SELECT_TO_SUBMIT);

            if (clickSubmit()) {
                console.log('[RT] Submitted');
                await waitForNewQuestion(question);
            } else {
                console.warn('[RT] Submit button not ready');
            }
        } catch (err) {
            console.error('[RT] Error:', err);
        }

        processing = false;
        lastQuestion = null;
    }

    function init() {
        console.log('[RT] Started');
        setTimeout(tick, DELAYS.INIT);
        setInterval(() => {
            if (!processing) tick();
        }, DELAYS.POLL_INTERVAL);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();