|
|
@@ -78,6 +78,16 @@
|
|
|
.dim { color: gray; }
|
|
|
/* Model-settings dialog (⚙ settings): per-agent client config for the
|
|
|
recipe's served model. */
|
|
|
+ #hfdlg { position: fixed; inset: 0; z-index: 45; display: none; align-items: center; justify-content: center;
|
|
|
+ background: rgba(0, 0, 0, .45); }
|
|
|
+ #hfdlg.on { display: flex; }
|
|
|
+ #hfdlg .panel { background: canvas; color: canvastext; border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
|
|
|
+ border-radius: 8px; padding: 14px 16px; width: min(900px, 94vw); max-height: 86vh; overflow: auto; }
|
|
|
+ #hfres .hfrow { padding: 4px 6px; cursor: pointer; border-bottom: 1px solid color-mix(in srgb, currentColor 10%, transparent); }
|
|
|
+ #hfres .hfrow:hover { background: color-mix(in srgb, currentColor 6%, canvas); }
|
|
|
+ #hfcard table { border-collapse: collapse; font-size: 12px; }
|
|
|
+ #hfcard td, #hfcard th { padding: 2px 8px; text-align: left; }
|
|
|
+ #hl { font-family: ui-monospace, monospace; }
|
|
|
#msdlg { position: fixed; inset: 0; z-index: 40; display: none; align-items: center; justify-content: center;
|
|
|
background: rgba(0, 0, 0, .45); }
|
|
|
#msdlg.on { display: flex; }
|
|
|
@@ -137,6 +147,7 @@
|
|
|
</label>
|
|
|
<button id="prev">‹</button><span id="count"></span><button id="next">›</button>
|
|
|
<button id="syncbtn" title="sync the recipe repos now (dashboard job; new recipes appear via the live stream)" style="display:none">⟳ sync</button>
|
|
|
+ <button id="hfbtn" title="search Hugging Face and pick a revision" style="display:none">⌕ HF</button>
|
|
|
<span id="live" class="dim" title="live updates">● off</span>
|
|
|
<button id="bell" title="updates" aria-label="updates">
|
|
|
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg>
|
|
|
@@ -145,6 +156,19 @@
|
|
|
<div id="notifbox" style="display:none"></div>
|
|
|
</div>
|
|
|
<table id="tbl"><thead></thead><tbody></tbody></table>
|
|
|
+<div id="hfdlg" role="dialog" aria-modal="true" aria-label="Hugging Face">
|
|
|
+ <div class="panel">
|
|
|
+ <div style="display:flex;gap:8px;align-items:center;margin-bottom:8px">
|
|
|
+ <b>Hugging Face</b>
|
|
|
+ <input id="hfq" placeholder="search models… (org/name or words)" style="flex:1;min-width:220px">
|
|
|
+ <button id="hfgo">search</button>
|
|
|
+ <button id="hfclose">close</button>
|
|
|
+ </div>
|
|
|
+ <div id="hfres" style="max-height:28vh;overflow:auto"></div>
|
|
|
+ <div id="hfcard" style="margin-top:8px"></div>
|
|
|
+ </div>
|
|
|
+</div>
|
|
|
+
|
|
|
<div id="msdlg" role="dialog" aria-modal="true" aria-labelledby="msdtitle">
|
|
|
<div class="panel">
|
|
|
<h2 id="msdtitle">Model settings</h2>
|
|
|
@@ -849,6 +873,59 @@ syncBtn.addEventListener("click", async () => {
|
|
|
}
|
|
|
setTimeout(() => { syncBtn.disabled = false; syncBtn.textContent = "⟳ sync"; }, 6000);
|
|
|
});
|
|
|
+// HF catalogue: search → card → pick a revision, copy `org/name@rev`.
|
|
|
+const hfDlg = document.getElementById("hfdlg");
|
|
|
+const hfQ = document.getElementById("hfq");
|
|
|
+const hfRes = document.getElementById("hfres");
|
|
|
+const hfCard = document.getElementById("hfcard");
|
|
|
+const hfBtn = document.getElementById("hfbtn");
|
|
|
+function hfOpen() { if (EMBED) hfBtn.style.display = ""; }
|
|
|
+async function hfSearch() {
|
|
|
+ const q = hfQ.value.trim();
|
|
|
+ if (!q) return;
|
|
|
+ hfRes.innerHTML = '<div class="dim">searching…</div>';
|
|
|
+ try {
|
|
|
+ const j = await (await fetch("/api/hf/search?limit=20&q=" + encodeURIComponent(q))).json();
|
|
|
+ if (!j.models || !j.models.length) { hfRes.innerHTML = '<div class="dim">nothing found</div>'; return; }
|
|
|
+ hfRes.innerHTML = j.models.map(m =>
|
|
|
+ `<div class="hfrow" data-id="${esc(m.id)}"><b>${esc(m.id)}</b>
|
|
|
+ <span class="dim">↓${m.downloads ?? 0} · ♥${m.likes ?? 0} · ${esc(String(m.updated || "").slice(0, 10))}${m.gated ? " · 🔒" : ""}</span></div>`).join("");
|
|
|
+ } catch (e) { hfRes.innerHTML = `<div class="dim">error: ${esc(String(e))}</div>`; }
|
|
|
+}
|
|
|
+async function hfLoadCard(repo, rev) {
|
|
|
+ hfCard.innerHTML = '<div class="dim">loading card…</div>';
|
|
|
+ try {
|
|
|
+ const j = await (await fetch("/api/hf/card?repo=" + encodeURIComponent(repo) + (rev ? "&revision=" + encodeURIComponent(rev) : ""))).json();
|
|
|
+ const branches = (j.branches || []).map(b =>
|
|
|
+ `<tr><td><a href="#" class="hfrev" data-rev="${esc(b.name)}">${esc(b.name)}</a></td>
|
|
|
+ <td><code>${esc(String(b.targetCommit || "").slice(0, 12))}</code></td>
|
|
|
+ <td><button class="hfcp" data-copy="${esc(repo + "@" + b.name)}">⧉ org/name@rev</button></td></tr>`).join("");
|
|
|
+ const files = (j.files || []).slice(0, 20).map(f =>
|
|
|
+ `<tr><td>${esc(f.path)}</td><td class="dim">${(f.size / 1e9).toFixed(2)} GB</td></tr>`).join("");
|
|
|
+ hfCard.innerHTML =
|
|
|
+ `<div><b>${esc(j.id || repo)}</b>
|
|
|
+ ${j.licence ? ` <span class="dim">licence ${esc(j.licence)}</span>` : ""}
|
|
|
+ ${j.task ? ` <span class="dim">· ${esc(j.task)}</span>` : ""}
|
|
|
+ ${j.gated ? ' <span style="color:#d2822a">🔒 gated</span>' : ""}</div>
|
|
|
+ <table><tr><th>revision</th><th>commit</th><th></th></tr>${branches}</table>
|
|
|
+ ${files ? `<div class="dim" style="margin-top:6px">files at ${esc(j.revision)} — ${(j.total_bytes / 1e9).toFixed(1)} GB</div>
|
|
|
+ <table>${files}</table>` : ""}`;
|
|
|
+ hfCard.querySelectorAll(".hfrev").forEach(a => a.addEventListener("click", e => {
|
|
|
+ e.preventDefault(); hfLoadCard(repo, a.dataset.rev);
|
|
|
+ }));
|
|
|
+ hfCard.querySelectorAll(".hfcp").forEach(b => b.addEventListener("click", async () => {
|
|
|
+ try { await navigator.clipboard.writeText(b.dataset.copy); b.textContent = "copied ✓";
|
|
|
+ setTimeout(() => b.textContent = "⧉ org/name@rev", 1200); } catch {}
|
|
|
+ }));
|
|
|
+ } catch (e) { hfCard.innerHTML = `<div class="dim">error: ${esc(String(e))}</div>`; }
|
|
|
+}
|
|
|
+hfBtn.addEventListener("click", () => { hfDlg.classList.add("on"); hfQ.focus(); });
|
|
|
+document.getElementById("hfclose").addEventListener("click", () => hfDlg.classList.remove("on"));
|
|
|
+document.getElementById("hfgo").addEventListener("click", hfSearch);
|
|
|
+hfQ.addEventListener("keydown", e => { if (e.key === "Enter") hfSearch(); });
|
|
|
+hfRes.addEventListener("click", e => {
|
|
|
+ const row = e.target.closest(".hfrow"); if (row) hfLoadCard(row.dataset.id);
|
|
|
+});
|
|
|
// column checkboxes
|
|
|
document.getElementById("colbox").innerHTML = COLS.map(c =>
|
|
|
`<label><input type="checkbox" data-k="${c[0]}" ${S.hidden.has(c[0]) ? "" : "checked"}> ${esc(c[1])}</label>`).join("");
|