Duplicate / ui.py
Theflame47's picture
Update ui.py
5d874f1 verified
Raw
History Blame Contribute Delete
14.2 kB
# 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>
"""
@router.get("/page/{n}", response_class=HTMLResponse)
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)))
@router.post("/build_all")
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)
@router.get("/status")
def status():
return JSONResponse(get_state())
@router.get("/search", response_class=HTMLResponse)
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))