Spaces:
Running
Running
| # ui.py | |
| import html, threading | |
| from typing import Any, Dict, List | |
| from fastapi import APIRouter, HTTPException | |
| from fastapi.responses import HTMLResponse, JSONResponse | |
| from data import ( | |
| get_pages, get_state, ensure_page_loaded, build_all_pages, is_complete, | |
| model_id, license_label_and_url, search_models, flatten_models | |
| ) | |
| router = APIRouter() | |
| def _render_table(models: List[Dict[str, Any]]) -> str: | |
| rows = [] | |
| for m in models: | |
| mid = html.escape(model_id(m)) | |
| desc = html.escape((m.get("description") or "")[:200]) | |
| thumb = m.get("cover_image_url") or m.get("cover_image") or "" | |
| vis = html.escape(m.get("visibility") or "") | |
| tags = m.get("tags") or m.get("categories") or [] | |
| tags_html = " ".join(f"<span class='tag'>{html.escape(str(t))}</span>" for t in tags[:8]) | |
| lic_label, lic_url = license_label_and_url(m) | |
| lic_label_h = html.escape(lic_label) | |
| lic_html = f"<a href='{html.escape(lic_url)}' target='_blank' rel='noopener'>{lic_label_h}</a>" if lic_url else lic_label_h | |
| src_links = [] | |
| if m.get("github_url"): | |
| src_links.append(f"<a href='{html.escape(m['github_url'])}' target='_blank' rel='noopener'>GitHub</a>") | |
| if m.get("paper_url"): | |
| src_links.append(f"<a href='{html.escape(m['paper_url'])}' target='_blank' rel='noopener'>Paper</a>") | |
| if lic_url: | |
| src_links.append(f"<a href='{html.escape(lic_url)}' target='_blank' rel='noopener'>License</a>") | |
| sources_html = " | ".join(src_links) | |
| run_ct = m.get("run_count") | |
| run_html = f"<span class='pill'>{run_ct:,} runs</span>" if isinstance(run_ct, int) else "" | |
| thumb_html = f"<img src='{html.escape(thumb)}' alt='' />" if thumb else "" | |
| rows.append( | |
| f"<tr>" | |
| f"<td class='thumb'>{thumb_html}</td>" | |
| f"<td class='id'>{mid}<br>{run_html}</td>" | |
| f"<td class='desc'>{desc}</td>" | |
| f"<td class='vis'>{vis}</td>" | |
| f"<td class='lic'>{lic_html}</td>" | |
| f"<td class='src'>{sources_html}</td>" | |
| f"<td class='tags'>{tags_html}</td>" | |
| f"</tr>" | |
| ) | |
| return ( | |
| "<table class='grid'>" | |
| "<thead><tr>" | |
| "<th>Thumbnail</th><th>ID / Runs</th><th>Description</th><th>Visibility</th>" | |
| "<th>License</th><th>Sources</th><th>Tags</th>" | |
| "</tr></thead><tbody>" | |
| + "\n".join(rows) + | |
| "</tbody></table>" | |
| ) | |
| def _render_pagination(current_page: int) -> str: | |
| pages = len(get_pages()) | |
| links = [] | |
| for i in range(1, pages + 1): | |
| if i == current_page: | |
| links.append(f"<span class='page current'>{i}</span>") | |
| else: | |
| links.append(f"<a class='page' href='/page/{i}'>{i}</a>") | |
| state = get_state() | |
| if state["more"]: | |
| links.append(f"<a class='page next' href='/page/{pages + 1}'>Load {pages + 1}</a>") | |
| return "<div class='pagination-rail'><div class='pagination'>" + " ".join(links) + "</div></div>" | |
| def _shell(page_index_0: int, html_table: str, *, search_query: str = "") -> str: | |
| page_num = page_index_0 + 1 | |
| state = get_state() | |
| search_disabled = "" if state["complete"] else "disabled" | |
| search_opacity = "1.0" if state["complete"] else "0.45" | |
| import html as _h | |
| return f""" | |
| <!doctype html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <title>Replicate Catalog β Page {page_num}</title> | |
| <style> | |
| :root {{ | |
| --bg: #ffffff; | |
| --fg: #111111; | |
| --muted: #444444; | |
| --border: #e5e5ea; | |
| --chip-bg: #f2f2f7; | |
| --link: #0366d6; | |
| --thead: #fafafa; | |
| }} | |
| body.dark {{ | |
| --bg: #0b0b0b; | |
| --fg: #efefef; | |
| --muted: #bbbbbb; | |
| --border: #2b2b2b; | |
| --chip-bg: #1a1a1a; | |
| --link: #6aa6ff; | |
| --thead: #111111; | |
| }} | |
| body {{ background: var(--bg); color: var(--fg); font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; margin: 24px; }} | |
| a {{ color: var(--link); }} | |
| .meta {{ margin-bottom: 12px; color: var(--muted); display:flex; gap:16px; align-items:center; flex-wrap:wrap; }} | |
| .grid {{ width:100%; border-collapse: collapse; }} | |
| .grid th, .grid td {{ border-bottom: 1px solid var(--border); padding: 10px; vertical-align: top; }} | |
| .grid th {{ text-align: left; background: var(--thead); }} | |
| .thumb img {{ width: 72px; height: 72px; object-fit: cover; border-radius: 8px; }} | |
| .id {{ font-weight: 600; white-space: nowrap; }} | |
| .desc {{ color: var(--fg); max-width: 700px; }} | |
| .vis {{ white-space: nowrap; color: var(--muted); }} | |
| .lic {{ white-space: nowrap; }} | |
| .src {{ font-size: 13px; }} | |
| .tags .tag {{ display: inline-block; padding: 2px 8px; margin: 2px; background: var(--chip-bg); border: 1px solid var(--border); border-radius: 999px; font-size: 12px; color: var(--fg); }} | |
| .pagination-rail {{ overflow-x: auto; -webkit-overflow-scrolling: touch; margin-top: 20px; border-top: 1px solid var(--border); padding-top: 12px; }} | |
| .pagination {{ white-space: nowrap; }} | |
| .page {{ display: inline-block; white-space: nowrap; padding: 6px 10px; margin-right: 6px; border: 1px solid var(--border); border-radius: 6px; text-decoration: none; color: var(--fg); }} | |
| .page:hover {{ background: var(--thead); }} | |
| .page.current {{ background: var(--fg); color: var(--bg); border-color: var(--fg); cursor: default; }} | |
| .page.next {{ font-weight: 600; }} | |
| .searchbar {{ display:flex; gap:8px; align-items:center; }} | |
| .searchbar input[type="text"] {{ width: 360px; padding:8px 10px; border:1px solid var(--border); border-radius:8px; background: var(--bg); color: var(--fg); opacity:{search_opacity}; }} | |
| .searchbar button {{ padding:8px 12px; border:1px solid var(--fg); background: var(--fg); color: var(--bg); border-radius:8px; opacity:{search_opacity}; cursor:pointer; }} | |
| .pill {{ display:inline-block; padding:2px 8px; border:1px solid var(--border); border-radius:999px; font-size:12px; color: var(--fg); }} | |
| #darkToggle {{ margin-left:auto; padding:6px 10px; border:1px solid var(--border); background:var(--bg); color:var(--fg); border-radius:8px; cursor:pointer; }} | |
| /* Commercial stats button + panel (above Log) */ | |
| #comBtn {{ position:fixed; right:20px; bottom:220px; padding:10px 14px; border:none; border-radius:999px; background:var(--fg); color:var(--bg); cursor:pointer; }} | |
| #comPanel {{ position:fixed; right:20px; bottom:270px; width:360px; max-height:60vh; overflow:auto; background:var(--bg); border:1px solid var(--border); border-radius:12px; box-shadow:0 10px 24px rgba(0,0,0,0.12); padding:12px; display:none; }} | |
| #comPanel h3 {{ margin:0 0 8px 0; font-size:14px; }} | |
| #comPanel .row {{ display:flex; justify-content:space-between; font-size:13px; padding:2px 0; }} | |
| #comBySpdx {{ margin-top:8px; font-size:12px; max-height:28vh; overflow:auto; }} | |
| /* Floating log button + panel β moved up */ | |
| #logBtn {{ position:fixed; right:20px; bottom:100px; padding:10px 14px; border:none; border-radius:999px; background:var(--fg); color:var(--bg); cursor:pointer; }} | |
| #logPanel {{ position:fixed; right:20px; bottom:150px; width:320px; max-height:50vh; overflow:auto; background:var(--bg); border:1px solid var(--border); border-radius:12px; box-shadow:0 10px 24px rgba(0,0,0,0.12); padding:12px; display:none; }} | |
| #logPanel h3 {{ margin:0 0 8px 0; font-size:14px; }} | |
| #logPanel .row {{ display:flex; justify-content:space-between; font-size:13px; padding:2px 0; }} | |
| #byLabel {{ margin-top:8px; font-size:12px; max-height:28vh; overflow:auto; }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="meta"> | |
| <strong>Replicate catalog</strong> | |
| <span class="pill">page {page_num} of {state['pages']} known</span> | |
| <span class="pill">more pages available: {"yes" if state["more"] else "no"}</span> | |
| <span class="pill">building: {"yes" if state["building"] else "no"}</span> | |
| <span class="pill">complete: {"yes" if state["complete"] else "no"}</span> | |
| <button id="darkToggle" onclick="toggleDark()">Toggle dark</button> | |
| </div> | |
| <form class="searchbar" method="GET" action="/search" onsubmit="return onSearchSubmit();" > | |
| <input id="q" name="q" type="text" placeholder="Search models, descriptions, tags" value="{_h.escape(search_query)}" {search_disabled} /> | |
| <button id="qbtn" type="submit" {search_disabled}>Search</button> | |
| <span id="searchHint" style="color:var(--muted); font-size:12px;">{'' if state["complete"] else 'Search available after catalog completes.'}</span> | |
| </form> | |
| {html_table} | |
| {_render_pagination(page_num)} | |
| <button id="comBtn" onclick="toggleCommercial()">Commercial stats</button> | |
| <div id="comPanel"> | |
| <h3>Commercial stats</h3> | |
| <div class="row"><span>Total</span><span id="c_tot">β</span></div> | |
| <div class="row"><span>Analyzed</span><span id="c_an">β</span></div> | |
| <div class="row"><span>Unknown</span><span id="c_un">β</span></div> | |
| <div class="row"><span>Commercial-friendly</span><span id="c_cf">β</span></div> | |
| <div class="row"><span>Copyleft</span><span id="c_cp">β</span></div> | |
| <div class="row"><span>Non-commercial</span><span id="c_nc">β</span></div> | |
| <div class="row"><span>No-derivatives</span><span id="c_nd">β</span></div> | |
| <div id="comBySpdx"></div> | |
| </div> | |
| <button id="logBtn" onclick="toggleLog()">Log</button> | |
| <div id="logPanel"> | |
| <h3>License log</h3> | |
| <div class="row"><span>Total models</span><span id="tot">β</span></div> | |
| <div class="row"><span>Known licenses</span><span id="kn">β</span></div> | |
| <div class="row"><span>Unknown licenses</span><span id="un">β</span></div> | |
| <div id="byLabel"></div> | |
| </div> | |
| <script> | |
| document.addEventListener("DOMContentLoaded", function() {{ | |
| fetch("/build_all", {{ method: "POST" }}).catch(()=>{{}}); | |
| const poll = () => {{ | |
| fetch("/status").then(r=>r.json()).then(s => {{ | |
| if (s.complete) {{ | |
| const q = document.getElementById("q"); | |
| const b = document.getElementById("qbtn"); | |
| const hint = document.getElementById("searchHint"); | |
| if (q && b) {{ | |
| q.disabled = false; b.disabled = false; | |
| q.style.opacity = "1.0"; b.style.opacity = "1.0"; | |
| if (hint) hint.textContent = ""; | |
| }} | |
| }} | |
| document.title = "Replicate Catalog β Page {page_num} (" + s.pages + " known)"; | |
| if (!s.complete) setTimeout(poll, 1200); | |
| }}).catch(()=>setTimeout(poll, 2000)); | |
| }}; | |
| poll(); | |
| try {{ | |
| if (localStorage.getItem('dark') === '1') document.body.classList.add('dark'); | |
| }} catch(e) {{}} | |
| }}); | |
| function onSearchSubmit() {{ | |
| const q = document.getElementById("q"); | |
| return q && !q.disabled; | |
| }} | |
| function toggleDark() {{ | |
| document.body.classList.toggle('dark'); | |
| try {{ | |
| localStorage.setItem('dark', document.body.classList.contains('dark') ? '1' : '0'); | |
| }} catch(e) {{}} | |
| }} | |
| function toggleCommercial() {{ | |
| const p = document.getElementById('comPanel'); | |
| p.style.display = (p.style.display === 'block') ? 'none' : 'block'; | |
| if (p.style.display === 'block') refreshCommercial(); | |
| }} | |
| function refreshCommercial() {{ | |
| fetch('/commercial/stats').then(r => r.json()).then(s => {{ | |
| document.getElementById('c_tot').textContent = s.total_models; | |
| document.getElementById('c_an').textContent = s.analyzed; | |
| document.getElementById('c_un').textContent = s.unknown; | |
| document.getElementById('c_cf').textContent = (s.buckets && s.buckets.commercial_friendly) || 0; | |
| document.getElementById('c_cp').textContent = (s.buckets && s.buckets.copyleft) || 0; | |
| document.getElementById('c_nc').textContent = (s.buckets && s.buckets.noncommercial) || 0; | |
| document.getElementById('c_nd').textContent = (s.buckets && s.buckets.no_derivatives) || 0; | |
| const wrap = document.getElementById('comBySpdx'); | |
| wrap.innerHTML = ''; | |
| Object.entries(s.by_spdx || {{}}).forEach(([k,v]) => {{ | |
| const d = document.createElement('div'); d.className='row'; | |
| d.innerHTML = `<span>${{k}}</span><span>${{v}}</span>`; | |
| wrap.appendChild(d); | |
| }}); | |
| }}).catch(()=>{{}}); | |
| }} | |
| function toggleLog() {{ | |
| const p = document.getElementById('logPanel'); | |
| p.style.display = (p.style.display === 'block') ? 'none' : 'block'; | |
| if (p.style.display === 'block') refreshLog(); | |
| }} | |
| function refreshLog() {{ | |
| fetch('/licenses/stats').then(r=>r.json()).then(s => {{ | |
| document.getElementById('tot').textContent = s.total; | |
| document.getElementById('kn').textContent = s.known; | |
| document.getElementById('un').textContent = s.unknown; | |
| const bl = document.getElementById('byLabel'); | |
| bl.innerHTML = ''; | |
| Object.entries(s.by_label || {{}}).sort((a,b)=>b[1]-a[1]).forEach(([k,v]) => {{ | |
| const d = document.createElement('div'); d.className='row'; | |
| d.innerHTML = `<span>${{k}}</span><span>${{v}}</span>`; | |
| bl.appendChild(d); | |
| }}); | |
| }}).catch(()=>{{}}); | |
| }} | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| def page(n: int): | |
| if n < 1: | |
| raise HTTPException(status_code=400, detail="Page must be >= 1") | |
| idx = n - 1 | |
| ensure_page_loaded(idx) | |
| pages = get_pages() | |
| if idx >= len(pages): | |
| raise HTTPException(status_code=404, detail="No more pages.") | |
| models = pages[idx]["results"] | |
| return HTMLResponse(_shell(idx, _render_table(models))) | |
| def build_all(): | |
| if is_complete(): | |
| return JSONResponse({"started": False, "reason": "complete"}, status_code=200) | |
| threading.Thread(target=build_all_pages, daemon=True).start() | |
| return JSONResponse({"started": True}, status_code=202) | |
| def status(): | |
| return JSONResponse(get_state()) | |
| def search(q: str = ""): | |
| idx = 0 | |
| base = get_pages()[0]["results"] if get_pages() else [] | |
| if is_complete() and q.strip(): | |
| results = search_models(q) | |
| content = _render_table(results) | |
| return HTMLResponse(_shell(idx, content, search_query=q)) | |
| else: | |
| content = _render_table(base) | |
| return HTMLResponse(_shell(idx, content, search_query=q)) |