ReadTheory

Automatically answers ReadTheory quiz questions using AI

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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