Automatically answers ReadTheory quiz questions using AI
// ==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();
}
})();