Education Perfect Hack

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

})();