PeacebinfLow's picture
Update app.py
e7e5b76 verified
import os
import json
import glob
import gradio as gr
from huggingface_hub import snapshot_download
DATASET_REPO_ID = "PeacebinfLow/mindseye-android-os-data" # change if needed
def load_apps_from_dataset():
local_dir = snapshot_download(
repo_id=DATASET_REPO_ID,
repo_type="dataset",
local_dir="dataset_cache",
local_dir_use_symlinks=False
)
app_files = glob.glob(os.path.join(local_dir, "apps", "*", "*.json"))
apps = []
errors = []
for fp in app_files:
try:
with open(fp, "r", encoding="utf-8") as f:
obj = json.load(f)
# minimal sanity check
if "id" in obj and "name" in obj and "category" in obj:
apps.append(obj)
else:
errors.append(f"Missing required fields in: {fp}")
except Exception as e:
errors.append(f"{fp}: {e}")
# stable ordering
apps.sort(key=lambda a: (a.get("category", ""), a.get("name", "")))
return apps, errors
def build_html(apps, source_label="snapshot_download"):
apps_json = json.dumps(apps, ensure_ascii=False)
return f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>MindsEye Android OS</title>
<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
body {{
background: radial-gradient(1200px 700px at 50% 30%, rgba(167,139,250,0.25), transparent 60%),
linear-gradient(135deg, #0a0e1a 0%, #1a0b2e 50%, #0a0e1a 100%);
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
display:flex;
align-items:center;
justify-content:center;
padding: 24px;
color: #fff;
}}
.stage {{
width: 100%;
max-width: 1200px;
display:flex;
align-items:flex-end;
justify-content:center;
gap: 24px;
}}
.footer-bar {{
position: fixed;
left: 0; right: 0; bottom: 0;
padding: 10px 14px;
background: rgba(10,13,21,0.75);
backdrop-filter: blur(10px);
border-top: 1px solid rgba(255,255,255,0.08);
display:flex;
justify-content:center;
gap: 10px;
font-size: 12px;
color: rgba(255,255,255,0.7);
}}
.btn {{
border: 1px solid rgba(255,255,255,0.14);
background: rgba(255,255,255,0.06);
color: rgba(255,255,255,0.85);
padding: 10px 14px;
border-radius: 12px;
cursor:pointer;
transition: transform .15s ease, background .15s ease;
user-select:none;
}}
.btn:hover {{ transform: scale(1.02); background: rgba(255,255,255,0.09); }}
/* Phone */
.phone {{
width: 390px;
height: 860px;
border-radius: 52px;
background: linear-gradient(145deg, #1a1d2e, #0f1219);
box-shadow: 0 35px 90px rgba(0,0,0,0.55), inset 0 0 40px rgba(255,255,255,0.03);
border: 10px solid #0a0d15;
position: relative;
overflow: hidden;
}}
.notch {{
position:absolute;
top: 14px;
left: 50%;
transform: translateX(-50%);
width: 140px;
height: 34px;
background: #0a0d15;
border-radius: 0 0 22px 22px;
z-index: 20;
}}
/* Status bar */
.status-bar {{
position: relative;
z-index: 10;
padding: 40px 18px 12px;
display:flex;
align-items:center;
justify-content:space-between;
color: rgba(255,255,255,0.85);
font-size: 12.5px;
letter-spacing: .2px;
}}
.status-center {{
font-weight: 650;
opacity: 0.95;
}}
.status-right {{
display:flex;
align-items:center;
gap: 8px;
}}
/* Screens wrapper */
.screens {{
position: relative;
height: calc(100% - 120px);
overflow: hidden;
}}
.screen {{
position:absolute;
inset: 0;
padding: 18px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}}
.screen::-webkit-scrollbar {{ width: 4px; }}
.screen::-webkit-scrollbar-thumb {{ background: rgba(255,255,255,0.18); border-radius: 10px; }}
/* Transition states */
.screen.hidden {{
pointer-events:none;
opacity: 0;
}}
.anim-in-right {{
animation: inRight .30s ease-out;
}}
.anim-out-right {{
animation: outRight .30s ease-in;
}}
.anim-home {{
animation: homePop .22s ease-out;
}}
@keyframes inRight {{
from {{ transform: translateX(18%); opacity: 0; }}
to {{ transform: translateX(0%); opacity: 1; }}
}}
@keyframes outRight {{
from {{ transform: translateX(0%); opacity: 1; }}
to {{ transform: translateX(18%); opacity: 0; }}
}}
@keyframes homePop {{
from {{ transform: scale(.98); opacity: .65; }}
to {{ transform: scale(1); opacity: 1; }}
}}
/* Launcher header */
.hero {{
display:flex;
flex-direction:column;
align-items:center;
gap: 10px;
padding-top: 6px;
padding-bottom: 14px;
}}
.hero-icon {{
width: 78px;
height: 78px;
border-radius: 20px;
background: linear-gradient(135deg, #a78bfa, #22d3ee);
display:flex;
align-items:center;
justify-content:center;
font-size: 34px;
box-shadow: 0 14px 30px rgba(167,139,250,0.25);
border: 1px solid rgba(255,255,255,0.12);
}}
.hero h1 {{
font-size: 28px;
font-weight: 800;
line-height: 1;
}}
.hero p {{
font-size: 13px;
color: rgba(255,255,255,0.6);
}}
.search {{
width: 100%;
margin-top: 8px;
background: rgba(255,255,255,0.07);
border: 1px solid rgba(255,255,255,0.10);
border-radius: 14px;
padding: 12px 14px;
display:flex;
gap: 10px;
align-items:center;
color: rgba(255,255,255,0.75);
}}
.search input {{
width: 100%;
background: transparent;
border: none;
outline: none;
color: rgba(255,255,255,0.90);
font-size: 13px;
}}
.section-title {{
margin-top: 16px;
margin-bottom: 10px;
display:flex;
align-items:center;
gap: 10px;
font-size: 11px;
letter-spacing: 1px;
text-transform: uppercase;
color: rgba(255,255,255,0.55);
font-weight: 650;
}}
.badge {{
font-size: 10px;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,0.10);
background: rgba(255,255,255,0.07);
color: rgba(255,255,255,0.75);
}}
.grid {{
display:grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}}
.app-btn {{
border: none;
background: transparent;
cursor:pointer;
display:flex;
flex-direction:column;
align-items:center;
gap: 8px;
padding: 8px 4px;
border-radius: 14px;
transition: transform .15s ease, background .15s ease;
}}
.app-btn:hover {{ background: rgba(167,139,250,0.10); }}
.app-btn:active {{ transform: scale(.96); }}
.icon {{
width: 58px;
height: 58px;
border-radius: 18px;
display:flex;
align-items:center;
justify-content:center;
font-size: 22px;
border: 1px solid rgba(255,255,255,0.12);
box-shadow: 0 10px 24px rgba(0,0,0,0.35);
}}
.name {{
font-size: 11px;
color: rgba(255,255,255,0.92);
text-align:center;
line-height: 1.15;
font-weight: 600;
}}
/* App screen */
.app-header {{
display:flex;
align-items:center;
gap: 12px;
padding: 6px 4px 10px;
}}
.app-header .icon {{
width: 54px; height: 54px;
}}
.app-title {{
display:flex;
flex-direction:column;
gap: 2px;
}}
.app-title h2 {{
font-size: 18px;
font-weight: 800;
line-height: 1.1;
}}
.app-title .sub {{
font-size: 12px;
color: rgba(255,255,255,0.55);
}}
.card {{
margin-top: 12px;
padding: 14px;
border-radius: 16px;
border: 1px solid rgba(167,139,250,0.22);
background: rgba(15, 23, 42, 0.55);
backdrop-filter: blur(10px);
}}
.card h3 {{
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
color: rgba(255,255,255,0.65);
margin-bottom: 8px;
}}
.card p {{
font-size: 13px;
color: rgba(255,255,255,0.78);
line-height: 1.45;
}}
.list {{
margin-top: 8px;
padding-left: 18px;
color: rgba(255,255,255,0.75);
font-size: 13px;
line-height: 1.5;
}}
.chips {{
display:flex;
flex-wrap:wrap;
gap: 8px;
margin-top: 10px;
}}
.chip {{
border: 1px solid rgba(255,255,255,0.14);
background: rgba(255,255,255,0.06);
padding: 8px 10px;
border-radius: 999px;
font-size: 12px;
color: rgba(255,255,255,0.85);
cursor:pointer;
transition: transform .15s ease, background .15s ease;
user-select:none;
}}
.chip:hover {{ transform: scale(1.02); background: rgba(34,211,238,0.10); }}
.repo-link {{
display:block;
margin-top: 10px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(34,211,238,0.22);
background: rgba(34,211,238,0.08);
color: rgba(255,255,255,0.88);
text-decoration:none;
font-size: 13px;
}}
.repo-link:hover {{
background: rgba(34,211,238,0.12);
}}
/* Nav bar */
.nav {{
position:absolute;
left:0; right:0; bottom:0;
height: 78px;
background: rgba(10, 13, 21, 0.92);
border-top: 1px solid rgba(255,255,255,0.06);
display:flex;
align-items:center;
justify-content:space-around;
padding: 0 28px;
z-index: 15;
backdrop-filter: blur(16px);
}}
.nav .navbtn {{
width: 44px;
height: 44px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,0.14);
background: rgba(255,255,255,0.07);
color: rgba(255,255,255,0.88);
cursor:pointer;
transition: transform .15s ease, background .15s ease;
display:flex;
align-items:center;
justify-content:center;
font-size: 18px;
user-select:none;
}}
.nav .navbtn:hover {{
transform: scale(1.05);
background: rgba(255,255,255,0.10);
}}
.nav .home {{
background: linear-gradient(135deg, rgba(167,139,250,0.95), rgba(34,211,238,0.85));
border: 1px solid rgba(255,255,255,0.16);
color: #0b1020;
font-weight: 900;
}}
/* Recents */
.recent-card {{
border: 1px solid rgba(255,255,255,0.12);
background: rgba(255,255,255,0.06);
border-radius: 18px;
padding: 14px;
margin-top: 12px;
cursor:pointer;
transition: transform .15s ease, background .15s ease;
}}
.recent-card:hover {{
transform: scale(1.01);
background: rgba(167,139,250,0.10);
}}
/* Color map */
.c-purple {{ background: linear-gradient(135deg, #a78bfa, #7c3aed); }}
.c-cyan {{ background: linear-gradient(135deg, #22d3ee, #3b82f6); }}
.c-amber {{ background: linear-gradient(135deg, #fbbf24, #f59e0b); }}
.c-emerald{{ background: linear-gradient(135deg, #10b981, #059669); }}
.c-rose {{ background: linear-gradient(135deg, #fb7185, #e11d48); }}
.c-indigo {{ background: linear-gradient(135deg, #818cf8, #4f46e5); }}
.c-orange {{ background: linear-gradient(135deg, #fb923c, #f97316); }}
.c-violet {{ background: linear-gradient(135deg, #8b5cf6, #7c3aed); }}
.c-blue {{ background: linear-gradient(135deg, #60a5fa, #2563eb); }}
.c-teal {{ background: linear-gradient(135deg, #2dd4bf, #0d9488); }}
</style>
</head>
<body>
<div class="stage">
<div class="phone" id="phone">
<div class="notch"></div>
<div class="status-bar">
<div id="timeText">--:--</div>
<div class="status-center" id="statusTitle">MindsEye</div>
<div class="status-right">
<span>📶</span>
<span id="batteryText">87%</span>
</div>
</div>
<div class="screens">
<div class="screen" id="launcherScreen"></div>
<div class="screen hidden" id="appScreen"></div>
<div class="screen hidden" id="recentsScreen"></div>
</div>
<div class="nav">
<div class="navbtn" id="btnBack" title="Back">◀</div>
<div class="navbtn home" id="btnHome" title="Home">⬤</div>
<div class="navbtn" id="btnRecents" title="Recents">▢</div>
</div>
</div>
</div>
<div class="footer-bar">
<button class="btn" id="refreshBtn">Refresh Dataset</button>
<div>Loaded {len(apps)} apps • source: {source_label}</div>
</div>
<script>
window.MINDSEYE_APPS = {apps_json};
</script>
<script>
// ------------ STATE ------------
const state = {{
current: "home", // "home" | "app:<id>" | "recents"
backstack: [], // stack of previous screens
recents: [], // list of appIds, most recent first
scroll: new Map(), // key -> scrollTop
query: "",
}};
const apps = window.MINDSEYE_APPS || [];
const appById = new Map(apps.map(a => [a.id, a]));
const categoriesOrder = [
"google-integration",
"core-system",
"binary-runtime",
"protocol",
"mindscript",
"sql-cloud"
];
const categoryLabels = {{
"google-integration": "GOOGLE INTEGRATION",
"core-system": "CORE SYSTEM",
"binary-runtime": "BINARY & RUNTIME",
"protocol": "PROTOCOL",
"mindscript": "MINDSCRIPT",
"sql-cloud": "SQL & CLOUD"
}};
// ------------ DOM ------------
const launcherScreen = document.getElementById("launcherScreen");
const appScreen = document.getElementById("appScreen");
const recentsScreen = document.getElementById("recentsScreen");
const statusTitle = document.getElementById("statusTitle");
const btnBack = document.getElementById("btnBack");
const btnHome = document.getElementById("btnHome");
const btnRecents = document.getElementById("btnRecents");
const refreshBtn = document.getElementById("refreshBtn");
// ------------ UTILS ------------
function colorClass(color) {{
const c = (color || "").toLowerCase();
const map = {{
purple: "c-purple",
cyan: "c-cyan",
amber: "c-amber",
emerald: "c-emerald",
rose: "c-rose",
indigo: "c-indigo",
orange: "c-orange",
violet: "c-violet",
blue: "c-blue",
teal: "c-teal"
}};
return map[c] || "c-purple";
}}
function setTimeLive() {{
const t = new Date();
const hh = String(t.getHours()).padStart(2,"0");
const mm = String(t.getMinutes()).padStart(2,"0");
document.getElementById("timeText").textContent = `${{hh}}:${{mm}}`;
}}
setTimeLive();
setInterval(setTimeLive, 1000 * 10);
// ------------ RENDER: LAUNCHER ------------
function renderLauncher() {{
statusTitle.textContent = "MindsEye";
const q = state.query.trim().toLowerCase();
const filtered = apps.filter(a => {{
if (!q) return true;
return (
(a.name || "").toLowerCase().includes(q) ||
(a.id || "").toLowerCase().includes(q) ||
(a.description || "").toLowerCase().includes(q) ||
(a.tags || []).join(" ").toLowerCase().includes(q)
);
}});
const grouped = new Map();
for (const cat of categoriesOrder) grouped.set(cat, []);
for (const a of filtered) {{
const cat = a.category || "other";
if (!grouped.has(cat)) grouped.set(cat, []);
grouped.get(cat).push(a);
}}
// header + search + sections
let html = `
<div class="hero">
<div class="hero-icon">🧠</div>
<h1>MindsEye OS</h1>
<p>${{apps.length}}+ Cognitive Automation Modules</p>
<div class="search">
<span>🔍</span>
<input id="searchInput" placeholder="Search apps..." value="${{escapeHtml(state.query)}}" />
</div>
</div>
`;
for (const cat of categoriesOrder) {{
const list = grouped.get(cat) || [];
if (!list.length) continue;
html += `
<div class="section-title">
${{categoryLabels[cat] || cat}}
<span class="badge">${{list.length}}</span>
</div>
<div class="grid">
${{list.map(a => `
<button class="app-btn" data-open-app="${{a.id}}">
<div class="icon ${{colorClass(a.color)}}">${{escapeHtml(a.icon || "⬡")}}</div>
<div class="name">${{escapeHtml(a.name || a.id)}}</div>
</button>
`).join("")}}
</div>
`;
}}
launcherScreen.innerHTML = html;
const input = document.getElementById("searchInput");
input.addEventListener("input", (e) => {{
state.query = e.target.value || "";
// keep user in launcher while typing
renderLauncher();
}});
}}
// ------------ RENDER: APP SCREEN ------------
function renderApp(appId) {{
const app = appById.get(appId);
if (!app) return;
statusTitle.textContent = app.name || "App";
const conns = Array.isArray(app.connections) ? app.connections : [];
const features = Array.isArray(app.features) ? app.features : [];
const tags = Array.isArray(app.tags) ? app.tags : [];
const connChips = conns
.filter(c => c && c.to && appById.has(c.to))
.map(c => {{
const target = appById.get(c.to);
return `<div class="chip" data-open-app="${{c.to}}">
${{escapeHtml(target.name || c.to)}} • ${{escapeHtml(c.type || "link")}}
</div>`;
}})
.join("");
const tagsChips = tags.slice(0, 10).map(t => `<div class="chip" style="cursor:default;">#${{escapeHtml(t)}}</div>`).join("");
appScreen.innerHTML = `
<div class="app-header">
<div class="icon ${{colorClass(app.color)}}">${{escapeHtml(app.icon || "⬡")}}</div>
<div class="app-title">
<h2>${{escapeHtml(app.name || app.id)}}</h2>
<div class="sub">${{escapeHtml(app.category || "")}} • v${{escapeHtml(app.version || "1.0.0")}}</div>
</div>
</div>
<div class="card">
<h3>Overview</h3>
<p>${{escapeHtml(app.description || "No description yet.")}}</p>
${{features.length ? `
<ul class="list">
${{features.map(f => `<li>${{escapeHtml(f)}}</li>`).join("")}}
</ul>
` : ""}}
${{app.github_repo ? `
<a class="repo-link" href="${{app.github_repo}}" target="_blank" rel="noreferrer">
Open GitHub Repo →
</a>
` : ""}}
</div>
<div class="card">
<h3>Connections</h3>
<p>Tap a connection to navigate through the ecosystem (Android-style app jumping).</p>
<div class="chips">
${{connChips || `<span style="color:rgba(255,255,255,.55);font-size:13px;">No connections defined.</span>`}}
</div>
</div>
<div class="card">
<h3>Tags</h3>
<div class="chips">
${{tagsChips || `<span style="color:rgba(255,255,255,.55);font-size:13px;">No tags yet.</span>`}}
</div>
</div>
`;
// restore scroll if known
const key = `app:${{appId}}`;
if (state.scroll.has(key)) {{
appScreen.scrollTop = state.scroll.get(key);
}} else {{
appScreen.scrollTop = 0;
}}
}}
// ------------ RENDER: RECENTS ------------
function renderRecents() {{
statusTitle.textContent = "Recents";
const items = state.recents
.filter(id => appById.has(id))
.slice(0, 5);
let html = `<div class="hero" style="padding-top:10px;">
<h1 style="font-size:22px;">Recent Apps</h1>
<p>Last opened apps. Tap to reopen.</p>
</div>`;
if (!items.length) {{
html += `<div class="card"><p>No recent apps yet. Open something first.</p></div>`;
}} else {{
for (const id of items) {{
const a = appById.get(id);
html += `
<div class="recent-card" data-open-app="${{id}}">
<div style="display:flex;align-items:center;gap:12px;">
<div class="icon ${{colorClass(a.color)}}" style="width:52px;height:52px;">${{escapeHtml(a.icon || "⬡")}}</div>
<div>
<div style="font-weight:800;">${{escapeHtml(a.name || a.id)}}</div>
<div style="font-size:12px;color:rgba(255,255,255,0.6);margin-top:2px;">
${{escapeHtml(a.category || "")}} • v${{escapeHtml(a.version || "1.0.0")}}
</div>
</div>
</div>
</div>
`;
}}
}}
recentsScreen.innerHTML = html;
const key = "recents";
if (state.scroll.has(key)) {{
recentsScreen.scrollTop = state.scroll.get(key);
}} else {{
recentsScreen.scrollTop = 0;
}}
}}
// ------------ NAVIGATION CORE ------------
function showScreen(which, animClass=null) {{
// save scroll positions
saveScroll();
// hide all
launcherScreen.classList.add("hidden");
appScreen.classList.add("hidden");
recentsScreen.classList.add("hidden");
// clear animations
launcherScreen.classList.remove("anim-in-right","anim-out-right","anim-home");
appScreen.classList.remove("anim-in-right","anim-out-right","anim-home");
recentsScreen.classList.remove("anim-in-right","anim-out-right","anim-home");
const el = which === "home" ? launcherScreen : (which === "recents" ? recentsScreen : appScreen);
el.classList.remove("hidden");
if (animClass) el.classList.add(animClass);
}}
function saveScroll() {{
// current screen scroll memory
if (state.current === "home") {{
state.scroll.set("home", launcherScreen.scrollTop);
}} else if (state.current === "recents") {{
state.scroll.set("recents", recentsScreen.scrollTop);
}} else if (state.current.startsWith("app:")) {{
state.scroll.set(state.current, appScreen.scrollTop);
}}
}}
function openApp(appId) {{
// push current to backstack
state.backstack.push(state.current);
state.current = `app:${{appId}}`;
// recents update
state.recents = [appId, ...state.recents.filter(x => x !== appId)];
state.recents = state.recents.slice(0, 10);
renderApp(appId);
showScreen("app", "anim-in-right");
}}
function goHome() {{
state.current = "home";
// optional: keep backstack? Android typically clears to home but back still returns to prior task sometimes.
// We'll keep it simple: clearing backstack = clean UX for portfolio.
state.backstack = [];
renderLauncher();
showScreen("home", "anim-home");
// restore scroll
if (state.scroll.has("home")) launcherScreen.scrollTop = state.scroll.get("home");
}}
function goRecents() {{
state.backstack.push(state.current);
state.current = "recents";
renderRecents();
showScreen("recents", "anim-in-right");
}}
function goBack() {{
if (!state.backstack.length) {{
// if you're on app screen with no history, go home
if (state.current !== "home") return goHome();
return;
}}
const prev = state.backstack.pop();
state.current = prev;
if (prev === "home") {{
renderLauncher();
showScreen("home", "anim-out-right");
if (state.scroll.has("home")) launcherScreen.scrollTop = state.scroll.get("home");
}} else if (prev === "recents") {{
renderRecents();
showScreen("recents", "anim-out-right");
}} else if (prev.startsWith("app:")) {{
const id = prev.split(":")[1];
renderApp(id);
showScreen("app", "anim-out-right");
}}
}}
// ------------ EVENT DELEGATION ------------
document.addEventListener("click", (e) => {{
const t = e.target.closest("[data-open-app]");
if (t) {{
const id = t.getAttribute("data-open-app");
if (id) openApp(id);
}}
}});
btnBack.addEventListener("click", goBack);
btnHome.addEventListener("click", goHome);
btnRecents.addEventListener("click", goRecents);
// "Refresh Dataset" - hard reload is simplest in Spaces
refreshBtn.addEventListener("click", () => window.location.reload());
// ------------ GESTURES (Android-ish) ------------
// Swipe from left edge -> back
// Swipe up from bottom -> home
let startX = 0, startY = 0, startT = 0, tracking = false;
const phone = document.getElementById("phone");
phone.addEventListener("touchstart", (e) => {{
const touch = e.touches[0];
startX = touch.clientX;
startY = touch.clientY;
startT = Date.now();
tracking = true;
}}, {{passive:true}});
phone.addEventListener("touchend", (e) => {{
if (!tracking) return;
tracking = false;
const touch = e.changedTouches[0];
const dx = touch.clientX - startX;
const dy = touch.clientY - startY;
const dt = Date.now() - startT;
// thresholds
const LEFT_EDGE = 50;
const BACK_SWIPE = 80;
const HOME_SWIPE = 90;
// back: start from left edge and swipe right
if (startX <= LEFT_EDGE && dx >= BACK_SWIPE && Math.abs(dy) < 90 && dt < 700) {{
goBack();
return;
}}
// home: swipe up from bottom area
const rect = phone.getBoundingClientRect();
const bottomZone = rect.bottom - 70;
if (startY >= bottomZone && (-dy) >= HOME_SWIPE && Math.abs(dx) < 120 && dt < 700) {{
goHome();
return;
}}
}}, {{passive:true}});
// ------------ HTML ESCAPE ------------
function escapeHtml(s) {{
return String(s ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}}
// ------------ INIT ------------
renderLauncher();
showScreen("home");
</script>
</body>
</html>
"""
with gr.Blocks() as demo:
apps, errors = load_apps_from_dataset()
html = build_html(apps)
gr.HTML(html)
if errors:
gr.Markdown("### Dataset Load Warnings")
gr.Markdown("```log\n" + "\n".join(errors) + "\n```")
demo.launch()