Very good ep hack. has support for lists and regular tasks. unfortunately no assessments yet :(
// ==UserScript==
// @name Education Perfect Hack
// @namespace https://github.com/BlastedMeteor44
// @version 3.5
// @description Very good ep hack. has support for lists and regular tasks. unfortunately no assessments yet :(
// @author @blastedmeteor44
// @icon https://yt3.googleusercontent.com/17hrdeXAdCoXUJ_u86ME_kne0JGAnV4sVveYhNmlbrFPt2_Cu19NoUX5MxXnDGBa4FD8hI0C1A=s900-c-k-c0x00ffffff-no-rj
// @match *://*.educationperfect.com/*
// @run-at document-start
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const vocabularyMap = {};
window.epAnswers = [];
let consoleVisible = false;
const panelState = { top: '80px', left: '20px' };
const modules = [
{
name: "skip information wait",
inputs: [
{ type: "toggle", update: true },
],
run: function(state) {
if (state) {
window._skipTimerInterval = setInterval(() => {
const ctrl = angular.element(document.getElementsByClassName('information-controls')).scope()?.self?.informationControls;
if (!ctrl?.timer?.running) return;
ctrl.secondsRemaining = 0;
ctrl.timer.completeEvent.dispatch();
}, 100);
} else {
clearInterval(window._skipTimerInterval);
window._skipTimerInterval = null;
}
}
},
{
name: "Anti Snitch",
inputs: [
{
type: "toggle",
name: "toggle anti snitch",
update: true
},
],
run: function(state) {
if(state){
startAntiSnitch();
}
else {
stopAntiSnitch();
}
}
},
]
// ------ anti focus snitch ------
const originalOpen = XMLHttpRequest.prototype.open;
const BLOCKED_REQUESTS = [
'SubmitTaskMonitoringStatus',
'GetTaskMonitoringStatus',
'UserProfileFactsPortal.GetValues',
'UserProfileFactsPortal.SetValues',
'shouldTrackDevConsole',
];
function startAntiSnitch() {
XMLHttpRequest.prototype.open = function (...args) {
const url = args[1] ?? '';
if (BLOCKED_REQUESTS.some(b => url.includes(b))) {
return;
}
originalOpen.apply(this, args);
};
window.mutationObserver = new MutationObserver(() => {
const popup = document.getElementById('focus-indicator');
if (popup) {
popup.remove();
document.title = 'EP';
}
});
window.mutationObserver.observe(document, { childList: true, subtree: true });
// spoof fullscreen API immediately - no Angular needed
Object.defineProperty(document, 'fullscreenElement', {
get: () => document.documentElement,
configurable: true
});
// defer Angular-dependent spoofs until focus-indicator exists
const waitForFtm = setInterval(() => {
const el = document.getElementById('focus-indicator');
try {
const ftm = angular.element(el).scope().$parent.self;
if (!ftm?.fullScreenService) return;
Object.defineProperty(ftm.fullScreenService, 'isApproximatelyFullScreen', {
get: () => true,
configurable: true
});
Object.defineProperty(ftm.fullScreenService, 'isFullScreen', {
get: () => true,
configurable: true
});
clearInterval(waitForFtm);
} catch(e) {
// Angular not ready yet, keep waiting
}
}, 1000);
window._ftmWaiter = waitForFtm;
}
function stopAntiSnitch() {
XMLHttpRequest.prototype.open = originalOpen;
window.mutationObserver?.disconnect();
clearInterval(window._ftmWaiter);
Object.defineProperty(document, 'fullscreenElement', {
get: () => null,
configurable: true
});
try {
const ftm = angular.element(el).scope().$parent.self;
if (ftm?.fullScreenService) {
delete ftm.fullScreenService.isApproximatelyFullScreen;
delete ftm.fullScreenService.isFullScreen;
}
} catch(e) {}
}
// ------ vocabulary list cheat ------
function ingestVocabFromNetwork(data) {
const translations = data?.result?.Translations;
if (!translations?.length) return 0;
let count = 0;
translations.forEach(item => {
const baseDefs = item.BaseLanguageDefinitions ?? [];
const targetDefs = item.TargetLanguageDefinitions ?? [];
if (!baseDefs.length || !targetDefs.length) return;
const primaryBase = (baseDefs.find(d => d.DisplayedAsAnswer) ?? baseDefs[0]).Text.trim();
const primaryTarget = (targetDefs.find(d => d.DisplayedAsAnswer) ?? targetDefs[0]).Text.trim();
baseDefs.forEach(d => {
const t = d.Text?.trim();
if (!t) return;
vocabularyMap[t] = primaryTarget;
vocabularyMap[t.toLowerCase()] = primaryTarget;
});
targetDefs.forEach(d => {
const t = d.Text?.trim();
if (!t) return;
vocabularyMap[t] = primaryBase;
vocabularyMap[t.toLowerCase()] = primaryBase;
});
count++;
});
if (count > 0) log(`${count} vocab pairs loaded from network`);
return count;
} // parses the vocabulary from the json
function extractCleanText(el) {
if (!el) return '';
const clone = el.cloneNode(true);
clone.querySelectorAll('button, svg, img, [aria-hidden]').forEach(n => n.remove());
return clone.textContent.trim();
} // parses the actual text from the vocab entry
function extractVocabList() {
const items = document.querySelectorAll(
'.preview-grid .stats-item, .vocab-list-item, [class*="vocabItem"], [class*="vocab-item"]'
);
if (!items.length) return 0;
let count = 0;
items.forEach(item => {
const targetEl = item.querySelector('.targetLanguage, [class*="targetLanguage"], [class*="target-language"]');
const baseEl = item.querySelector('.baseLanguage, [class*="baseLanguage"], [class*="base-language"]');
if (!targetEl || !baseEl) return;
const targetText = extractCleanText(targetEl);
const baseText = extractCleanText(baseEl);
if (!targetText || !baseText) return;
vocabularyMap[targetText] = baseText;
vocabularyMap[targetText.toLowerCase()] = baseText;
vocabularyMap[baseText] = targetText;
vocabularyMap[baseText.toLowerCase()] = targetText;
count++;
});
if (count > 0) log(`${count} vocab pairs from DOM`);
return count;
} // gets the vocab list from the html
function findVocabAnswer(question) {
if (!question) return null;
const q = question.trim();
if (vocabularyMap[q]) return vocabularyMap[q];
if (vocabularyMap[q.toLowerCase()]) return vocabularyMap[q.toLowerCase()];
for (const part of q.split(';').map(s => s.trim()).filter(Boolean)) {
if (vocabularyMap[part]) return vocabularyMap[part];
if (vocabularyMap[part.toLowerCase()]) return vocabularyMap[part.toLowerCase()];
}
return null;
} //matches vocab question to answer
// ------ tasks ------
function setupNetworkIntercept() {
if (XMLHttpRequest.prototype.send._epHooked) return;
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function (body) {
try {
if (body && typeof body === 'string') {
if (body.includes('GetPreGameDataForClassicActivity') ||
body.includes('GetListData')) {
this.addEventListener('load', function () {
try {
const response = JSON.parse(this.responseText);
ingestVocabFromNetwork(response);
} catch (e) {
log('Vocab network parse error: ' + e.message);
}
});
}
if (body.includes('GetQuestionsWithOptimisedMedia')) {
log('Intercepting question fetch');
this.addEventListener('load', function () {
try {
const response = JSON.parse(this.responseText);
const questions = response?.result?.Questions;
if (!questions) return;
window.epAnswers = questions.map(q => {
const correctOnes = [];
let modelAnswer = null;
q.Definition.Components?.forEach(comp => {
comp.Options?.forEach(opt => {
if (opt.Correct === 'true' || opt.Correct === true) {
correctOnes.push(opt.TextTemplate || opt.Description);
}
});
comp.Gaps?.forEach(gap => {
gap.CorrectOptions?.forEach(ans => correctOnes.push(ans));
});
if (comp.ComponentTypeCode === 'LONG_ANSWER_COMPONENT' && comp.ModelAnswerHTML) {
const div = document.createElement('div');
div.innerHTML = comp.ModelAnswerHTML;
modelAnswer = div.innerText.trim();
}
});
return { title: q.Definition.Title, answers: correctOnes, modelAnswer };
});
log(`${window.epAnswers.length} task questions loaded`);
} catch (e) {
log('Error parsing intercept: ' + e.message);
}
});
}
}
} catch (_) {}
return originalSend.apply(this, arguments);
};
XMLHttpRequest.prototype.send._epHooked = true;
log('Network interceptor active');
} // starts the json answer file interceptor for tasks
function getTaskQuestion() {
try {
const el = document.querySelector('.question-container') || document.querySelector('[class*="Question"]');
if (!el) return null;
const vm = window.angular.element(el).scope().self;
const q = vm.model.currentQuestion;
return q.specifiedDisplayName || q.questionDef?.Title || null;
} catch (_) { return null; }
} // get which question you are on
function getTaskModelAnswer() {
try {
const container = document.querySelector('.question-container');
if (!container) return null;
const scope = window.angular.element(container).scope();
const components = scope.self.model.currentQuestion.questionDef.Components;
const longAnswer = components.find(c => c.ComponentTypeCode === 'LONG_ANSWER_COMPONENT');
if (!longAnswer) return null;
const div = document.createElement('div');
div.innerHTML = longAnswer.ModelAnswerHTML;
return div.innerText.trim() || null;
} catch (_) { return null; }
} // gets the model answer for the written response
function lookupTaskAnswer(title) {
const stored = window.epAnswers?.find(q => q.title === title);
if (!stored) return null;
if (stored.modelAnswer) return stored.modelAnswer;
if (stored.answers?.length) return stored.answers.join(' | ');
return null;
} // matches task question to answer
// ------ universal ------
function resolveAnswer() {
const taskTitle = getTaskQuestion();
if (taskTitle) {
const modelAns = getTaskModelAnswer();
if (modelAns) return { answer: modelAns, source: 'model answer', question: taskTitle };
const intercepted = lookupTaskAnswer(taskTitle);
if (intercepted) return { answer: intercepted, source: 'task intercept', question: taskTitle };
}
if (Object.keys(vocabularyMap).length === 0) extractVocabList();
const questionEl = document.querySelector('#question-text');
const questionText = questionEl?.textContent.trim() || taskTitle;
if (questionText) {
const vocabAns = findVocabAnswer(questionText);
if (vocabAns) return { answer: vocabAns, source: 'vocab', question: questionText };
}
return null;
} // full answer resolver wrapper
function copyToClipboard(text) {
navigator.clipboard.writeText(text ?? '').catch(() => {});
}
// ------ Module loader ------
function loadModules() {
const modulePanel = document.getElementById("ep-panel-modules");
if (!modulePanel) return;
modulePanel.innerHTML = '';
modules.forEach((module) => {
const moduleBox = document.createElement("div");
moduleBox.style.cssText = "margin-bottom: 8px; padding: 4px; border-bottom: 1px solid #ffffff22;";
const label = document.createElement("p");
label.textContent = module.name;
label.style.cssText = "margin: 0 0 4px 0; color: #00ff88; font-weight: bold;";
moduleBox.appendChild(label);
const description = document.createElement("p");
description.textContent = module.description || "";
description.style.cssText = 'margin: 0 0 4px 0; color: #ffffff22; font-size: 10px;';
moduleBox.appendChild(description);
// tracks the current value of every input in order
const inputValues = module.inputs.map(input => {
if (input.type === "toggle") return false;
if (input.type === "input") return input.default ?? "";
return null; // buttons contribute null
});
function fireRun() {
const runFn = module.run;
if (runFn) runFn(...inputValues);
}
module.inputs.forEach((input, index) => {
// ----- toggle -----
if (input.type === "toggle") {
const btn = document.createElement("button");
btn.textContent = input.name ?? module.name;
btn.dataset.active = "false";
btn.style.cssText = "background:#1a1a1a; color:#ccc; border:1px solid #444; border-radius:4px; padding:2px 8px; cursor:pointer; font-size:11px;";
btn.addEventListener("click", () => {
const isActive = btn.dataset.active === "true";
btn.dataset.active = String(!isActive);
btn.style.background = !isActive ? "#00ff8833" : "#1a1a1a";
btn.style.color = !isActive ? "#00ff88" : "#ccc";
inputValues[index] = !isActive;
if (input.update) fireRun();
});
moduleBox.appendChild(btn);
}
// ----- pushbutton -----
else if (input.type === "button") {
const btn = document.createElement("button");
btn.textContent = input.name ?? module.name;
btn.style.cssText = "background:#1a1a1a; color:#ccc; border:1px solid #444; border-radius:4px; padding:2px 8px; cursor:pointer; font-size:11px; margin-right:4px;";
btn.addEventListener("mousedown", () => { btn.style.background = "#00ff8833"; btn.style.color = "#00ff88"; });
btn.addEventListener("mouseup", () => { btn.style.background = "#1a1a1a"; btn.style.color = "#ccc"; });
btn.addEventListener("mouseleave",() => { btn.style.background = "#1a1a1a"; btn.style.color = "#ccc"; });
btn.addEventListener("click", () => {
if (input.update) fireRun();
});
moduleBox.appendChild(btn);
}
// ----- text input -----
else if (input.type === "input") {
const wrapper = document.createElement("div");
wrapper.style.cssText = "display:flex; align-items:center; gap:4px; margin-top:2px;";
if (input.name) {
const inputLabel = document.createElement("span");
inputLabel.textContent = input.name + ":";
inputLabel.style.cssText = "font-size:10px; color:#aaa; white-space:nowrap;";
wrapper.appendChild(inputLabel);
}
const textInput = document.createElement("input");
textInput.type = "text";
textInput.placeholder = input.placeholder ?? "";
textInput.value = input.default ?? "";
textInput.style.cssText = "background:#1a1a1a; color:#fff; border:1px solid #444; border-radius:4px; padding:2px 6px; font-size:11px; width:80px; outline:none;";
textInput.addEventListener("focus", () => textInput.style.borderColor = "#00ff88");
textInput.addEventListener("blur", () => textInput.style.borderColor = "#444");
const updateValue = () => { inputValues[index] = textInput.value; };
if (input.trigger === "change") {
textInput.addEventListener("input", () => { updateValue(); if (input.update) fireRun(); });
} else {
textInput.addEventListener("input", updateValue);
textInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") { e.preventDefault(); if (input.update) fireRun(); }
});
}
wrapper.appendChild(textInput);
if (input.button !== false) {
const confirmBtn = document.createElement("button");
confirmBtn.textContent = typeof input.button === "string" ? input.button : "Set";
confirmBtn.style.cssText = "background:#1a1a1a; color:#ccc; border:1px solid #444; border-radius:4px; padding:2px 6px; cursor:pointer; font-size:11px;";
confirmBtn.addEventListener("mousedown", () => { confirmBtn.style.background = "#00ff8833"; confirmBtn.style.color = "#00ff88"; });
confirmBtn.addEventListener("mouseup", () => { confirmBtn.style.background = "#1a1a1a"; confirmBtn.style.color = "#ccc"; });
confirmBtn.addEventListener("mouseleave",() => { confirmBtn.style.background = "#1a1a1a"; confirmBtn.style.color = "#ccc"; });
confirmBtn.addEventListener("click", () => { updateValue(); if (input.update) fireRun(); });
wrapper.appendChild(confirmBtn);
}
moduleBox.appendChild(wrapper);
}
});
modulePanel.appendChild(moduleBox);
});
}
// ------ UI ------
function buildPanel() {
if (document.getElementById('ep-unified-panel')) return;
const panel = document.createElement('div');
panel.id = 'ep-unified-panel';
// We wrap the content in a flex container to create two columns
panel.innerHTML = `
<div id="ep-panel-header">
<span id="ep-panel-title">EP cheat</span>
<div style="display:flex;gap:8px;align-items:center">
<span id="ep-panel-close" title="Close (Alt+K)" style="cursor:pointer;opacity:.6;font-size:12px">✕</span>
</div>
</div>
<div style="display: flex; flex-direction: row;">
<div id="ep-left-col" style="flex: 1; border-right: 1px solid #ffffff22;">
<div id="ep-panel-source"></div>
<div id="ep-panel-answer">Waiting…</div>
<div id="ep-panel-log"></div>
</div>
<div id="ep-right-col" style="flex: 1; padding: 10px; min-width: 200px;">
<div style="color: #00ff88; margin-bottom: 5px; font-weight: bold;">Modules</div>
<div id="ep-panel-modules" style="font-size: 11px; color: #ccc;">
Modules
</div>
</div>
</div>
<div id="ep-panel-footer">
<span style="opacity:.4;font-size:10px">Alt+A · get answer | Alt+K · toggle | Alt+L · load vocab</span><p></p>
<span style="opacity:.4;font-size:10px">please dm @blastedmeteor44 on discord to help out with this mod! i really would appreciate it</span>
</div>
`;
Object.assign(panel.style, {
position: 'fixed', top: panelState.top, left: panelState.left,
width: '550px', background: '#0a0a0a', color: '#ffffff',
fontFamily: 'Arial', fontSize: '12px', border: '1px solid #ffffff66',
borderRadius: '8px', zIndex: '2147483647',
boxShadow: '0 0 28px rgba(0,255,136,.18)', userSelect: 'none', overflow: 'hidden',
});
const header = panel.querySelector('#ep-panel-header');
Object.assign(header.style, {
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '6px 10px', background: '#141414', cursor: 'move',
borderBottom: '1px solid #ffffff22',
});
Object.assign(panel.querySelector('#ep-panel-source').style, {
padding: '3px 10px', fontSize: '10px', color: '#00aa55',
borderBottom: '1px solid #ffffff11', minHeight: '16px',
});
Object.assign(panel.querySelector('#ep-panel-answer').style, {
padding: '10px', fontSize: '16px', fontWeight: 'bold', color: '#ffffff',
minHeight: '38px', height: '100px', wordBreak: 'break-word',
borderBottom: '1px solid #00ff8822', overflowY: 'scroll', resize: 'vertical',
});
Object.assign(panel.querySelector('#ep-panel-log').style, {
padding: '5px 10px', maxHeight: '90px', height: '20px',
overflowY: 'auto', fontSize: '10px', color: '#ffffff',
lineHeight: '1.5', resize: 'vertical',
});
Object.assign(panel.querySelector('#ep-panel-footer').style, {
padding: '4px 10px', borderTop: '1px solid #00ff8811',
background: '#0d0d0d', textAlign: 'center',
});
let dragging = false, ox = 0, oy = 0;
header.addEventListener('mousedown', e => {
dragging = true; ox = e.clientX - panel.offsetLeft; oy = e.clientY - panel.offsetTop;
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (!dragging) return;
panel.style.left = (e.clientX - ox) + 'px';
panel.style.top = (e.clientY - oy) + 'px';
});
document.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false;
panelState.top = panel.style.top; panelState.left = panel.style.left;
});
panel.querySelector('#ep-panel-close').addEventListener('click', togglePanel);
document.body.appendChild(panel);
consoleVisible = true;
loadModules();
}
function destroyPanel() {
const p = document.getElementById('ep-unified-panel');
if (!p) return;
panelState.top = p.style.top; panelState.left = p.style.left;
p.remove(); consoleVisible = false;
}
function togglePanel() {
document.getElementById('ep-unified-panel') ? destroyPanel() : buildPanel();
}
function setAnswer(text, source) {
const a = document.querySelector('#ep-panel-answer');
const s = document.querySelector('#ep-panel-source');
if (a) a.textContent = text;
if (s) s.textContent = source ? `via ${source}` : '';
}
function log(message) {
console.log('[EP Cheat]', message);
const logDiv = document.querySelector('#ep-panel-log');
if (!logDiv) return;
const line = document.createElement('div');
line.textContent = message;
logDiv.appendChild(line);
logDiv.scrollTop = logDiv.scrollHeight;
}
window.getAnswer = function () {
if (!document.getElementById('ep-unified-panel')) buildPanel();
const result = resolveAnswer();
if (result) {
log(`Q: ${result.question}`);
setAnswer(result.answer, result.source);
copyToClipboard(result.answer);
} else {
setAnswer('No answer found', '');
log(`No answer — vocab map has ${Object.keys(vocabularyMap).length} entries`);
}
};
// ------ module builder ------
function buildModuleBuilder() {
if (document.getElementById('ep-module-builder')) return;
const HELPERS = {
'getInfoControls': {
label: 'Info Controls (ctrl)',
code: `const ctrl = angular.element(document.getElementsByClassName('information-controls')).scope()?.self?.informationControls;`
},
'getFtm': {
label: 'Focus Tracking Manager (ftm)',
code: `const ftm = angular.element(document.getElementById('focus-indicator')).scope()?.$parent?.self;`
},
'getTaskScope': {
label: 'Task Question Scope (taskScope)',
code: `const taskScope = angular.element(document.querySelector('.question-container') || document.querySelector('[class*="Question"]')).scope()?.self;`
},
'getVocabMap': {
label: 'Vocab Map (vocabularyMap)',
code: `const vocabMap = window.vocabularyMap ?? {};`
},
'clipboard': {
label: 'Copy to Clipboard',
code: `const copy = (text) => navigator.clipboard.writeText(text).catch(() => {});`
},
'interval': {
label: 'Safe Interval (auto-cleared on toggle off)',
code: `window._moduleInterval = setInterval(() => {\n // your code here\n }, 100);`
},
'observer': {
label: 'Mutation Observer (auto-cleared on toggle off)',
code: `window._moduleObserver = new MutationObserver(() => {\n // your code here\n });\n window._moduleObserver.observe(document, { childList: true, subtree: true });`
},
};
const popup = document.createElement('div');
popup.id = 'ep-module-builder';
popup.innerHTML = `
<div id="ep-mb-header" style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;background:#141414;cursor:move;border-bottom:1px solid #ffffff22;">
<span style="color:#00ff88;font-weight:bold;font-size:13px;">Module Builder</span>
<span id="ep-mb-close" style="cursor:pointer;opacity:.6;font-size:12px;">✕</span>
</div>
<div style="padding:10px;display:flex;flex-direction:column;gap:8px;">
<div style="display:flex;flex-direction:column;gap:3px;">
<label style="font-size:10px;color:#aaa;">Module Name</label>
<input id="ep-mb-name" type="text" placeholder="my module" style="background:#1a1a1a;color:#fff;border:1px solid #444;border-radius:4px;padding:3px 7px;font-size:12px;outline:none;" />
</div>
<div style="display:flex;flex-direction:column;gap:3px;">
<label style="font-size:10px;color:#aaa;">Description</label>
<input id="ep-mb-desc" type="text" placeholder="what does this module do?" style="background:#1a1a1a;color:#fff;border:1px solid #444;border-radius:4px;padding:3px 7px;font-size:12px;outline:none;" />
</div>
<div style="display:flex;flex-direction:column;gap:4px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<label style="font-size:10px;color:#aaa;">Inputs</label>
<select id="ep-mb-input-type" style="background:#1a1a1a;color:#ccc;border:1px solid #444;border-radius:4px;padding:2px 5px;font-size:11px;outline:none;">
<option value="toggle">Toggle</option>
<option value="button">Button</option>
<option value="input">Text Input</option>
</select>
<button id="ep-mb-add-input" style="background:#1a1a1a;color:#00ff88;border:1px solid #00ff8866;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px;">+ Add</button>
</div>
<div id="ep-mb-inputs-list" style="display:flex;flex-direction:column;gap:4px;max-height:120px;overflow-y:auto;"></div>
</div>
<div style="display:flex;flex-direction:column;gap:3px;">
<label style="font-size:10px;color:#aaa;">Import Helpers</label>
<div id="ep-mb-helpers" style="display:flex;flex-direction:column;gap:3px;max-height:120px;overflow-y:auto;">
${Object.entries(HELPERS).map(([key, h]) => `
<label style="display:flex;align-items:center;gap:6px;font-size:11px;color:#ccc;cursor:pointer;">
<input type="checkbox" data-helper="${key}" style="accent-color:#00ff88;" />
${h.label}
</label>
`).join('')}
</div>
</div>
<div style="display:flex;flex-direction:column;gap:3px;">
<label style="font-size:10px;color:#aaa;">Run Function Body</label>
<textarea id="ep-mb-run" rows="5" placeholder="// args: ...inputValues\n// e.g. state for toggle, value for input" style="background:#1a1a1a;color:#fff;border:1px solid #444;border-radius:4px;padding:3px 7px;font-size:11px;outline:none;resize:vertical;font-family:monospace;"></textarea>
</div>
<div style="display:flex;gap:6px;">
<button id="ep-mb-preview" style="flex:1;background:#1a1a1a;color:#ccc;border:1px solid #444;border-radius:4px;padding:4px;cursor:pointer;font-size:11px;">Preview Code</button>
<button id="ep-mb-inject" style="flex:1;background:#00ff8822;color:#00ff88;border:1px solid #00ff8866;border-radius:4px;padding:4px;cursor:pointer;font-size:11px;">Inject Module</button>
</div>
<div id="ep-mb-preview-box" style="display:none;flex-direction:column;gap:3px;">
<label style="font-size:10px;color:#aaa;">Generated Code</label>
<textarea id="ep-mb-code" rows="10" readonly style="background:#0d0d0d;color:#00ff88;border:1px solid #ffffff22;border-radius:4px;padding:5px 7px;font-size:10px;font-family:monospace;outline:none;resize:vertical;"></textarea>
<button id="ep-mb-copy" style="background:#1a1a1a;color:#ccc;border:1px solid #444;border-radius:4px;padding:3px;cursor:pointer;font-size:11px;">Copy</button>
</div>
</div>
`;
Object.assign(popup.style, {
position: 'fixed', top: '80px', left: '600px',
width: '340px', background: '#0a0a0a', color: '#fff',
fontFamily: 'Arial, sans-serif', fontSize: '12px', border: '1px solid #ffffff66',
borderRadius: '8px', zIndex: '2147483647',
boxShadow: '0 0 28px rgba(0,255,136,.18)', userSelect: 'none', overflow: 'hidden',
});
document.body.appendChild(popup);
// --- Dragging Logic ---
let dragging = false, ox = 0, oy = 0;
const header = popup.querySelector('#ep-mb-header');
header.addEventListener('mousedown', e => {
dragging = true; ox = e.clientX - popup.offsetLeft; oy = e.clientY - popup.offsetTop;
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (!dragging) return;
popup.style.left = (e.clientX - ox) + 'px';
popup.style.top = (e.clientY - oy) + 'px';
});
document.addEventListener('mouseup', () => { dragging = false; });
popup.querySelector('#ep-mb-close').addEventListener('click', () => popup.remove());
// --- Add Input Row ---
const inputsList = popup.querySelector('#ep-mb-inputs-list');
function addInputRow(type) {
const row = document.createElement('div');
row.dataset.type = type;
row.style.cssText = 'display:flex;align-items:center;gap:4px;background:#111;border:1px solid #ffffff11;border-radius:4px;padding:4px 6px;';
const tag = document.createElement('span');
tag.textContent = type;
tag.style.cssText = 'font-size:10px;color:#00ff88;min-width:42px;';
row.appendChild(tag);
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.placeholder = 'label';
nameInput.style.cssText = 'flex:1;background:#1a1a1a;color:#fff;border:1px solid #333;border-radius:3px;padding:2px 5px;font-size:11px;outline:none;';
row.appendChild(nameInput);
const updateCheck = document.createElement('input');
updateCheck.type = 'checkbox';
updateCheck.title = 'update (fires run)';
updateCheck.checked = true;
updateCheck.style.cssText = 'accent-color:#00ff88;';
row.appendChild(updateCheck);
if (type === 'input') {
const placeholderInput = document.createElement('input');
placeholderInput.type = 'text';
placeholderInput.placeholder = 'hint';
placeholderInput.style.cssText = 'width:44px;background:#1a1a1a;color:#fff;border:1px solid #333;border-radius:3px;padding:2px 5px;font-size:11px;outline:none;';
row.appendChild(placeholderInput);
}
const removeBtn = document.createElement('button');
removeBtn.textContent = '✕';
removeBtn.style.cssText = 'background:none;color:#ff4444;border:none;cursor:pointer;font-size:11px;padding:0 2px;';
removeBtn.addEventListener('click', () => row.remove());
row.appendChild(removeBtn);
inputsList.appendChild(row);
}
popup.querySelector('#ep-mb-add-input').addEventListener('click', () => {
addInputRow(popup.querySelector('#ep-mb-input-type').value);
});
// --- Helper Logic ---
function getSelectedHelpers() {
return [...popup.querySelectorAll('#ep-mb-helpers input:checked')]
.map(cb => HELPERS[cb.dataset.helper].code);
}
// --- Code Generation ---
function generateCode() {
const name = popup.querySelector('#ep-mb-name').value.trim() || 'unnamed module';
const desc = popup.querySelector('#ep-mb-desc').value.trim();
const runBody = popup.querySelector('#ep-mb-run').value.trim() || '// todo';
const helpers = getSelectedHelpers();
const hasInterval = [...popup.querySelectorAll('#ep-mb-helpers input:checked')].some(cb => cb.dataset.helper === 'interval');
const hasObserver = [...popup.querySelectorAll('#ep-mb-helpers input:checked')].some(cb => cb.dataset.helper === 'observer');
const inputRows = [...inputsList.querySelectorAll('div[data-type]')];
const inputsDefs = inputRows.map(row => {
const type = row.dataset.type;
const els = row.querySelectorAll('input');
const label = els[0].value.trim() || type;
const update = els[1].checked;
if (type === 'input') {
return ` { type: "input", name: "${label}", update: ${update}, placeholder: "${els[2]?.value.trim() || ''}" }`;
}
return ` { type: "${type}", name: "${label}", update: ${update} }`;
}).filter(Boolean).join(',\n');
const helperLines = helpers.length ? helpers.map(h => ' ' + h).join('\n') + '\n' : '';
let cleanupLines = '';
if (hasInterval || hasObserver) {
cleanupLines = '\n // auto cleanup\n if (!args[0]) {\n' +
(hasInterval ? ' clearInterval(window._moduleInterval); window._moduleInterval = null;\n' : '') +
(hasObserver ? ' window._moduleObserver?.disconnect(); window._moduleObserver = null;\n' : '') +
' }';
}
const indentedBody = runBody.split('\n').map(line => ' ' + line).join('\n');
return `{
name: "${name}",${desc ? `\n description: "${desc}",` : ''}
inputs: [
${inputsDefs}
],
run: function(...args) {
${helperLines}
${indentedBody}
${cleanupLines}
}
}`;
}
// --- Button Actions ---
popup.querySelector('#ep-mb-preview').addEventListener('click', () => {
popup.querySelector('#ep-mb-code').value = generateCode();
const box = popup.querySelector('#ep-mb-preview-box');
box.style.display = box.style.display === 'none' ? 'flex' : 'none';
});
popup.querySelector('#ep-mb-copy').addEventListener('click', () => {
navigator.clipboard.writeText(popup.querySelector('#ep-mb-code').value);
});
popup.querySelector('#ep-mb-inject').addEventListener('click', () => {
try {
const codeString = `return ${generateCode()}`;
const moduleObj = new Function('...args', codeString)();
// Assume 'modules', 'loadModules', and 'log' exist in your global context
if (typeof modules !== 'undefined') {
modules.push(moduleObj);
if (typeof loadModules === 'function') loadModules();
if (typeof log === 'function') log(`Module "${moduleObj.name}" injected`);
}
popup.remove();
} catch (e) {
console.error(e);
if (typeof log === 'function') log('Inject error: ' + e.message);
}
});
}
function destroyModuleBuilder() {
const box = document.getElementById('ep-module-builder');
if (!box) return;
box.remove();
}
window.buildModulebuilder = buildModuleBuilder;
document.addEventListener('keydown', e => {
if (e.altKey && e.code === 'KeyA') { e.preventDefault(); window.getAnswer(); } // get answer
if (e.altKey && e.code === 'KeyK') { e.preventDefault(); togglePanel(); } // toggle panel
if (e.altKey && e.code === 'KeyL') { // refresh vocabulary list
e.preventDefault();
const count = extractVocabList();
log(count > 0 ? `Vocab refreshed: ${count} pairs` : 'No vocab DOM elements found');
}
}, true);
setupNetworkIntercept();
if (document.body) { // if the body exists then build the panel
buildPanel();
} else { // or wait for the body to load and do it
document.addEventListener('DOMContentLoaded', () => buildPanel());
}
})();