ReadTheory

Automatically answers ReadTheory quiz questions using AI

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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