NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name HuggingFace Enhanced Search
// @namespace https://github.com/Ehab-0/huggingfacesearch
// @version 3.0.0
// @description Advanced filters, presets, VRAM estimates, saved profiles, and more for the HuggingFace models page
// @author Ehab-0
// @homepageURL https://github.com/Ehab-0/huggingfacesearch
// @supportURL https://github.com/Ehab-0/huggingfacesearch/issues
// @license MIT
// @match https://huggingface.co/models*
// @icon https://huggingface.co/favicon.ico
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ── Quick-filter presets ────────────────────────────────────────────────
const PRESETS = [
{
label: 'Uncensored', emoji: '🔓',
keywords: [
'uncensored', 'abliterated', 'abliterate', 'unfiltered',
'de-aligned', 'unaligned', 'zero-refusal', 'non-refusing',
'neutrally-aligned', 'raw', 'base-model', 'dolphin', 'hermes',
'nsfw', 'roleplay', 'rp', 'spicy', 'unhinged',
'jailbroken', 'dark-llm', 'malicious',
],
},
{
label: 'Code', emoji: '💻',
keywords: ['code', 'coder', 'codegen', 'starcoder', 'deepseek-coder', 'codellama', 'qwen2.5-coder', 'programming', 'devstral'],
},
{
label: 'Vision', emoji: '👁️',
keywords: ['vision', 'vl', 'vlm', 'image-text', 'multimodal', 'llava', 'cogvlm', 'minicpm-v', 'pixtral', 'molmo'],
},
{
label: 'Embed', emoji: '📐',
keywords: ['embed', 'embedding', 'bge', 'e5', 'gte', 'nomic-embed', 'sentence-transformers', 'feature-extraction'],
},
{ label: 'GGUF', emoji: '📦', keywords: ['gguf'] },
{
label: 'MoE', emoji: '🧩',
keywords: ['moe', 'mixture-of-experts', 'mixtral', 'dbrx', 'grok', 'switch', 'olmoe'],
},
];
// ── Styles ──────────────────────────────────────────────────────────────
GM_addStyle(`
/* ── Sidebar section ───────────────────────────────────── */
#hf-af-sidebar {
border-bottom: 1px solid var(--border-primary, rgba(209,213,219,.3));
padding-bottom: 16px;
margin-bottom: 16px;
}
#hf-af-sidebar .hf-af-header {
display: flex; align-items: center; justify-content: space-between;
cursor: pointer; user-select: none; padding: 4px 0; margin-bottom: 4px;
}
#hf-af-sidebar .hf-af-header h3 {
margin: 0; font-size: 0.875rem; font-weight: 600;
color: var(--text-primary, #e6edf3);
display: flex; align-items: center; gap: 6px;
}
#hf-af-sidebar .hf-af-toggle {
font-size: 14px; color: var(--text-secondary, #8b949e);
transition: transform 0.2s;
}
#hf-af-sidebar.collapsed .hf-af-toggle { transform: rotate(-90deg); }
#hf-af-sidebar.collapsed .hf-af-body,
#hf-af-sidebar.collapsed .hf-af-actions { display: none; }
#hf-af-sidebar .hf-af-body {
display: flex; flex-direction: column; gap: 10px;
padding-right: 1.5rem;
}
#hf-af-sidebar .hf-af-group { display: flex; flex-direction: column; gap: 4px; }
#hf-af-sidebar .hf-af-group label {
font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.4px; color: var(--text-secondary, #8b949e);
}
#hf-af-sidebar .hf-af-label-row {
display: flex; align-items: center; justify-content: space-between;
}
#hf-af-sidebar .hf-af-row { display: flex; gap: 6px; align-items: center; }
#hf-af-sidebar .hf-af-row > * { flex: 1; min-width: 0; }
#hf-af-sidebar .hf-af-select,
#hf-af-sidebar .hf-af-input {
background: var(--bg-primary, #0d1117);
border: 1px solid var(--border-primary, #30363d);
border-radius: 6px; color: var(--text-primary, #c9d1d9);
padding: 5px 6px; font-size: 0.8rem; outline: none;
transition: border-color 0.15s; width: 100%; box-sizing: border-box;
}
#hf-af-sidebar .hf-af-select:focus,
#hf-af-sidebar .hf-af-input:focus { border-color: #58a6ff; }
#hf-af-sidebar .hf-af-input::placeholder { color: #484f58; }
#hf-af-sidebar .hf-af-select { cursor: pointer; }
/* Quick-filter preset chips */
#hf-af-sidebar .hf-af-presets { display: flex; flex-wrap: wrap; gap: 5px; }
#hf-af-sidebar .hf-af-preset-chip {
display: inline-flex; align-items: center; gap: 3px;
background: var(--bg-secondary, #21262d);
border: 1px solid var(--border-primary, #30363d);
border-radius: 14px; padding: 3px 8px; font-size: 0.75rem;
color: var(--text-secondary, #8b949e);
cursor: pointer; user-select: none; transition: all 0.15s;
white-space: nowrap;
}
#hf-af-sidebar .hf-af-preset-chip:hover { border-color: #58a6ff; color: var(--text-primary, #c9d1d9); }
#hf-af-sidebar .hf-af-preset-chip.active {
background: rgba(56,139,253,.15); border-color: #58a6ff; color: #58a6ff;
}
#hf-af-sidebar .hf-af-preset-badge {
font-size: 0.6rem; background: rgba(56,139,253,.3); color: #58a6ff;
border-radius: 6px; padding: 0 4px; margin-left: 2px; min-width: 14px;
text-align: center;
}
/* AND/OR toggle */
#hf-af-sidebar .hf-af-mode-toggle {
font-size: 0.6rem; font-weight: 700; padding: 1px 6px;
border-radius: 4px; cursor: pointer; user-select: none;
border: 1px solid var(--border-primary, #30363d);
background: var(--bg-secondary, #21262d);
color: var(--text-secondary, #8b949e);
transition: all 0.15s; text-transform: uppercase;
}
#hf-af-sidebar .hf-af-mode-toggle:hover { border-color: #58a6ff; }
#hf-af-sidebar .hf-af-mode-toggle.mode-and {
background: rgba(56,139,253,.15); border-color: #58a6ff; color: #58a6ff;
}
/* Download quick chips */
#hf-af-sidebar .hf-af-dl-chips { display: flex; gap: 4px; flex-wrap: wrap; }
#hf-af-sidebar .hf-af-dl-chip {
font-size: 0.65rem; padding: 1px 6px; border-radius: 4px;
border: 1px solid var(--border-primary, #30363d);
background: var(--bg-secondary, #21262d);
color: var(--text-secondary, #8b949e);
cursor: pointer; transition: all 0.15s;
}
#hf-af-sidebar .hf-af-dl-chip:hover { border-color: #58a6ff; color: var(--text-primary, #c9d1d9); }
/* Profiles */
#hf-af-sidebar .hf-af-profiles-row {
display: flex; gap: 4px; align-items: center;
}
#hf-af-sidebar .hf-af-profiles-row select { flex: 1; }
#hf-af-sidebar .hf-af-profile-btn {
padding: 4px 6px; border-radius: 4px; border: 1px solid var(--border-primary, #30363d);
background: var(--bg-secondary, #21262d); color: var(--text-secondary, #8b949e);
cursor: pointer; font-size: 0.7rem; white-space: nowrap;
}
#hf-af-sidebar .hf-af-profile-btn:hover { border-color: #58a6ff; color: var(--text-primary, #c9d1d9); }
#hf-af-sidebar .hf-af-actions {
display: flex; gap: 6px; margin-top: 6px; padding-right: 1.5rem;
}
#hf-af-sidebar .hf-af-btn {
flex: 1; padding: 6px 10px; border: none; border-radius: 6px;
font-size: 0.8rem; font-weight: 600; cursor: pointer; transition: background 0.15s;
}
#hf-af-sidebar .hf-af-btn-primary { background: #238636; color: #fff; }
#hf-af-sidebar .hf-af-btn-primary:hover { background: #2ea043; }
#hf-af-sidebar .hf-af-btn-secondary {
background: var(--bg-secondary, #21262d); color: var(--text-primary, #c9d1d9);
border: 1px solid var(--border-primary, #30363d);
}
#hf-af-sidebar .hf-af-btn-secondary:hover { background: var(--border-primary, #30363d); }
/* ── Inline results ────────────────────────────────────── */
#hf-af-results-bar {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 0; font-size: 0.8rem; color: var(--text-primary, #c9d1d9);
flex-wrap: wrap; gap: 6px;
border-bottom: 1px solid var(--border-primary, #30363d);
margin-bottom: 8px;
}
.hf-af-close-results {
background: var(--bg-secondary, #21262d);
border: 1px solid var(--border-primary, #30363d);
color: var(--text-primary, #c9d1d9);
padding: 4px 10px; border-radius: 6px; cursor: pointer; font-size: 0.75rem;
}
.hf-af-close-results:hover { background: var(--border-primary, #30363d); }
.hf-af-load-more {
background: #238636; border: none; color: #fff;
padding: 4px 12px; border-radius: 6px; cursor: pointer;
font-size: 0.75rem; font-weight: 600; margin-left: 6px;
}
.hf-af-load-more:hover { background: #2ea043; }
.hf-af-result-card {
display: block; padding: 12px 0;
border-bottom: 1px solid var(--border-primary, rgba(48,54,61,.5));
text-decoration: none; color: inherit; transition: background 0.1s;
}
.hf-af-result-card:hover { background: rgba(56,139,253,.04); }
.hf-af-card-header {
display: flex; align-items: center; gap: 6px; margin-bottom: 4px;
}
.hf-af-card-title {
font-size: 0.9rem; font-weight: 600; color: #58a6ff; flex: 1;
}
.hf-af-card-actions {
display: flex; gap: 4px; align-items: center; flex-shrink: 0;
}
.hf-af-icon-btn {
background: var(--bg-secondary, #21262d);
border: 1px solid var(--border-primary, #30363d);
color: var(--text-secondary, #8b949e);
padding: 2px 6px; border-radius: 4px; cursor: pointer;
font-size: 0.7rem; transition: all 0.15s; white-space: nowrap;
}
.hf-af-icon-btn:hover { border-color: #58a6ff; color: #58a6ff; }
.hf-af-card-meta {
font-size: 0.75rem; color: var(--text-secondary, #8b949e);
display: flex; flex-wrap: wrap; gap: 4px 12px;
}
.hf-af-card-meta span {
display: inline-flex; align-items: center; gap: 2px;
}
.hf-af-vram {
color: #d29922; font-weight: 500;
}
.hf-af-tag {
display: inline-block; background: rgba(31,41,55,.5);
border: 1px solid var(--border-primary, #30363d);
border-radius: 4px; padding: 0 5px; font-size: 0.68rem;
color: var(--text-secondary, #8b949e); cursor: pointer;
transition: all 0.15s;
}
.hf-af-tag:hover { border-color: #58a6ff; color: #58a6ff; }
.hf-af-tag-exclude {
border-color: #f85149; color: #f85149; text-decoration: line-through;
}
/* Expandable card details */
.hf-af-card-details {
margin-top: 6px; padding: 8px 10px;
background: var(--bg-secondary, rgba(22,27,34,.5));
border-radius: 6px; font-size: 0.72rem;
color: var(--text-secondary, #8b949e);
}
.hf-af-card-details .hf-af-detail-row {
display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 4px;
}
.hf-af-card-details .hf-af-detail-label {
font-weight: 600; color: var(--text-primary, #c9d1d9);
min-width: 50px;
}
.hf-af-expand-btn {
font-size: 0.65rem; transition: transform 0.15s; display: inline-block;
}
.hf-af-spinner {
display: inline-block; width: 12px; height: 12px;
border: 2px solid #30363d; border-top-color: #58a6ff;
border-radius: 50%; animation: hf-af-spin 0.6s linear infinite;
margin-right: 4px; vertical-align: middle;
}
@keyframes hf-af-spin { to { transform: rotate(360deg); } }
.hf-af-empty {
text-align: center; padding: 40px 12px; color: var(--text-secondary, #8b949e);
}
.hf-af-empty p:first-child { font-size: 1rem; margin-bottom: 6px; }
/* Toast notification */
.hf-af-toast {
position: fixed; bottom: 20px; right: 20px; z-index: 10000;
background: #238636; color: #fff; padding: 8px 16px;
border-radius: 8px; font-size: 0.8rem; font-weight: 600;
box-shadow: 0 4px 12px rgba(0,0,0,.4);
animation: hf-af-toast-in 0.3s ease, hf-af-toast-out 0.3s ease 1.5s forwards;
}
@keyframes hf-af-toast-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@keyframes hf-af-toast-out { to { opacity: 0; transform: translateY(10px); } }
/* Keyboard shortcut hint */
.hf-af-kbd {
font-size: 0.55rem; color: #484f58; font-family: monospace;
margin-left: 4px;
}
`);
// ── Helpers ─────────────────────────────────────────────────────────────
function fmt(n) {
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
return String(n);
}
function fmtParams(n) {
if (!n && n !== 0) return null;
if (n >= 1e12) return (n / 1e12).toFixed(1) + 'T';
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
if (n >= 1e6) return (n / 1e6).toFixed(0) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(0) + 'K';
return String(n);
}
function timeAgo(dateStr) {
const diff = Date.now() - new Date(dateStr).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
const days = Math.floor(hrs / 24);
if (days < 30) return `${days}d ago`;
const months = Math.floor(days / 30);
if (months < 12) return `${months}mo ago`;
return `${Math.floor(months / 12)}y ago`;
}
function parseParamVal(value, unit) {
const num = parseFloat(value);
if (isNaN(num)) return null;
return num * ({ M: 1e6, B: 1e9, T: 1e12 }[unit] || 1e9);
}
function getDateThreshold(preset) {
const d = 86400000;
return {
'1w': Date.now() - 7 * d, '2w': Date.now() - 14 * d,
'1m': Date.now() - 30 * d, '3m': Date.now() - 90 * d,
'6m': Date.now() - 180 * d, '1y': Date.now() - 365 * d,
}[preset] || null;
}
function showToast(msg) {
const t = document.createElement('div');
t.className = 'hf-af-toast';
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 2000);
}
// ── Extract parameter count ────────────────────────────────────────────
function extractParamCount(model) {
if (model.safetensors?.total) return model.safetensors.total;
if (model.safetensors?.parameters) {
const p = model.safetensors.parameters;
if (typeof p === 'number') return p;
if (typeof p === 'object') {
const vals = Object.values(p).filter(v => typeof v === 'number');
if (vals.length) return Math.max(...vals);
}
}
if (model.id) {
const matches = [...model.id.matchAll(/(?:^|[-_/.])(\d+(?:\.\d+)?)(b|m|t)(?:[-_/.]|$)/gi)];
if (matches.length) {
const last = matches[matches.length - 1];
return parseFloat(last[1]) * ({ M: 1e6, B: 1e9, T: 1e12 }[last[2].toUpperCase()] || 1e9);
}
}
return null;
}
// ── VRAM estimation ────────────────────────────────────────────────────
function estimateVRAM(paramCount) {
if (!paramCount) return null;
// FP16: ~2 bytes per param + ~20% overhead for activations/KV cache
const fp16GB = (paramCount * 2 * 1.2) / 1e9;
// Q4: ~0.5 bytes per param + overhead
const q4GB = (paramCount * 0.5 * 1.2) / 1e9;
return { fp16: fp16GB, q4: q4GB };
}
function fmtVRAM(gb) {
if (gb >= 1) return gb.toFixed(1) + ' GB';
return (gb * 1024).toFixed(0) + ' MB';
}
// ── Keyword matching ───────────────────────────────────────────────────
function modelMatchesKeywords(model, keywords) {
if (!keywords || !keywords.length) return true;
const haystack = ((model.id || '') + ' ' + (model.tags || []).join(' ') + ' ' + (model.pipeline_tag || '')).toLowerCase();
return keywords.some(kw => haystack.includes(kw.toLowerCase()));
}
function modelMatchesExclude(model, excludeKeywords) {
if (!excludeKeywords || !excludeKeywords.length) return true; // passes = not excluded
const haystack = ((model.id || '') + ' ' + (model.tags || []).join(' ') + ' ' + (model.pipeline_tag || '')).toLowerCase();
return !excludeKeywords.some(kw => haystack.includes(kw.toLowerCase()));
}
// ── API fetching ───────────────────────────────────────────────────────
async function fetchModels({ sort, direction = -1, limit = 200, offset = 0, search = '' }) {
const params = new URLSearchParams();
params.set('sort', sort);
params.set('direction', String(direction));
params.set('limit', String(limit));
params.append('expand[]', 'safetensors');
params.append('expand[]', 'lastModified');
if (offset) params.set('offset', String(offset));
if (search) params.set('search', search);
const pageUrl = new URL(window.location.href);
for (const [key, val] of pageUrl.searchParams.entries()) {
if (['library', 'dataset', 'language', 'license', 'other', 'pipeline_tag'].includes(key)) {
params.set(key, val);
}
}
const resp = await fetch(`https://huggingface.co/api/models?${params}`);
if (!resp.ok) throw new Error(`API ${resp.status}`);
return resp.json();
}
// ── State ──────────────────────────────────────────────────────────────
let currentOffset = 0;
let currentFilters = {};
let allResults = [];
let seenIds = new Set();
let kwOffsets = {};
let activePresets = new Set();
let presetMode = 'or'; // 'or' | 'and'
let originalResultsHTML = null;
let originalPaginationHTML = null;
let presetResultCounts = {}; // { presetIdx: count }
const BATCH = 200;
const MAX_SCAN = 3000;
const MAX_SCAN_PER_KW = 1000;
const MAX_SCAN_TOTAL = 5000;
// ── DOM references ────────────────────────────────────────────────────
let resultsContainer = null;
let paginationNav = null;
let topBar = null;
function findPageElements() {
const sections = document.querySelectorAll('main section');
if (sections.length < 2) return false;
const resultsSection = sections[1];
topBar = resultsSection.children[0];
resultsContainer = resultsSection.querySelector(':scope > div.relative');
paginationNav = resultsSection.querySelector(':scope > nav');
return !!(resultsContainer);
}
// ── Gather filter values ──────────────────────────────────────────────
function gatherFilters() {
const presetKeywords = [];
const presetKeywordSets = []; // for AND mode: array of arrays
for (const idx of activePresets) {
presetKeywords.push(...PRESETS[idx].keywords);
presetKeywordSets.push(PRESETS[idx].keywords);
}
const excludeRaw = document.getElementById('hf-af-exclude')?.value?.trim() || '';
const excludeKeywords = excludeRaw ? excludeRaw.split(/[,\s]+/).filter(Boolean) : [];
return {
dateField: document.getElementById('hf-af-date-field').value,
dateRange: document.getElementById('hf-af-date-range').value,
minDl: document.getElementById('hf-af-min-dl').value,
maxDl: document.getElementById('hf-af-max-dl').value,
minLikes: document.getElementById('hf-af-min-likes').value,
maxLikes: document.getElementById('hf-af-max-likes').value,
minParams: document.getElementById('hf-af-min-params').value,
minParamsU: document.getElementById('hf-af-min-params-unit').value,
maxParams: document.getElementById('hf-af-max-params').value,
maxParamsU: document.getElementById('hf-af-max-params-unit').value,
sort: document.getElementById('hf-af-sort').value,
search: document.getElementById('hf-af-search').value.trim(),
excludeKeywords,
presetKeywords,
presetKeywordSets,
presetMode,
presetLabels: [...activePresets].map(i => PRESETS[i].label),
};
}
// ── Client-side filter predicates ──────────────────────────────────────
function passesBase(m, f) {
if (f.dateRange) {
const thr = getDateThreshold(f.dateRange);
if (thr) {
const d = new Date(m[f.dateField] || m.lastModified || m.createdAt).getTime();
if (d < thr) return false;
}
}
const dl = m.downloads || 0;
if (f.minDl && dl < +f.minDl) return false;
if (f.maxDl && dl > +f.maxDl) return false;
const lk = m.likes || 0;
if (f.minLikes && lk < +f.minLikes) return false;
if (f.maxLikes && lk > +f.maxLikes) return false;
if (f.minParams || f.maxParams) {
const pc = extractParamCount(m);
if (pc === null) return false;
if (f.minParams) { const v = parseParamVal(f.minParams, f.minParamsU); if (v && pc < v) return false; }
if (f.maxParams) { const v = parseParamVal(f.maxParams, f.maxParamsU); if (v && pc > v) return false; }
}
// Exclude keywords
if (!modelMatchesExclude(m, f.excludeKeywords)) return false;
return true;
}
function passesPresetKeywords(m, f) {
if (!f.presetKeywords.length) return true;
if (f.presetMode === 'and' && f.presetKeywordSets.length > 1) {
// AND mode: must match at least one keyword from EACH active preset
return f.presetKeywordSets.every(kwSet => modelMatchesKeywords(m, kwSet));
}
// OR mode: match any keyword from any active preset
return modelMatchesKeywords(m, f.presetKeywords);
}
function passes(m, f) {
if (!passesBase(m, f)) return false;
if (!passesPresetKeywords(m, f)) return false;
return true;
}
function sortResults(arr, sortKey) {
if (sortKey === 'params') {
arr.sort((a, b) => (extractParamCount(b) || 0) - (extractParamCount(a) || 0));
return;
}
const key = sortKey === 'trending' ? 'likes' : sortKey;
if (key === 'downloads' || key === 'likes') {
arr.sort((a, b) => (b[key] || 0) - (a[key] || 0));
} else if (key === 'lastModified' || key === 'createdAt') {
arr.sort((a, b) => new Date(b[key] || 0) - new Date(a[key] || 0));
}
}
// ── URL persistence ───────────────────────────────────────────────────
function encodeFiltersToHash(filters) {
const p = new URLSearchParams();
if (filters.dateField !== 'lastModified') p.set('df', filters.dateField);
if (filters.dateRange) p.set('dr', filters.dateRange);
if (filters.minDl) p.set('minDl', filters.minDl);
if (filters.maxDl) p.set('maxDl', filters.maxDl);
if (filters.minLikes) p.set('minLk', filters.minLikes);
if (filters.maxLikes) p.set('maxLk', filters.maxLikes);
if (filters.minParams) p.set('minP', filters.minParams + filters.minParamsU);
if (filters.maxParams) p.set('maxP', filters.maxParams + filters.maxParamsU);
if (filters.sort !== 'downloads') p.set('sort', filters.sort);
if (filters.search) p.set('q', filters.search);
if (activePresets.size) p.set('presets', [...activePresets].join('.'));
if (presetMode === 'and') p.set('mode', 'and');
if (filters.excludeKeywords.length) p.set('ex', filters.excludeKeywords.join(','));
const str = p.toString();
if (str) {
history.replaceState(null, '', window.location.pathname + window.location.search + '#hf-af=' + str);
}
}
function decodeFiltersFromHash() {
const hash = window.location.hash;
if (!hash.startsWith('#hf-af=')) return null;
const p = new URLSearchParams(hash.slice(7));
const f = {};
f.dateField = p.get('df') || 'lastModified';
f.dateRange = p.get('dr') || '';
f.minDl = p.get('minDl') || '';
f.maxDl = p.get('maxDl') || '';
f.minLikes = p.get('minLk') || '';
f.maxLikes = p.get('maxLk') || '';
f.sort = p.get('sort') || 'downloads';
f.search = p.get('q') || '';
f.mode = p.get('mode') || 'or';
f.exclude = p.get('ex') || '';
// Parse param values
const minP = p.get('minP');
if (minP) {
const m = minP.match(/^([\d.]+)([BMT])$/i);
if (m) { f.minParams = m[1]; f.minParamsU = m[2].toUpperCase(); }
}
const maxP = p.get('maxP');
if (maxP) {
const m = maxP.match(/^([\d.]+)([BMT])$/i);
if (m) { f.maxParams = m[1]; f.maxParamsU = m[2].toUpperCase(); }
}
const presets = p.get('presets');
if (presets) f.activePresets = presets.split('.').map(Number);
return f;
}
function applyDecodedFilters(f) {
if (!f) return;
document.getElementById('hf-af-date-field').value = f.dateField;
document.getElementById('hf-af-date-range').value = f.dateRange;
document.getElementById('hf-af-min-dl').value = f.minDl;
document.getElementById('hf-af-max-dl').value = f.maxDl;
document.getElementById('hf-af-min-likes').value = f.minLikes;
document.getElementById('hf-af-max-likes').value = f.maxLikes;
document.getElementById('hf-af-sort').value = f.sort;
document.getElementById('hf-af-search').value = f.search;
document.getElementById('hf-af-exclude').value = f.exclude || '';
if (f.minParams) { document.getElementById('hf-af-min-params').value = f.minParams; document.getElementById('hf-af-min-params-unit').value = f.minParamsU || 'B'; }
if (f.maxParams) { document.getElementById('hf-af-max-params').value = f.maxParams; document.getElementById('hf-af-max-params-unit').value = f.maxParamsU || 'B'; }
if (f.mode === 'and') {
presetMode = 'and';
const toggle = document.getElementById('hf-af-mode-toggle');
if (toggle) { toggle.textContent = 'AND'; toggle.classList.add('mode-and'); }
}
activePresets.clear();
if (f.activePresets) {
for (const idx of f.activePresets) {
activePresets.add(idx);
const chip = document.querySelector(`.hf-af-preset-chip[data-idx="${idx}"]`);
if (chip) chip.classList.add('active');
}
}
}
// ── Saved profiles (GM storage) ────────────────────────────────────────
function getProfiles() {
try { return JSON.parse(GM_getValue('hf_af_profiles', '{}')); }
catch { return {}; }
}
function saveProfile(name) {
const profiles = getProfiles();
profiles[name] = {
dateField: document.getElementById('hf-af-date-field').value,
dateRange: document.getElementById('hf-af-date-range').value,
minDl: document.getElementById('hf-af-min-dl').value,
maxDl: document.getElementById('hf-af-max-dl').value,
minLikes: document.getElementById('hf-af-min-likes').value,
maxLikes: document.getElementById('hf-af-max-likes').value,
minParams: document.getElementById('hf-af-min-params').value,
minParamsU: document.getElementById('hf-af-min-params-unit').value,
maxParams: document.getElementById('hf-af-max-params').value,
maxParamsU: document.getElementById('hf-af-max-params-unit').value,
sort: document.getElementById('hf-af-sort').value,
search: document.getElementById('hf-af-search').value,
exclude: document.getElementById('hf-af-exclude').value,
activePresets: [...activePresets],
mode: presetMode,
};
GM_setValue('hf_af_profiles', JSON.stringify(profiles));
refreshProfileDropdown();
showToast(`Profile "${name}" saved`);
}
function loadProfile(name) {
const profiles = getProfiles();
const p = profiles[name];
if (!p) return;
document.getElementById('hf-af-date-field').value = p.dateField || 'lastModified';
document.getElementById('hf-af-date-range').value = p.dateRange || '';
document.getElementById('hf-af-min-dl').value = p.minDl || '';
document.getElementById('hf-af-max-dl').value = p.maxDl || '';
document.getElementById('hf-af-min-likes').value = p.minLikes || '';
document.getElementById('hf-af-max-likes').value = p.maxLikes || '';
document.getElementById('hf-af-min-params').value = p.minParams || '';
document.getElementById('hf-af-min-params-unit').value = p.minParamsU || 'B';
document.getElementById('hf-af-max-params').value = p.maxParams || '';
document.getElementById('hf-af-max-params-unit').value = p.maxParamsU || 'B';
document.getElementById('hf-af-sort').value = p.sort || 'downloads';
document.getElementById('hf-af-search').value = p.search || '';
document.getElementById('hf-af-exclude').value = p.exclude || '';
presetMode = p.mode || 'or';
const toggle = document.getElementById('hf-af-mode-toggle');
if (toggle) {
toggle.textContent = presetMode.toUpperCase();
toggle.classList.toggle('mode-and', presetMode === 'and');
}
activePresets.clear();
document.querySelectorAll('.hf-af-preset-chip').forEach(c => c.classList.remove('active'));
for (const idx of (p.activePresets || [])) {
activePresets.add(idx);
const chip = document.querySelector(`.hf-af-preset-chip[data-idx="${idx}"]`);
if (chip) chip.classList.add('active');
}
showToast(`Loaded "${name}"`);
}
function deleteProfile(name) {
const profiles = getProfiles();
delete profiles[name];
GM_setValue('hf_af_profiles', JSON.stringify(profiles));
refreshProfileDropdown();
showToast(`Deleted "${name}"`);
}
function refreshProfileDropdown() {
const sel = document.getElementById('hf-af-profile-select');
if (!sel) return;
const profiles = getProfiles();
const names = Object.keys(profiles);
sel.innerHTML = '<option value="">Profiles…</option>' +
names.map(n => `<option value="${n}">${n}</option>`).join('');
}
// ── Search ─────────────────────────────────────────────────────────────
async function runSearch(filters, fresh) {
if (fresh) { currentOffset = 0; allResults = []; seenIds = new Set(); kwOffsets = {}; presetResultCounts = {}; }
const target = allResults.length + 50;
const btn = document.getElementById('hf-af-apply');
if (btn) { btn.disabled = true; btn.textContent = 'Searching…'; }
try {
if (filters.presetKeywords.length > 0) {
const keywords = [...new Set(filters.presetKeywords)];
let totalScanned = 0;
let anyProgress = true;
while (allResults.length < target && totalScanned < MAX_SCAN_TOTAL && anyProgress) {
anyProgress = false;
for (const kw of keywords) {
if (allResults.length >= target) break;
if (kwOffsets[kw] === -1) continue;
if (!kwOffsets[kw]) kwOffsets[kw] = 0;
const searchTerm = filters.search ? filters.search + ' ' + kw : kw;
const batch = await fetchModels({ sort: filters.sort === 'params' ? 'downloads' : filters.sort, offset: kwOffsets[kw], search: searchTerm });
if (!batch.length) { kwOffsets[kw] = -1; continue; }
anyProgress = true;
for (const m of batch) {
if (seenIds.has(m.id)) continue;
seenIds.add(m.id);
if (passesBase(m, filters) && passesPresetKeywords(m, filters)) {
allResults.push(m);
// Track which presets this model matched
for (const [i, preset] of PRESETS.entries()) {
if (activePresets.has(i) && modelMatchesKeywords(m, preset.keywords)) {
presetResultCounts[i] = (presetResultCounts[i] || 0) + 1;
}
}
}
}
kwOffsets[kw] += batch.length;
totalScanned += batch.length;
if (batch.length < BATCH) kwOffsets[kw] = -1;
if (kwOffsets[kw] >= MAX_SCAN_PER_KW) kwOffsets[kw] = -1;
}
}
sortResults(allResults, filters.sort);
const exhausted = Object.values(kwOffsets).every(v => v === -1) || totalScanned >= MAX_SCAN_TOTAL;
currentOffset = totalScanned;
encodeFiltersToHash(filters);
updatePresetBadges();
renderResults(allResults, filters, exhausted, totalScanned);
} else {
let scanned = 0;
let exhausted = false;
while (allResults.length < target && scanned < MAX_SCAN) {
const batch = await fetchModels({ sort: filters.sort === 'params' ? 'downloads' : filters.sort, offset: currentOffset, search: filters.search });
if (!batch.length) { exhausted = true; break; }
for (const m of batch) {
if (seenIds.has(m.id)) continue;
seenIds.add(m.id);
if (passes(m, filters)) allResults.push(m);
}
currentOffset += batch.length;
scanned += batch.length;
if (batch.length < BATCH) { exhausted = true; break; }
}
if (scanned >= MAX_SCAN) exhausted = true;
sortResults(allResults, filters.sort);
encodeFiltersToHash(filters);
renderResults(allResults, filters, exhausted, currentOffset);
}
} catch (err) {
console.error('[HF-AF]', err);
showInlineError(err.message);
} finally {
if (btn) { btn.disabled = false; btn.textContent = 'Search'; }
}
}
// ── Preset badges ─────────────────────────────────────────────────────
function updatePresetBadges() {
for (const idx of activePresets) {
const chip = document.querySelector(`.hf-af-preset-chip[data-idx="${idx}"]`);
if (!chip) continue;
let badge = chip.querySelector('.hf-af-preset-badge');
const count = presetResultCounts[idx] || 0;
if (count > 0) {
if (!badge) {
badge = document.createElement('span');
badge.className = 'hf-af-preset-badge';
chip.appendChild(badge);
}
badge.textContent = count;
} else if (badge) {
badge.remove();
}
}
}
function clearPresetBadges() {
document.querySelectorAll('.hf-af-preset-badge').forEach(b => b.remove());
}
// ── Similar models ────────────────────────────────────────────────────
function searchSimilar(model) {
const pc = extractParamCount(model);
if (pc) {
// Set param range to ±50%
const minP = pc * 0.5;
const maxP = pc * 1.5;
let minVal, minUnit, maxVal, maxUnit;
if (minP >= 1e9) { minVal = (minP / 1e9).toFixed(1); minUnit = 'B'; }
else { minVal = (minP / 1e6).toFixed(0); minUnit = 'M'; }
if (maxP >= 1e9) { maxVal = (maxP / 1e9).toFixed(1); maxUnit = 'B'; }
else { maxVal = (maxP / 1e6).toFixed(0); maxUnit = 'M'; }
document.getElementById('hf-af-min-params').value = minVal;
document.getElementById('hf-af-min-params-unit').value = minUnit;
document.getElementById('hf-af-max-params').value = maxVal;
document.getElementById('hf-af-max-params-unit').value = maxUnit;
}
if (model.pipeline_tag) {
document.getElementById('hf-af-search').value = model.pipeline_tag;
}
// Keep current date range and presets, trigger search
document.getElementById('hf-af-apply').click();
}
// ── Tag click → filter ────────────────────────────────────────────────
function filterByTag(tagText, isExclude) {
if (isExclude) {
const exInput = document.getElementById('hf-af-exclude');
const current = exInput.value.trim();
if (current.toLowerCase().includes(tagText.toLowerCase())) return;
exInput.value = current ? current + ', ' + tagText : tagText;
} else {
document.getElementById('hf-af-search').value = tagText;
}
document.getElementById('hf-af-apply').click();
}
// ── Inline results rendering ──────────────────────────────────────────
function stashOriginals() {
if (originalResultsHTML === null && resultsContainer) {
originalResultsHTML = resultsContainer.innerHTML;
}
if (originalPaginationHTML === null && paginationNav) {
originalPaginationHTML = paginationNav.outerHTML;
}
}
function restoreOriginals() {
if (originalResultsHTML !== null && resultsContainer) {
resultsContainer.innerHTML = originalResultsHTML;
originalResultsHTML = null;
}
if (paginationNav) paginationNav.style.display = '';
document.getElementById('hf-af-results-bar')?.remove();
clearPresetBadges();
// Clear hash
if (window.location.hash.startsWith('#hf-af=')) {
history.replaceState(null, '', window.location.pathname + window.location.search);
}
}
function showInlineError(msg) {
stashOriginals();
if (paginationNav) paginationNav.style.display = 'none';
resultsContainer.innerHTML = `
<div id="hf-af-results-bar">
<span style="color:#f85149">Error: ${msg}</span>
<button class="hf-af-close-results" id="hf-af-close-btn">✕ Clear</button>
</div>`;
document.getElementById('hf-af-close-btn').addEventListener('click', restoreOriginals);
}
function renderResults(models, filters, exhausted, totalScanned) {
stashOriginals();
if (paginationNav) paginationNav.style.display = 'none';
// Build filter summary
const parts = [];
if (filters.dateRange) {
const lb = { '1w': 'last week', '2w': 'last 2 weeks', '1m': 'last month', '3m': 'last 3 months', '6m': 'last 6 months', '1y': 'last year' };
parts.push(`${filters.dateField === 'createdAt' ? 'Created' : 'Updated'} ${lb[filters.dateRange]}`);
}
if (filters.minDl) parts.push(`≥${fmt(+filters.minDl)} dl`);
if (filters.maxDl) parts.push(`≤${fmt(+filters.maxDl)} dl`);
if (filters.minLikes) parts.push(`≥${filters.minLikes} ♥`);
if (filters.minParams) parts.push(`≥${filters.minParams}${filters.minParamsU}`);
if (filters.maxParams) parts.push(`≤${filters.maxParams}${filters.maxParamsU}`);
if (filters.presetLabels.length) {
const joiner = filters.presetMode === 'and' ? ' & ' : ' + ';
parts.push(filters.presetLabels.join(joiner));
}
if (filters.excludeKeywords.length) parts.push(`-${filters.excludeKeywords.join(',-')}`);
if (filters.search) parts.push(`"${filters.search}"`);
const sortLb = { downloads: 'downloads', likes: 'likes', trending: 'trending', lastModified: 'recent update', createdAt: 'created', params: 'param count' };
// Results bar
let barEl = document.getElementById('hf-af-results-bar');
if (!barEl) {
barEl = document.createElement('div');
barEl.id = 'hf-af-results-bar';
resultsContainer.parentElement.insertBefore(barEl, resultsContainer);
}
barEl.innerHTML = `
<div>
<strong>${models.length}</strong> found
${parts.length ? ' · ' + parts.join(' · ') : ''}
· by ${sortLb[filters.sort] || filters.sort}
<span style="color:var(--text-secondary,#484f58);margin-left:6px">(scanned ${fmt(totalScanned)})</span>
</div>
<div style="display:flex;align-items:center;gap:6px">
${!exhausted ? '<button class="hf-af-load-more" id="hf-af-more-btn">Load more</button>' : '<span style="color:var(--text-secondary,#484f58);font-size:0.7rem">All loaded</span>'}
<button class="hf-af-close-results" id="hf-af-close-btn">✕ Clear</button>
</div>`;
// Build model cards
let html = '';
for (let i = 0; i < models.length; i++) {
const m = models[i];
const pc = extractParamCount(m);
const ps = pc ? fmtParams(pc) : null;
const vram = estimateVRAM(pc);
const tags = m.tags || [];
const detailId = `hf-af-detail-${i}`;
html += `<div class="hf-af-result-card" data-idx="${i}">
<div class="hf-af-card-header">
<a href="/${m.id}" class="hf-af-card-title">#${i + 1} · ${m.id}</a>
<div class="hf-af-card-actions">
<button class="hf-af-icon-btn hf-af-copy-btn" data-id="${m.id}" title="Copy model ID">📋</button>
<button class="hf-af-icon-btn hf-af-similar-btn" data-idx="${i}" title="Find similar">🔍</button>
<button class="hf-af-icon-btn hf-af-expand-toggle" data-detail="${detailId}" title="Show details"><span class="hf-af-expand-btn">▸</span></button>
</div>
</div>
<div class="hf-af-card-meta">
${m.pipeline_tag ? `<span class="hf-af-tag" data-tag="${m.pipeline_tag}">${m.pipeline_tag}</span>` : ''}
${ps ? `<span>📐 ${ps}</span>` : ''}
${vram ? `<span class="hf-af-vram" title="FP16: ~${fmtVRAM(vram.fp16)} / Q4: ~${fmtVRAM(vram.q4)}">💾 Q4:${fmtVRAM(vram.q4)} FP16:${fmtVRAM(vram.fp16)}</span>` : ''}
<span>⬇ ${fmt(m.downloads || 0)}</span>
<span>♥ ${fmt(m.likes || 0)}</span>
${m.createdAt ? `<span>🆕 ${timeAgo(m.createdAt)}</span>` : ''}
${m.lastModified ? `<span>✏️ ${timeAgo(m.lastModified)}</span>` : ''}
${m.library_name ? `<span class="hf-af-tag" data-tag="${m.library_name}">${m.library_name}</span>` : ''}
</div>
<div class="hf-af-card-details" id="${detailId}" style="display:none">
<div class="hf-af-detail-row">
<span class="hf-af-detail-label">Tags:</span>
${tags.length ? tags.map(t => `<span class="hf-af-tag" data-tag="${t}">${t}</span>`).join(' ') : '<span style="color:#484f58">none</span>'}
</div>
${m.author ? `<div class="hf-af-detail-row"><span class="hf-af-detail-label">Author:</span> <a href="/${m.author}" style="color:#58a6ff;text-decoration:none">${m.author}</a></div>` : ''}
${pc ? `<div class="hf-af-detail-row"><span class="hf-af-detail-label">Params:</span> ${pc.toLocaleString()}</div>` : ''}
${vram ? `<div class="hf-af-detail-row"><span class="hf-af-detail-label">VRAM:</span> FP32: ~${fmtVRAM(vram.fp16 * 2)} | FP16: ~${fmtVRAM(vram.fp16)} | Q8: ~${fmtVRAM(vram.q4 * 2)} | Q4: ~${fmtVRAM(vram.q4)}</div>` : ''}
${m.sha ? `<div class="hf-af-detail-row"><span class="hf-af-detail-label">SHA:</span> <span style="font-family:monospace;font-size:0.65rem">${m.sha.slice(0, 12)}</span></div>` : ''}
</div>
</div>`;
}
if (!models.length) {
html = `<div class="hf-af-empty">
<p>No models match your filters</p>
<p>Try broadening your time range or lowering the minimum thresholds.</p>
</div>`;
}
resultsContainer.innerHTML = html;
// Wire up buttons
document.getElementById('hf-af-close-btn').addEventListener('click', restoreOriginals);
const moreBtn = document.getElementById('hf-af-more-btn');
if (moreBtn) {
moreBtn.addEventListener('click', () => {
moreBtn.disabled = true;
moreBtn.textContent = 'Loading…';
runSearch(currentFilters, false);
});
}
// Copy model ID buttons
resultsContainer.querySelectorAll('.hf-af-copy-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
navigator.clipboard.writeText(btn.dataset.id).then(() => {
btn.textContent = '✓';
setTimeout(() => btn.textContent = '📋', 1500);
});
});
});
// Similar model buttons
resultsContainer.querySelectorAll('.hf-af-similar-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const idx = +btn.dataset.idx;
searchSimilar(allResults[idx]);
});
});
// Expand/collapse detail buttons
resultsContainer.querySelectorAll('.hf-af-expand-toggle').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const detailEl = document.getElementById(btn.dataset.detail);
const arrow = btn.querySelector('.hf-af-expand-btn');
if (detailEl.style.display === 'none') {
detailEl.style.display = 'block';
arrow.style.transform = 'rotate(90deg)';
} else {
detailEl.style.display = 'none';
arrow.style.transform = '';
}
});
});
// Clickable tags (left-click = filter, right-click = exclude)
resultsContainer.querySelectorAll('.hf-af-tag[data-tag]').forEach(tag => {
tag.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
filterByTag(tag.dataset.tag, false);
});
tag.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
filterByTag(tag.dataset.tag, true);
});
});
}
// ── Build sidebar section ──────────────────────────────────────────────
function createSidebarSection() {
const el = document.createElement('div');
el.id = 'hf-af-sidebar';
const presetChips = PRESETS.map((p, i) =>
`<span class="hf-af-preset-chip" data-idx="${i}">${p.emoji || ''} ${p.label}</span>`
).join('');
el.innerHTML = `
<div class="hf-af-header">
<h3>⚡ Advanced Filters <span class="hf-af-kbd">Ctrl+Shift+F</span></h3>
<span class="hf-af-toggle">▾</span>
</div>
<div class="hf-af-body">
<div class="hf-af-group">
<div class="hf-af-label-row">
<label>Quick filters</label>
<span class="hf-af-mode-toggle" id="hf-af-mode-toggle" title="Toggle AND/OR matching for presets">OR</span>
</div>
<div class="hf-af-presets">${presetChips}</div>
</div>
<div class="hf-af-group">
<label>Time range</label>
<div class="hf-af-row">
<select id="hf-af-date-field" class="hf-af-select">
<option value="lastModified">Updated within</option>
<option value="createdAt">Created within</option>
</select>
<select id="hf-af-date-range" class="hf-af-select">
<option value="">Any time</option>
<option value="1w">Last week</option>
<option value="2w">Last 2 weeks</option>
<option value="1m">Last month</option>
<option value="3m">Last 3 months</option>
<option value="6m">Last 6 months</option>
<option value="1y">Last year</option>
</select>
</div>
</div>
<div class="hf-af-group">
<label>Downloads</label>
<div class="hf-af-dl-chips">
<span class="hf-af-dl-chip" data-val="1000">1k+</span>
<span class="hf-af-dl-chip" data-val="10000">10k+</span>
<span class="hf-af-dl-chip" data-val="100000">100k+</span>
<span class="hf-af-dl-chip" data-val="1000000">1M+</span>
</div>
<div class="hf-af-row">
<input type="number" id="hf-af-min-dl" class="hf-af-input" placeholder="Min" min="0">
<input type="number" id="hf-af-max-dl" class="hf-af-input" placeholder="Max" min="0">
</div>
</div>
<div class="hf-af-group">
<label>Likes</label>
<div class="hf-af-row">
<input type="number" id="hf-af-min-likes" class="hf-af-input" placeholder="Min" min="0">
<input type="number" id="hf-af-max-likes" class="hf-af-input" placeholder="Max" min="0">
</div>
</div>
<div class="hf-af-group">
<label>Parameter size</label>
<div class="hf-af-row">
<input type="number" id="hf-af-min-params" class="hf-af-input" placeholder="Min" min="0" step="0.1">
<select id="hf-af-min-params-unit" class="hf-af-select" style="flex:0.4">
<option value="B">B</option><option value="M">M</option><option value="T">T</option>
</select>
<span style="color:var(--text-secondary,#484f58)">–</span>
<input type="number" id="hf-af-max-params" class="hf-af-input" placeholder="Max" min="0" step="0.1">
<select id="hf-af-max-params-unit" class="hf-af-select" style="flex:0.4">
<option value="B">B</option><option value="M">M</option><option value="T">T</option>
</select>
</div>
</div>
<div class="hf-af-group">
<label>Sort by</label>
<select id="hf-af-sort" class="hf-af-select">
<option value="downloads">Most downloads</option>
<option value="likes">Most likes</option>
<option value="trending">Trending</option>
<option value="lastModified">Recently updated</option>
<option value="createdAt">Recently created</option>
<option value="params">Parameter count ↓</option>
</select>
</div>
<div class="hf-af-group">
<label>Search</label>
<input type="text" id="hf-af-search" class="hf-af-input" placeholder="e.g. Qwen, Llama, code…">
</div>
<div class="hf-af-group">
<label>Exclude keywords</label>
<input type="text" id="hf-af-exclude" class="hf-af-input" placeholder="e.g. gguf, quantized, awq">
</div>
<div class="hf-af-group">
<label>Profiles</label>
<div class="hf-af-profiles-row">
<select id="hf-af-profile-select" class="hf-af-select">
<option value="">Profiles…</option>
</select>
<button class="hf-af-profile-btn" id="hf-af-profile-load">Load</button>
<button class="hf-af-profile-btn" id="hf-af-profile-save">Save</button>
<button class="hf-af-profile-btn" id="hf-af-profile-del">✕</button>
</div>
</div>
</div>
<div class="hf-af-actions">
<button class="hf-af-btn hf-af-btn-primary" id="hf-af-apply">Search</button>
<button class="hf-af-btn hf-af-btn-secondary" id="hf-af-reset">Reset</button>
</div>
`;
// ── Event handlers ─────────────────────────────────────────────────
// Preset chip toggling
el.querySelectorAll('.hf-af-preset-chip').forEach(chip => {
chip.addEventListener('click', () => {
const idx = +chip.dataset.idx;
if (activePresets.has(idx)) { activePresets.delete(idx); chip.classList.remove('active'); }
else { activePresets.add(idx); chip.classList.add('active'); }
// Remove badge when toggling
const badge = chip.querySelector('.hf-af-preset-badge');
if (badge) badge.remove();
});
});
// AND/OR mode toggle
el.querySelector('#hf-af-mode-toggle').addEventListener('click', (e) => {
e.stopPropagation();
presetMode = presetMode === 'or' ? 'and' : 'or';
const toggle = el.querySelector('#hf-af-mode-toggle');
toggle.textContent = presetMode.toUpperCase();
toggle.classList.toggle('mode-and', presetMode === 'and');
});
// Download quick chips
el.querySelectorAll('.hf-af-dl-chip').forEach(chip => {
chip.addEventListener('click', () => {
document.getElementById('hf-af-min-dl').value = chip.dataset.val;
});
});
// Collapse toggle
el.querySelector('.hf-af-header').addEventListener('click', () => {
el.classList.toggle('collapsed');
});
// Search
el.querySelector('#hf-af-apply').addEventListener('click', () => {
if (!findPageElements()) return;
currentFilters = gatherFilters();
stashOriginals();
let barEl = document.getElementById('hf-af-results-bar');
if (!barEl) {
barEl = document.createElement('div');
barEl.id = 'hf-af-results-bar';
resultsContainer.parentElement.insertBefore(barEl, resultsContainer);
}
barEl.innerHTML = `<span><span class="hf-af-spinner"></span> Searching models…</span>
<button class="hf-af-close-results" id="hf-af-close-btn">✕ Clear</button>`;
document.getElementById('hf-af-close-btn').addEventListener('click', restoreOriginals);
resultsContainer.innerHTML = '';
if (paginationNav) paginationNav.style.display = 'none';
runSearch(currentFilters, true);
});
// Reset
el.querySelector('#hf-af-reset').addEventListener('click', () => {
document.getElementById('hf-af-date-field').value = 'lastModified';
document.getElementById('hf-af-date-range').value = '';
document.getElementById('hf-af-min-dl').value = '';
document.getElementById('hf-af-max-dl').value = '';
document.getElementById('hf-af-min-likes').value = '';
document.getElementById('hf-af-max-likes').value = '';
document.getElementById('hf-af-min-params').value = '';
document.getElementById('hf-af-max-params').value = '';
document.getElementById('hf-af-min-params-unit').value = 'B';
document.getElementById('hf-af-max-params-unit').value = 'B';
document.getElementById('hf-af-sort').value = 'downloads';
document.getElementById('hf-af-search').value = '';
document.getElementById('hf-af-exclude').value = '';
activePresets.clear();
presetMode = 'or';
const toggle = el.querySelector('#hf-af-mode-toggle');
toggle.textContent = 'OR';
toggle.classList.remove('mode-and');
el.querySelectorAll('.hf-af-preset-chip.active').forEach(c => c.classList.remove('active'));
clearPresetBadges();
restoreOriginals();
});
// Enter key in search/exclude fields
['hf-af-search', 'hf-af-exclude'].forEach(id => {
el.querySelector('#' + id)?.addEventListener('keydown', e => {
if (e.key === 'Enter') el.querySelector('#hf-af-apply').click();
});
});
// Profile buttons
el.querySelector('#hf-af-profile-save').addEventListener('click', () => {
const name = prompt('Profile name:');
if (name?.trim()) saveProfile(name.trim());
});
el.querySelector('#hf-af-profile-load').addEventListener('click', () => {
const sel = document.getElementById('hf-af-profile-select');
if (sel.value) loadProfile(sel.value);
});
el.querySelector('#hf-af-profile-del').addEventListener('click', () => {
const sel = document.getElementById('hf-af-profile-select');
if (sel.value && confirm(`Delete profile "${sel.value}"?`)) deleteProfile(sel.value);
});
return el;
}
// ── Init ────────────────────────────────────────────────────────────────
let savedFilterState = null;
function saveFilterState() {
const sidebar = document.getElementById('hf-af-sidebar');
if (!sidebar) return;
savedFilterState = {
dateField: document.getElementById('hf-af-date-field')?.value,
dateRange: document.getElementById('hf-af-date-range')?.value,
minDl: document.getElementById('hf-af-min-dl')?.value,
maxDl: document.getElementById('hf-af-max-dl')?.value,
minLikes: document.getElementById('hf-af-min-likes')?.value,
maxLikes: document.getElementById('hf-af-max-likes')?.value,
minParams: document.getElementById('hf-af-min-params')?.value,
minParamsU: document.getElementById('hf-af-min-params-unit')?.value,
maxParams: document.getElementById('hf-af-max-params')?.value,
maxParamsU: document.getElementById('hf-af-max-params-unit')?.value,
sort: document.getElementById('hf-af-sort')?.value,
search: document.getElementById('hf-af-search')?.value,
exclude: document.getElementById('hf-af-exclude')?.value,
activePresets: [...activePresets],
collapsed: sidebar.classList.contains('collapsed'),
mode: presetMode,
};
}
function restoreFilterState() {
if (!savedFilterState) return;
const s = savedFilterState;
if (s.dateField) document.getElementById('hf-af-date-field').value = s.dateField;
if (s.dateRange) document.getElementById('hf-af-date-range').value = s.dateRange;
if (s.minDl) document.getElementById('hf-af-min-dl').value = s.minDl;
if (s.maxDl) document.getElementById('hf-af-max-dl').value = s.maxDl;
if (s.minLikes) document.getElementById('hf-af-min-likes').value = s.minLikes;
if (s.maxLikes) document.getElementById('hf-af-max-likes').value = s.maxLikes;
if (s.minParams) document.getElementById('hf-af-min-params').value = s.minParams;
if (s.minParamsU) document.getElementById('hf-af-min-params-unit').value = s.minParamsU;
if (s.maxParams) document.getElementById('hf-af-max-params').value = s.maxParams;
if (s.maxParamsU) document.getElementById('hf-af-max-params-unit').value = s.maxParamsU;
if (s.sort) document.getElementById('hf-af-sort').value = s.sort;
if (s.search) document.getElementById('hf-af-search').value = s.search;
if (s.exclude) document.getElementById('hf-af-exclude').value = s.exclude;
presetMode = s.mode || 'or';
const toggle = document.getElementById('hf-af-mode-toggle');
if (toggle) {
toggle.textContent = presetMode.toUpperCase();
toggle.classList.toggle('mode-and', presetMode === 'and');
}
activePresets.clear();
for (const idx of (s.activePresets || [])) {
activePresets.add(idx);
const chip = document.querySelector(`.hf-af-preset-chip[data-idx="${idx}"]`);
if (chip) chip.classList.add('active');
}
if (s.collapsed) document.getElementById('hf-af-sidebar')?.classList.add('collapsed');
}
function inject() {
if (document.getElementById('hf-af-sidebar')) return;
const sections = document.querySelectorAll('main section');
if (sections.length < 2) return;
const sidebar = sections[0];
const filterContainer = sidebar.querySelector('div.mb-20');
if (!filterContainer) return;
const sidebarSection = createSidebarSection();
filterContainer.insertBefore(sidebarSection, filterContainer.firstChild);
// Restore filter state or apply URL hash
restoreFilterState();
refreshProfileDropdown();
// If we have URL hash filters and no saved state, apply them
if (!savedFilterState) {
const hashFilters = decodeFiltersFromHash();
if (hashFilters) {
applyDecodedFilters(hashFilters);
// Auto-search if hash had filters
findPageElements();
setTimeout(() => document.getElementById('hf-af-apply')?.click(), 300);
}
}
findPageElements();
}
// MutationObserver for SPA re-renders
let reinjectTimer = null;
function scheduleReinject() {
if (reinjectTimer) return;
reinjectTimer = setTimeout(() => {
reinjectTimer = null;
if (!document.getElementById('hf-af-sidebar')) {
inject();
}
}, 100);
}
const observer = new MutationObserver((mutations) => {
if (!document.getElementById('hf-af-sidebar')) {
scheduleReinject();
return;
}
for (const m of mutations) {
if (m.removedNodes.length) {
for (const n of m.removedNodes) {
if (n.id === 'hf-af-sidebar' || (n.querySelector && n.querySelector('#hf-af-sidebar'))) {
scheduleReinject();
return;
}
}
}
}
});
function init() {
inject();
const main = document.querySelector('main');
if (main) {
observer.observe(main, { childList: true, subtree: true });
}
// SPA navigation hooks
window.addEventListener('popstate', () => setTimeout(inject, 200));
const origPush = history.pushState;
history.pushState = function () {
origPush.apply(this, arguments);
saveFilterState();
setTimeout(inject, 200);
};
const origReplace = history.replaceState;
history.replaceState = function () {
origReplace.apply(this, arguments);
saveFilterState();
setTimeout(inject, 200);
};
// Keyboard shortcut: Ctrl+Shift+F to focus search
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === 'F') {
e.preventDefault();
const sidebar = document.getElementById('hf-af-sidebar');
if (sidebar?.classList.contains('collapsed')) {
sidebar.classList.remove('collapsed');
}
document.getElementById('hf-af-search')?.focus();
}
});
}
// Periodic state save
setInterval(() => {
if (document.getElementById('hf-af-sidebar')) saveFilterState();
}, 2000);
if (document.readyState === 'complete') init();
else window.addEventListener('load', init);
})();