Education Perfect Hack

Very good ep hack. has support for lists and regular tasks. unfortunately no assessments yet :(

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==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 &nbsp;|&nbsp; Alt+K · toggle &nbsp;|&nbsp; 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());
    }

})();