ReadTheory

Automatically answers ReadTheory quiz questions using AI

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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