Auto-fills answers on Education Perfect with the help of AI.
// ==UserScript==
// @name EP Answer Assistant chat - Full version
// @namespace https://educationperfect.com
// @version 4.0.0
// @description Auto-fills answers on Education Perfect with the help of AI.
// @author lllons and Otjl12
// @match https://app.educationperfect.com/*
// @match https://*.educationperfect.com/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const debug = true;
const hints = true;
const hintsList = {
list: "Auto-types at your cursor. Press Enter to submit.",
activity: "Auto-selects or type the correct answer using AI.",
}
const CFG = {
fuzzyThreshold : 10,
typeDelay : 0,
toastDuration : 5000,
pollInterval : 0,
cooldown : 0.5,
typeCooldown : 0.1,
};
const SEL = {
targetLang : '.targetLanguage.question-label',
baseLang : '.baseLanguage.question-label',
answerInput : '#answer-text',
prompt : '.prompt.ng-binding',
};
let answerMap = {};
let enabled = true;
let lastFilled = '';
let filling = false;
let cooldownUntil = 0;
let observer = null;
let pollTimer = null;
let activeEditable = null;
let pageChanging = false;
let lastTypeTime = 0;
let auto = true;
let panelUnlocked = false;
let vocabUnlocked = false;
let aiUnlocked = false;
window.addEventListener('beforeunload', () => { pageChanging = true; });
document.addEventListener('focusin', e => {
const el = e.target;
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el.isContentEditable) {
activeEditable = el;
}
});
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
// ── Strip everything after the first semicolon (including the semicolon) ──
function stripAlts(s) {
if (!s) return s;
const idx = s.indexOf(';');
return idx === -1 ? s : s.slice(0, idx).trim();
}
function norm(s) {
return (s || '')
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
// Updated regex below to allow math symbols: +, -, *, /, x, =
.replace(/[^a-z0-9\s+\-*/x=]/g, '')
.replace(/\s+/g, ' ')
.trim();
}
function similarity(a, b) {
if (!a || !b) return 0;
if (a === b) return 1;
const la = a.length, lb = b.length;
if (Math.abs(la - lb) / Math.max(la, lb) > 0.55) return 0;
const dp = Array.from({ length: la + 1 }, (_, i) => [i]);
for (let j = 0; j <= lb; j++) dp[0][j] = j;
for (let i = 1; i <= la; i++)
for (let j = 1; j <= lb; j++)
dp[i][j] = a[i-1] === b[j-1]
? dp[i-1][j-1]
: 1 + Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]);
return 1 - dp[la][lb] / Math.max(la, lb);
}
function findAnswer(raw) {
const q = norm(raw);
if (!q) return null;
if (answerMap[q]) return answerMap[q];
for (const [k, v] of Object.entries(answerMap))
if (k.includes(q) || q.includes(k)) return v;
let best = 0, bestVal = null;
for (const [k, v] of Object.entries(answerMap)) {
const sc = similarity(q, k);
if (sc > best) { best = sc; bestVal = v; }
}
return best >= CFG.fuzzyThreshold ? bestVal : null;
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
function fullList() {
const full = document.getElementById("full-list-switcher");
if (full) full.click();
setTimeout(() => {
document.querySelector('#slim-scroll-content .preview-grid')?.lastElementChild?.scrollIntoView({ block: 'end' });
setTimeout(() => {
loadAnswers();
}, 1000);
document.getElementsByClassName("main-text ng-binding infinity")[0].click();
}, 500);
}
function loadAnswers() {
document.querySelector('#slim-scroll-content .preview-grid')?.lastElementChild?.scrollIntoView({ block: 'end' });
const map = {};
let count = 0;
const targets = [...document.querySelectorAll(SEL.targetLang)];
const bases = [...document.querySelectorAll(SEL.baseLang)];
const len = Math.min(targets.length, bases.length);
for (let i = 0; i < len; i++) {
// Strip semicolon alts from BOTH sides when building the map
const target = stripAlts((targets[i].textContent || '').trim());
const base = stripAlts((bases[i].textContent || '').trim());
if (target && base) {
map[norm(target)] = base;
map[norm(base)] = target;
count++;
}
}
answerMap = map;
lastFilled = '';
cooldownUntil = 0;
updatePanel(count);
showToast(count > 0 ? `✅ ${count} pairs loaded` : `⚠️ No vocab found`);
console.log('[EP] Loaded', count, 'pairs');
document.getElementById("start-button-main-label").click();
return count;
}
function getQuestionWord() {
const JUNK = /^(replay|hint|submit|electronic|voice|translate|from|to|french|english|writing|reading|listening|dictation|speaking|practise|pronunciation|master|advanced|unit|vocab|list|\d+%?)$/i;
const span = document.getElementById("question-text");
if (!span) return null;
const text = (span.textContent || '').trim();
console.log('[EP] Question text:', text);
if (text.length >= 2 && text.length <= 80 && !JUNK.test(text)) return text;
return null;
}
function setNativeValue(el, value) {
const proto = Object.getPrototypeOf(el);
const descriptor = Object.getOwnPropertyDescriptor(proto, 'value');
if (descriptor && descriptor.set) descriptor.set.call(el, value);
else el.value = value;
}
async function typeAtCursor(el, text) {
if (!el) return;
el.focus();
if (el.isContentEditable) {
const sel = window.getSelection();
if (!sel.rangeCount) return;
let range = sel.getRangeAt(0);
for (const ch of text) {
range.deleteContents();
const node = document.createTextNode(ch);
range.insertNode(node);
range.setStartAfter(node);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: ch }));
if (CFG.typeDelay > 0) await sleep(CFG.typeDelay);
}
return;
}
for (const ch of text) {
const start = el.selectionStart ?? el.value.length;
const end = el.selectionEnd ?? start;
const newValue = el.value.slice(0, start) + ch + el.value.slice(end);
setNativeValue(el, newValue);
el.setSelectionRange(start + 1, start + 1);
el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: ch }));
if (CFG.typeDelay > 0) await sleep(CFG.typeDelay);
}
}
async function tryFill() {
if (!enabled || filling || pageChanging) return;
if (Date.now() < cooldownUntil) return;
if (Date.now() - lastTypeTime < CFG.typeCooldown) return;
const input = activeEditable || document.activeElement || document.querySelector(SEL.answerInput);
if (!input) return;
if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {
if ((input.value || '').trim().length > 0) return;
}
const word = getQuestionWord();
if (!word) return;
let answer = findAnswer(word);
if (!answer) {
cooldownUntil = Date.now() + CFG.cooldown;
setDebug(`No match: "${word}"`);
return;
}
// Strip semicolon alts from the answer at fill time too (safety net)
answer = stripAlts(answer);
const key = `${word}→${answer}`;
if (key === lastFilled) return;
lastFilled = key;
filling = true;
lastTypeTime = Date.now();
setDebug(`Typing: "${word}" → "${answer}"`);
console.log(`[EP] "${word}" → "${answer}"`);
try {
await typeAtCursor(input, answer);
showToast(`💡 ${answer.length > 48 ? answer.slice(0,48)+'…' : answer}`);
} catch (e) {
console.warn('[EP] Typing error:', e);
}
filling = false;
}
function startObserver() {
if (observer) observer.disconnect();
observer = new MutationObserver(() => { tryFill(); });
observer.observe(document.body, { childList: true, subtree: true });
}
function startPolling() {
if (pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(() => { tryFill(); }, CFG.pollInterval);
}
// ── UI ────────────────────────────────────────────────────────────────────
let panel, countEl, toggleBtn, debugEl, autoBtn, aiBtn;
function setDebug(msg) { if (debugEl) debugEl.textContent = msg; }
function buildPanel() {
const style = document.createElement('style');
style.textContent = `
#ep-panel{position:fixed;top:72px;right:16px;z-index:2147483647;width:240px;
background:#0f1120;border:1px solid #222540;border-radius:14px;
box-shadow:0 12px 44px rgba(0,0,0,.65);
font:13px/1.45 'Segoe UI',system-ui,sans-serif;color:#c4ccec;user-select:none}
#ep-handle{display:flex;align-items:center;justify-content:space-between;
padding:10px 14px;border-bottom:1px solid #1c2040;cursor:grab}
#ep-handle:active{cursor:grabbing}
#ep-logo{font-weight:700;color:#6aa3f8}
#ep-x{background:none;border:none;color:#666;font-size:16px;cursor:pointer}
#ep-body{padding:14px}
#ep-count{font-size:12px;margin-bottom:12px}
#ep-count.ok{color:#4ad97d}#ep-count.warn{color:#ffb347}
#ep-btns{display:flex;gap:8px;margin-bottom:12px}
.ep-btn{flex:1;padding:8px;border:none;border-radius:8px;cursor:pointer;
background:#1b213a;color:#c4ccec;font-weight:600}
.ep-btn:hover{background:#28304d}
.paused{background:#6d2222 !important}
#ep-hint{font-size:11px;color:#68708f;line-height:1.5}
#ep-debug{margin-top:10px;font-size:10px;color:#58607a;word-break:break-word}
#ep-toast{position:fixed;bottom:22px;right:18px;z-index:2147483647;
background:#0f1120;border:1px solid #222540;border-radius:11px;
padding:10px 17px;color:#c4ccec;box-shadow:0 8px 32px rgba(0,0,0,.6);
opacity:0;transform:translateY(7px);transition:opacity .18s,transform .18s;
pointer-events:none;max-width:270px}
#ep-toast.show{opacity:1;transform:translateY(0)}
`;
document.head.appendChild(style);
panel = document.createElement('div');
panel.id = 'ep-panel';
panel.innerHTML = `
<div id="ep-handle">
<span id="ep-logo">EP Assistant</span>
<button id="ep-x">✕</button>
</div>
<div id="ep-body">
<div id="ep-count">Not loaded</div>
<div id="ep-btns">
<button class="ep-btn" id="ep-auto">Auto</button>
<button class="ep-btn" id="ep-load">Load</button>
<button class="ep-btn" id="ep-toggle">⏸ Pause</button>
<button class="ep-btn" id="ep-ai">AI</button>
</div>
<div id="ep-hint">Select a task to activate.</div>
<div id="ep-debug"></div>
</div>
`;
document.body.appendChild(panel);
countEl = panel.querySelector('#ep-count');
toggleBtn = panel.querySelector('#ep-toggle');
debugEl = panel.querySelector('#ep-debug');
autoBtn = panel.querySelector('#ep-auto');
aiBtn = panel.querySelector('#ep-ai');
autoBtn.addEventListener('click', () => {
showToast('Auto mode');
auto = !auto;
autoBtn.textContent = auto ? 'Auto' : 'Manual';
if (vocabUnlocked && auto) fullList();
});
panel.querySelector('#ep-load').addEventListener('click', fullList);
panel.querySelector('#ep-x').addEventListener('click', () => { panel.style.display = 'none'; });
toggleBtn.addEventListener('click', () => {
enabled = !enabled;
toggleBtn.textContent = enabled ? '⏸ Pause' : '▶ Resume';
toggleBtn.classList.toggle('paused', !enabled);
showToast(enabled ? '▶ Resumed' : '⏸ Paused');
});
aiBtn.addEventListener('click', () => {
showToast('🤖 AI mode');
// Put your AI functionality here
});
makeDraggable(panel, panel.querySelector('#ep-handle'));
updatePanelVisibility();
}
function updatePanel(count) {
if (!countEl) return;
countEl.textContent = count > 0 ? `✅ ${count} pairs loaded` : '❌ No vocab found';
countEl.className = count > 0 ? 'ok' : 'warn';
}
let toastEl, toastTimer;
function showToast(msg) {
if (!toastEl) { toastEl = document.createElement('div'); toastEl.id='ep-toast'; document.body.appendChild(toastEl); }
toastEl.textContent = msg;
toastEl.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastEl.classList.remove('show'), CFG.toastDuration);
}
function makeDraggable(el, handle) {
handle.addEventListener('mousedown', e => {
e.preventDefault();
const r=el.getBoundingClientRect(),ox=e.clientX-r.left,oy=e.clientY-r.top;
const mv=e=>{el.style.right='auto';el.style.left=(e.clientX-ox)+'px';el.style.top=(e.clientY-oy)+'px'};
const up=()=>{document.removeEventListener('mousemove',mv);document.removeEventListener('mouseup',up)};
document.addEventListener('mousemove',mv);document.addEventListener('mouseup',up);
});
}
function updatePanelVisibility() {
const url = window.location.href.toLowerCase();
if (url.includes('list-starter')) {
vocabUnlocked = true;
}
if (url.includes('activity-starter')) {
aiUnlocked = true;
}
if (vocabUnlocked &&
!url.includes('list-starter') &&
!url.includes('game')) {
vocabUnlocked = false;
answerMap = {};
panel.querySelector('#ep-count').textContent = 'Not loaded';
}
if (aiUnlocked &&
!url.includes('activity-starter') &&
!url.includes('game')) {
aiUnlocked = false;
}
const loadBtn = document.querySelector('#ep-load');
const pauseBtn = document.querySelector('#ep-toggle');
const countEl = document.querySelector('#ep-count');
const aiBtn = document.querySelector('#ep-ai');
const hintTxt = document.querySelector('#ep-hint');
const debugTxt = document.querySelector('#ep-debug');
if (loadBtn) loadBtn.style.display = vocabUnlocked ? '' : 'none';
if (pauseBtn) pauseBtn.style.display = vocabUnlocked ? '' : 'none';
if (countEl) countEl.style.display = vocabUnlocked ? '' : 'none';
if (aiBtn) aiBtn.style.display = aiUnlocked ? '' : 'none';
if (hintTxt) hintTxt.style.display = hints ? '' : 'none';
if (vocabUnlocked) hintTxt.innerHTML = hintsList.list;
if (aiUnlocked) hintTxt.innerHTML = hintsList.activity;
if (debugTxt) debugTxt.style.display = debug ? '' : 'none';
if (url.includes('list-starter') && auto) {
setTimeout(() => {
const count = fullList();
if (count === 0) showToast('⚠️ Open vocab list first then press Load');
}, 500);
}
if (aiUnlocked && auto) {
if (url.includes('activity-starter')) {
setTimeout(() => {
const startBtn = document.getElementById("start-button-school");
if (startBtn) startBtn.click();
}, 2000);
}
if (url.includes('game')) {
setInterval(() => {
const bar = document.querySelector('.game-action-bar.sa-action-bar');
console.log('[EP] Checking for action bar:', bar);
if (bar) {
const type = bar.classList[2];
console.log('[EP] Detected action type:', type);
if (type === 'information') {
setTimeout(() => {
document.querySelector("#sa-navigation-controls > div.sa-navigation-controls-content.h-group.v-align-center.h-align-space-between.align-right > div.information-controls.ng-isolate-scope > div > button").click();
}, 3500);
}
}
}, 3000);
}
}
}
function init() {
buildPanel();
const url = window.location.href.toLowerCase();
if (url.includes('list-starter') && auto) {
setTimeout(() => {
const count = fullList();
if (count === 0) showToast('⚠️ Open vocab list first then press Load');
}, 4000);
}
startObserver();
startPolling();
updatePanelVisibility();
let lastUrl = location.href;
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
updatePanelVisibility();
}
}, 500);
console.log('[EP Assistant v4.0.0] Ready');
}
if (document.readyState === 'loading')
document.addEventListener('DOMContentLoaded', init);
else
setTimeout(init, 1000);
})();