karlexmarin Claude Opus 4.7 (1M context) commited on
Commit
778145c
·
1 Parent(s): 7bc5d7c

v0.7.4: HF Hub search-as-you-type autocomplete on all model id inputs

Browse files

NEW js/hf_autocomplete.js — wraps any text input with a dropdown that hits the public huggingface.co/api/models endpoint as the user types. Debounced (300 ms), keyboard-navigable (↑/↓/Enter/Esc), positioned via fixed coords on body so it never gets clipped.

Wired to all 5 HF id inputs at startup via attachAllHfAutocompletes():
- #hf-id (Recipe mode)
- #profile-hf-id (Profile mode)
- #unmask-id (🪟 Unmask)
- #template-id (📜 Chat-template)
- #quant-id (⚖️ Quant)

Each search result row shows: model id (mono) + downloads + likes + library_name. Filtered to text-generation pipeline by default. Sorted by downloads descending so popular official releases surface first.

LIVE API VERIFIED — top-5 results for common queries:
llama-3 → meta-llama/Llama-3.1-8B-Instruct (9.7M ⬇)
mistral → mistralai/Mistral-7B-Instruct-v0.2 (2.6M ⬇)
qwen → Qwen/Qwen3-0.6B (19M ⬇)
phi-3 → microsoft/Phi-3-mini-4k-instruct (761K ⬇)

Network failures degrade gracefully (silent — user can still type full id manually). Idempotent attach (safe to call twice on same input).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (3) hide show
  1. js/hf_autocomplete.js +166 -0
  2. js/main.js +5 -0
  3. style.css +32 -0
js/hf_autocomplete.js ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // HF Hub autocomplete — wraps any text input with a search-as-you-type
2
+ // dropdown that hits https://huggingface.co/api/models. Browser-only, no auth.
3
+ //
4
+ // Usage:
5
+ // import { attachHfAutocomplete } from "./hf_autocomplete.js";
6
+ // attachHfAutocomplete(document.getElementById("my-id-input"), {
7
+ // pipeline: "text-generation", // filter (or null for all)
8
+ // onSelect: (id) => { ... },
9
+ // });
10
+ //
11
+ // Idempotent: calling twice on same input is a no-op.
12
+
13
+ const ATTACHED = new WeakSet();
14
+
15
+ function escapeHtml(s) {
16
+ return String(s).replace(/[&<>"']/g, c =>
17
+ ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
18
+ }
19
+
20
+ function formatDownloads(n) {
21
+ if (n === null || n === undefined) return "?";
22
+ if (n >= 1e9) return (n / 1e9).toFixed(1) + "B";
23
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
24
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
25
+ return String(n);
26
+ }
27
+
28
+ export function attachHfAutocomplete(inputEl, options = {}) {
29
+ if (!inputEl || ATTACHED.has(inputEl)) return;
30
+ ATTACHED.add(inputEl);
31
+
32
+ const {
33
+ pipeline = "text-generation",
34
+ limit = 15,
35
+ debounceMs = 300,
36
+ minChars = 2,
37
+ onSelect = null,
38
+ } = options;
39
+
40
+ // Floating dropdown attached to body so it never gets clipped by parents.
41
+ const dropdown = document.createElement("div");
42
+ dropdown.className = "hf-autocomplete-dropdown";
43
+ dropdown.style.display = "none";
44
+ document.body.appendChild(dropdown);
45
+
46
+ let timeoutId = null;
47
+ let activeIndex = -1;
48
+ let results = [];
49
+ let lastQuery = "";
50
+
51
+ function positionDropdown() {
52
+ const rect = inputEl.getBoundingClientRect();
53
+ dropdown.style.position = "fixed";
54
+ dropdown.style.left = rect.left + "px";
55
+ dropdown.style.top = (rect.bottom + 2) + "px";
56
+ dropdown.style.width = Math.max(rect.width, 280) + "px";
57
+ dropdown.style.zIndex = "10000";
58
+ }
59
+
60
+ function render() {
61
+ if (!results.length) { dropdown.style.display = "none"; return; }
62
+ dropdown.innerHTML = results.map((r, i) => `
63
+ <div class="hf-result ${i === activeIndex ? "active" : ""}" data-id="${escapeHtml(r.id)}">
64
+ <span class="hf-result-id">${escapeHtml(r.id)}</span>
65
+ <span class="hf-result-meta">⬇ ${formatDownloads(r.downloads)} · ❤ ${formatDownloads(r.likes)}${r.library_name ? " · " + escapeHtml(r.library_name) : ""}</span>
66
+ </div>
67
+ `).join("");
68
+ positionDropdown();
69
+ dropdown.style.display = "block";
70
+ }
71
+
72
+ function close() {
73
+ dropdown.style.display = "none";
74
+ activeIndex = -1;
75
+ }
76
+
77
+ function pick(id) {
78
+ inputEl.value = id;
79
+ close();
80
+ if (onSelect) onSelect(id);
81
+ inputEl.dispatchEvent(new Event("change", { bubbles: true }));
82
+ }
83
+
84
+ async function search(q) {
85
+ if (q.length < minChars) { results = []; render(); return; }
86
+ if (q === lastQuery) return; // dedupe rapid typing
87
+ lastQuery = q;
88
+ const params = new URLSearchParams({
89
+ search: q,
90
+ limit: String(limit),
91
+ sort: "downloads",
92
+ direction: "-1",
93
+ });
94
+ if (pipeline) params.set("filter", pipeline);
95
+ try {
96
+ const resp = await fetch(`https://huggingface.co/api/models?${params}`);
97
+ if (!resp.ok) { results = []; render(); return; }
98
+ const data = await resp.json();
99
+ // Filter out odd entries (gated/private won't appear publicly anyway)
100
+ results = (Array.isArray(data) ? data : [])
101
+ .filter(r => r.id && typeof r.id === "string")
102
+ .slice(0, limit);
103
+ activeIndex = -1;
104
+ render();
105
+ } catch (e) {
106
+ // Network failure → silent; user can still type the id manually.
107
+ results = []; render();
108
+ }
109
+ }
110
+
111
+ inputEl.addEventListener("input", (e) => {
112
+ clearTimeout(timeoutId);
113
+ timeoutId = setTimeout(() => search(e.target.value.trim()), debounceMs);
114
+ });
115
+
116
+ inputEl.addEventListener("focus", (e) => {
117
+ const v = e.target.value.trim();
118
+ if (v.length >= minChars) search(v);
119
+ });
120
+
121
+ // Click on a result picks it. Use mousedown to fire before input loses focus.
122
+ dropdown.addEventListener("mousedown", (e) => {
123
+ e.preventDefault();
124
+ const item = e.target.closest(".hf-result");
125
+ if (item) pick(item.dataset.id);
126
+ });
127
+
128
+ // Keyboard nav
129
+ inputEl.addEventListener("keydown", (e) => {
130
+ if (dropdown.style.display === "none" || !results.length) return;
131
+ if (e.key === "ArrowDown") {
132
+ e.preventDefault();
133
+ activeIndex = Math.min(activeIndex + 1, results.length - 1);
134
+ render();
135
+ } else if (e.key === "ArrowUp") {
136
+ e.preventDefault();
137
+ activeIndex = Math.max(activeIndex - 1, -1);
138
+ render();
139
+ } else if (e.key === "Enter" && activeIndex >= 0) {
140
+ e.preventDefault();
141
+ pick(results[activeIndex].id);
142
+ } else if (e.key === "Escape") {
143
+ close();
144
+ }
145
+ });
146
+
147
+ // Click outside or blur → close (small delay so click on dropdown still fires)
148
+ inputEl.addEventListener("blur", () => setTimeout(close, 150));
149
+
150
+ // Reposition on scroll/resize when dropdown is open
151
+ window.addEventListener("scroll", () => {
152
+ if (dropdown.style.display === "block") positionDropdown();
153
+ }, true);
154
+ window.addEventListener("resize", () => {
155
+ if (dropdown.style.display === "block") positionDropdown();
156
+ });
157
+ }
158
+
159
+ // Convenience: attach to all 5 known HF-id inputs in TAF Agent.
160
+ export function attachAllHfAutocompletes() {
161
+ const ids = ["hf-id", "profile-hf-id", "unmask-id", "template-id", "quant-id"];
162
+ for (const id of ids) {
163
+ const el = document.getElementById(id);
164
+ if (el) attachHfAutocomplete(el);
165
+ }
166
+ }
js/main.js CHANGED
@@ -16,6 +16,11 @@ import { sniffChatTemplate } from "./chat_template_sniffer.js";
16
  import { parseVotesCSV, computeArenaCI, SAMPLE_VOTES_CSV } from "./arena_ci.js";
17
  import { rateAllBenchmarks, BENCHMARK_DB } from "./contamination_prior.js";
18
  import { predictQuantShift, predictAllSchemes, QUANT_SCHEMES } from "./quant_regime.js";
 
 
 
 
 
19
 
20
  const TAF_BROWSER_URL = "python/taf_browser.py";
21
  const ENABLE_WEBLLM = true;
 
16
  import { parseVotesCSV, computeArenaCI, SAMPLE_VOTES_CSV } from "./arena_ci.js";
17
  import { rateAllBenchmarks, BENCHMARK_DB } from "./contamination_prior.js";
18
  import { predictQuantShift, predictAllSchemes, QUANT_SCHEMES } from "./quant_regime.js";
19
+ import { attachAllHfAutocompletes } from "./hf_autocomplete.js";
20
+
21
+ // Attach HF Hub search-as-you-type to all 5 model id inputs (Profile, Recipe,
22
+ // Unmask, Template, Quant). Hits public huggingface.co/api/models. Idempotent.
23
+ attachAllHfAutocompletes();
24
 
25
  const TAF_BROWSER_URL = "python/taf_browser.py";
26
  const ENABLE_WEBLLM = true;
style.css CHANGED
@@ -33,6 +33,38 @@
33
  flex: 1;
34
  }
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  /* v0.7.2 — Arena-Elo CI reconstructor */
37
  .arena-result { margin-top: 0.6em; }
38
  .arena-table, .arena-ties-table {
 
33
  flex: 1;
34
  }
35
 
36
+ /* v0.7.4 — HF Hub autocomplete dropdown (attached to body) */
37
+ .hf-autocomplete-dropdown {
38
+ background: #12181f;
39
+ border: 1px solid rgba(88, 166, 255, 0.4);
40
+ border-radius: 6px;
41
+ max-height: 320px;
42
+ overflow-y: auto;
43
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
44
+ font-size: 0.92em;
45
+ }
46
+ .hf-autocomplete-dropdown .hf-result {
47
+ display: flex;
48
+ flex-direction: column;
49
+ padding: 0.5em 0.7em;
50
+ cursor: pointer;
51
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
52
+ }
53
+ .hf-autocomplete-dropdown .hf-result:last-child { border-bottom: none; }
54
+ .hf-autocomplete-dropdown .hf-result:hover,
55
+ .hf-autocomplete-dropdown .hf-result.active {
56
+ background: rgba(88, 166, 255, 0.15);
57
+ }
58
+ .hf-autocomplete-dropdown .hf-result-id {
59
+ font-family: monospace;
60
+ color: #c9d1d9;
61
+ }
62
+ .hf-autocomplete-dropdown .hf-result-meta {
63
+ font-size: 0.8em;
64
+ opacity: 0.65;
65
+ margin-top: 0.15em;
66
+ }
67
+
68
  /* v0.7.2 — Arena-Elo CI reconstructor */
69
  .arena-result { margin-top: 0.6em; }
70
  .arena-table, .arena-ties-table {