Ehab / HuggingFace Enhanced Search

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