malcolmrey commited on
Commit
2e17c72
·
verified ·
1 Parent(s): bb8d6a1

Upload 2 files

Browse files
Files changed (2) hide show
  1. data-huggingface-loras.js +0 -0
  2. index.html +481 -1
data-huggingface-loras.js ADDED
The diff for this file is too large to render. See raw diff
 
index.html CHANGED
@@ -10,6 +10,7 @@
10
  <script type="text/javascript" src="data-hf-images.js"></script>
11
  <script type="text/javascript" src="data-thumbnails.js"></script>
12
  <script type="text/javascript" src="data-hf-uploaded.js"></script>
 
13
  <script type="text/javascript" src="data-person-classifications.js"></script>
14
  <script type="text/javascript" src="data-person-tags.js"></script>
15
  <script type="text/javascript" src="data-person-urls.js"></script>
@@ -468,6 +469,177 @@
468
  padding: 0 4px;
469
  }
470
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  .element {
472
  background: var(--card-bg);
473
  backdrop-filter: blur(10px);
@@ -1878,6 +2050,32 @@
1878
  color: var(--accent-blue);
1879
  }
1880
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1881
  .modal-mobile-tabs {
1882
  display: none;
1883
  width: 100%;
@@ -1934,13 +2132,16 @@
1934
  <a href="https://huggingface.com/malcolmrey" target="_blank">🤗 <span class="link-text">HuggingFace</span></a>
1935
  <a href="https://buymeacoffee.com/malcolmrey" target="_blank">☕ <span class="link-text">BuyMeACoffee</span></a>
1936
  <a href="https://reddit.com/r/malcolmrey" target="_blank">💬 <span class="link-text">Reddit</span></a>
 
 
 
1937
  <button id="toggleAdvanced" class="mode-select advanced-toggle-btn" onclick="toggleAdvancedOptions()" title="Advanced Options">
1938
  ⚙️
1939
  </button>
1940
  </div>
1941
  </div>
1942
 
1943
- <div class="controls">
1944
  <div class="search-container">
1945
  <input
1946
  id="search"
@@ -2303,6 +2504,67 @@
2303
  <button id="lastPageBtnBottom" class="pagination-btn" onclick="lastPage()">Last ⇥</button>
2304
  </div>
2305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2306
  <!-- Recent Updates Modal -->
2307
  <div id="dailyUploadsModal" class="modal-overlay" onclick="closeDailyUploadsModalOnOverlay(event)">
2308
  <div class="modal-content daily-modal-content" onclick="event.stopPropagation()">
@@ -2493,10 +2755,228 @@
2493
  updateTagsFilterCount();
2494
  }
2495
 
 
 
 
 
 
 
 
 
2496
  // Call on page load
2497
  initTheme();
2498
  initSamplesToggle();
2499
  initTagsFilterGroup();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2500
 
2501
  // Pagination state
2502
  let currentPage = 1;
 
10
  <script type="text/javascript" src="data-hf-images.js"></script>
11
  <script type="text/javascript" src="data-thumbnails.js"></script>
12
  <script type="text/javascript" src="data-hf-uploaded.js"></script>
13
+ <script type="text/javascript" src="data-huggingface-loras.js"></script>
14
  <script type="text/javascript" src="data-person-classifications.js"></script>
15
  <script type="text/javascript" src="data-person-tags.js"></script>
16
  <script type="text/javascript" src="data-person-urls.js"></script>
 
469
  padding: 0 4px;
470
  }
471
 
472
+ .hf-loras-section {
473
+ display: none;
474
+ max-width: 1400px;
475
+ margin: 0 auto 24px;
476
+ padding: 0 4px;
477
+ }
478
+
479
+ .hf-loras-section.active {
480
+ display: block;
481
+ }
482
+
483
+ .hf-loras-section-header {
484
+ display: flex;
485
+ align-items: center;
486
+ gap: 12px;
487
+ margin-bottom: 16px;
488
+ flex-wrap: wrap;
489
+ }
490
+
491
+ .hf-loras-section-header h2 {
492
+ font-size: 18px;
493
+ margin: 0;
494
+ color: var(--text-primary);
495
+ }
496
+
497
+ .hf-loras-summary {
498
+ color: var(--text-secondary);
499
+ font-size: 13px;
500
+ }
501
+
502
+ .hf-loras-search-container {
503
+ position: relative;
504
+ flex: 1 1 220px;
505
+ min-width: 180px;
506
+ max-width: 400px;
507
+ }
508
+
509
+ .hf-loras-search-input {
510
+ width: 100%;
511
+ padding: 10px 90px 10px 14px;
512
+ background: var(--input-bg);
513
+ border: 2px solid var(--input-border);
514
+ border-radius: 10px;
515
+ color: var(--input-text);
516
+ font-size: 14px;
517
+ font-family: inherit;
518
+ transition: all 0.3s;
519
+ }
520
+
521
+ .hf-loras-search-input:focus {
522
+ outline: none;
523
+ border-color: var(--accent-blue);
524
+ box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.1);
525
+ }
526
+
527
+ .hf-loras-repo-link {
528
+ font-size: 20px;
529
+ line-height: 1;
530
+ text-decoration: none;
531
+ opacity: 0.7;
532
+ transition: opacity 0.2s, transform 0.2s;
533
+ }
534
+
535
+ .hf-loras-repo-link:hover {
536
+ opacity: 1;
537
+ transform: scale(1.15);
538
+ }
539
+
540
+ #hfLorasPaginationTop, #hfLorasPaginationBottom {
541
+ justify-content: space-between;
542
+ }
543
+
544
+ .hf-loras-pager-nav {
545
+ display: flex;
546
+ align-items: center;
547
+ gap: 8px;
548
+ flex-wrap: wrap;
549
+ }
550
+
551
+ .hf-loras-page-size-label {
552
+ display: flex;
553
+ align-items: center;
554
+ gap: 8px;
555
+ color: var(--text-muted);
556
+ font-size: 13px;
557
+ }
558
+
559
+ .hf-loras-search-input::placeholder {
560
+ color: var(--text-muted);
561
+ }
562
+
563
+ .hf-loras-grid {
564
+ display: grid;
565
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
566
+ gap: 16px;
567
+ }
568
+
569
+ .hf-lora-card {
570
+ background: var(--card-bg);
571
+ backdrop-filter: blur(10px);
572
+ border-radius: 16px;
573
+ overflow: hidden;
574
+ border: 1px solid var(--card-border);
575
+ box-shadow: 0 4px 16px var(--shadow-color);
576
+ display: flex;
577
+ flex-direction: column;
578
+ }
579
+
580
+ .hf-lora-card-name {
581
+ padding: 10px 12px;
582
+ font-weight: 600;
583
+ font-size: 13px;
584
+ color: var(--input-text);
585
+ border-bottom: 1px solid var(--card-border);
586
+ background: var(--input-bg);
587
+ white-space: nowrap;
588
+ overflow: hidden;
589
+ text-overflow: ellipsis;
590
+ }
591
+
592
+ .hf-lora-card-image {
593
+ position: relative;
594
+ width: 100%;
595
+ padding-top: 100%;
596
+ background: var(--input-bg);
597
+ overflow: hidden;
598
+ }
599
+
600
+ .hf-lora-card-image img {
601
+ position: absolute;
602
+ inset: 0;
603
+ width: 100%;
604
+ height: 100%;
605
+ object-fit: cover;
606
+ }
607
+
608
+ .hf-lora-no-sample {
609
+ position: absolute;
610
+ inset: 0;
611
+ display: flex;
612
+ align-items: center;
613
+ justify-content: center;
614
+ color: var(--text-muted);
615
+ font-size: 12px;
616
+ }
617
+
618
+ .hf-lora-card-footer {
619
+ display: flex;
620
+ align-items: center;
621
+ justify-content: space-between;
622
+ padding: 8px 12px;
623
+ font-size: 11px;
624
+ color: var(--text-secondary);
625
+ gap: 8px;
626
+ }
627
+
628
+ .hf-lora-card-footer a {
629
+ color: var(--link-color);
630
+ text-decoration: none;
631
+ }
632
+
633
+ .hf-lora-card-footer a:hover {
634
+ color: var(--link-hover);
635
+ }
636
+
637
+ .hf-loras-empty {
638
+ color: var(--text-muted);
639
+ font-size: 13px;
640
+ padding: 16px 0;
641
+ }
642
+
643
  .element {
644
  background: var(--card-bg);
645
  backdrop-filter: blur(10px);
 
2050
  color: var(--accent-blue);
2051
  }
2052
 
2053
+ /* Display is controlled inline via JS (hidden until repo data exists), not by this rule. */
2054
+ .hf-loras-toggle-btn {
2055
+ width: 44px;
2056
+ height: 44px; /* Match link height approximately */
2057
+ min-width: 0 !important; /* Override mode-select min-width */
2058
+ padding: 0 !important; /* Override mode-select padding */
2059
+ border-radius: 8px; /* Match links */
2060
+ background: var(--input-bg);
2061
+ border: 1px solid var(--input-border);
2062
+ color: var(--text-secondary);
2063
+ font-size: 20px;
2064
+ cursor: pointer;
2065
+ align-items: center;
2066
+ justify-content: center;
2067
+ transition: all 0.2s;
2068
+ box-shadow: none;
2069
+ appearance: none;
2070
+ background-image: none !important; /* Remove arrow icon from mode-select */
2071
+ }
2072
+
2073
+ .hf-loras-toggle-btn:hover, .hf-loras-toggle-btn.active {
2074
+ background: var(--hover-bg);
2075
+ border-color: var(--accent-blue);
2076
+ color: var(--accent-blue);
2077
+ }
2078
+
2079
  .modal-mobile-tabs {
2080
  display: none;
2081
  width: 100%;
 
2132
  <a href="https://huggingface.com/malcolmrey" target="_blank">🤗 <span class="link-text">HuggingFace</span></a>
2133
  <a href="https://buymeacoffee.com/malcolmrey" target="_blank">☕ <span class="link-text">BuyMeACoffee</span></a>
2134
  <a href="https://reddit.com/r/malcolmrey" target="_blank">💬 <span class="link-text">Reddit</span></a>
2135
+ <button id="hfLorasToggleBtn" class="mode-select hf-loras-toggle-btn" onclick="toggleHfLorasSection()" title="HuggingFace LoRA Repos" style="display: none;">
2136
+ 🤗
2137
+ </button>
2138
  <button id="toggleAdvanced" class="mode-select advanced-toggle-btn" onclick="toggleAdvancedOptions()" title="Advanced Options">
2139
  ⚙️
2140
  </button>
2141
  </div>
2142
  </div>
2143
 
2144
+ <div class="controls" id="regularBrowseControls">
2145
  <div class="search-container">
2146
  <input
2147
  id="search"
 
2504
  <button id="lastPageBtnBottom" class="pagination-btn" onclick="lastPage()">Last ⇥</button>
2505
  </div>
2506
 
2507
+ <!-- Hidden by default (toggled via the 🤗 button next to the profile links above), so it
2508
+ never sits on top of Mal's Models, which stays the default view. -->
2509
+ <div class="hf-loras-section" id="hfLorasSection">
2510
+ <div class="hf-loras-section-header">
2511
+ <h2>🤗 HuggingFace LoRA Repos</h2>
2512
+ <select id="hfLorasRepoSelect" class="mode-select" onchange="onHfLorasRepoChange()"></select>
2513
+ <a
2514
+ id="hfLorasRepoLink"
2515
+ class="hf-loras-repo-link"
2516
+ href="#"
2517
+ target="_blank"
2518
+ rel="noopener"
2519
+ title="View on HuggingFace"
2520
+ style="display: none;"
2521
+ >🔗</a>
2522
+ <div class="hf-loras-search-container">
2523
+ <input
2524
+ id="hfLorasSearch"
2525
+ type="text"
2526
+ class="hf-loras-search-input"
2527
+ placeholder="Search LoRAs..."
2528
+ oninput="onHfLorasSearchInput(this.value)"
2529
+ />
2530
+ <button class="clear-btn" onclick="clearHfLorasSearch()">Clear</button>
2531
+ </div>
2532
+ <span id="hfLorasSummary" class="hf-loras-summary"></span>
2533
+ </div>
2534
+
2535
+ <div id="hfLorasPaginationTop" class="pagination-controls" style="display: none;">
2536
+ <span class="hf-loras-pager-nav">
2537
+ <button class="pagination-btn hf-loras-prev-btn" onclick="hfLorasPreviousPage()">← Previous</button>
2538
+ <span class="pagination-info hf-loras-page-info"></span>
2539
+ <button class="pagination-btn hf-loras-next-btn" onclick="hfLorasNextPage()">Next →</button>
2540
+ </span>
2541
+ <label class="hf-loras-page-size-label">
2542
+ Per page:
2543
+ <select id="hfLorasPageSize" class="mode-select" onchange="onHfLorasPageSizeChange()">
2544
+ <option value="10">10</option>
2545
+ <option value="25">25</option>
2546
+ <option value="30" selected>30</option>
2547
+ <option value="50">50</option>
2548
+ <option value="100">100</option>
2549
+ <option value="250">250</option>
2550
+ <option value="500">500</option>
2551
+ <option value="1000">1000</option>
2552
+ <option value="all">All</option>
2553
+ </select>
2554
+ </label>
2555
+ </div>
2556
+
2557
+ <div id="hfLorasGrid" class="hf-loras-grid"></div>
2558
+
2559
+ <div id="hfLorasPaginationBottom" class="pagination-controls" style="display: none;">
2560
+ <span class="hf-loras-pager-nav">
2561
+ <button class="pagination-btn hf-loras-prev-btn" onclick="hfLorasPreviousPage()">← Previous</button>
2562
+ <span class="pagination-info hf-loras-page-info"></span>
2563
+ <button class="pagination-btn hf-loras-next-btn" onclick="hfLorasNextPage()">Next →</button>
2564
+ </span>
2565
+ </div>
2566
+ </div>
2567
+
2568
  <!-- Recent Updates Modal -->
2569
  <div id="dailyUploadsModal" class="modal-overlay" onclick="closeDailyUploadsModalOnOverlay(event)">
2570
  <div class="modal-content daily-modal-content" onclick="event.stopPropagation()">
 
2755
  updateTagsFilterCount();
2756
  }
2757
 
2758
+ // Search/pagination state for the HF loras view — deliberately separate from the regular
2759
+ // browser's currentPage/totalPages/filteredResults, which belong to a different dataset.
2760
+ // Declared here, ahead of initHfLoras() below, since `let`/`const` (unlike `function`) are
2761
+ // not hoisted — referencing them from a call this early in the script threw otherwise.
2762
+ let hfLorasSearchTerm = '';
2763
+ let hfLorasCurrentPage = 1;
2764
+ let hfLorasTotalPages = 1;
2765
+
2766
  // Call on page load
2767
  initTheme();
2768
  initSamplesToggle();
2769
  initTagsFilterGroup();
2770
+ initHfLoras();
2771
+
2772
+ // HuggingFace LoRA repos section (data-huggingface-loras.js, generated via
2773
+ // `npm run huggingface-loras:generate` from the repos in HUGGINGFACE_LORA_REPOS).
2774
+ // Sample images and safetensors downloads are hotlinked straight to
2775
+ // huggingface.co — nothing is mirrored locally, matching the other data-hf-*.js
2776
+ // sources on this page.
2777
+ // Repo file paths often carry spaces/parentheses (real filenames seen in these repos:
2778
+ // "K2 Dichen Lachman (vrtlDichenLachman)-ID.webp"). Encode each path segment, not the
2779
+ // whole path, so a subfolder's own "/" separators survive.
2780
+ function encodeHfPath(path) {
2781
+ return path.split('/').map(encodeURIComponent).join('/');
2782
+ }
2783
+
2784
+ function formatHfLoraBytes(bytes) {
2785
+ if (bytes === null || bytes === undefined) return '';
2786
+ const units = ['B', 'KB', 'MB', 'GB'];
2787
+ let value = bytes;
2788
+ let unitIndex = 0;
2789
+ while (value >= 1024 && unitIndex < units.length - 1) {
2790
+ value /= 1024;
2791
+ unitIndex += 1;
2792
+ }
2793
+ return `${value.toFixed(1)} ${units[unitIndex]}`;
2794
+ }
2795
+
2796
+ function populateHfLorasRepoSelect() {
2797
+ const select = document.getElementById('hfLorasRepoSelect');
2798
+ const registry = window.hfLorasRegistry || {};
2799
+ const repoIds = Object.keys(registry);
2800
+
2801
+ select.innerHTML = repoIds
2802
+ .map((repoId) => `<option value="${escapeHtml(repoId)}">${escapeHtml(repoId)}</option>`)
2803
+ .join('');
2804
+
2805
+ // Section visibility is controlled by toggleHfLorasSection() (the 🤗 button), not here —
2806
+ // this only decides whether there's anything to toggle to in the first place.
2807
+ const toggleBtn = document.getElementById('hfLorasToggleBtn');
2808
+ if (toggleBtn) {
2809
+ toggleBtn.style.display = repoIds.length === 0 ? 'none' : 'flex';
2810
+ }
2811
+ }
2812
+
2813
+ // A switch, not an additional panel: showing the HuggingFace repos hides Malcolm Rey's own
2814
+ // models (search/filters, the person grid, both pagination bars) rather than stacking below
2815
+ // them, and vice versa. Default on page load is Malcolm Rey's models, since #hfLorasSection
2816
+ // starts without the `active` class that makes it visible at all.
2817
+ function toggleHfLorasSection() {
2818
+ const hfSection = document.getElementById('hfLorasSection');
2819
+ const btn = document.getElementById('hfLorasToggleBtn');
2820
+ const regularControls = document.getElementById('regularBrowseControls');
2821
+ const mainContent = document.getElementById('mainContent');
2822
+ const paginationTop = document.getElementById('paginationControls');
2823
+ const paginationBottom = document.getElementById('paginationControlsBottom');
2824
+
2825
+ const showingHfLoras = !hfSection.classList.contains('active');
2826
+
2827
+ hfSection.classList.toggle('active', showingHfLoras);
2828
+ btn.classList.toggle('active', showingHfLoras);
2829
+ regularControls.style.display = showingHfLoras ? 'none' : '';
2830
+ mainContent.style.display = showingHfLoras ? 'none' : '';
2831
+
2832
+ if (showingHfLoras) {
2833
+ paginationTop.style.display = 'none';
2834
+ paginationBottom.style.display = 'none';
2835
+ } else {
2836
+ // Its own visibility depends on page count, independent of this switch — let it decide
2837
+ // rather than forcing 'flex' back on regardless of whether there's more than one page.
2838
+ updatePaginationControls();
2839
+ }
2840
+ }
2841
+
2842
+ function onHfLorasSearchInput(value) {
2843
+ hfLorasSearchTerm = value;
2844
+ hfLorasCurrentPage = 1;
2845
+ renderHfLorasRepo();
2846
+ }
2847
+
2848
+ function clearHfLorasSearch() {
2849
+ document.getElementById('hfLorasSearch').value = '';
2850
+ onHfLorasSearchInput('');
2851
+ }
2852
+
2853
+ function onHfLorasRepoChange() {
2854
+ hfLorasCurrentPage = 1;
2855
+ renderHfLorasRepo();
2856
+ }
2857
+
2858
+ function onHfLorasPageSizeChange() {
2859
+ hfLorasCurrentPage = 1;
2860
+ renderHfLorasRepo();
2861
+ }
2862
+
2863
+ function getHfLorasPageSize() {
2864
+ const value = document.getElementById('hfLorasPageSize').value;
2865
+ return value === 'all' ? Infinity : parseInt(value, 10);
2866
+ }
2867
+
2868
+ function hfLorasPreviousPage() {
2869
+ if (hfLorasCurrentPage > 1) {
2870
+ hfLorasCurrentPage -= 1;
2871
+ renderHfLorasRepo();
2872
+ }
2873
+ }
2874
+
2875
+ function hfLorasNextPage() {
2876
+ if (hfLorasCurrentPage < hfLorasTotalPages) {
2877
+ hfLorasCurrentPage += 1;
2878
+ renderHfLorasRepo();
2879
+ }
2880
+ }
2881
+
2882
+ // The per-page select lives inside the same bar as Previous/Next, but must stay reachable
2883
+ // even when there's only one page (e.g. "All" is selected) — otherwise picking "All" would
2884
+ // be a dead end with no way back to a smaller page size. Only `.hf-loras-pager-nav` (the
2885
+ // Previous/count/Next trio) hides in that case; the bar itself, and the select, do not.
2886
+ function updateHfLorasPaginationControls(matchCount, hasData) {
2887
+ const pageSize = getHfLorasPageSize();
2888
+ const showNav = hasData && matchCount > pageSize;
2889
+ const infoText = `${hfLorasCurrentPage}/${hfLorasTotalPages}`;
2890
+
2891
+ document.querySelectorAll('#hfLorasPaginationTop, #hfLorasPaginationBottom').forEach((el) => {
2892
+ el.style.display = hasData ? 'flex' : 'none';
2893
+ });
2894
+ document.querySelectorAll('.hf-loras-pager-nav').forEach((el) => {
2895
+ el.style.display = showNav ? 'flex' : 'none';
2896
+ });
2897
+ document.querySelectorAll('.hf-loras-page-info').forEach((el) => {
2898
+ el.textContent = infoText;
2899
+ });
2900
+ document.querySelectorAll('.hf-loras-prev-btn').forEach((el) => {
2901
+ el.disabled = hfLorasCurrentPage <= 1;
2902
+ });
2903
+ document.querySelectorAll('.hf-loras-next-btn').forEach((el) => {
2904
+ el.disabled = hfLorasCurrentPage >= hfLorasTotalPages;
2905
+ });
2906
+ }
2907
+
2908
+ function renderHfLorasRepo() {
2909
+ const select = document.getElementById('hfLorasRepoSelect');
2910
+ const grid = document.getElementById('hfLorasGrid');
2911
+ const summary = document.getElementById('hfLorasSummary');
2912
+ const repoLink = document.getElementById('hfLorasRepoLink');
2913
+ const registry = window.hfLorasRegistry || {};
2914
+ const snapshot = registry[select.value];
2915
+
2916
+ if (!snapshot) {
2917
+ grid.innerHTML = '<div class="hf-loras-empty">No data for this repo yet — run `npm run huggingface-loras:generate`.</div>';
2918
+ summary.textContent = '';
2919
+ repoLink.style.display = 'none';
2920
+ updateHfLorasPaginationControls(0, false);
2921
+ return;
2922
+ }
2923
+
2924
+ repoLink.href = `https://huggingface.co/${encodeHfPath(snapshot.repoId)}`;
2925
+ repoLink.style.display = '';
2926
+
2927
+ const term = hfLorasSearchTerm.trim().toLowerCase();
2928
+ const filtered = term
2929
+ ? snapshot.loras.filter((lora) => lora.name.toLowerCase().includes(term))
2930
+ : snapshot.loras;
2931
+
2932
+ const pageSize = getHfLorasPageSize();
2933
+ hfLorasTotalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
2934
+ if (hfLorasCurrentPage > hfLorasTotalPages) hfLorasCurrentPage = hfLorasTotalPages;
2935
+
2936
+ const fetchedLabel = new Date(snapshot.fetchedAt).toLocaleString();
2937
+ summary.textContent = term
2938
+ ? `${filtered.length} of ${snapshot.loras.length} LoRA(s) · fetched ${fetchedLabel}`
2939
+ : `${snapshot.loras.length} LoRA(s) · fetched ${fetchedLabel}`;
2940
+
2941
+ // pageSize is Infinity for "All". (hfLorasCurrentPage - 1) * Infinity is NaN when the
2942
+ // page is 1 (0 * Infinity), and Infinity itself otherwise — either way `slice` on that
2943
+ // start returns nothing. Substituting the actual filtered length sidesteps Infinity
2944
+ // arithmetic entirely rather than special-casing every place it could leak in.
2945
+ const effectivePageSize = pageSize === Infinity ? Math.max(filtered.length, 1) : pageSize;
2946
+ const start = (hfLorasCurrentPage - 1) * effectivePageSize;
2947
+ const pageItems = filtered.slice(start, start + effectivePageSize);
2948
+
2949
+ grid.innerHTML = pageItems.length === 0
2950
+ ? '<div class="hf-loras-empty">No matching LoRAs.</div>'
2951
+ : pageItems.map((lora) => {
2952
+ const sampleFile = lora.sampleFiles[0];
2953
+ const sampleUrl = sampleFile
2954
+ ? `https://huggingface.co/${snapshot.repoId}/resolve/${snapshot.revision}/${encodeHfPath(sampleFile)}`
2955
+ : null;
2956
+ const downloadUrl = `https://huggingface.co/${snapshot.repoId}/resolve/${snapshot.revision}/${encodeHfPath(lora.safetensorsFile)}?download=true`;
2957
+ const imageHtml = sampleUrl
2958
+ ? `<img src="${sampleUrl}" alt="${escapeHtml(lora.name)}" loading="lazy" />`
2959
+ : '<div class="hf-lora-no-sample">No sample</div>';
2960
+
2961
+ return `
2962
+ <div class="hf-lora-card">
2963
+ <div class="hf-lora-card-name" title="${escapeHtml(lora.name)}">${escapeHtml(lora.name)}</div>
2964
+ <div class="hf-lora-card-image">${imageHtml}</div>
2965
+ <div class="hf-lora-card-footer">
2966
+ <span>${formatHfLoraBytes(lora.safetensorsSizeBytes)}</span>
2967
+ <a href="${downloadUrl}" target="_blank" rel="noopener">⬇️ Download</a>
2968
+ </div>
2969
+ </div>
2970
+ `;
2971
+ }).join('');
2972
+
2973
+ updateHfLorasPaginationControls(filtered.length, true);
2974
+ }
2975
+
2976
+ function initHfLoras() {
2977
+ populateHfLorasRepoSelect();
2978
+ renderHfLorasRepo();
2979
+ }
2980
 
2981
  // Pagination state
2982
  let currentPage = 1;