HuggingFace Model & Dataset Date Filter

Adds a custom dropdown to filter HF models and datasets by date. Persists selection across page reloads.

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 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         HuggingFace Model & Dataset Date Filter
// @description  Adds a custom dropdown to filter HF models and datasets by date. Persists selection across page reloads.
// @match        *://huggingface.co/models*
// @match        *://huggingface.co/datasets*
// @icon         https://huggingface.co/favicon.ico
// @grant        none
// @license      MIT
// @version      1.0
// @namespace    https://greasyfork.org/users/1457508
// ==/UserScript==

(function () {
    'use strict';

    // ─── Constants ────────────────────────────────────────────────────────────

    const isDatasets = location.pathname.startsWith('/datasets');
    const STORAGE_KEY_VAL   = isDatasets ? 'hf_filter_date_value_datasets'  : 'hf_filter_date_value_models';
    const STORAGE_KEY_LABEL = isDatasets ? 'hf_filter_date_label_datasets'  : 'hf_filter_date_label_models';

    const DAY = 24 * 60 * 60 * 1000;
    const TIME_INTERVALS = [
        { label: 'Past Day',        value:   1 * DAY    },
        { label: 'Past 3 Days',     value:   3 * DAY    },
        { label: 'Past Week',       value:   7 * DAY    },
        { label: 'Past Month',      value:  30 * DAY    },
        { label: 'Past 3 Months',   value:  91 * DAY    },
        { label: 'Past 6 Months',   value: 183 * DAY    },
        { label: 'Past Year',       value: 365 * DAY    },
        { label: 'All Time',        value: 0            },
    ];

    // SVG icons as constants to avoid repetition and typos
    const ICON_CLOCK = `
        <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
             fill="none" stroke="currentColor" stroke-width="2"
             stroke-linecap="round" stroke-linejoin="round"
             class="mr-1.5 opacity-70" aria-hidden="true">
            <circle cx="12" cy="12" r="10"/>
            <polyline points="12 6 12 12 16 14"/>
        </svg>`;

    // ─── State ────────────────────────────────────────────────────────────────

    let filterTimer   = null;
    let isFiltering   = false;   // guard against observer ↔ filter feedback
    let docClickBound = false;   // ensure document listener is added only once

    // ─── Core filter logic ────────────────────────────────────────────────────

    function getCutoff() {
        const wrapper = document.getElementById('hf-time-filter-wrapper');
        return wrapper ? parseInt(wrapper.dataset.value, 10) : 0;
    }

    function filterModels() {
        const cutoff = getCutoff();
        const cards  = document.querySelectorAll('article.overview-card-wrapper');

        isFiltering = true;

        if (cutoff === 0) {
            cards.forEach(c => { c.style.display = ''; });
        } else {
            const now = Date.now();
            cards.forEach(card => {
                const timeEl    = card.querySelector('time[datetime]');
                const modelDate = timeEl ? Date.parse(timeEl.getAttribute('datetime')) : NaN;
                card.style.display = (!timeEl || isNaN(modelDate) || (now - modelDate > cutoff)) ? 'none' : '';
            });
        }

        // Yield to the browser before re-enabling the observer guard so that
        // the style changes we just made don't immediately re-trigger filtering.
        requestAnimationFrame(() => { isFiltering = false; });
    }

    function scheduleFilter() {
        clearTimeout(filterTimer);
        filterTimer = setTimeout(filterModels, 50);
    }

    // ─── UI injection ─────────────────────────────────────────────────────────

    function injectUI() {
        if (document.getElementById('hf-time-filter-wrapper')) return;

        // Find the Sort button — use trim() to be resilient to whitespace changes
        const sortBtn = Array.from(document.querySelectorAll('button'))
            .find(b => b.textContent.trim().startsWith('Sort:'));
        if (!sortBtn) return;

        const container = sortBtn.parentElement?.parentElement;
        if (!container) return;

        // Mirror the Sort button's classes (minus layout helpers we override)
        const mirroredClasses = Array.from(sortBtn.classList)
            .filter(c => c !== 'w-full')
            .join(' ');

        const savedValue = localStorage.getItem(STORAGE_KEY_VAL)   ?? '0';
        const savedLabel = localStorage.getItem(STORAGE_KEY_LABEL) ?? 'Time Filter: Off';

        // ── Build wrapper ──
        const wrapper = document.createElement('div');
        wrapper.id        = 'hf-time-filter-wrapper';
        wrapper.className = 'relative inline-block ml-2 text-sm';
        wrapper.dataset.value = savedValue;

        // ── Build trigger button ──
        const btn = document.createElement('button');
        btn.id        = 'hf-time-filter-btn';
        btn.type      = 'button';
        btn.className = mirroredClasses;
        btn.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding-right:0.75rem;';
        btn.setAttribute('aria-haspopup', 'listbox');
        btn.setAttribute('aria-expanded',  'false');
        btn.setAttribute('aria-controls',  'hf-time-filter-menu');

        btn.innerHTML = `
            <span style="display:flex;align-items:center;">
                ${ICON_CLOCK}
                <span id="hf-time-filter-text" class="whitespace-nowrap">${savedLabel}</span>
            </span>`;

        // ── Build dropdown menu ──
        const menu = document.createElement('div');
        menu.id        = 'hf-time-filter-menu';
        menu.role      = 'listbox';
        menu.className = 'hidden absolute top-full z-20 mt-1 w-auto min-w-full max-w-xs overflow-hidden rounded-xl border border-gray-100 bg-white shadow-lg left-0 dark:border-gray-800 dark:bg-gray-950';

        const ul = document.createElement('ul');
        ul.className = 'm-0 min-w-full';
        ul.setAttribute('role', 'presentation');

        TIME_INTERVALS.forEach(interval => {
            const isActive = String(interval.value) === savedValue;

            const li = document.createElement('li');
            li.setAttribute('role', 'option');
            li.setAttribute('aria-selected', String(isActive));

            const optBtn = document.createElement('button');
            optBtn.type              = 'button';
            optBtn.className         = 'hf-time-option flex w-full cursor-pointer items-center whitespace-nowrap px-3 py-1.5 text-left hover:bg-gray-50 dark:hover:bg-gray-800 leading-tight text-gray-700 dark:text-gray-200';
            optBtn.dataset.value     = String(interval.value);
            optBtn.dataset.label     = interval.label;
            // Highlight the active option
            if (isActive) optBtn.style.fontWeight = '600';

            optBtn.innerHTML = `<span class="truncate">${interval.label}</span>`;
            li.appendChild(optBtn);
            ul.appendChild(li);
        });

        menu.appendChild(ul);
        wrapper.appendChild(btn);
        wrapper.appendChild(menu);
        container.appendChild(wrapper);

        // ── Events ──

        // Toggle open/close
        btn.addEventListener('click', e => {
            e.stopPropagation();
            const isHidden = menu.classList.toggle('hidden');
            btn.setAttribute('aria-expanded', String(!isHidden));
        });

        // Option selected
        ul.addEventListener('click', e => {
            const optBtn = e.target.closest('.hf-time-option');
            if (!optBtn) return;

            const val   = optBtn.dataset.value;
            const label = optBtn.dataset.label;

            // Update state
            wrapper.dataset.value = val;
            document.getElementById('hf-time-filter-text').textContent = label;
            localStorage.setItem(STORAGE_KEY_VAL,   val);
            localStorage.setItem(STORAGE_KEY_LABEL, label);

            // Update active styling
            ul.querySelectorAll('.hf-time-option').forEach(b => {
                b.style.fontWeight = (b.dataset.value === val) ? '600' : '';
                b.closest('li').setAttribute('aria-selected', String(b.dataset.value === val));
            });

            closeMenu();
            filterModels();
        });

        // Close on Escape
        document.addEventListener('keydown', e => {
            if (e.key === 'Escape') closeMenu();
        });

        // Close on outside click (registered once globally)
        if (!docClickBound) {
            docClickBound = true;
            document.addEventListener('click', e => {
                const w = document.getElementById('hf-time-filter-wrapper');
                if (w && !w.contains(e.target)) closeMenu();
            });
        }

        // Apply saved filter immediately if active
        if (savedValue !== '0') filterModels();
    }

    function closeMenu() {
        const menu = document.getElementById('hf-time-filter-menu');
        const btn  = document.getElementById('hf-time-filter-btn');
        if (menu) menu.classList.add('hidden');
        if (btn)  btn.setAttribute('aria-expanded', 'false');
    }

    // ─── MutationObserver ─────────────────────────────────────────────────────

    const observer = new MutationObserver(() => {
        // Re-inject the UI if HF's own navigation wiped it
        if (!document.getElementById('hf-time-filter-wrapper')) {
            injectUI();
        }

        // Re-filter newly loaded cards, but skip when we caused the mutations
        if (!isFiltering) {
            scheduleFilter();
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // ─── Bootstrap ────────────────────────────────────────────────────────────

    injectUI();

})();