Models / app.py
benresnic's picture
Update app.py
75df9f8 verified
Raw History Blame Contribute Delete
62.8 kB
# app.py (Shiny for Python UI + Starlette JSON API, published state persisted to HF Dataset Hub)
from __future__ import annotations
import asyncio
import base64
import json
import os
import hmac
import io
from pathlib import Path
from uuid import uuid4
from urllib.request import Request as URLRequest, urlopen
from urllib.error import HTTPError, URLError
from shiny import App, ui
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.routing import Mount, Route
import smtplib
from email.message import EmailMessage
# -----------------------------------------------------------------------------
# HF Hub persistence (FREE): save/load cards_state.json from a dataset repo
# -----------------------------------------------------------------------------
# Required:
# - Space secret HF_TOKEN = write token
# Optional:
# - Space secret HF_STATE_REPO = "username/models_state"
# If omitted, we infer username from SPACE_ID and use "<user>/models_state"
# -----------------------------------------------------------------------------
HF_TOKEN = (os.environ.get("HF_TOKEN") or "").strip()
HF_STATE_FILE = (os.environ.get("HF_STATE_FILE") or "cards_state.json").strip()
HF_STATE_REPO_TYPE = "dataset"
SECTIONS = ("Models", "Methods", "Miscellaneous")
def _infer_owner_from_space() -> str:
sid = os.environ.get("SPACE_ID") or os.environ.get("HF_SPACE_ID") or ""
if isinstance(sid, str) and "/" in sid:
return sid.split("/", 1)[0].strip()
return ""
def _normalize_section(x) -> str:
s = str(x or "").strip().lower()
mapping = {
"models": "Models",
"model": "Models",
"methods": "Methods",
"method": "Methods",
"miscellaneous": "Miscellaneous",
"misc": "Miscellaneous",
}
return mapping.get(s, "Models")
def _notification_type(section: str) -> str:
s = _normalize_section(section)
return {
"Models": "Model",
"Methods": "Method",
"Miscellaneous": "Miscellaneous",
}.get(s, "Item")
_owner = _infer_owner_from_space()
HF_STATE_REPO = (os.environ.get("HF_STATE_REPO") or (f"{_owner}/models_state" if _owner else "")).strip()
HUB_ENABLED = bool(HF_TOKEN and HF_STATE_REPO)
try:
from huggingface_hub import hf_hub_download, HfApi, CommitOperationAdd
except Exception:
HUB_ENABLED = False
# -----------------------------------------------------------------------------
# Shared persistence for PUBLISHED cards (local cache file + Hub sync)
# -----------------------------------------------------------------------------
def _pick_state_dir() -> Path:
p = Path("/tmp")
p.mkdir(parents=True, exist_ok=True)
return p
STATE_DIR = _pick_state_dir()
STATE_PATH = STATE_DIR / "cards_state.json"
STATE_LOCK = asyncio.Lock()
SERVER_PASSWORD = (os.environ.get("password") or "").strip()
SMTP_HOST = os.environ.get("SMTP_HOST", "").strip()
SMTP_PORT = int(os.environ.get("SMTP_PORT", "587"))
SMTP_USER = os.environ.get("SMTP_USER", "").strip()
SMTP_PASS = os.environ.get("SMTP_PASS", "").strip()
SMTP_FROM = os.environ.get("SMTP_FROM", SMTP_USER).strip()
SMTP_TO = [x.strip() for x in os.environ.get("SMTP_TO", "").split(",") if x.strip()]
EMAIL_ENABLED = bool(SMTP_HOST and SMTP_USER and SMTP_PASS and SMTP_TO)
# -----------------------------------------------------------------------------
# ntfy notifications
# -----------------------------------------------------------------------------
# Optional secrets:
# - NTFY_URL (default: https://ntfy.sh)
# - NTFY_TOPIC (default: Models) -> publishes to https://ntfy.sh/Models
# - NTFY_TOKEN (Bearer auth, optional)
# - NTFY_USER / NTFY_PASS (Basic auth, optional)
# -----------------------------------------------------------------------------
NTFY_URL = (os.environ.get("NTFY_URL") or "https://ntfy.sh").strip().rstrip("/")
NTFY_TOPIC = (os.environ.get("NTFY_TOPIC") or "Models").strip().strip("/")
NTFY_TOKEN = (os.environ.get("NTFY_TOKEN") or "").strip()
NTFY_USER = (os.environ.get("NTFY_USER") or "").strip()
NTFY_PASS = (os.environ.get("NTFY_PASS") or "").strip()
NTFY_ENABLED = bool(NTFY_URL and NTFY_TOPIC)
def _send_email(subject: str, body: str) -> None:
if not EMAIL_ENABLED:
return
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = SMTP_FROM
msg["To"] = ", ".join(SMTP_TO)
msg.set_content(body)
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=20) as s:
s.starttls()
s.login(SMTP_USER, SMTP_PASS)
s.send_message(msg)
def _send_ntfy(title: str, body: str, *, tags: list[str] | None = None, priority: int | str | None = None) -> None:
if not NTFY_ENABLED:
return
try:
topic = NTFY_TOPIC.lstrip("/")
url = f"{NTFY_URL}/{topic}"
headers = {
"Content-Type": "text/plain; charset=utf-8",
"Title": title,
}
clean_tags = []
for t in (tags or []):
s = str(t or "").strip().replace(",", " ")
if s:
clean_tags.append(s)
if clean_tags:
headers["Tags"] = ",".join(clean_tags)
if priority is not None:
headers["Priority"] = str(priority)
if NTFY_TOKEN:
headers["Authorization"] = f"Bearer {NTFY_TOKEN}"
elif NTFY_USER and NTFY_PASS:
raw = f"{NTFY_USER}:{NTFY_PASS}".encode("utf-8")
headers["Authorization"] = "Basic " + base64.b64encode(raw).decode("ascii")
req = URLRequest(
url,
data=body.encode("utf-8"),
headers=headers,
method="POST",
)
with urlopen(req, timeout=20) as resp:
resp.read()
except (HTTPError, URLError, OSError):
pass
except Exception:
pass
def _hub_pull_into_local() -> None:
"""If local state missing, download cards_state.json from dataset repo."""
if not HUB_ENABLED:
return
if STATE_PATH.exists():
return
try:
downloaded = hf_hub_download(
repo_id=HF_STATE_REPO,
repo_type=HF_STATE_REPO_TYPE,
filename=HF_STATE_FILE,
token=HF_TOKEN,
)
STATE_PATH.write_text(Path(downloaded).read_text(encoding="utf-8"), encoding="utf-8")
except Exception:
pass
def _hub_push_state(state: dict) -> None:
"""Upload current state JSON to dataset repo."""
if not HUB_ENABLED:
return
try:
api = HfApi(token=HF_TOKEN)
content = json.dumps(state, indent=2, ensure_ascii=False).encode("utf-8")
op = CommitOperationAdd(
path_in_repo=HF_STATE_FILE,
path_or_fileobj=io.BytesIO(content),
)
api.create_commit(
repo_id=HF_STATE_REPO,
repo_type=HF_STATE_REPO_TYPE,
operations=[op],
commit_message="Update cards_state.json",
)
except Exception:
pass
def _normalize_tags(x) -> list[str]:
if x is None:
return []
if isinstance(x, str):
x = [t.strip() for t in x.split(",")]
if not isinstance(x, list):
return []
out: list[str] = []
for t in x:
if t is None:
continue
s = str(t).strip()
if s and s not in out:
out.append(s)
return out
def _new_published_card(payload: dict, *, card_id: str | None = None) -> dict:
return {
"id": card_id or str(uuid4())[:12],
"section": _normalize_section(payload.get("section")),
"model": str(payload.get("model") or "").strip(),
"tags": _normalize_tags(payload.get("tags")),
"how_it_works": str(payload.get("how_it_works") or "").strip(),
"use_cases": str(payload.get("use_cases") or "").strip(),
"link": str(payload.get("link") or "").strip(),
"author": str(payload.get("author") or "").strip(),
}
def _normalize_state(obj: dict) -> dict:
if not isinstance(obj, dict):
obj = {}
version = obj.get("version", 0)
if not isinstance(version, int):
version = 0
cards = obj.get("cards", [])
if not isinstance(cards, list):
cards = []
normalized = []
for c in cards:
if not isinstance(c, dict):
continue
normalized.append(
{
"id": str(c.get("id") or str(uuid4())[:12]),
"section": _normalize_section(c.get("section") or c.get("tab") or c.get("category")),
"model": str(c.get("model") or c.get("title") or ""),
"tags": _normalize_tags(c.get("tags") or c.get("categories") or []),
"how_it_works": str(c.get("how_it_works") or c.get("how") or ""),
"use_cases": str(c.get("use_cases") or c.get("use") or ""),
"link": str(c.get("link") or c.get("resource_url") or ""),
"author": str(c.get("author") or c.get("by") or ""),
}
)
return {"version": version, "cards": normalized}
def _read_state_sync() -> dict:
_hub_pull_into_local()
if not STATE_PATH.exists():
return {"version": 0, "cards": []}
try:
return _normalize_state(json.loads(STATE_PATH.read_text(encoding="utf-8")))
except Exception:
return {"version": 0, "cards": []}
def _write_state_sync(state: dict) -> None:
tmp = STATE_PATH.with_suffix(".tmp")
tmp.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8")
tmp.replace(STATE_PATH)
_hub_push_state(state)
async def get_state() -> dict:
async with STATE_LOCK:
return _read_state_sync()
async def set_state(state: dict) -> None:
async with STATE_LOCK:
_write_state_sync(state)
def _json(data: dict, status_code: int = 200) -> JSONResponse:
return JSONResponse(data, status_code=status_code, headers={"Cache-Control": "no-store"})
def _check_password(request: Request, payload: dict | None = None) -> bool:
if not SERVER_PASSWORD:
return False
provided = request.headers.get("X-Password")
if not provided and isinstance(payload, dict):
provided = payload.get("password")
if not isinstance(provided, str):
return False
return hmac.compare_digest(provided.strip(), SERVER_PASSWORD)
# -----------------------------------------------------------------------------
# API: published cards only
# -----------------------------------------------------------------------------
async def api_cards(request: Request) -> Response:
if request.method == "GET":
return _json(await get_state())
if request.method == "POST":
try:
payload = await request.json()
except Exception:
payload = {}
if not isinstance(payload, dict) or not _check_password(request, payload):
msg = (
"Unauthorized (check Space secret 'password')"
if SERVER_PASSWORD
else "Password not configured"
)
return _json({"error": msg}, status_code=401)
card = _new_published_card(payload)
if not card["model"] or not card["how_it_works"] or not card["use_cases"]:
return _json(
{"error": "Model, How it Works, and Use Cases are required"},
status_code=400,
)
state = await get_state()
state["cards"].append(card)
state["version"] += 1
await set_state(state)
if EMAIL_ENABLED:
subject = f"New {card.get('section', 'Models')} card posted: {card.get('model', '')}"
body = (
f"Section: {card.get('section','')}\n"
f"Model: {card.get('model','')}\n"
f"Tags: {', '.join(card.get('tags', []) or [])}\n"
f"By: {card.get('author','')}\n"
f"Link: {card.get('link','')}\n"
)
asyncio.create_task(asyncio.to_thread(_send_email, subject, body))
if NTFY_ENABLED:
item_type = _notification_type(card.get("section", "Models"))
maker = card.get("author", "").strip() or "Unknown"
name = card.get("model", "").strip() or "Untitled"
title = f"New {item_type} Added"
body = f"{name} by {maker}"
asyncio.create_task(
asyncio.to_thread(
_send_ntfy,
title,
body,
tags=["tada"],
priority=3,
)
)
return _json(state)
return _json({"error": "Method not allowed"}, status_code=405)
async def api_card_id(request: Request) -> Response:
card_id = request.path_params.get("card_id", "")
state = await get_state()
cards = state.get("cards", [])
idx = next((i for i, c in enumerate(cards) if c.get("id") == card_id), None)
if idx is None:
return _json({"error": "Card not found"}, status_code=404)
if request.method == "PUT":
try:
payload = await request.json()
except Exception:
payload = {}
if not isinstance(payload, dict) or not _check_password(request, payload):
msg = "Unauthorized (check Space secret 'password')" if SERVER_PASSWORD else "Password not configured"
return _json({"error": msg}, status_code=401)
updated = _new_published_card(payload, card_id=card_id)
if not updated["model"] or not updated["how_it_works"] or not updated["use_cases"]:
return _json({"error": "Model, How it Works, and Use Cases are required"}, status_code=400)
cards[idx] = updated
state["version"] += 1
await set_state(state)
return _json(state)
if request.method == "DELETE":
if not _check_password(request):
msg = "Unauthorized (check Space secret 'password')" if SERVER_PASSWORD else "Password not configured"
return _json({"error": msg}, status_code=401)
cards.pop(idx)
state["version"] += 1
await set_state(state)
return _json(state)
return _json({"error": "Method not allowed"}, status_code=405)
# -----------------------------------------------------------------------------
# UI
# -----------------------------------------------------------------------------
app_ui = ui.page_fluid(
ui.tags.style(
r"""
:root{
--navy:#062a4d;
--navy2:#041f3a;
--cardBorder:#d8e1ee;
--cardBg:#ffffff;
--muted:#6b778c;
--headerBg:#f3f6fb;
--good:#167d3a;
}
body { background: #f3f6fb; }
input:not([type="checkbox"]):not([type="radio"]),
select {
width: 100%;
box-sizing: border-box;
border: 1px solid #d8e1ee;
border-radius: 12px;
padding: 10px 12px;
font-size: 14px;
outline: none;
background: white;
}
input[readonly]:not([type="checkbox"]):not([type="radio"]) {
background:#f8fafc;
}
select[disabled]{
background:#f8fafc;
}
textarea{
width:100%;
box-sizing:border-box;
border: 1px solid #d8e1ee;
border-radius: 12px;
padding: 10px 12px;
font-size: 14px;
outline: none;
background: white;
resize: none;
overflow: auto;
line-height: 1.25;
}
textarea[readonly]{ background:#f8fafc; }
label{
display:block;
font-size: 12px;
color: var(--muted);
margin: 0 0 5px;
font-weight: 750;
letter-spacing: .2px;
}
.topbar{
background: linear-gradient(0deg, var(--navy2), var(--navy));
padding: 12px 14px;
border-radius: 0 0 14px 14px;
box-shadow: 0 2px 10px rgba(0,0,0,.12);
display:flex;
justify-content:space-between;
align-items:center;
gap: 12px;
margin-bottom: 8px;
}
.top-left{
display:flex;
align-items:center;
gap: 10px;
min-width: 0;
flex: 1 1 auto;
}
.chips-row{
display:flex;
gap: 8px;
align-items:center;
overflow-x:auto;
padding: 2px 4px;
min-width: 0;
flex: 1 1 auto;
}
.chips-row::-webkit-scrollbar{ display:none; }
.chip{
border: 1px solid rgba(255,255,255,.25);
background: rgba(255,255,255,.12);
color: #fff;
border-radius: 999px;
padding: 7px 10px;
font-size: 12px;
font-weight: 850;
cursor: pointer;
user-select:none;
white-space: nowrap;
}
.chip.active{
background: rgba(255,255,255,.95);
color: var(--navy);
border-color: rgba(0,0,0,.08);
}
#clear-filters{
border-radius: 12px;
padding: 9px 12px;
font-weight: 900;
font-size: 13px;
cursor:pointer;
white-space: nowrap;
background: rgba(255,255,255,.12);
color: #fff;
border: 1px solid rgba(255,255,255,.25);
}
#add-btn{
background: white;
color: var(--navy);
border: 1px solid rgba(0,0,0,.08);
border-radius: 12px;
padding: 12px 20px;
font-weight: 950;
font-size: 22px;
line-height: 1;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,.14);
flex: 0 0 auto;
}
.section-tabs{
display:flex;
gap: 10px;
padding: 0 16px 10px 16px;
flex-wrap: wrap;
}
.section-tab{
border: 1px solid #d8e1ee;
background: white;
color: #1f2a44;
border-radius: 999px;
padding: 9px 14px;
font-size: 13px;
font-weight: 900;
cursor:pointer;
}
.section-tab.active{
background: var(--navy);
color: white;
border-color: var(--navy);
}
.err{
margin: 0 16px 10px 16px;
padding: 10px 12px;
border-radius: 10px;
background: #fff1f2;
border: 1px solid #fecdd3;
color: #9f1239;
font-size: 13px;
display:none;
}
.cards-grid{
display:grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 22px;
padding: 10px 16px 22px 16px;
width: 100%;
margin: 0;
align-items:start;
}
@media (max-width: 900px){
.cards-grid{ grid-template-columns: 1fr; }
}
.card{
background: var(--cardBg);
border: 1px solid var(--cardBorder);
border-radius: 14px;
box-shadow: 0 1px 0 rgba(0,0,0,.02);
aspect-ratio: 1 / 1;
width: 100%;
overflow: hidden;
display:flex;
flex-direction:column;
min-height: 0;
}
.card-header{
background: var(--headerBg);
display:flex;
align-items:center;
justify-content:space-between;
gap: 10px;
padding: 10px 10px 8px 10px;
border-bottom: 1px solid #eef2f8;
flex: 0 0 auto;
}
.header-left{
display:flex;
align-items:center;
gap: 10px;
flex: 1 1 auto;
min-width: 0;
}
.title-input, .title-text{
flex: 1 1 auto;
min-width: 140px;
border: none;
outline: none;
background: transparent;
padding: 6px 8px;
border-radius: 10px;
font-size: 16px;
font-weight: 650;
color: #1f2a44;
}
.title-input::placeholder{ color: #7b879c; font-weight: 650; }
.card.has-title .title-input{ font-weight: 950; }
.title-input:focus{
background: rgba(255,255,255,.85);
border: 1px solid #d8e1ee;
}
.title-text{ font-weight: 950; }
.tag-bar{
display:flex;
gap: 6px;
align-items:center;
flex: 0 0 auto;
max-width: 52%;
overflow:auto;
padding-bottom: 2px;
}
.tag-bar::-webkit-scrollbar{ display:none; }
.tag-chip{
border: 1px solid #d8e1ee;
background: #fff;
color: #1f2a44;
border-radius: 999px;
padding: 6px 9px;
font-size: 11px;
font-weight: 850;
display:inline-flex;
align-items:center;
gap: 6px;
cursor:pointer;
white-space: nowrap;
}
.tag-chip .rm{ font-weight: 950; opacity: .6; }
.tag-btn{
border: 1px dashed #cfd9ea;
background: transparent;
border-radius: 999px;
padding: 6px 10px;
font-size: 11px;
font-weight: 900;
cursor:pointer;
white-space:nowrap;
}
.hdr-right{
display:flex;
align-items:center;
gap: 8px;
flex: 0 0 auto;
}
.section-pill{
font-size: 11px;
font-weight: 950;
padding: 6px 10px;
border-radius: 999px;
background: #edf4ff;
border: 1px solid #cfe0ff;
color: #1f4d8f;
user-select:none;
}
.draft-badge{
font-size: 11px;
font-weight: 950;
padding: 6px 10px;
border-radius: 999px;
background: #fff7ed;
border: 1px solid #fed7aa;
color: #9a3412;
user-select:none;
}
.post-btn, .edit-btn{
border-radius: 12px;
padding: 8px 12px;
font-weight: 950;
border: 1px solid rgba(0,0,0,.08);
cursor:pointer;
white-space:nowrap;
background: #fff;
color: #1f2a44;
}
.post-btn{
background: #eaf7ef;
color: var(--good);
}
.post-btn:disabled{ opacity: .55; cursor: not-allowed; }
.del-btn{
border: none;
background: transparent;
color: #7b879c;
font-weight: 900;
font-size: 20px;
cursor:pointer;
padding: 0 8px;
flex: 0 0 auto;
}
.card-main{
padding: 12px;
display:flex;
flex-direction:column;
gap: 12px;
flex: 1 1 auto;
min-height: 0;
overflow:hidden;
}
.panel{
display:flex;
flex-direction:column;
flex: 1 1 0;
min-height: 0;
}
.panel textarea{
flex: 1 1 0;
min-height: 0;
}
.card-footer{
padding: 10px 12px 12px 12px;
border-top: 1px solid #eef2f8;
flex: 0 0 auto;
}
.footer-row{
display:flex;
align-items:flex-end;
gap: 10px;
}
.section-col{
flex: 0 0 155px;
min-width: 130px;
}
.link-col{
flex: 1 1 auto;
min-width: 0;
}
.by-col{
flex: 0 0 auto;
display:flex;
align-items:center;
gap: 8px;
white-space: nowrap;
font-size: 12px;
font-weight: 900;
color:#1f2a44;
}
.by-input{
width: 160px;
padding: 8px 10px;
border-radius: 10px;
font-size: 12px;
}
.by-display{
width: 200px;
text-align:right;
font-size: 12px;
font-weight: 900;
color:#1f2a44;
overflow:hidden;
text-overflow: ellipsis;
}
.resource-link{ display:inline-block; margin-top: 8px; font-size: 13px; }
.empty{
margin: 0 16px;
padding: 18px;
background: white;
border: 1px dashed #cfd9ea;
border-radius: 12px;
color: #41516b;
}
.dlg-backdrop{
position: fixed;
inset: 0;
background: rgba(0,0,0,.35);
display:flex;
align-items:center;
justify-content:center;
z-index: 999999;
padding: 14px;
}
.dlg{
width: min(560px, calc(100vw - 28px));
max-height: 80vh;
background: white;
border-radius: 14px;
box-shadow: 0 10px 30px rgba(0,0,0,.25);
display:flex;
flex-direction:column;
overflow:hidden;
}
.dlg .head{
padding: 12px 14px 10px 14px;
border-bottom: 1px solid #eef2f8;
}
.dlg .head h3{ margin: 0 0 6px 0; font-size: 16px; }
.dlg .muted{ color: var(--muted); font-size: 13px; }
.dlg .body{
padding: 12px 14px;
overflow:auto;
}
.dlg .foot{
padding: 10px 14px 12px 14px;
border-top: 1px solid #eef2f8;
display:flex;
gap: 10px;
justify-content:flex-end;
}
.btn{
border-radius: 12px;
padding: 9px 12px;
font-weight: 900;
cursor:pointer;
border: 1px solid #d8e1ee;
background: #ffffff;
}
.btn-primary{
background:#eaf7ef;
border-color:#bbf7d0;
color: var(--good);
}
.btn-danger{
border: 1px solid #fecdd3;
background: #fff1f2;
color: #9f1239;
}
.btn[disabled]{ opacity: .55; cursor: not-allowed; }
.checklist{
display:flex;
flex-direction:column;
gap: 6px;
}
.checkrow{
display:flex;
align-items:center;
gap: 10px;
padding: 6px 8px;
border-radius: 10px;
}
.checkrow:hover{ background:#f6f8fe; }
.checkrow input[type="checkbox"]{ width:auto !important; }
.checkrow .lbl{
font-weight: 850;
font-size: 12px;
color:#1f2a44;
line-height: 1.1;
}
.row{
display:flex;
gap: 10px;
align-items:center;
margin-top: 10px;
}
@media (max-width: 900px){
.footer-row{
flex-wrap: wrap;
align-items:flex-start;
}
.section-col, .link-col, .by-col{
flex: 1 1 100%;
}
.by-display{
width: auto;
text-align: left;
}
}
"""
),
ui.tags.div(
{"class": "topbar"},
ui.tags.div(
{"class": "top-left"},
ui.tags.div({"id": "chips-row", "class": "chips-row"}),
ui.tags.button("Clear", {"id": "clear-filters", "type": "button"}),
),
ui.tags.button("+", {"id": "add-btn", "type": "button"}),
),
ui.tags.div({"id": "section-tabs", "class": "section-tabs"}),
ui.tags.div({"id": "err", "class": "err"}),
ui.tags.div({"id": "grid", "class": "cards-grid"}),
ui.tags.script(
r"""
(() => {
const DRAFTS_KEY = "model_cards_drafts_v6";
const SECTION_KEY = "model_cards_section_v1";
const grid = document.getElementById("grid");
const addBtn = document.getElementById("add-btn");
const errBox = document.getElementById("err");
const chipsRow = document.getElementById("chips-row");
const clearBtn = document.getElementById("clear-filters");
const sectionTabs = document.getElementById("section-tabs");
const SECTIONS = ["Models", "Methods", "Miscellaneous"];
const COMMON_TAGS = [
"Regression","Classification","Boosting","Bagging","Supervised","Unsupervised",
"Clustering","Dimensionality Reduction","Linear","Additive","Deep Learning",
"Neural Net","Transformer","Ensemble","Bayesian","Causal","Multilevel",
"NLP","Tree-based","Black Box"
];
const state = { version: null, published: [] };
let drafts = loadDrafts();
const saveTimers = new Map();
const filterTags = new Set();
let currentSection = normalizeSection(localStorage.getItem(SECTION_KEY) || "Models");
function normalizeSection(s){
const v = String(s || "").trim().toLowerCase();
if(v === "models" || v === "model") return "Models";
if(v === "methods" || v === "method") return "Methods";
if(v === "miscellaneous" || v === "misc") return "Miscellaneous";
return "Models";
}
function saveCurrentSection(){
localStorage.setItem(SECTION_KEY, currentSection);
}
function sectionOptions(selected){
const s = normalizeSection(selected);
return SECTIONS.map(x => (
`<option value="${escapeAttr(x)}" ${x === s ? "selected" : ""}>${escapeHtml(x)}</option>`
)).join("");
}
function showErr(msg){
if(!msg){ errBox.style.display="none"; errBox.textContent=""; return; }
errBox.style.display="block";
errBox.textContent = msg;
}
function escapeHtml(str){
return String(str ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function escapeAttr(str){
return String(str ?? "")
.replaceAll("&", "&amp;")
.replaceAll('"', "&quot;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
}
async function api(path, opts={}){
const res = await fetch(path, {
headers: { "Content-Type": "application/json", ...(opts.headers || {}) },
cache: "no-store",
...opts
});
const data = await res.json().catch(() => ({}));
if(!res.ok){
throw new Error(data?.error || ("HTTP " + res.status));
}
return data;
}
function uid(){
return "draft_" + (Date.now().toString(36) + Math.random().toString(36).slice(2, 8));
}
function loadDrafts(){
try{
const raw = localStorage.getItem(DRAFTS_KEY);
const arr = JSON.parse(raw || "[]");
if(!Array.isArray(arr)) return [];
return arr.map(d => ({
id: String(d.id || uid()),
section: normalizeSection(d.section || "Models"),
model: String(d.model || ""),
tags: Array.isArray(d.tags) ? d.tags.map(String) : [],
how_it_works: String(d.how_it_works || ""),
use_cases: String(d.use_cases || ""),
link: String(d.link || ""),
author: String(d.author || ""),
}));
}catch(e){
return [];
}
}
function saveDrafts(){
localStorage.setItem(DRAFTS_KEY, JSON.stringify(drafts));
}
function uniq(arr){
const out = [];
const seen = new Set();
for(const x of arr){
const s = String(x || "").trim();
if(!s) continue;
if(seen.has(s)) continue;
seen.add(s);
out.push(s);
}
return out;
}
function tagUniverse(){
const fromPublished = [];
const fromDrafts = [];
for(const c of state.published){
if(normalizeSection(c.section) !== currentSection) continue;
(c.tags || []).forEach(t => fromPublished.push(t));
}
for(const d of drafts){
if(normalizeSection(d.section) !== currentSection) continue;
(d.tags || []).forEach(t => fromDrafts.push(t));
}
return uniq([...COMMON_TAGS, ...fromPublished, ...fromDrafts]).sort((a,b)=>a.localeCompare(b));
}
function passesFilter(card){
if(normalizeSection(card.section) !== currentSection) return false;
if(filterTags.size === 0) return true;
const tags = new Set((card.tags || []).map(String));
for(const f of filterTags){
if(tags.has(f)) return true;
}
return false;
}
function renderSectionTabs(){
sectionTabs.innerHTML = SECTIONS.map(s => {
const active = s === currentSection ? "active" : "";
return `<button class="section-tab ${active}" data-section="${escapeAttr(s)}" type="button">${escapeHtml(s)}</button>`;
}).join("");
sectionTabs.querySelectorAll(".section-tab").forEach(btn => {
btn.addEventListener("click", (e) => {
e.preventDefault();
currentSection = normalizeSection(btn.getAttribute("data-section"));
saveCurrentSection();
renderSectionTabs();
renderFilterChips();
render();
});
});
}
function renderFilterChips(){
const all = tagUniverse();
chipsRow.innerHTML = all.map(t => {
const active = filterTags.has(t) ? "active" : "";
return `<span class="chip ${active}" data-tag="${escapeAttr(t)}">${escapeHtml(t)}</span>`;
}).join("");
chipsRow.querySelectorAll(".chip").forEach(ch => {
ch.addEventListener("click", () => {
const t = ch.getAttribute("data-tag");
if(filterTags.has(t)) filterTags.delete(t);
else filterTags.add(t);
renderFilterChips();
render();
});
});
}
clearBtn.addEventListener("click", (e) => {
e.preventDefault();
filterTags.clear();
renderFilterChips();
render();
});
function openDialog({title, subtitle, bodyHtml, primaryText, primaryClass, canSubmit, onSubmit, onOpen}){
const backdrop = document.createElement("div");
backdrop.className = "dlg-backdrop";
backdrop.innerHTML = `
<div class="dlg" role="dialog" aria-modal="true">
<div class="head">
<h3>${escapeHtml(title)}</h3>
<div class="muted">${escapeHtml(subtitle || "")}</div>
</div>
<div class="body">${bodyHtml}</div>
<div class="foot">
<button class="btn" data-act="cancel" type="button">Cancel</button>
<button class="btn ${primaryClass || ""}" data-act="primary" type="button" disabled>${escapeHtml(primaryText || "Confirm")}</button>
</div>
</div>
`;
document.body.appendChild(backdrop);
const btnCancel = backdrop.querySelector('[data-act="cancel"]');
const btnPrimary = backdrop.querySelector('[data-act="primary"]');
function close(){ backdrop.remove(); }
function refresh(){
try{
btnPrimary.disabled = !(canSubmit ? canSubmit(backdrop) : true);
}catch{
btnPrimary.disabled = true;
}
}
async function submit(){
if(btnPrimary.disabled) return;
try{
await onSubmit(backdrop);
}catch(e){
showErr(String(e.message || e));
return;
}
close();
}
btnCancel.addEventListener("click", (e) => { e.preventDefault(); close(); });
backdrop.addEventListener("click", (e) => { if(e.target === backdrop) close(); });
btnPrimary.addEventListener("click", (e) => { e.preventDefault(); submit(); });
backdrop.addEventListener("keydown", (e) => {
if(e.key === "Enter"){
const active = document.activeElement;
if(active && active.tagName === "TEXTAREA") return;
if(!btnPrimary.disabled){
e.preventDefault();
submit();
}
}
});
refresh();
if(onOpen) onOpen({backdrop, refresh, close});
return { close, refresh };
}
function openTagsDialog({selected, onApply}){
const all = tagUniverse();
const selectedSet = new Set(selected);
const listHtml = all.map(t => {
const checked = selectedSet.has(t) ? "checked" : "";
return `
<div class="checkrow">
<input type="checkbox" class="tg" data-tag="${escapeAttr(t)}" ${checked}/>
<div class="lbl">${escapeHtml(t)}</div>
</div>
`;
}).join("");
openDialog({
title: "Tags",
subtitle: "Check all that apply.",
bodyHtml: `
<div class="checklist">${listHtml}</div>
<div class="row">
<input id="custom" type="text" placeholder="Add custom tag…" />
<button class="btn" id="addCustom" type="button">Add</button>
</div>
`,
primaryText: "Apply",
primaryClass: "btn-primary",
canSubmit: () => true,
onSubmit: async (backdrop) => {
const checked = [];
backdrop.querySelectorAll(".tg").forEach(cb => {
if(cb.checked) checked.push(cb.getAttribute("data-tag"));
});
onApply(checked.filter(Boolean));
},
onOpen: ({backdrop, refresh}) => {
const custom = backdrop.querySelector("#custom");
const addBtn = backdrop.querySelector("#addCustom");
function addCustom(){
const t = (custom.value || "").trim();
if(!t) return;
const wrap = backdrop.querySelector(".checklist");
const row = document.createElement("div");
row.className = "checkrow";
row.innerHTML = `<input type="checkbox" class="tg" data-tag="${escapeAttr(t)}" checked/><div class="lbl">${escapeHtml(t)}</div>`;
wrap.prepend(row);
custom.value = "";
renderFilterChips();
refresh();
}
addBtn.addEventListener("click", (e) => { e.preventDefault(); addCustom(); });
custom.addEventListener("keydown", (e) => {
if(e.key === "Enter"){
e.preventDefault();
addCustom();
}
});
custom.focus();
}
});
}
function draftFilled(d){
return (d.model || "").trim().length > 0
&& (d.how_it_works || "").trim().length > 0
&& (d.use_cases || "").trim().length > 0;
}
function render(){
const visibleDrafts = drafts.filter(passesFilter);
const visiblePublished = state.published.filter(passesFilter);
if(visibleDrafts.length === 0 && visiblePublished.length === 0){
grid.innerHTML = `<div class="empty"><b>No cards in ${escapeHtml(currentSection)}.</b><div>Click the + to add a draft, then Post to publish.</div></div>`;
return;
}
grid.innerHTML = "";
// Drafts
visibleDrafts.forEach((d) => {
const card = document.createElement("div");
card.className = "card";
const modelText = (d.model || "").trim();
if(modelText) card.classList.add("has-title");
let tags = Array.isArray(d.tags) ? d.tags.map(String) : [];
card.innerHTML = `
<div class="card-header">
<div class="header-left">
<input class="title-input" type="text" value="${escapeAttr(d.model)}" placeholder="Model" />
<div class="tag-bar" style="display:${modelText ? "flex" : "none"};">
<span class="tags"></span>
<button class="tag-btn" type="button">Tags</button>
</div>
</div>
<div class="hdr-right">
<span class="section-pill">${escapeHtml(normalizeSection(d.section))}</span>
<span class="draft-badge">Draft</span>
<button class="post-btn" type="button" ${draftFilled(d) ? "" : "disabled"}>Post</button>
<button class="del-btn" type="button" title="Delete draft">×</button>
</div>
</div>
<div class="card-main">
<div class="panel">
<label>How it Works</label>
<textarea class="how">${escapeHtml(d.how_it_works)}</textarea>
</div>
<div class="panel">
<label>Use Cases</label>
<textarea class="use">${escapeHtml(d.use_cases)}</textarea>
</div>
</div>
<div class="card-footer">
<div class="footer-row">
<div class="section-col">
<label>Tab</label>
<select class="section">${sectionOptions(d.section)}</select>
</div>
<div class="link-col">
<label>Link</label>
<input type="text" class="link" value="${escapeAttr(d.link)}" placeholder="https://..." />
<div class="linkwrap"></div>
</div>
<div class="by-col">
<span>By:</span>
<input class="by-input" type="text" value="${escapeAttr(d.author)}" placeholder="name" />
</div>
</div>
</div>
`;
const titleEl = card.querySelector(".title-input");
const howEl = card.querySelector(".how");
const useEl = card.querySelector(".use");
const linkEl = card.querySelector(".link");
const byEl = card.querySelector(".by-input");
const sectionEl = card.querySelector(".section");
const linkWrap = card.querySelector(".linkwrap");
const tagBar = card.querySelector(".tag-bar");
const tagsWrap = card.querySelector(".tags");
const tagBtn = card.querySelector(".tag-btn");
const postBtn = card.querySelector(".post-btn");
const delBtn = card.querySelector(".del-btn");
const sectionPill = card.querySelector(".section-pill");
function updateLink(){
const url = (linkEl.value || "").trim();
linkWrap.innerHTML = url
? `<a class="resource-link" href="${escapeAttr(url)}" target="_blank" rel="noopener noreferrer">Open link</a>`
: "";
}
function getDraft(){
return drafts.find(x => x.id === d.id) || null;
}
function setDraft(patch){
const i = drafts.findIndex(x => x.id === d.id);
if(i === -1) return;
drafts[i] = { ...drafts[i], ...patch };
drafts[i].section = normalizeSection(drafts[i].section);
saveDrafts();
}
function updateHeader(){
const hasTitle = (titleEl.value || "").trim().length > 0;
card.classList.toggle("has-title", hasTitle);
tagBar.style.display = hasTitle ? "flex" : "none";
sectionPill.textContent = normalizeSection(sectionEl.value);
const cur = getDraft();
if(cur) postBtn.disabled = !draftFilled(cur);
}
function renderTags(){
tagsWrap.innerHTML = "";
tags.forEach(t => {
const chip = document.createElement("span");
chip.className = "tag-chip";
chip.innerHTML = `${escapeHtml(t)} <span class="rm">×</span>`;
chip.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
tags = tags.filter(x => x !== t);
setDraft({ tags });
renderTags();
renderFilterChips();
updateHeader();
});
tagsWrap.appendChild(chip);
});
}
function scheduleDraftSave(){
const existing = saveTimers.get(d.id);
if(existing) clearTimeout(existing);
saveTimers.set(d.id, setTimeout(() => {
setDraft({
section: normalizeSection(sectionEl.value || "Models"),
model: titleEl.value || "",
how_it_works: howEl.value || "",
use_cases: useEl.value || "",
link: linkEl.value || "",
author: byEl.value || "",
tags
});
updateLink();
updateHeader();
renderFilterChips();
}, 150));
}
tagBtn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
if(!(titleEl.value || "").trim()) return;
openTagsDialog({
selected: tags,
onApply: (next) => {
tags = next.slice();
setDraft({ tags });
renderTags();
renderFilterChips();
updateHeader();
}
});
});
postBtn.addEventListener("click", () => {
const cur = getDraft();
if(!cur || !draftFilled(cur)) return;
openDialog({
title: "Post this card?",
subtitle: "This will publish the card for everyone to see.",
bodyHtml: `
<label>Password</label>
<input type="password" id="pw" value="" />
`,
primaryText: "Post",
primaryClass: "btn-primary",
canSubmit: (b) => (b.querySelector("#pw").value || "").trim().length > 0,
onSubmit: async (b) => {
const password = (b.querySelector("#pw").value || "").trim();
const payload = {
password,
section: cur.section,
model: cur.model,
tags: cur.tags || [],
how_it_works: cur.how_it_works,
use_cases: cur.use_cases,
link: cur.link,
author: cur.author,
};
const data = await api("/api/cards", {
method: "POST",
body: JSON.stringify(payload),
headers: { "X-Password": password },
});
drafts = drafts.filter(x => x.id !== cur.id);
saveDrafts();
state.version = data.version;
state.published = data.cards;
showErr("");
renderSectionTabs();
renderFilterChips();
render();
},
onOpen: ({backdrop, refresh}) => {
const pw = backdrop.querySelector("#pw");
pw.focus();
pw.addEventListener("input", refresh);
}
});
});
delBtn.addEventListener("click", () => {
if(!confirm("Delete this draft? (Only on your computer.)")) return;
drafts = drafts.filter(x => x.id !== d.id);
saveDrafts();
renderSectionTabs();
renderFilterChips();
render();
});
[titleEl, howEl, useEl, linkEl, byEl].forEach(el => el.addEventListener("input", scheduleDraftSave));
sectionEl.addEventListener("change", () => {
setDraft({ section: normalizeSection(sectionEl.value) });
renderSectionTabs();
renderFilterChips();
render();
});
updateLink();
renderTags();
updateHeader();
grid.appendChild(card);
});
// Published
visiblePublished.forEach((c) => {
const card = document.createElement("div");
card.className = "card";
const tags = Array.isArray(c.tags) ? c.tags.map(String) : [];
const section = normalizeSection(c.section);
card.innerHTML = `
<div class="card-header">
<div class="header-left">
<div class="title-text">${escapeHtml(c.model || "")}</div>
<div class="tag-bar">
${tags.map(t => `<span class="tag-chip" style="cursor:default;">${escapeHtml(t)}</span>`).join("")}
</div>
</div>
<div class="hdr-right">
<span class="section-pill">${escapeHtml(section)}</span>
<button class="edit-btn" type="button">Edit</button>
<button class="del-btn" type="button" title="Delete published">×</button>
</div>
</div>
<div class="card-main">
<div class="panel">
<label>How it Works</label>
<textarea readonly>${escapeHtml(c.how_it_works || "")}</textarea>
</div>
<div class="panel">
<label>Use Cases</label>
<textarea readonly>${escapeHtml(c.use_cases || "")}</textarea>
</div>
</div>
<div class="card-footer">
<div class="footer-row">
<div class="section-col">
<label>Tab</label>
<select disabled>${sectionOptions(section)}</select>
</div>
<div class="link-col">
<label>Link</label>
<input readonly value="${escapeAttr(c.link || "")}" />
<div class="linkwrap"></div>
</div>
<div class="by-col">
<div class="by-display">${c.author ? ("By: " + escapeHtml(c.author)) : ""}</div>
</div>
</div>
</div>
`;
const linkWrap = card.querySelector(".linkwrap");
const editBtn = card.querySelector(".edit-btn");
const delBtn = card.querySelector(".del-btn");
const url = (c.link || "").trim();
linkWrap.innerHTML = url
? `<a class="resource-link" href="${escapeAttr(url)}" target="_blank" rel="noopener noreferrer">Open link</a>`
: "";
delBtn.addEventListener("click", () => {
openDialog({
title: "Delete published card?",
subtitle: "This is permanent and affects everyone viewing this Space.",
bodyHtml: `
<div class="checklist" style="margin-bottom:10px;">
<div class="checkrow">
<input type="checkbox" id="c1"/>
<div class="lbl">I understand this is permanent</div>
</div>
<div class="checkrow">
<input type="checkbox" id="c2"/>
<div class="lbl">I confirm I want to delete this card</div>
</div>
</div>
<label>Password</label>
<input type="password" id="pw" value="" />
`,
primaryText: "Delete",
primaryClass: "btn-danger",
canSubmit: (b) => b.querySelector("#c1").checked
&& b.querySelector("#c2").checked
&& (b.querySelector("#pw").value || "").trim().length > 0,
onSubmit: async (b) => {
const password = (b.querySelector("#pw").value || "").trim();
const data = await api(`/api/cards/${encodeURIComponent(c.id)}`, {
method: "DELETE",
headers: { "X-Password": password },
});
state.version = data.version;
state.published = data.cards || [];
showErr("");
renderSectionTabs();
renderFilterChips();
render();
},
onOpen: ({backdrop, refresh}) => {
const pw = backdrop.querySelector("#pw");
const c1 = backdrop.querySelector("#c1");
const c2 = backdrop.querySelector("#c2");
pw.focus();
[pw, c1, c2].forEach(el => el.addEventListener("input", refresh));
[c1, c2].forEach(el => el.addEventListener("change", refresh));
}
});
});
editBtn.addEventListener("click", () => {
let tagsLocal = tags.slice();
openDialog({
title: "Edit & Republish",
subtitle: "Edits go live only after you enter the password and republish.",
bodyHtml: `
<div style="display:grid; gap:10px;">
<div>
<label>Model</label>
<input id="m" type="text" value="${escapeAttr(c.model || "")}" />
</div>
<div>
<label>Tab</label>
<select id="s">${sectionOptions(section)}</select>
</div>
<div>
<label>Tags</label>
<div id="tags" style="display:flex; flex-wrap:wrap; gap:8px; align-items:center;"></div>
<div style="margin-top:8px;">
<button class="tag-btn" id="tagBtn" type="button">Tags</button>
</div>
</div>
<div>
<label>How it Works</label>
<textarea id="h" style="height:90px;">${escapeHtml(c.how_it_works || "")}</textarea>
</div>
<div>
<label>Use Cases</label>
<textarea id="u" style="height:90px;">${escapeHtml(c.use_cases || "")}</textarea>
</div>
<div class="footer-row" style="align-items:flex-end;">
<div class="link-col">
<label>Link</label>
<input id="l" type="text" value="${escapeAttr(c.link || "")}" />
</div>
<div class="by-col" style="padding-bottom:2px;">
<span>By:</span>
<input id="a" class="by-input" type="text" value="${escapeAttr(c.author || "")}" placeholder="name" />
</div>
</div>
<div>
<label>Password</label>
<input id="pw" type="password" value="" />
</div>
</div>
`,
primaryText: "Republish",
primaryClass: "btn-primary",
canSubmit: (b) => (b.querySelector("#pw").value || "").trim().length > 0
&& (b.querySelector("#m").value || "").trim().length > 0
&& (b.querySelector("#h").value || "").trim().length > 0
&& (b.querySelector("#u").value || "").trim().length > 0,
onSubmit: async (b) => {
const password = (b.querySelector("#pw").value || "").trim();
const payload = {
password,
section: normalizeSection(b.querySelector("#s").value || "Models"),
model: b.querySelector("#m").value || "",
tags: tagsLocal,
how_it_works: b.querySelector("#h").value || "",
use_cases: b.querySelector("#u").value || "",
link: b.querySelector("#l").value || "",
author: b.querySelector("#a").value || "",
};
const data = await api(`/api/cards/${encodeURIComponent(c.id)}`, {
method: "PUT",
body: JSON.stringify(payload),
headers: { "X-Password": password },
});
state.version = data.version;
state.published = data.cards || [];
showErr("");
renderSectionTabs();
renderFilterChips();
render();
},
onOpen: ({backdrop, refresh}) => {
const tagsWrap = backdrop.querySelector("#tags");
const tagBtn = backdrop.querySelector("#tagBtn");
const pw = backdrop.querySelector("#pw");
function renderEditTags(){
tagsWrap.innerHTML = "";
tagsLocal.forEach(t => {
const chip = document.createElement("span");
chip.className = "tag-chip";
chip.innerHTML = `${escapeHtml(t)} <span class="rm">×</span>`;
chip.addEventListener("click", (e) => {
e.preventDefault();
tagsLocal = tagsLocal.filter(x => x !== t);
renderEditTags();
renderFilterChips();
refresh();
});
tagsWrap.appendChild(chip);
});
}
tagBtn.addEventListener("click", (e) => {
e.preventDefault();
openTagsDialog({
selected: tagsLocal,
onApply: (next) => {
tagsLocal = next.slice();
renderEditTags();
renderFilterChips();
refresh();
}
});
});
["m","h","u","l","a","pw"].forEach(id => {
backdrop.querySelector("#" + id).addEventListener("input", refresh);
});
backdrop.querySelector("#s").addEventListener("change", refresh);
pw.focus();
renderEditTags();
refresh();
}
});
});
grid.appendChild(card);
});
}
addBtn.addEventListener("click", () => {
drafts.unshift({
id: uid(),
section: currentSection,
model:"",
tags:[],
how_it_works:"",
use_cases:"",
link:"",
author:""
});
saveDrafts();
renderSectionTabs();
renderFilterChips();
render();
});
async function refreshPublished(){
try{
const active = document.activeElement;
const isTyping = active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA" || active.tagName === "SELECT");
if(isTyping) return;
const data = await api("/api/cards", { method: "GET" });
if(data.version !== state.version){
state.version = data.version;
state.published = data.cards || [];
showErr("");
renderSectionTabs();
renderFilterChips();
render();
}
}catch(e){
showErr(String(e.message || e));
}
}
(async () => {
saveCurrentSection();
await refreshPublished();
renderSectionTabs();
renderFilterChips();
render();
setInterval(refreshPublished, 2500);
})();
})();
"""
),
)
def shiny_server(input, output, session):
pass
app_shiny = App(app_ui, shiny_server)
routes = [
Route("/api/cards", endpoint=api_cards, methods=["GET", "POST"]),
Route("/api/cards/{card_id}", endpoint=api_card_id, methods=["PUT", "DELETE"]),
Mount("/", app=app_shiny),
]
app = Starlette(routes=routes)