ORAC Userscript

Custom tags in orac, hidden problems, difficulty approximation, searching upgrade, custom styling & ordering, editorials

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

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.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         ORAC Userscript
// @namespace    http://tampermonkey.net/
// @version      v3.4.2
// @description  Custom tags in orac, hidden problems, difficulty approximation, searching upgrade, custom styling & ordering, editorials
// @author       a_person31415
// @match        https://orac2.info/hub/personal/*
// @match        https://orac.amt.edu.au/hub/personal/*
// @match        https://orac2.info/problem/*
// @match        https://orac.amt.edu.au/problem/*
// @match        https://orac2.info/*
// @match        https://orac.amt.edu.au/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=orac2.info
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_listValues
// @grant        GM_deleteValue
// @grant        unsafeWindow
// ==/UserScript==

(function() {
    'use strict';

    function $(selector) {
        let contents = document.querySelectorAll(selector);
        if(contents.length == 1) {return contents[0];}
        return contents;
    }

    let TAG_ORDER = ["starter", "training", "aic", "aio", "acio", "aiio", "alpha", "fario", "precamp", "camp", "seln", "apio", "ioi","cpp-practice", "testing", "custom-tags"];
    let NEW_TAGS = ["starter", "training", "aic", "acio", "cpp-practice", "custom-tags"];

    GM_addStyle(`
        .badge-tag { margin-right: 4px; cursor: pointer; background-color: #f8f9fa; color: #333; user-select: none;}
        .badge-tag.selected { background-color: #d9534f !important; color: white !important; border-color: #d43f3a !important; }
        .badge-tag:hover { opacity: 0.8; }
        .progress-column, .difficulty-column {bottom-border: 1px solid #dee2e6;}
        .difficulty-column {width: 30%;}
        .progress-column {width:20%;}
        .kooltable {border-spacing: 0 10px; border-collapse: separate; margin-bottom: 0rem !important;}
        .koolbutton {border: none; border-radius: .5rem; outline: none; cursor: pointer; user-select: none;}
        .koolbutton:focus {outline: none;}
        .koolbutton:hover {opacity: 0.8;}

        .slider-container { display: flex; align-items: center; width: 350px; height: 40px; }
        #rangevalue { font-size: 14px; font-weight: bold; color: #4CAF50; white-space: nowrap; min-width: 80px; }
        .slider-track { position: absolute; height: 5px; width: 100%; background: #ddd; top: 50%; transform: translateY(-50%); border-radius: 5px; z-index: 1; }
        .slider-range { position: absolute; height: 5px; background: #4CAF50; top: 50%; transform: translateY(-50%); border-radius: 5px; z-index: 2; }

        input[type=range] { position: absolute; width: 100%; height: 30px; top: 50%; transform: translateY(-50%); left: 0; margin: 0; pointer-events: none; -webkit-appearance: none; background: transparent; z-index: 3; }
        input[type=range]::-webkit-slider-thumb { pointer-events: all; width: 16px; height: 16px; border-radius: 50%; background: #4CAF50; cursor: pointer; -webkit-appearance: none; border: 2px solid #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
        input[type=range]::-moz-range-thumb { pointer-events: all; width: 16px; height: 16px; border-radius: 50%; background: #4CAF50; cursor: pointer; border: 2px solid #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
        input[type=range]:focus { outline: none; }

        .custom-search {border-radius: 0.5rem; border: 1px solid gray; margin-left: 0.25rem; height: 25px;}
        .custom-search:focus, .custom-search:hover {outline: 1px solid #343a40}

        .problemset-display[data-visible="false"] {height: 0; padding-top: 0; padding-bottom: 0; margin-top: 0; margin-bottom: 0; overflow: hidden; border: none;}
        tr[data-visible="false"] {height: 0 !important; padding-top: 0 !important; padding-bottom: 0 !important; margin-top: 0 !important; margin-bottom: 0 !important; overflow: hidden !important; border: none; visibility: hidden;}
        tr[data-visible="false"] td {padding: 0 !important; margin: 0 !important; height: 0 !important; font-size: 0 !important; line-height: 0 !important; border: none !important;}
    `);

    function waitForElm(selector) {
        return new Promise(resolve => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }
            const observer = new MutationObserver(mutations => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    }

    function nospace(text) {
        return text.replace(" ", "koolspace")
    }

    function styled_details(name, content, id) {
        let buttonp = document.createElement("p");
        let button = document.createElement("a");
        button.className = "btn btn-primary collapsed";
        button.setAttribute("data-toggle", "collapse");
        button.setAttribute("href", "#" + id);
        button.setAttribute("role", "button");
        button.setAttribute("aria-expanded", "false");
        button.innerText = name;
        buttonp.appendChild(button);

        let container = document.createElement("div");
        container.classList.add("collapse");
        container.id = id;
        container.setAttribute("style", "");
        let body = document.createElement("div");
        body.className = "card card-body";
        body.innerHTML = content;
        container.appendChild(body);

        let wrapper = document.createElement("span");
        wrapper.appendChild(buttonp);
        wrapper.appendChild(container);

        return wrapper;
    }

    async function problemdata(id) {
        const res = await fetch("https://raw.githubusercontent.com/aperson31415/informatics/refs/heads/main/ORAC%20Userscript/data.json");
        const doc = new DOMParser().parseFromString(await res.text(), 'text/html');
        const rawjson = doc.body.innerHTML;
        const obj = JSON.parse(rawjson);
        if(Object.keys(obj).includes(id) || Object.keys(obj).includes(id.toString())) {
            return obj[id];
        } else {
            return null;
        }
    }

    function getProblemId(url) {
        const segments = url.pathname.split('/').filter(s => s.length > 0);
        return segments[segments.length - 1];
    }

    function injectStyles(cssUrls) {
        cssUrls.forEach(url => {
            const link = document.createElement('link');
            link.rel = 'stylesheet';
            link.type = 'text/css';
            link.href = url;
            document.head.appendChild(link);
        });
    }

    const old_problems = ["aic00p1","aic00p2","aic00p3","aic00p4","aic01p1","aic01p2","aic01p3","aic01p4","aic02p1","aic02p2","aic02p3","aic03p2","aic03p3","aic03p4","aic04p1","aic04p2","aic04p3","aic04p4","aic04p5","aic98p1","aic98p2","aic98p3","aic99p1","aic99p2","aic99p3","dscannons","dsfenwickxor","dsinversioncounting","dslazyupdate","dsrangetreeupdates","dssupplies","monthly05p1","monthly05p2","monthly05p3","precamppublic2"];

    const checkInterval = setInterval(() => {
        const h1 = document.querySelector("h1");
        if (h1) {
            clearInterval(checkInterval);
            checkAndRestore();
        }
    }, 100);

    async function checkAndRestore() {
        if (window.location.href.includes("problem") && !window.location.href.includes("hof") && !window.location.href.includes("submission")) {
            const url = new URL(window.location.href);
            const problem_id = getProblemId(url);
            const h1 = document.querySelector("h1");
            const pageHeading = h1 ? h1.innerText.toLowerCase().trim() : "";

            const isErrorPage = pageHeading.includes('access denied') ||
                                pageHeading.includes('page requested does not exist') ||
                                pageHeading.includes('page not found');

            if (old_problems.includes(problem_id) && isErrorPage) {

                document.body.innerHTML = `<div style="text-align:center; margin-top:100px; font-family:sans-serif;"><h2>Restoring ${problem_id}...</h2></div>`;

                const cdnBase = `https://cdn.jsdelivr.net/gh/aperson31415/informatics@main/wayback_raw/${problem_id}`;
                const navHtml = `
                    <div style="background:#1a1a1a; color:white; padding:12px 20px; font-family:sans-serif; display:flex; justify-content:space-between; align-items:center; border-bottom: 2px solid #3498db; position:relative; z-index:9999;">
                        <span style="font-size: 14px;">Restored Problem: <strong style="color:#3498db;">${problem_id}</strong></span>
                        <a href="https://orac2.info/hub/personal" style="color:white; text-decoration:none; font-size: 14px; background:#3498db; padding: 5px 12px; border-radius: 4px; font-weight:bold;">← Hub</a>
                    </div>`;

                try {
                    // Try HTML first
                    const htmlResp = await fetch(`${cdnBase}.html`);
                    if (htmlResp.ok) {
                        const htmlText = await htmlResp.text();
                        if (!htmlText.includes("404: Not Found")) {
                            document.open();
                            // Write nav + content
                            document.write(navHtml + htmlText);
                            document.close();
                            injectStyles(["https://github.com/aperson31415/informatics/blob/main/ORAC%20Userscript/ben-aioc.css", "https://github.com/aperson31415/informatics/blob/main/ORAC%20Userscript/bootstrap_min.css", "https://github.com/aperson31415/informatics/blob/main/ORAC%20Userscript/bootstrap_responsive.css"]);
                            return;
                        }
                    }

                    // Try PDF second
                    const pdfResp = await fetch(`${cdnBase}.pdf`);
                    if (pdfResp.ok) {
                        const pdfBlob = await pdfResp.blob();
                        if (pdfBlob.size > 500) {
                            document.body.innerHTML = `
                                <style>body,html{margin:0;padding:0;height:100%;overflow:hidden;display:flex;flex-direction:column;}</style>
                                ${navHtml}
                                <embed src="${cdnBase}.pdf" type="application/pdf" style="flex-grow:1; width:100%;" />
                            `;
                            injectStyles(["https://github.com/aperson31415/informatics/blob/main/ORAC%20Userscript/ben-aioc.css", "https://github.com/aperson31415/informatics/blob/main/ORAC%20Userscript/bootstrap_min.css", "https://github.com/aperson31415/informatics/blob/main/ORAC%20Userscript/bootstrap_responsive.css"]);
                            return;
                        }
                    }

                    document.body.innerHTML = navHtml + `<h1 style="text-align:center; margin-top:50px;">Backup not found.</h1>`;
                } catch (e) {
                    document.body.innerHTML = navHtml + `<h1 style="text-align:center; margin-top:50px;">Fetch error.</h1>`;
                }
            }
        }
    }

    if(window.location.href.includes("problem")) {
        const url = new URL(window.location.href);
        const problem_id = url.pathname.split("/").filter(Boolean);

        if(problem_id.length == 2) {
            waitForElm(".container-xl").then((elm) => {
                // load
                document.querySelectorAll(".container-xl")[2].innerHTML += `<div id="notes">
    <h2 class="mt-5">Notes</h2>
    Small: <input id="smallnote" type="text" placeholder="Nothing yet!" style="width:80%"><br><br>
    Large:<br>

    <textarea id="bignote" style="width: 100%; height: 301px;" placeholder="Nothing yet!"></textarea>
    </div>`;
                let small = GM_getValue("smallnote" + problem_id[1], "");
                let big = GM_getValue("bignote" + problem_id[1], "");
                document.querySelector("#smallnote").value = small;
                document.querySelector("#bignote").value = big;

                // save
                document.querySelector("#smallnote").addEventListener('input', () => {
                    GM_setValue("smallnote" + problem_id[1], document.querySelector("#smallnote").value);
                });

                document.querySelector("#bignote").addEventListener('input', () => {
                    GM_setValue("bignote" + problem_id[1], document.querySelector("#bignote").value);
                });
            });
        }
    }

    if(window.location.href.includes("problem") && window.location.href.includes("submissions")) {
        let aliases = {
            "py": "python",
            "Py": "python",
            "python": "python",
            "Python": "python",
            "C++": "cpp",
            "c++": "cpp",
            "txt": "plaintext",
            "Txt": "plaintext",
            "text": "plaintext",
            "Text": "plaintext",
            "Java": "java",
            "java": "java"
        };
        let parent = document.querySelectorAll(".container-xl")[2];
        let title = document.createElement("h2");
        title.innerText = "Hints";
        title.classList.add("mt-5");
        parent.appendChild(title);

        const url = new URL(window.location.href);
        const problem_id = parseInt(url.pathname.split("/").filter(Boolean)[1]);

        (async () => {
            if(document.querySelector(".table") == null || document.querySelector(".table") == undefined) {
                let wompwomp = document.createElement("p");
                wompwomp.innerText = "Please attempt this problem before looking at hints";
                parent.appendChild(wompwomp);
            } else {
                let problem_data = await problemdata(problem_id);
                if(problem_data == null || problem_data == "" || problem_data == undefined || problem_data == []) {
                    let wompwomp = document.createElement("p");
                    wompwomp.innerText = "Sorry, no hints or solutions are available for this problem as of now. Try coming back to this problem later.";
                    parent.appendChild(wompwomp);
                } else {
                    // Hints
                    if(problem_data.hints != null && problem_data.hints != undefined) {
                        Object.keys(problem_data.hints).forEach((hk) => {
                            let hint_key = nospace(hk);
                            let content = "";
                            problem_data.hints[hk].forEach((para) => {
                                content += `<p>` + para + `</p>`;
                            });
                            let curr_hint = styled_details(hk, content, "hint"+hint_key);
                            curr_hint.appendChild(document.createElement("br"));
                            parent.appendChild(curr_hint);
                        });
                    }

                    // Code
                    if(problem_data.solutions != null && problem_data.solutions != undefined) {
                        Object.keys(problem_data.solutions).forEach((lg) => {
                            let lang = nospace(lg);
                            let codeelem = problem_data.solutions[lg];
                            let code = `<pre><code class="language-` + aliases[lg] + ` hljs ` + aliases[lg] + `" id="customsol`+lang+`">`;
                            codeelem.forEach((line) => {
                                code += line + "\n";
                            });
                            code += `</code></pre>`;
                            let content = styled_details(lg + " solution", code, "sol" + lang);
                            parent.appendChild(content);
                            hljs.highlightBlock(document.getElementById("customsol"+lang));
                            hljs.lineNumbersBlock(document.getElementById("customsol"+lang), {
                                singleLine: false,
                                startFrom: 1
                            });
                        });
                    }

                    if(document.querySelector(".inline-code") != [] && document.querySelector(".inline-code") != null && document.querySelector(".inline-code") != undefined) {
                        document.getElementsByClassName("inline-code").forEach((elem) => {
                            hljs.highlightBlock(elem);
                            hljs.lineNumbersBlock(elem, {
                                singleLine: false,
                                startFrom: 1
                            });
                        });
                    }

                    let last_update = document.createElement("p");
                    last_update.innerText = "Last updated on " + problem_data.updated;
                    parent.appendChild(last_update);
                }
            }
        })();
    }

    function tag_element(content) {
        let elem = document.createElement("span");
        elem.classList.add("badge", "badge-tag");
        elem.innerText = content;
        return elem;
    }

    function render_tags_on_problem_page() {
            if (!window.bookmark_section) return;
            window.bookmark_section.innerText = "Custom Tags: ";
            let curr_tags = GM_getValue("rev_tags", {});
            if (curr_tags[window.location.href]) {
                curr_tags[window.location.href].forEach(t => window.bookmark_section.appendChild(tag_element(t)));
            }
            let plus_tag = tag_element("+");
            let minus_tag = tag_element("-");
        plus_tag.classList.add("plus");
            minus_tag.classList.add("minus");
            window.bookmark_section.appendChild(plus_tag);
            window.bookmark_section.appendChild(minus_tag);
        }

        function rating_estimation_elem(solves) {
            let rating_benchmarks = {
                800: ["0 &#9733;", "#A52A2A"],
                500: ["1 &#9733;", "#808080"],
                300: ["2 &#9733;", "#008000"],
                200: ["3 &#9733;", "#03A89E"],
                100: ["4 &#9733;", "#0000FF"],
                50: ["5 &#9733;", "#a0a"],
                10: ["6 &#9733;", "#bb0"],
                5: ["7 &#9733;", "#ffa200"],
                2: ["8 &#9733;", "#ff7b00"],
                0: ["9 &#9733;", "#ff0000"]
            };

            let lower_bound = 0, stars = -1;

            if (solves >= 800) {lower_bound = 800; stars = 0;}
            else if(solves >= 500) {lower_bound = 500; stars = 1;}
            else if(solves >= 300) {lower_bound = 300; stars = 2;}
            else if(solves >= 200) {lower_bound = 200; stars = 3;}
            else if(solves >= 100) {lower_bound = 100; stars = 4;}
            else if(solves >= 50) {lower_bound = 50; stars = 5;}
            else if(solves >= 10) {lower_bound = 10; stars = 6;}
            else if(solves >= 5) {lower_bound = 5; stars = 7;}
            else if(solves >= 2) {lower_bound = 2; stars = 8;}
            else {lower_bound = 0; stars = 9;}

            let elem = document.createElement("b");
            elem.style.color = rating_benchmarks[lower_bound][1];
            elem.innerHTML = rating_benchmarks[lower_bound][0];

            let uncoloured = document.createElement("span");
            uncoloured.style.color = "#000000";

            if (solves != 1) {
                uncoloured.innerHTML = " (" + (solves) + " solves)";
            } else {
                uncoloured.innerHTML = " (1 solve)";
            }

            elem.appendChild(uncoloured);

            return {"elem":elem, "stars":stars};
        }

         function new_tag() {
            let tag_name = prompt("Name of tag to add to problem");
            if (!tag_name) return;
            let tag_content = GM_getValue("tags", {});
            let rev_tagcontent = GM_getValue("rev_tags", {});
            if (tag_content[tag_name]) {
                tag_content[tag_name].push(window.location.href);
            } else {
                if(confirm("Create new tag '" + tag_name + "'?")) {
                    tag_content[tag_name] = [window.location.href];
                } else return;
            }
            if (!rev_tagcontent[window.location.href]) rev_tagcontent[window.location.href] = [];
            rev_tagcontent[window.location.href].push(tag_name);
            GM_setValue("tags", tag_content);
            GM_setValue("rev_tags", rev_tagcontent);
            render_tags_on_problem_page();
        }

        function del_tag() {
            let tag_name = prompt("Name of tag to delete from problem");
            if (!tag_name) return;
            let tag_content = GM_getValue("tags", {});
            let rev_tagcontent = GM_getValue("rev_tags", {});
            if (tag_content[tag_name].includes(window.location.href)) {
                tag_content[tag_name] = tag_content[tag_name].filter(loc => loc != window.location.href);
                if(tag_content[tag_name].length == 0) {
                    let should_deltag = confirm("Fully remove tag '" + tag_name + "'?");
                    if (should_deltag) {
                        delete tag_content[tag_name];
                    }
                }
                rev_tagcontent[window.location.href] = rev_tagcontent[window.location.href].filter(tag => tag != tag_name);
                GM_setValue("tags", tag_content);
                GM_setValue("rev_tags", rev_tagcontent);
                render_tags_on_problem_page();
            }
        }

    if(window.location.href.includes("problem") && !window.location.href.includes("hof") && !window.location.href.includes("submission")) {
        let score_badge = document.querySelector(".badge.hub-badge");
        if (score_badge) {
            // Save current score/style immediately to its own key
            GM_setValue("score_" + window.location.href, {
                score: score_badge.innerText.trim(),
                style: score_badge.className
            });
            let name = $("h1.mb-0")?.innerText.trim();
            if (name) GM_setValue("name_" + window.location.href, name);
        }

        if ($("tbody")[1]) {
            window.stats_bar = document.createElement("tr");
            window.bookmark_section = document.createElement("td");
            let diff_bar = document.createElement("td");
            diff_bar.style.textAlign = "right";
            document.querySelector(".mb-3").classList.add("kooltable");

            (async () => {
                let original_diffelem = rating_estimation_elem(await set_solvecount(window.location.href)).elem;
                original_diffelem.classList.add("text-nowrap");
                diff_bar.appendChild(original_diffelem);
            })();

            window.stats_bar.appendChild(window.bookmark_section);
            window.stats_bar.appendChild(diff_bar);
            ($("tbody")[1]).appendChild(window.stats_bar);
            render_tags_on_problem_page();
        }

        // Difficulty

        async function get_solvecount(problem_url) {
            let url = problem_url + "/hof";
            try {
                const res = await fetch(url);
                const doc = new DOMParser().parseFromString(await res.text(), 'text/html');

                // Standard problem page
                let solversList = doc.querySelector("#solversList");
                if (solversList) {
                    return doc.querySelector("b")?.innerText || "0";
                }

                // Leaderboard/Contest style problem
                let solves = 0;
                let solve_elems = doc.querySelectorAll(".solvecount");
                solve_elems.forEach((elem) => {
                    if (elem.innerText.trim() === "100") {
                        solves++;
                    }
                });
                return solves.toString();
            } catch (e) {
                return "0";
            }
        }

        async function set_solvecount(problem_url) {
            let solves = GM_getValue("solves_" + problem_url, -1);
            if (solves == -1) {
                let fetched = await get_solvecount(problem_url);
                GM_setValue("solves_" + problem_url, fetched);
                solves = fetched;
            }
            return solves;
        }
    }

    if (window.location.href.includes("personal")) {
        unsafeWindow.toggleSetTagSelected = function(elem) {
            if (elem.classList.contains("selected-tag")) {
                elem.classList.remove("selected-tag");
                localStorage.removeItem('selected-tag');
            } else {
                elem.classList.add("selected-tag");
                localStorage.setItem('selected-tag', elem.textContent.trim());
            }
            reloadHidden();
        };
    } else if(window.location.href.includes("hof")) {
        unsafeWindow.toggleLanguageFilter = function(elem, language) {
            elem.classList.toggle('selected-tag');

            // Filter submissions
            if (!elem.classList.contains('selected-tag')) {
                language = null;
            }
            const solvers = document.getElementById('solversList').getElementsByClassName('solver');
            for (let i = 0; i < solvers.length; i++) {
                solvers[i].hidden = (language && !solvers[i].classList.contains(language));
            }

            hideLastComma();
        }
    }

    if (window.location.href.includes("personal")) {
        async function get_badge_data(url) {
            // Check individual storage key first
            let entry = GM_getValue("score_" + url, null);

            if (!entry || typeof entry !== 'object') {
                try {
                    const res = await fetch(url);
                    const doc = new DOMParser().parseFromString(await res.text(), 'text/html');
                    let score_el = doc.querySelector(".badge.hub-badge");
                    entry = score_el ? { score: score_el.innerText.trim(), style: score_el.className }
                                     : { score: "0", style: "badge hub-badge badge-no-submission" };
                    // Save result immediately after fetch
                    GM_setValue("score_" + url, entry);
                } catch (e) {
                    entry = { score: "0", style: "badge hub-badge badge-no-submission" };
                }
            }
            return entry;
        }

        async function load_from_data() {
            let data = {
                "sets": [
                    { "tag":"training", "set_name":"Implementation Problems", "set_id":"impl", "old":false, "problems": [
                        { "problem_name":"Bernard's Magic Needles", "problem_link":"https://orac2.info/problem/1106/", "old":false},
                        { "problem_name":"A Not So Simple Sort", "problem_link":"https://orac2.info/problem/1105/", "old":false },
                        { "problem_name":"Pie a la Mode", "problem_link":"https://orac2.info/problem/1111/", "old":false },
                        { "problem_name":"Stacks", "problem_link":"https://orac2.info/problem/745/", "old":false },
                        { "problem_name":"Queues", "problem_link":"https://orac2.info/problem/752/", "old":false },
                        { "problem_name":"Bracket Matching", "problem_link":"https://orac2.info/problem/1107/", "old":false },
                        { "problem_name":"Twin Primes", "problem_link":"https://orac2.info/problem/872/", "old":false },
                        { "problem_name":"Pairs", "problem_link":"https://orac2.info/problem/947/", "old":false },
                        { "problem_name":"Swapsies", "problem_link":"https://orac2.info/problem/1104/", "old":false }
                    ]},
                    { "tag":"training", "set_name":"Graph Problems", "set_id":"graph", "old":false, "problems": [
                        { "problem_name":"Adjacency Lists", "problem_link":"https://orac2.info/problem/1108", "old":false },
                        { "problem_name":"Quicksort", "problem_link":"https://orac2.info/problem/659/", "old":false },
                        { "problem_name":"Gossip Chains", "problem_link":"https://orac2.info/problem/738/", "old":false },
                        { "problem_name":"King Arthur", "problem_link":"https://orac2.info/problem/978/", "old":false }
                    ]},
                    { "tag":"training", "set_name":"Bitmask Problems", "set_id":"bitmask", "old":false, "problems": [
                        { "problem_name":"Tiling", "problem_link":"https://orac2.info/problem/935/", "old":false },
                        { "problem_name":"Odd Jobs", "problem_link":"https://orac2.info/problem/951/", "old":false },
                        { "problem_name":"King Arthur", "problem_link":"https://orac2.info/problem/978/", "old":false }
                    ]},
                    { "tag":"cpp-practice", "set_name":"C++ Starter Problems", "set_id":"cpp-starter", "old":false, "problems": [
                        { "problem_name":"Addition (C++ Only)", "problem_link":"https://orac2.info/problem/1309/", "old":false },
                        { "problem_name":"Counting to Infinity (C++ Only)", "problem_link":"https://orac2.info/problem/1307/", "old":false },
                        { "problem_name":"Triple Hunting (C++ Only)", "problem_link":"https://orac2.info/problem/1306/", "old":false },
                        { "problem_name":"Sitting or Standing (C++ Only)", "problem_link":"https://orac2.info/problem/1308/", "old":false },
                        { "problem_name":"A Dish Best served Cold (C++ Only)", "problem_link":"https://orac2.info/problem/1310/", "old":false }
                    ]},
                    { "tag":"aic", "set_name":"AIC 1998", "set_id":"aic98", "old":true, "problems": [
                        { "problem_name":"Shopping Malls", "problem_link":"https://orac2.info/problem/aic98p1", "old":true},
                        { "problem_name":"Anagram Solver", "problem_link":"https://orac2.info/problem/aic98p2", "old":true},
                        { "problem_name":"Building Integers", "problem_link":"https://orac2.info/problem/aic98p3", "old":true},
                    ]},
                    { "tag":"aic", "set_name":"AIC 1999", "set_id":"aic99", "old":true, "problems": [
                        { "problem_name":"Hailstone Sequences", "problem_link":"https://orac2.info/problem/aic99p1", "old":true},
                        { "problem_name":"Hunt-a-Word", "problem_link":"https://orac2.info/problem/aic99p2", "old":true},
                        { "problem_name":"Cartography", "problem_link":"https://orac2.info/problem/aic99p3", "old":true},
                    ]},
                    { "tag":"aic", "set_name":"AIC 2000", "set_id":"aic00", "old":true, "problems": [
                        { "problem_name":"Academic Espionage", "problem_link":"https://orac2.info/problem/aic00p1", "old":true},
                        { "problem_name":"Keeping Secret", "problem_link":"https://orac2.info/problem/aic00p2", "old":true},
                        { "problem_name":"Analysing Bach", "problem_link":"https://orac2.info/problem/aic00p3", "old":true},
                        { "problem_name":"Displaying Paintings", "problem_link":"https://orac2.info/problem/aic00p4", "old":true},
                    ]},
                    { "tag":"aic", "set_name":"AIC 2001", "set_id":"aic01", "old":true, "problems": [
                        { "problem_name":"Flowers", "problem_link":"https://orac2.info/problem/aic01p1", "old":true},
                        { "problem_name":"Cartography III", "problem_link":"https://orac2.info/problem/aic01p2", "old":true},
                        { "problem_name":"Spies", "problem_link":"https://orac2.info/problem/aic01p3", "old":true},
                        { "problem_name":"Mobiles", "problem_link":"https://orac2.info/problem/aic01p4", "old":true},
                    ]},
                    { "tag":"aic", "set_name":"AIC 2002", "set_id":"aic02", "old":true, "problems": [
                        { "problem_name":"Halloween", "problem_link":"https://orac2.info/problem/aic02p1", "old":true},
                        { "problem_name":"Cartography II", "problem_link":"https://orac2.info/problem/aic02p2", "old":true},
                        { "problem_name":"Bureaucratic Bungling", "problem_link":"https://orac2.info/problem/aic02p3", "old":true}
                    ]},
                    { "tag":"aic", "set_name":"AIC 2003", "set_id":"aic03", "old":true, "problems": [
                        { "problem_name":"Word Wrap", "problem_link":"https://orac2.info/problem/aic03p2", "old":true},
                        { "problem_name":"Stacking Numbers", "problem_link":"https://orac2.info/problem/aic03p3", "old":true},
                        { "problem_name":"Handwriting Recognition", "problem_link":"https://orac2.info/problem/aic03p4", "old":true}
                    ]},
                    { "tag":"aic", "set_name":"AIC 2004", "set_id":"aic04", "old":true, "problems": [
                        { "problem_name":"Bugs", "problem_link":"https://orac2.info/problem/aic04p1", "old":true},
                        { "problem_name":"AFL", "problem_link":"https://orac2.info/problem/aic04p2", "old":true},
                        { "problem_name":"Atlantis", "problem_link":"https://orac2.info/problem/aic04p3", "old":true},
                        { "problem_name":"Zig-Zag Cipher", "problem_link":"https://orac2.info/problem/aic04p4", "old":true},
                        { "problem_name":"Bouncy Ball", "problem_link":"https://orac2.info/problem/aic04p5", "old":true},
                    ]},
                    { "tag":"training", "set_name":"Data Structure Excercises", "set_id":"trainingds","old":true, "problems": [
                        { "problem_name":"Cannons", "problem_link":"https://orac2.info/problem/dscannons", "old":true},
                        { "problem_name":"XOR Queries", "problem_link":"https://orac2.info/problem/dsfenwickxor", "old":true},
                        { "problem_name":"Inversion Counting", "problem_link":"https://orac2.info/problem/dsinversioncounting", "old":true},
                        { "problem_name":"Lazy Updates", "problem_link":"https://orac2.info/problem/dslazyupdate", "old":true},
                        { "problem_name":"Min Tree with Updates", "problem_link":"https://orac2.info/problem/dsrangetreeupdates", "old":true},
                        { "problem_name":"Supplies", "problem_link":"https://orac2.info/problem/dssupplies", "old":true}
                    ]},
                    { "tag":"training", "set_name":"2005 April Monthly Problems", "set_id":"monthly05", "old":true, "problems": [
                        { "problem_name":"Anagrammatic Primes", "problem_link":"https://orac2.info/problem/monthly05p1", "old":true},
                        { "problem_name":"Packing Pentominoes", "problem_link":"https://orac2.info/problem/monthly05p2", "old":true},
                        { "problem_name":"Nine Clocks", "problem_link":"https://orac2.info/problem/monthly05p3", "old":true},
                        { "problem_name":"Reverse Polish Notation", "problem_link":"https://orac2.info/problem/precamppublic2", "old":true}
                    ]},
                    { "tag":"aiio", "set_name":"2020 AIIO", "set_id":"aiio20", "old":true, "problems": [
                        { "problem_name":"L-Bot", "problem_link":"https://orac2.info/problem/aiio20lbot", "old":true},
                        { "problem_name":"Metromole", "problem_link":"https://orac2.info/problem/aiio20metro", "old":true},
                        { "problem_name":"Purview", "problem_link":"https://orac2.info/problem/aiio20purview", "old":true},
                        ]
                    },
                    { "tag":"training", "set_name":"Various Problems", "set_id":"misc_wayback", "old":true, "problems": [
                     { "problem_name":"AIIO 2016 Greece", "problem_link":"https://orac2.info/problem/aiio16greece", "old":true},
                    { "problem_name":"Selection Exam 2012 Career", "problem_link":"https://orac2.info/problem/seln12career", "old":true},
                    { "problem_name":"Selection Camp Problems", "problem_link":"https://orac2.info/problem/selnprac", "old":true},
                    { "problem_name":"2013 December Camp Beta Trial Exam I", "problem_link":"https://orac2.info/problem/trial1beta2013dec","old":true}
                    ]}
                ]
            };

            let c_tags = GM_getValue("tags", {});
            for(let t in c_tags) {
                data.sets.push({
                    tag: "custom-tags", set_name: "Custom Tag: " + t, set_id: "c-" + t.replace(/\s+/g, '-'),
                    problems: c_tags[t].map(l => ({ problem_name: GM_getValue("name_" + l, "Unknown"), problem_link: l })),
                    tooltip_text: "Tags: " + t
                });
            }

            const tags_cont = $(".tags-container");

            let curr_tags = $(".badge", tags_cont);
            let ORIGINAL_TAGS = [];
            curr_tags.forEach((tag) => {
                ORIGINAL_TAGS.push(tag.innerText);
            });

            tags_cont.innerHTML = `<span class="tags-description">Tags:</span>`;
            TAG_ORDER.forEach(name => {
                if (!(NEW_TAGS.includes(name))) {
                    if (!(ORIGINAL_TAGS.includes(name))) {
                        return;
                    }
                }

                let badge = tag_element(name);
                badge.setAttribute("onclick", "toggleSetTagSelected(this);");
                tags_cont.appendChild(badge);
            });

            for (let set of data.sets) {
                let div = document.createElement("div");
                div.id = "custom_set-" + set.set_id;
                div.className = `problemset-display set-table set-tag-${set.tag}`;

                let tooltip = set.tooltip_text || "Tags: " + set.tag;
                if(!set.old) {
                    div.innerHTML = `
                        <table class="table table-sm mt-0 mb-0 pointer" data-toggle="collapse" data-target="#collapse-${set.set_id}">
                            <thead class="thead-dark"><tr>
                                <th scope="col"><span class="set-title mr-auto">${set.set_name}</span></th>
                                <th scope="col" class="progress-column">
                                    <div class="d-flex align-items-center">
                                        <span class="badge badge-secondary mr-auto score-display">Fetching scores...</span>
                                        <span class="fas fa-lg fa-tag" data-toggle="tooltip" data-original-title="${tooltip}"></span>
                                    </div>
                                </th>
                            </tr></thead>
                        </table>
                        <div id="collapse-${set.set_id}" class="collapse show set-problems">
                            <table class="table table-sm mt-0 mb-0"><tbody id="table-${set.set_id}"></tbody></table>
                        </div>`;
                } else {
                    div.innerHTML = `
                        <table class="table table-sm mt-0 mb-0 pointer" data-toggle="collapse" data-target="#collapse-${set.set_id}">
                            <thead class="thead-dark"><tr>
                                <th scope="col"><span class="set-title mr-auto">${set.set_name}</span></th>
                                <th scope="col" class="progress-column">
                                    <div class="d-flex align-items-center">
                                    <span class="badge badge-secondary mr-auto score-display">N/A</span>
                                    <span class="fas fa-lg fa-tag" data-toggle="tooltip" data-original-title="${tooltip}"></span>
                                    </div>
                                </th>
                            </tr></thead>
                        </table>
                        <div id="collapse-${set.set_id}" class="collapse show set-problems">
                            <table class="table table-sm mt-0 mb-0"><tbody id="table-${set.set_id}"></tbody></table>
                        </div>`;
                }
                document.getElementById("show-sets").appendChild(div);

                let score_elems = $("td.progress-column");
                score_elems.forEach((elem) => {
                    if (elem.innerHTML.includes("Viewed")) {
                        elem.innerHTML = `Not attempted`;
                    }
                });

                let starter_sets = ["starter", "starterset1", "starterset1challenge", "starterset2", "starterset2challenge", "starterset3", "starterset3challenge"];
                starter_sets.forEach((set_name) => {
                    $(`[data-target="#problem-set-`+set_name+`"]`).parentElement.classList.add("set-tag-starter");
                    $(`[data-target="#problem-set-`+set_name+`"]`).children[0].children[0].children[1].children[0].children[1].setAttribute("data-original-title", "Tags: starter");
                });

                let acio_sets = ["acio25"];
                acio_sets.forEach((set_name) => {
                    $(`[data-target="#problem-set-`+set_name+`"]`).parentElement.classList.add("set-tag-acio");
                    $(`[data-target="#problem-set-`+set_name+`"]`).children[0].children[0].children[1].children[0].children[1].setAttribute("data-original-title", "Tags: acio");
                });

                let badgePromises = set.problems.map(async (p) => {
                    let tr = document.createElement("tr");
                    tr.innerHTML = `<td><a href="${p.problem_link}">${p.problem_name}</a></td><td class="progress-column"></td>`;
                    document.getElementById("table-" + set.set_id).appendChild(tr);

                    if (p.old) {
                        tr.setAttribute("old", true);
                        let badge_node = document.createElement("span");
                        badge_node.className = "badge badge-secondary";
                        badge_node.innerText = "N/A";
                        tr.children[1].appendChild(badge_node);
                        return null;
                    }

                    let badge_node = document.createElement("span");
                    badge_node.className = "badge badge-secondary";
                    badge_node.innerText = "Fetching score...";
                    tr.children[1].appendChild(badge_node);

                    let entry = await get_badge_data(p.problem_link);
                    badge_node.innerText = entry.score;
                    badge_node.className = entry.style;
                    return entry;
                });

                Promise.all(badgePromises).then((results) => {
                    let total = results.reduce((sum, res) => sum + (parseInt(res.score) || 0), 0);
                    let attempted = results.some(res => !res.style.includes("badge-no-submission"));
                    let max = results.length * 100;

                    const score_el = div.querySelector('.score-display');
                    score_el.innerText = `${total} / ${max}`;

                    let hClass = "badge-no-submission";
                    if (attempted) {
                        if (total === max) hClass = "badge-cmsgreen";
                        else if (total > 0) hClass = "badge-cmsyellow";
                        else hClass = "badge-cmsred";
                    }
                    score_el.className = 'badge mr-auto ' + hClass;
                });
            }
            if (typeof window.updateSetDisplay === 'function') window.updateSetDisplay();
            if (typeof querySelector !== 'undefined') $('[data-toggle="tooltip"]').tooltip();
        }
        load_from_data();

        // Solve counts
        function rating_estimation_elem(solves) {
            let rating_benchmarks = {
                800: ["0 &#9733;", "#A52A2A"],
                500: ["1 &#9733;", "#808080"],
                300: ["2 &#9733;", "#008000"],
                200: ["3 &#9733;", "#03A89E"],
                100: ["4 &#9733;", "#0000FF"],
                50: ["5 &#9733;", "#a0a"],
                10: ["6 &#9733;", "#bb0"],
                5: ["7 &#9733;", "#ffa200"],
                2: ["8 &#9733;", "#ff7b00"],
                0: ["9 &#9733;", "#ff0000"]
            };

            let lower_bound = 0, stars = -1;

            if (solves >= 800) {lower_bound = 800; stars = 0;}
            else if(solves >= 500) {lower_bound = 500; stars = 1;}
            else if(solves >= 300) {lower_bound = 300; stars = 2;}
            else if(solves >= 200) {lower_bound = 200; stars = 3;}
            else if(solves >= 100) {lower_bound = 100; stars = 4;}
            else if(solves >= 50) {lower_bound = 50; stars = 5;}
            else if(solves >= 10) {lower_bound = 10; stars = 6;}
            else if(solves >= 5) {lower_bound = 5; stars = 7;}
            else if(solves >= 2) {lower_bound = 2; stars = 8;}
            else {lower_bound = 0; stars = 9;}

            let elem = document.createElement("b");
            elem.style.color = rating_benchmarks[lower_bound][1];
            elem.innerHTML = rating_benchmarks[lower_bound][0];

            let uncoloured = document.createElement("span");
            uncoloured.style.color = "#000000";

            if (solves != 1) {
                uncoloured.innerHTML = " (" + (solves) + " solves)";
            } else {
                uncoloured.innerHTML = " (1 solve)";
            }

            elem.appendChild(uncoloured);

            return {"elem":elem, "stars":stars};
        }

        function star_display(stars) {
            let rating_benchmarks = {
                0: ["0 &#9733;", "#A52A2A"],
                1: ["1 &#9733;", "#808080"],
                2: ["2 &#9733;", "#008000"],
                3: ["3 &#9733;", "#03A89E"],
                4: ["4 &#9733;", "#0000FF"],
                5: ["5 &#9733;", "#a0a"],
                6: ["6 &#9733;", "#bb0"],
                7: ["7 &#9733;", "#ffa200"],
                8: ["8 &#9733;", "#ff7b00"],
                9: ["9 &#9733;", "#ff0000"]
            };
            let elem = document.createElement("b");
            elem.innerHTML = rating_benchmarks[stars][0];
            elem.style.color = rating_benchmarks[stars][1];

            return elem;
        }

        // --- SOLVE COUNT LOGIC ---

        async function get_solvecount(problem_url) {
            let url = problem_url + "/hof";
            try {
                const res = await fetch(url);
                const doc = new DOMParser().parseFromString(await res.text(), 'text/html');

                // Standard problem page
                let solversList = doc.querySelector("#solversList");
                if (solversList) {
                    return doc.querySelector("b")?.innerText || "0";
                }

                // Leaderboard/Contest style problem
                let solves = 0;
                let solve_elems = doc.querySelectorAll(".solvecount");
                solve_elems.forEach((elem) => {
                    if (elem.innerText.trim() == "100") {
                        solves++;
                    }
                });
                return solves.toString();
            } catch (e) {
                return "0";
            }
        }

        async function set_solvecount(problem_url) {
            let solves = GM_getValue("solves_" + problem_url, -1);
            if (solves == -1) {
                let fetched = await get_solvecount(problem_url);
                GM_setValue("solves_" + problem_url, fetched);
                solves = fetched;
            }
            return solves;
        }

        function parent_fullysolved(problem) {
            for (let neighbour of problem.parentElement.children) {
                if (neighbour.children[1].classList.contains("difficulty-column")) {
                    if (!(neighbour.children[1].innerHTML.includes("solve"))) {
                        return false;
                    }
                } else {
                    return false;
                }
            }

            return true;
        }

        //solves

        function load_solves() {
            (async () => {
                let sets = document.querySelectorAll(".collapse.show.set-problems");

                for (const set of sets) {
                    let table = set.querySelector("table");
                    if (!table) continue;

                    let headerRow = set.parentElement.children[0].querySelector("tr");;
                    if (headerRow && !headerRow.querySelector(".th-diff")) {
                        let th = document.createElement("th");
                        th.classList.add("difficulty-column", "th-diff");
                        th.innerHTML = "";
                        // Insert after the first header (Problem Name)
                        headerRow.children[0].after(th);
                    }

                    let rows = table.querySelectorAll("tbody tr");
                    let fetchQueue = [];

                    rows.forEach(row => {
                        // Check if this row is a header row
                        if (row.querySelector("th") || row.classList.contains("thead-dark")) return;
                        // Prevent duplicate columns if script re-runs
                        if (row.querySelector(".difficulty-column")) return;
                        // Check if old
                        if (row.hasAttribute("old")) {
                            let diffTd = document.createElement("td");
                            diffTd.classList.add("difficulty-column");
                            diffTd.innerHTML = "N/A (Old Problem)";
                            row.children[0].after(diffTd);
                            return;
                        }

                        let diffTd = document.createElement("td");
                        diffTd.classList.add("difficulty-column");
                        diffTd.innerHTML = "";
                        row.children[0].after(diffTd);

                        let link = row.querySelector("a")?.href;
                        if (link) {
                            fetchQueue.push({ cell: diffTd, url: link });
                        }
                    });

                    for (const item of fetchQueue) {
                        let result = await set_solvecount(item.url);
                        item.cell.innerHTML = "";
                        item.cell.appendChild(rating_estimation_elem(result).elem);
                        let diff_col = item.cell.parentElement.parentElement.parentElement.parentElement.parentElement.querySelector("table tr .difficulty-column");

                        if(diff_col.hasAttribute("min_stars")) {
                            diff_col.setAttribute("min_stars", Math.min(diff_col.getAttribute("min_stars"), rating_estimation_elem(result).stars));
                        } else {
                            diff_col.setAttribute("min_stars", rating_estimation_elem(result).stars)
                        }

                        if(diff_col.hasAttribute("max_stars")) {
                            diff_col.setAttribute("max_stars", Math.max(diff_col.getAttribute("max_stars"), rating_estimation_elem(result).stars));
                        } else {
                            diff_col.setAttribute("max_stars", rating_estimation_elem(result).stars);
                        }

                        if(parent_fullysolved(item.cell.parentElement)) {
                            if (diff_col.getAttribute("max_stars") == diff_col.getAttribute("min_stars")) {
                                diff_col.innerHTML = '';
                                diff_col.appendChild(star_display(diff_col.getAttribute("max_stars")));
                            } else {
                                diff_col.innerHTML = '';
                                let star_range_elem_to = document.createElement("span");
                                star_range_elem_to.innerText = " to ";

                                let star_range_elem = document.createElement("span");
                                star_range_elem.appendChild(star_display(diff_col.getAttribute("min_stars")));
                                star_range_elem.appendChild(star_range_elem_to)
                                star_range_elem.appendChild(star_display(diff_col.getAttribute("max_stars")));

                                diff_col.appendChild(star_range_elem);
                            }
                        }
                    }
                }
            })();
        }

        load_solves();

        let btn_solves = document.createElement("button");
        btn_solves.classList.add("koolbutton");
        btn_solves.innerHTML = "&nbsp;&nbsp;&nbsp;&nbsp;";
        let label_solves = document.createElement("label");
        label_solves.style.marginLeft = "0.45rem";
        label_solves.innerHTML = "Reload solve rates";

        btn_solves.addEventListener("click", async () => {
            const keys = await GM_listValues();
            for (const key of keys) {
                if (key.includes("solves_")) {
                    await GM_deleteValue(key);
                }
            }

            window.location.href = window.location.href;
            load_solves();
        });
        $(".custom-control.custom-switch.mb-sm-1")[1].after(label_solves);
        $(".custom-control.custom-switch.mb-sm-1")[1].after(btn_solves);

        // Difficulty selection
        let diff_choicecont = document.createElement("div");
        diff_choicecont.classList.add(".tags-container");

        let diff_choicelabel = document.createElement("span");
        diff_choicelabel.classList.add(".tags-description");
        diff_choicelabel.innerText = "Difficulty:";

        diff_choicecont.appendChild(diff_choicelabel);

        // Slider

        let slidercont = document.createElement("div"); slidercont.classList.add("slider-container");
        let slidertrack = document.createElement("div"); slidertrack.classList.add("slider-track");
        let sliderrange = document.createElement("div"); sliderrange.classList.add("slider-range"); sliderrange.id = "slider-range";
        let minslide = document.createElement("input"); minslide.id = "minRange"; minslide.setAttribute("type", "range"); minslide.setAttribute("min", "0"); minslide.setAttribute("max", "9"); minslide.setAttribute("value", "0");
        let maxslide = document.createElement("input"); maxslide.id = "maxRange"; maxslide.setAttribute("type", "range"); maxslide.setAttribute("min", "0"); maxslide.setAttribute("max", "9"); maxslide.setAttribute("value", "9");
        let sliderdisplay = document.createElement("span"); sliderdisplay.id = "rangevalue"; sliderdisplay.innerText = "0 &#9733; - 9 &#9733;";

        let sliderWrapper = document.createElement("div");
        sliderWrapper.style = "position:relative; width:200px; height:30px; margin-left:10px;";
        sliderWrapper.append(slidertrack, sliderrange, minslide, maxslide);
        slidercont.append(sliderdisplay, sliderWrapper);
        $(".tags-container").after(slidercont);
        updateSlider();

        function filterDiff() {
            updateSlider();
            $(".problemset-display").forEach((set) => {
                let max_allowed = parseInt(maxslide.value);
                let min_allowed = parseInt(minslide.value);
                let curr_min = parseInt(set.querySelector("table .difficulty-column").getAttribute("min_stars"));
                let curr_max = parseInt(set.querySelector("table .difficulty-column").getAttribute("max_stars"));

                if(!(curr_min >= min_allowed && curr_min <= max_allowed && curr_max >= min_allowed && curr_max <= max_allowed)) {
                    set.style.display = "none";
                } else {
                    if (!set.hidden) {
                        set.style.display = "block";
                    }
                }
            });
        }

        function updateSlider() {
            let minVal = parseInt(minslide.value);
            let maxVal = parseInt(maxslide.value);

            if (minVal > maxVal) {
                minVal = maxVal;
                minslide.value = minVal;
            }
            if (maxVal < minVal) {
                maxVal = minVal;
                maxslide.value = maxVal;
            }

            sliderdisplay.innerHTML = `<span style="color: #212529; font-weight: 400; font-size: 1rem;">Difficulty: </span>`;
            sliderdisplay.appendChild(star_display(minVal));
            sliderdisplay.innerHTML += `<span style="color: #212529; font-weight: 400; font-size: 1rem;"> - </span>`;
            sliderdisplay.appendChild(star_display(maxVal));

            // Update green range highlight
            const percentMin = (minVal / minslide.max) * 100;
            const percentMax = (maxVal / maxslide.max) * 100;
            sliderrange.style.left = percentMin + "%";
            sliderrange.style.width = (percentMax - percentMin) + "%";
        }

        minslide.addEventListener('input', filterDiff);
        maxslide.addEventListener('input', filterDiff);

        // Search bar
        let searchcont = document.createElement("div"); searchcont.classList.add("search-container");
        let searchlabel = document.createElement("span"); searchlabel.innerText = "Includes: ";
        let searchbar = document.createElement("input"); searchbar.classList.add("custom-search"); searchbar.setAttribute("placeholder", "Type a phrase");
        searchcont.appendChild(searchlabel); searchcont.appendChild(searchbar);
        $(".slider-container").after(searchcont);

        searchbar.addEventListener('input', filterSearch);

        function filterSearch() {
            let searchInput = document.querySelector(".custom-search");
            if (!searchInput) return;
            let searchphrase = searchInput.value.toLowerCase();

            document.querySelectorAll(".problemset-display.set-table").forEach((set) => {
                let oneProblemMatches = false;

                // 1. Check if the Category/Set Title matches the search
                let titleHeader = set.querySelector('[scope="col"]');
                let setTitle = titleHeader ? titleHeader.innerText.toLowerCase() : "";
                let setTitleMatches = setTitle.includes(searchphrase);

                let collapseDiv = set.querySelector(".collapse");
                if (!collapseDiv) return;

                let problems = collapseDiv.querySelectorAll("tr");
                problems.forEach((problem) => {
                    if (problem.querySelector("th") || problem.closest("thead")) return;

                    let problemLink = problem.querySelector("a");
                    if (!problemLink) return;

                    let problemName = problemLink.innerText.toLowerCase();
                    let problemMatches = problemName.includes(searchphrase);

                    if (searchphrase === "" || problemMatches || setTitleMatches) {
                        problem.setAttribute("data-visible", "true");
                        oneProblemMatches = true;
                    } else {
                        problem.setAttribute("data-visible", "false");
                    }
                });

                if (searchphrase === "" || setTitleMatches || oneProblemMatches) {
                    set.setAttribute("data-visible", "true");
                } else {
                    set.setAttribute("data-visible", "false");
                }
            });
        }
    }

    // add kactl
    waitForElm(".collapse.navbar-collapse").then((elm) => {
            elm.children[0].innerHTML += `<a class="nav-item nav-link user-links " href="/hub/notes/">Notes</a>`;
            elm.children[0].innerHTML += `<a class="nav-item nav-link user-links " href="https://aperson31415.github.io/informatics/">Github</a>`;
            elm.children[0].innerHTML += `<a class="nav-item nav-link user-links " href="/problemset">Problemsets</a>`;
        });

    if(window.location.href == "https://orac2.info/hub/notes/" || window.location.href == "https://orac.amt.edu.au/hub/notes") {
        waitForElm("p").then((elm) => {
            document.title = "Informatics Notes - ORAC";
            let cdnBase = "https://cdn.jsdelivr.net/gh/aperson31415/informatics@main/kactl.pdf?time=" + Date.now();
            document.querySelectorAll("div.container-xl")[1].innerHTML = `<embed class="embed-responsive-item" src="${cdnBase}" type="application/pdf" style="flex-grow:1; width:100%; height:80vh;" />`;
        });
    }

    /*add problemset*/
    if (window.location.href.startsWith("https://orac2.info/problemset")) {
    const libraryKey = 'orac_library';

    // --- 1. UI INITIALIZATION ---
    const setupFullUI = () => {
        document.head.innerHTML = `<title>orac2 problemsets</title>`;
        document.body.innerHTML = `
            <div id="app">
                <div class="sidebar">
                    <h1><a href="https://orac2.info/hub/personal">orac2 problemsets</a></h1>
                    <div class="tag-input-zone">
                        <h3>Add Problem</h3>
                        <input type="text" id="url-input" placeholder="Paste problem URL here...">
                        <input type="text" id="tag-input" placeholder="Tags (comma separated)">
                        <button id="add-btn">Fetch & Add to Set</button>
                    </div>
                    <div class="info-footer">
                        <p><strong>How it works:</strong> Paste any ORAC link. I'll visit the page in the background to grab the exact title from the header.</p>
                    </div>
                </div>
                <div class="main-content">
                    <div id="sets-grid" class="sets-grid"></div>
                </div>
            </div>
        `;

        const style = document.createElement('style');
        style.innerHTML = `
            body { margin: 0; font-family: 'Inter', system-ui, sans-serif; background: #f8fafc; color: #1e293b; }
            #app { display: grid; grid-template-columns: 340px 1fr; height: 100vh; }

            .sidebar { background: #0f172a; color: white; padding: 40px 30px; box-shadow: 4px 0 20px rgba(0,0,0,0.2); z-index: 10; }
            .main-content { padding: 40px; overflow-y: auto; }

            h1 { font-size: 24px; margin-bottom: 40px; color: #38bdf8; font-weight: 800; letter-spacing: -0.5px; }
            h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 1.5px; color: #94a3b8; margin-bottom: 20px; }

            .tag-input-zone { display: flex; flex-direction: column; gap: 15px; }
            input { padding: 14px; border-radius: 10px; border: 1px solid #334155; background: #1e293b; color: white; font-size: 14px; outline: none; transition: border 0.2s; }
            input:focus { border-color: #38bdf8; }

            #add-btn {
                background: #38bdf8; color: #0f172a; border: none; padding: 14px; border-radius: 10px;
                cursor: pointer; font-weight: 700; margin-top: 10px; transition: transform 0.1s, background 0.2s;
            }
            #add-btn:hover { background: #7dd3fc; }
            #add-btn:active { transform: scale(0.98); }
            #add-btn:disabled { background: #475569; color: #94a3b8; cursor: not-allowed; }

            .info-footer { margin-top: 50px; font-size: 13px; color: #64748b; line-height: 1.6; padding-top: 20px; border-top: 1px solid #334155; }

            .sets-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 30px; }
            .set-card { background: white; border-radius: 16px; padding: 24px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06); border: 1px solid #e2e8f0; }
            .set-title {
                font-weight: 700; font-size: 14px; text-transform: uppercase; color: #64748b;
                margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;
            }
            .set-count { background: #f1f5f9; padding: 2px 10px; border-radius: 20px; font-size: 12px; }

            .prob-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 0; border-bottom: 1px solid #f1f5f9; }
            .prob-item:last-child { border-bottom: none; }
            .prob-link { color: #0f172a; text-decoration: none; font-size: 15px; font-weight: 600; display: block; max-width: 240px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
            .prob-link:hover { color: #38bdf8; }
            .remove-btn { color: #cbd5e1; cursor: pointer; font-size: 20px; line-height: 1; padding: 5px; }
            .remove-btn:hover { color: #ef4444; }
        `;
        document.head.appendChild(style);
    };

    // --- 2. CORE LOGIC ---
    const render = () => {
        const library = GM_getValue(libraryKey, {});
        const setsGrid = document.getElementById('sets-grid');
        if (!setsGrid) return;

        setsGrid.innerHTML = '';
        const sets = {};

        Object.keys(library).forEach(url => {
            library[url].tags.forEach(tag => {
                if (!sets[tag]) sets[tag] = [];
                sets[tag].push({ url, ...library[url] });
            });
        });

        if (Object.keys(sets).length === 0) {
            setsGrid.innerHTML = `
                <div style="grid-column: 1/-1; text-align: center; color: #94a3b8; margin-top: 15vh;">
                    <h2 style="font-size: 24px; color: #cbd5e1;">Your Workspace is Clear</h2>
                    <p>Paste a problem URL on the left to start categorizing.</p>
                </div>`;
            return;
        }

        Object.keys(sets).sort().forEach(tag => {
            const card = document.createElement('div');
            card.className = 'set-card';
            card.innerHTML = `<div class="set-title"><span>${tag}</span> <span class="set-count">${sets[tag].length}</span></div>`;

            sets[tag].forEach(p => {
                const item = document.createElement('div');
                item.className = 'prob-item';
                item.innerHTML = `
                    <a href="${p.url}" class="prob-link" target="_blank" title="${p.title}">${p.title}</a>
                    <span class="remove-btn" title="Remove">&times;</span>
                `;
                item.querySelector('.remove-btn').onclick = () => removeTag(p.url, tag);
                card.appendChild(item);
            });
            setsGrid.appendChild(card);
        });
    };

    const removeTag = (url, tag) => {
        let lib = GM_getValue(libraryKey, {});
        if (!lib[url]) return;
        lib[url].tags = lib[url].tags.filter(t => t !== tag);
        if (lib[url].tags.length === 0) delete lib[url];
        GM_setValue(libraryKey, lib);
        render();
    };

    const handleAdd = async () => {
        const urlInput = document.getElementById('url-input');
        const tagInput = document.getElementById('tag-input');
        const addBtn = document.getElementById('add-btn');

        let url = urlInput.value.trim();
        const rawTags = tagInput.value.trim();

        if (!url || !rawTags) {
            alert("Need a URL and at least one tag.");
            return;
        }

        // Ensure absolute URL
        if (!url.startsWith('http')) {
            url = 'https://orac2.info' + (url.startsWith('/') ? '' : '/') + url;
        }

        addBtn.innerText = "Visiting Page...";
        addBtn.disabled = true;

        try {
            // Fetch the actual page content
            const response = await fetch(url);
            if (!response.ok) throw new Error("Could not reach that URL.");

            const html = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');

            // Targeted scrape of the H1
            const titleElem = doc.querySelector('h1.mb-0');
            const title = titleElem ? titleElem.innerText.trim() : "Unknown Title";

            let lib = GM_getValue(libraryKey, {});
            const newTags = rawTags.split(',').map(t => t.trim().toLowerCase()).filter(Boolean);
            const existingTags = lib[url]?.tags || [];

            lib[url] = {
                title: title,
                tags: Array.from(new Set([...existingTags, ...newTags]))
            };

            GM_setValue(libraryKey, lib);

            urlInput.value = '';
            tagInput.value = '';
            render();
        } catch (e) {
            alert("Error: " + e.message);
        } finally {
            addBtn.innerText = "Fetch & Add to Set";
            addBtn.disabled = false;
        }
    };

    // --- 3. RUN ---
    setupFullUI();
    render();

    document.getElementById('add-btn').addEventListener('click', handleAdd);
    document.getElementById('tag-input').addEventListener('keypress', (e) => {
        if (e.key === 'Enter') handleAdd();
    });
}

    // Add this once at the very top of your script
document.addEventListener('click', function(e) {
    // We check the class list because classes survive the re-render
    if (e.target.classList.contains('plus')) {
        e.preventDefault();
        e.stopPropagation();
        // Call your logic here
        new_tag();
    }
    else if (e.target.classList.contains('minus')) {
        e.preventDefault();
        e.stopPropagation();
        del_tag();
    }
}, true); // The 'true' makes this a 'Capture' event—it strikes first!
})();