PeacebinfLow commited on
Commit
159c51a
·
verified ·
1 Parent(s): e78e671

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +646 -766
app.py CHANGED
@@ -1,126 +1,25 @@
1
  import os
2
  import json
3
- from datetime import datetime
4
- from typing import Any, Dict, List, Optional, Tuple
 
5
 
6
  import gradio as gr
7
  from huggingface_hub import snapshot_download
8
 
9
 
10
- # -----------------------------
11
  # Config
12
- # -----------------------------
13
- DATASET_REPO_ID = "PeacebinfLow/mindseye-android-os-data"
14
- DATASET_REPO_TYPE = "dataset"
15
-
16
- # If dataset missing / empty, Space still runs using fallback apps.
17
- FALLBACK_APPS = [
18
- {
19
- "id": "workspace-automation",
20
- "name": "Workspace Automation",
21
- "category": "google-integration",
22
- "version": "1.0.0",
23
- "icon": "workspace",
24
- "color": "purple",
25
- "github_repo": "https://github.com/PEACEBINFLOW/mindseye-workspace-automation",
26
- "metadata": {
27
- "description": "Automates Google Workspace activity into ledger events (Gmail, Drive, Calendar).",
28
- "tags": ["google", "workspace", "automation", "ledger"],
29
- "status": "active",
30
- },
31
- "features": [
32
- "Gmail event capture",
33
- "Drive change tracking",
34
- "Calendar timeline ingestion",
35
- "Ledger-first memory",
36
- ],
37
- "functions": {
38
- "launch": {"type": "app_launch", "action": "open_main_screen", "parameters": {"screen": "overview"}},
39
- "open_docs": {"type": "navigation_action", "action": "open_screen", "parameters": {"screen": "docs"}},
40
- },
41
- "connections": [
42
- {"to_app": "google-ledger", "type": "data_flow", "direction": "outbound", "description": "workspace events → ledger"}
43
- ],
44
- "ui": {"default_screen": "overview", "screens": ["overview", "functions", "connections", "docs"]},
45
- },
46
- {
47
- "id": "google-ledger",
48
- "name": "Google Ledger",
49
- "category": "google-integration",
50
- "version": "1.0.0",
51
- "icon": "ledger",
52
- "color": "cyan",
53
- "github_repo": "https://github.com/PEACEBINFLOW/mindseye-google-ledger",
54
- "metadata": {
55
- "description": "Central ledger for time-labeled organizational memory built from Google signals.",
56
- "tags": ["ledger", "memory", "google"],
57
- "status": "active",
58
- },
59
- "features": [
60
- "Time-labeled ledger entries",
61
- "Auditable event history",
62
- "Queryable memory",
63
- ],
64
- "functions": {
65
- "launch": {"type": "app_launch", "action": "open_main_screen", "parameters": {"screen": "overview"}},
66
- "search": {"type": "search_index", "action": "query_ledger", "parameters": {"scope": "all"}},
67
- },
68
- "connections": [
69
- {"to_app": "dashboard", "type": "query", "direction": "outbound", "description": "ledger → dashboard"}
70
- ],
71
- "ui": {"default_screen": "overview", "screens": ["overview", "functions", "connections"]},
72
- },
73
- {
74
- "id": "law-n-network",
75
- "name": "LAW-N Network",
76
- "category": "core-system",
77
- "version": "1.0.0",
78
- "icon": "network",
79
- "color": "violet",
80
- "github_repo": "https://github.com/PEACEBINFLOW/minds-eye-law-n-network",
81
- "metadata": {
82
- "description": "Network-native routing of signals and workflows across the system.",
83
- "tags": ["network", "routing", "signals", "law-n"],
84
- "status": "active",
85
- },
86
- "features": ["Signal routing", "Policy-aware transport", "Network-native intelligence"],
87
- "functions": {
88
- "launch": {"type": "app_launch", "action": "open_main_screen", "parameters": {"screen": "overview"}}
89
- },
90
- "connections": [
91
- {"to_app": "data-splitter", "type": "routing", "direction": "outbound", "description": "signals → splitter"}
92
- ],
93
- "ui": {"default_screen": "overview", "screens": ["overview", "connections"]},
94
- },
95
- ]
96
-
97
- FALLBACK_TUTORIALS = [
98
- {
99
- "id": "lesson-01",
100
- "title": "Lesson 01 — What is MindsEye?",
101
- "level": "beginner",
102
- "goal": "Explain ledger-first organizational memory and why it matters.",
103
- "sections": [
104
- {"type": "text", "content": "MindsEye records prompts, executions, decisions, and outcomes as immutable, time-labeled events."},
105
- {"type": "concepts", "items": ["ledger-first memory", "immutability", "auditability", "replayable intelligence"]},
106
- ],
107
- },
108
- {
109
- "id": "lesson-02",
110
- "title": "Lesson 02 — Google Integration",
111
- "level": "beginner",
112
- "goal": "Show how Workspace signals become structured ledger events.",
113
- "sections": [
114
- {"type": "text", "content": "Google apps produce signals. MindsEye converts those into ledger entries you can query and audit."},
115
- ],
116
- },
117
- ]
118
 
119
 
120
- # -----------------------------
121
- # Helpers: Loading dataset files
122
- # -----------------------------
123
- def _read_json(path: str) -> Optional[Dict[str, Any]]:
124
  try:
125
  with open(path, "r", encoding="utf-8") as f:
126
  return json.load(f)
@@ -128,730 +27,711 @@ def _read_json(path: str) -> Optional[Dict[str, Any]]:
128
  return None
129
 
130
 
131
- def _safe_listdir(path: str) -> List[str]:
132
- try:
133
- return os.listdir(path)
134
- except Exception:
135
- return []
136
-
137
-
138
- def load_dataset_snapshot(repo_id: str) -> Tuple[Dict[str, Any], List[Dict[str, Any]], List[Dict[str, Any]]]:
139
- """
140
- Returns: (meta, apps, tutorials)
141
- - meta contains categories, tags, version-info, connections graph if found
142
- - apps is a flat list of app documents
143
- - tutorials is a list of tutorial documents
144
- """
145
- meta: Dict[str, Any] = {
146
- "repo_id": repo_id,
147
- "loaded_from": "fallback",
148
- "loaded_at": datetime.utcnow().isoformat() + "Z",
149
- }
150
- apps: List[Dict[str, Any]] = []
151
- tutorials: List[Dict[str, Any]] = []
152
-
153
- try:
154
- local_dir = snapshot_download(repo_id=repo_id, repo_type=DATASET_REPO_TYPE)
155
- meta["loaded_from"] = "snapshot_download"
156
- meta["local_dir"] = local_dir
157
- except Exception as e:
158
- meta["error"] = f"snapshot_download failed: {e}"
159
- return meta, FALLBACK_APPS, FALLBACK_TUTORIALS
160
-
161
- # metadata
162
- metadata_dir = os.path.join(local_dir, "metadata")
163
- if os.path.isdir(metadata_dir):
164
- for name in ["categories.json", "tags.json", "version-info.json", "connections-graph.json"]:
165
- p = os.path.join(metadata_dir, name)
166
- if os.path.isfile(p):
167
- meta[name.replace(".json", "")] = _read_json(p)
168
-
169
- # tutorials
170
- tutorials_dir = os.path.join(local_dir, "tutorials")
171
- if os.path.isdir(tutorials_dir):
172
- for fname in _safe_listdir(tutorials_dir):
173
- if fname.endswith(".json"):
174
- doc = _read_json(os.path.join(tutorials_dir, fname))
175
- if isinstance(doc, dict):
176
- tutorials.append(doc)
177
-
178
- # apps (walk)
179
- apps_dir = os.path.join(local_dir, "apps")
180
- if os.path.isdir(apps_dir):
181
- for cat in _safe_listdir(apps_dir):
182
- cat_dir = os.path.join(apps_dir, cat)
183
- if not os.path.isdir(cat_dir):
184
- continue
185
- for fname in _safe_listdir(cat_dir):
186
- if not fname.endswith(".json"):
187
- continue
188
- doc = _read_json(os.path.join(cat_dir, fname))
189
- if isinstance(doc, dict) and doc.get("id") and doc.get("name"):
190
- apps.append(doc)
191
-
192
- # Fallback if empty
193
- if not apps:
194
- meta["warning"] = "No apps found in dataset. Using fallback apps."
195
- apps = FALLBACK_APPS
196
-
197
- if not tutorials:
198
- meta["warning_tutorials"] = "No tutorials found in dataset. Using fallback tutorials."
199
- tutorials = FALLBACK_TUTORIALS
200
-
201
- return meta, apps, tutorials
202
-
203
-
204
- def group_apps_by_category(apps: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
205
- grouped: Dict[str, List[Dict[str, Any]]] = {}
206
- for app in apps:
207
- cat = (app.get("category") or "other").strip()
208
- grouped.setdefault(cat, []).append(app)
209
- # sort apps by name
210
- for cat in grouped:
211
- grouped[cat] = sorted(grouped[cat], key=lambda x: (x.get("name") or "").lower())
212
- return grouped
213
-
214
-
215
- def find_app(apps: List[Dict[str, Any]], app_id: str) -> Optional[Dict[str, Any]]:
216
- for a in apps:
217
- if a.get("id") == app_id:
218
- return a
219
- return None
220
-
221
-
222
- def compact_json(obj: Any) -> str:
223
- return json.dumps(obj, indent=2, ensure_ascii=False)
224
-
225
-
226
- # -----------------------------
227
- # UI Rendering
228
- # -----------------------------
229
- ANDROID_CSS = """
230
- :root{
231
- --bg: #0b1220;
232
- --surface: #0f172a;
233
- --surface2: #111c33;
234
- --text: #e5e7eb;
235
- --muted: #94a3b8;
236
- --accent: #7c3aed;
237
- --good: #22c55e;
238
- --warn: #f59e0b;
239
- --bad: #ef4444;
240
- --card: rgba(255,255,255,0.06);
241
- --card2: rgba(255,255,255,0.09);
242
- --border: rgba(255,255,255,0.10);
243
- --shadow: 0 12px 30px rgba(0,0,0,0.35);
244
- }
245
-
246
- body {
247
- background: radial-gradient(1200px 600px at 20% 0%, rgba(124,58,237,0.22), transparent),
248
- radial-gradient(1200px 600px at 80% 20%, rgba(34,197,94,0.10), transparent),
249
- var(--bg) !important;
250
- }
251
-
252
- #android_shell {
253
- max-width: 980px;
254
- margin: 0 auto;
255
- }
256
-
257
- .status_bar {
258
- display:flex;
259
- align-items:center;
260
- justify-content:space-between;
261
- padding: 10px 14px;
262
- border: 1px solid var(--border);
263
- background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.03));
264
- border-radius: 18px;
265
- box-shadow: var(--shadow);
266
- }
267
-
268
- .status_left, .status_center, .status_right {
269
- display:flex;
270
- align-items:center;
271
- gap: 10px;
272
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
273
- color: var(--text);
274
- font-size: 13px;
275
- }
276
-
277
- .status_center {
278
- font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
279
- font-weight: 650;
280
- letter-spacing: 0.2px;
281
- }
282
-
283
- .pill {
284
- padding: 4px 10px;
285
- border: 1px solid var(--border);
286
- border-radius: 999px;
287
- background: rgba(255,255,255,0.05);
288
- color: var(--muted);
289
- }
290
-
291
- .launcher_grid {
292
- display:grid;
293
- grid-template-columns: repeat(4, minmax(0, 1fr));
294
- gap: 12px;
295
- margin-top: 14px;
296
- }
297
 
298
- @media (max-width: 640px){
299
- .launcher_grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
300
- }
301
 
302
- .app_tile {
303
- border: 1px solid var(--border);
304
- background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.03));
305
- border-radius: 18px;
306
- padding: 14px 12px;
307
- box-shadow: var(--shadow);
308
- cursor: pointer;
309
- transition: transform 120ms ease, background 120ms ease;
310
- }
311
 
312
- .app_tile:hover {
313
- transform: translateY(-2px);
314
- background: linear-gradient(180deg, rgba(255,255,255,0.10), rgba(255,255,255,0.04));
315
- }
316
 
317
- .app_icon {
318
- width: 44px;
319
- height: 44px;
320
- border-radius: 14px;
321
- display:flex;
322
- align-items:center;
323
- justify-content:center;
324
- font-weight: 800;
325
- letter-spacing: 0.3px;
326
- color: white;
327
- background: rgba(124,58,237,0.9);
328
- border: 1px solid rgba(255,255,255,0.12);
329
- }
 
330
 
331
- .app_name {
332
- margin-top: 10px;
333
- color: var(--text);
334
- font-weight: 700;
335
- font-size: 14px;
336
- line-height: 1.2;
337
- }
338
 
339
- .app_meta {
340
- margin-top: 6px;
341
- color: var(--muted);
342
- font-size: 12px;
343
- }
344
-
345
- .panel {
346
- margin-top: 14px;
347
- border: 1px solid var(--border);
348
- background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.03));
349
- border-radius: 18px;
350
- padding: 14px;
351
- box-shadow: var(--shadow);
352
- }
353
-
354
- .h2 {
355
- font-size: 16px;
356
- font-weight: 800;
357
- color: var(--text);
358
- margin: 0 0 10px 0;
359
- }
360
-
361
- .small {
362
- color: var(--muted);
363
- font-size: 12px;
364
- }
365
 
366
- .nav_bar {
367
- display:flex;
368
- gap: 10px;
369
- margin-top: 14px;
 
 
 
370
  }
371
 
372
- .nav_btn {
373
- flex: 1;
374
- border-radius: 14px !important;
375
- border: 1px solid var(--border) !important;
376
- background: rgba(255,255,255,0.06) !important;
377
- color: var(--text) !important;
378
- font-weight: 700 !important;
379
- }
380
 
381
- .nav_btn:hover {
382
- background: rgba(255,255,255,0.10) !important;
 
 
 
 
 
383
  }
384
 
385
- .kv {
386
- display:grid;
387
- grid-template-columns: 160px 1fr;
388
- gap: 8px 12px;
389
- font-size: 13px;
390
- }
391
 
392
- .k {
393
- color: var(--muted);
394
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
395
- }
396
-
397
- .v {
398
- color: var(--text);
399
- }
 
 
 
400
 
401
- code, pre {
402
- background: rgba(0,0,0,0.35) !important;
403
- border: 1px solid var(--border) !important;
404
- border-radius: 14px !important;
405
- }
406
 
407
- a { color: #93c5fd !important; }
408
- """
409
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
 
411
- def color_to_css(color: str) -> str:
412
- # Simple palette mapping
413
- palette = {
414
- "purple": "rgba(124,58,237,0.90)",
415
- "cyan": "rgba(34,211,238,0.85)",
416
- "indigo": "rgba(99,102,241,0.85)",
417
- "amber": "rgba(245,158,11,0.85)",
418
- "emerald": "rgba(16,185,129,0.85)",
419
- "sky": "rgba(56,189,248,0.85)",
420
- "violet": "rgba(139,92,246,0.85)",
421
- "slate": "rgba(100,116,139,0.75)",
422
- "teal": "rgba(20,184,166,0.80)",
423
- "lime": "rgba(163,230,53,0.70)",
424
- "blue": "rgba(59,130,246,0.85)",
425
- "rose": "rgba(244,63,94,0.80)",
426
- "orange": "rgba(249,115,22,0.80)",
427
- "zinc": "rgba(161,161,170,0.65)",
428
- "red": "rgba(239,68,68,0.80)",
429
- "green": "rgba(34,197,94,0.80)",
430
- "stone": "rgba(168,162,158,0.70)",
431
- "fuchsia": "rgba(217,70,239,0.75)",
432
- "gray": "rgba(148,163,184,0.65)",
433
- }
434
- return palette.get((color or "").lower().strip(), "rgba(124,58,237,0.90)")
435
-
436
-
437
- def render_status_bar(title: str, meta: Dict[str, Any]) -> str:
438
- now = datetime.now().strftime("%H:%M")
439
- loaded_from = meta.get("loaded_from", "unknown")
440
- badge = f"source:{loaded_from}"
441
- return f"""
442
- <div class="status_bar">
443
- <div class="status_left">
444
- <span class="pill">{now}</span>
445
- <span class="pill">{badge}</span>
446
- </div>
447
- <div class="status_center">{title}</div>
448
- <div class="status_right">
449
- <span class="pill">battery 95%</span>
450
- <span class="pill">signal ●●●●</span>
451
- </div>
452
- </div>
453
- """
 
 
 
 
 
 
 
 
 
 
 
 
454
 
455
 
456
- def render_launcher(apps: List[Dict[str, Any]], meta: Dict[str, Any], query: str = "", category_filter: str = "all") -> str:
 
 
 
457
  q = (query or "").strip().lower()
 
458
  filtered = []
459
  for a in apps:
460
- if category_filter != "all" and (a.get("category") != category_filter):
461
  continue
462
- if not q:
463
- filtered.append(a)
464
- continue
465
- hay = " ".join([
466
- str(a.get("id", "")),
467
- str(a.get("name", "")),
468
- str(a.get("category", "")),
469
- " ".join((a.get("metadata") or {}).get("tags", []) or []),
470
- str((a.get("metadata") or {}).get("description", "")),
471
- ]).lower()
472
- if q in hay:
473
- filtered.append(a)
474
-
475
- grouped = group_apps_by_category(filtered)
476
-
477
- # Build grid tiles as HTML buttons (we’ll use a dropdown to select app, then a launch button)
478
- # In gradio, direct click handlers on arbitrary HTML tiles are not reliable without JS hacks.
479
- # So we keep the launcher grid visual + use a “Launch” control under it.
480
- tiles = ""
481
  for cat, items in grouped.items():
482
- tiles += f'<div class="panel"><div class="h2">{cat}</div>'
483
- tiles += '<div class="launcher_grid">'
484
- for app in items:
485
- bg = color_to_css(app.get("color", "purple"))
486
- initials = (app.get("name", "App")[:2]).upper()
487
- tiles += f"""
488
- <div class="app_tile">
489
- <div class="app_icon" style="background:{bg}">{initials}</div>
490
- <div class="app_name">{app.get("name","")}</div>
491
- <div class="app_meta">{app.get("id","")} · v{app.get("version","1.0.0")}</div>
492
  </div>
 
493
  """
494
- tiles += "</div></div>"
495
-
496
- header = render_status_bar("MindsEye OS — Launcher", meta)
497
- hint = f"""
498
- <div class="panel">
499
- <div class="h2">How to use</div>
500
- <div class="small">
501
- Search or filter apps, then select an app from the dropdown and press <b>Launch App</b>.
502
- This keeps the UI stable in Gradio without sketchy JS click hooks.
503
- </div>
504
- </div>
505
- """
506
- return f'<div id="android_shell">{header}{hint}{tiles}</div>'
507
-
508
-
509
- def render_app_detail(app: Dict[str, Any], meta: Dict[str, Any]) -> str:
510
- header = render_status_bar(f"App — {app.get('name','')}", meta)
511
-
512
- md = app.get("metadata") or {}
513
- desc = md.get("description", "")
514
- tags = md.get("tags") or []
515
- features = app.get("features") or []
516
- functions = app.get("functions") or {}
517
- connections = app.get("connections") or []
518
-
519
- info = f"""
520
- <div class="panel">
521
- <div class="h2>Overview</div>
522
- <div class="kv">
523
- <div class="k">App ID</div><div class="v">{app.get("id","")}</div>
524
- <div class="k">Category</div><div class="v">{app.get("category","")}</div>
525
- <div class="k">Version</div><div class="v">{app.get("version","")}</div>
526
- <div class="k">Repo</div><div class="v"><a href="{app.get("github_repo","")}" target="_blank">{app.get("github_repo","")}</a></div>
527
- </div>
528
- <p class="small" style="margin-top:10px;">{desc}</p>
529
- <div class="small"><b>Tags:</b> {", ".join(tags) if tags else "—"}</div>
530
- </div>
531
- """
532
-
533
- feats = "<div class='panel'><div class='h2'>Features</div>"
534
- if features:
535
- feats += "<ul>"
536
- for f in features:
537
- feats += f"<li class='small'>{f}</li>"
538
- feats += "</ul>"
539
- else:
540
- feats += "<div class='small'>No features listed.</div>"
541
- feats += "</div>"
542
-
543
- funcs = "<div class='panel'><div class='h2'>Functions</div>"
544
- if functions:
545
- funcs += "<pre><code>" + compact_json(functions) + "</code></pre>"
546
- else:
547
- funcs += "<div class='small'>No functions defined.</div>"
548
- funcs += "</div>"
549
-
550
- conns = "<div class='panel'><div class='h2'>Connections</div>"
551
- if connections:
552
- conns += "<pre><code>" + compact_json(connections) + "</code></pre>"
553
- else:
554
- conns += "<div class='small'>No connections defined.</div>"
555
- conns += "</div>"
556
-
557
- return f'<div id="android_shell">{header}{info}{feats}{funcs}{conns}</div>'
558
-
559
-
560
- def render_tutorials(tutorials: List[Dict[str, Any]], meta: Dict[str, Any], selected_id: Optional[str] = None) -> str:
561
- header = render_status_bar("MindsEye OS — Tutorials", meta)
562
- if not selected_id and tutorials:
563
- selected_id = tutorials[0].get("id")
564
-
565
- lesson = None
566
- for t in tutorials:
567
- if t.get("id") == selected_id:
568
- lesson = t
569
- break
570
- if not lesson and tutorials:
571
- lesson = tutorials[0]
572
-
573
- left = "<div class='panel'><div class='h2'>Lessons</div><ul>"
574
- for t in tutorials:
575
- tid = t.get("id", "")
576
- title = t.get("title", tid)
577
- left += f"<li class='small'><b>{tid}</b> — {title}</li>"
578
- left += "</ul></div>"
579
-
580
- right = "<div class='panel'><div class='h2'>Lesson Detail</div>"
581
- if lesson:
582
- right += f"<div class='small'><b>{lesson.get('title','')}</b> · level: {lesson.get('level','')}</div>"
583
- right += f"<div class='small' style='margin-top:8px;'><b>Goal:</b> {lesson.get('goal','')}</div>"
584
- sections = lesson.get("sections") or []
585
- right += "<div style='margin-top:10px;'>"
586
- for s in sections:
587
- st = s.get("type")
588
- if st == "text":
589
- right += f"<p class='small'>{s.get('content','')}</p>"
590
- elif st == "concepts":
591
- items = s.get("items") or []
592
- right += "<div class='small'><b>Concepts:</b><ul>"
593
- for it in items:
594
- right += f"<li>{it}</li>"
595
- right += "</ul></div>"
596
- elif st == "apps":
597
- items = s.get("items") or []
598
- right += "<div class='small'><b>Apps:</b> " + ", ".join(items) + "</div>"
599
- elif st == "exercise":
600
- right += "<div class='small'><b>Exercise:</b> " + str(s.get("prompt", "")) + "</div>"
601
- right += "</div>"
602
- else:
603
- right += "<div class='small'>No tutorials found.</div>"
604
- right += "</div>"
605
 
606
  return f"""
607
- <div id="android_shell">
608
- {header}
609
- <div style="display:grid;grid-template-columns: 1fr; gap: 14px; margin-top:14px;">
610
- {left}
611
- {right}
612
- </div>
613
- </div>
614
- """
615
-
616
-
617
- def render_recents(recents: List[str], apps: List[Dict[str, Any]], meta: Dict[str, Any]) -> str:
618
- header = render_status_bar("MindsEye OS — Recents", meta)
619
- items = []
620
- for app_id in recents[::-1]:
621
- a = find_app(apps, app_id)
622
- if a:
623
- items.append(a)
624
-
625
- body = "<div class='panel'><div class='h2'>Recent Apps</div>"
626
- if not items:
627
- body += "<div class='small'>No recent apps yet. Launch something from the launcher.</div>"
628
- else:
629
- body += "<ul>"
630
- for a in items:
631
- body += f"<li class='small'><b>{a.get('name')}</b> — {a.get('id')}</li>"
632
- body += "</ul>"
633
- body += "</div>"
634
- return f'<div id="android_shell">{header}{body}</div>'
 
 
 
635
 
636
 
637
- # -----------------------------
638
- # Gradio App Logic
639
- # -----------------------------
640
- def boot() -> Tuple[Dict[str, Any], List[Dict[str, Any]], List[Dict[str, Any]]]:
641
- return load_dataset_snapshot(DATASET_REPO_ID)
 
 
 
 
 
 
 
642
 
643
 
644
- def build_category_choices(apps: List[Dict[str, Any]], meta: Dict[str, Any]) -> List[Tuple[str, str]]:
645
- # value, label
646
- base = [("all", "All Categories")]
647
- categories_doc = (meta.get("categories") or {}).get("categories") if isinstance(meta.get("categories"), dict) else None
648
- if categories_doc:
649
- for c in categories_doc:
650
- base.append((c["id"], c["label"]))
651
- return base
652
 
653
- # fallback: derive from apps
654
- cats = sorted(list({(a.get("category") or "other") for a in apps}))
655
- for c in cats:
656
- base.append((c, c))
657
- return base
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
 
659
 
660
- def on_refresh(meta: Dict[str, Any], apps: List[Dict[str, Any]], tutorials: List[Dict[str, Any]]):
661
- meta2, apps2, tutorials2 = boot()
662
- categories = build_category_choices(apps2, meta2)
663
- # default views
664
- launcher_html = render_launcher(apps2, meta2)
665
- return meta2, apps2, tutorials2, categories, launcher_html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
 
667
 
668
- def nav_home(meta: Dict[str, Any], apps: List[Dict[str, Any]], query: str, cat: str):
669
- return render_launcher(apps, meta, query=query, category_filter=cat)
 
 
 
 
 
 
 
 
 
670
 
671
 
672
- def nav_recents(meta: Dict[str, Any], apps: List[Dict[str, Any]], recents: List[str]):
673
- return render_recents(recents, apps, meta)
674
 
675
 
676
- def nav_back(meta: Dict[str, Any], apps: List[Dict[str, Any]], stack: List[str], current: Optional[str], query: str, cat: str):
677
- # stack stores previous app_ids
678
- if stack:
679
- prev = stack.pop()
680
- app = find_app(apps, prev)
681
- if app:
682
- return stack, prev, render_app_detail(app, meta)
683
- # if no stack, go home
684
- return stack, None, render_launcher(apps, meta, query=query, category_filter=cat)
685
 
686
 
687
- def launch_app(meta: Dict[str, Any], apps: List[Dict[str, Any]], app_id: str, stack: List[str], current: Optional[str], recents: List[str]):
688
- app = find_app(apps, app_id)
689
- if not app:
690
- # fallback: do nothing, keep launcher
691
- return stack, current, recents, "<div class='panel'><div class='h2'>Error</div><div class='small'>App not found.</div></div>"
692
 
693
- # push current to back stack
694
- if current:
695
- stack.append(current)
696
 
697
- # add to recents (unique, keep last 8)
698
- if app_id in recents:
699
- recents = [x for x in recents if x != app_id]
700
- recents.append(app_id)
701
- recents = recents[-8:]
 
 
702
 
703
- return stack, app_id, recents, render_app_detail(app, meta)
 
 
 
 
 
704
 
 
 
705
 
706
- def select_tutorial(meta: Dict[str, Any], tutorials: List[Dict[str, Any]], tutorial_id: str):
707
- return render_tutorials(tutorials, meta, selected_id=tutorial_id)
 
 
 
 
 
 
 
 
 
 
708
 
 
 
 
 
 
 
 
 
 
 
 
709
 
710
- with gr.Blocks(css=ANDROID_CSS, theme=gr.themes.Soft()) as demo:
711
- meta_state = gr.State({})
712
- apps_state = gr.State([])
713
- tutorials_state = gr.State([])
714
- back_stack_state = gr.State([]) # list of app_ids
715
- current_app_state = gr.State(None) # app_id
716
- recents_state = gr.State([]) # list of app_ids
717
-
718
- gr.Markdown("")
719
-
720
- with gr.Row():
721
- refresh_btn = gr.Button("Refresh Dataset", variant="secondary")
722
- status = gr.Markdown("")
723
 
724
- with gr.Row():
725
- search_query = gr.Textbox(label="Search apps", placeholder="Search name / id / tags / description…", scale=2)
726
- category_filter = gr.Dropdown(label="Category", choices=[("all", "All Categories")], value="all", scale=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
727
 
728
- launcher_view = gr.HTML()
 
 
 
 
 
 
 
 
 
729
 
730
- with gr.Row():
731
- app_select = gr.Dropdown(label="Select app to launch", choices=[], value=None, scale=2)
732
- launch_btn = gr.Button("Launch App", variant="primary", scale=1)
 
 
733
 
734
- with gr.Row():
735
- home_btn = gr.Button("Home", elem_classes=["nav_btn"])
736
- back_btn = gr.Button("Back", elem_classes=["nav_btn"])
737
- recents_btn = gr.Button("Recents", elem_classes=["nav_btn"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
738
 
739
- gr.Markdown("")
 
 
 
 
 
 
 
740
 
741
- with gr.Tab("Tutorials"):
742
- tutorial_select = gr.Dropdown(label="Select lesson", choices=[], value=None)
743
- tutorial_view = gr.HTML()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
744
 
745
- with gr.Tab("Raw Debug"):
746
- debug_meta = gr.JSON(label="Meta")
747
- debug_counts = gr.JSON(label="Counts")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
748
 
749
- # Boot
750
- def _boot_ui():
751
- meta, apps, tutorials = boot()
752
- categories = build_category_choices(apps, meta)
753
- launcher_html = render_launcher(apps, meta)
754
 
755
- app_choices = [(a["id"], f'{a["name"]} ({a["id"]})') for a in apps]
756
- tutorial_choices = [(t["id"], t.get("title", t["id"])) for t in tutorials]
757
- tutorial_default = tutorials[0]["id"] if tutorials else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
758
 
759
- meta_msg = f"Loaded {len(apps)} apps · {len(tutorials)} tutorials · source: {meta.get('loaded_from')}"
760
- debug_c = {"apps": len(apps), "tutorials": len(tutorials)}
761
 
762
- return (
763
- meta, apps, tutorials,
764
- categories, launcher_html,
765
- app_choices,
766
- tutorial_choices, tutorial_default,
767
- render_tutorials(tutorials, meta, selected_id=tutorial_default),
768
- meta, debug_c,
769
- meta_msg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
770
  )
771
 
772
- demo.load(
773
- _boot_ui,
774
- inputs=[],
775
- outputs=[
776
- meta_state, apps_state, tutorials_state,
777
- category_filter, launcher_view,
778
- app_select,
779
- tutorial_select, tutorial_select,
780
- tutorial_view,
781
- debug_meta, debug_counts,
782
- status
783
- ],
784
- )
785
-
786
- # Refresh
787
- def _do_refresh(meta, apps, tutorials):
788
- meta2, apps2, tutorials2 = boot()
789
- categories = build_category_choices(apps2, meta2)
790
- launcher_html = render_launcher(apps2, meta2)
791
-
792
- app_choices = [(a["id"], f'{a["name"]} ({a["id"]})') for a in apps2]
793
- tutorial_choices = [(t["id"], t.get("title", t["id"])) for t in tutorials2]
794
- tutorial_default = tutorials2[0]["id"] if tutorials2 else None
795
-
796
- msg = f"Refreshed. Loaded {len(apps2)} apps · {len(tutorials2)} tutorials · source: {meta2.get('loaded_from')}"
797
- debug_c = {"apps": len(apps2), "tutorials": len(tutorials2)}
798
-
799
- return (
800
- meta2, apps2, tutorials2,
801
- categories, launcher_html,
802
- app_choices,
803
- tutorial_choices, tutorial_default,
804
- render_tutorials(tutorials2, meta2, selected_id=tutorial_default),
805
- meta2, debug_c,
806
- msg
807
  )
808
 
809
- refresh_btn.click(
810
- _do_refresh,
811
- inputs=[meta_state, apps_state, tutorials_state],
812
- outputs=[
813
- meta_state, apps_state, tutorials_state,
814
- category_filter, launcher_view,
815
- app_select,
816
- tutorial_select, tutorial_select,
817
- tutorial_view,
818
- debug_meta, debug_counts,
819
- status
820
- ]
821
- )
822
 
823
- # Search + Filter updates launcher view
824
- def _update_launcher(meta, apps, q, cat):
825
- return render_launcher(apps, meta, query=q, category_filter=cat)
 
 
826
 
827
- search_query.change(_update_launcher, inputs=[meta_state, apps_state, search_query, category_filter], outputs=[launcher_view])
828
- category_filter.change(_update_launcher, inputs=[meta_state, apps_state, search_query, category_filter], outputs=[launcher_view])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
829
 
830
- # Launch selected app
831
- def _launch(meta, apps, app_id, stack, current, recents):
832
- if not app_id:
833
- return stack, current, recents, render_launcher(apps, meta)
834
- return launch_app(meta, apps, app_id, stack, current, recents)
835
 
836
- launch_btn.click(
837
- _launch,
838
- inputs=[meta_state, apps_state, app_select, back_stack_state, current_app_state, recents_state],
839
- outputs=[back_stack_state, current_app_state, recents_state, launcher_view]
840
- )
841
-
842
- # Nav buttons
843
- home_btn.click(nav_home, inputs=[meta_state, apps_state, search_query, category_filter], outputs=[launcher_view])
844
- recents_btn.click(nav_recents, inputs=[meta_state, apps_state, recents_state], outputs=[launcher_view])
845
 
846
- back_btn.click(
847
- nav_back,
848
- inputs=[meta_state, apps_state, back_stack_state, current_app_state, search_query, category_filter],
849
- outputs=[back_stack_state, current_app_state, launcher_view]
850
- )
 
 
 
 
 
 
 
851
 
852
- # Tutorials selector
853
- tutorial_select.change(select_tutorial, inputs=[meta_state, tutorials_state, tutorial_select], outputs=[tutorial_view])
854
 
855
 
856
  if __name__ == "__main__":
857
- demo.launch()
 
1
  import os
2
  import json
3
+ import time
4
+ from dataclasses import dataclass
5
+ from typing import Dict, List, Any, Tuple, Optional
6
 
7
  import gradio as gr
8
  from huggingface_hub import snapshot_download
9
 
10
 
11
+ # =========================
12
  # Config
13
+ # =========================
14
+ DATASET_REPO_ID = os.getenv("MINDSEYE_DATASET", "PeacebinfLow/mindseye-android-os-data")
15
+ DATASET_REVISION = os.getenv("MINDSEYE_DATASET_REVISION", None) # optional pin
16
+ MAX_APPS = int(os.getenv("MINDSEYE_MAX_APPS", "200"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
 
19
+ # =========================
20
+ # Helpers
21
+ # =========================
22
+ def _safe_read_json(path: str) -> Optional[dict]:
23
  try:
24
  with open(path, "r", encoding="utf-8") as f:
25
  return json.load(f)
 
27
  return None
28
 
29
 
30
+ def _slug(s: str) -> str:
31
+ return "".join(ch.lower() if ch.isalnum() else "-" for ch in s).strip("-")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
 
 
 
33
 
34
+ def _now_hhmm() -> str:
35
+ return time.strftime("%-I:%M %p") if os.name != "nt" else time.strftime("%I:%M %p").lstrip("0")
 
 
 
 
 
 
 
36
 
 
 
 
 
37
 
38
+ # =========================
39
+ # Data Model
40
+ # =========================
41
+ @dataclass
42
+ class AppItem:
43
+ id: str
44
+ name: str
45
+ category: str
46
+ description: str
47
+ repo: str
48
+ version: str
49
+ color: str
50
+ icon_label: str
51
+ tags: List[str]
52
 
 
 
 
 
 
 
 
53
 
54
+ CATEGORY_ORDER = [
55
+ "google-integration",
56
+ "core-system",
57
+ "binary-runtime",
58
+ "protocol",
59
+ "mindscript",
60
+ "sql-cloud",
61
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
+ CATEGORY_LABELS = {
64
+ "google-integration": "GOOGLE INTEGRATION",
65
+ "core-system": "CORE SYSTEM",
66
+ "binary-runtime": "BINARY & RUNTIME",
67
+ "protocol": "PROTOCOL",
68
+ "mindscript": "MINDSCRIPT",
69
+ "sql-cloud": "SQL & CLOUD",
70
  }
71
 
 
 
 
 
 
 
 
 
72
 
73
+ DEFAULT_CATEGORY_COLORS = {
74
+ "google-integration": "#7C3AED",
75
+ "core-system": "#2563EB",
76
+ "binary-runtime": "#0EA5E9",
77
+ "protocol": "#F59E0B",
78
+ "mindscript": "#A855F7",
79
+ "sql-cloud": "#06B6D4",
80
  }
81
 
 
 
 
 
 
 
82
 
83
+ def load_dataset_repo_apps() -> Tuple[List[AppItem], List[dict], str]:
84
+ """
85
+ Pulls the dataset repo snapshot and loads all apps JSON.
86
+ Returns: (apps, tutorials, source_string)
87
+ """
88
+ local_dir = snapshot_download(
89
+ repo_id=DATASET_REPO_ID,
90
+ repo_type="dataset",
91
+ revision=DATASET_REVISION,
92
+ ignore_patterns=["*.png", "*.jpg", "*.jpeg", "*.gif", "*.mp4", "*.zip"],
93
+ )
94
 
95
+ apps: List[AppItem] = []
96
+ tutorials: List[dict] = []
 
 
 
97
 
98
+ apps_root = os.path.join(local_dir, "apps")
99
+ tutorials_root = os.path.join(local_dir, "tutorials")
100
 
101
+ # Load apps
102
+ if os.path.isdir(apps_root):
103
+ for category in sorted(os.listdir(apps_root)):
104
+ cat_dir = os.path.join(apps_root, category)
105
+ if not os.path.isdir(cat_dir):
106
+ continue
107
+ for fn in sorted(os.listdir(cat_dir)):
108
+ if not fn.endswith(".json"):
109
+ continue
110
+ path = os.path.join(cat_dir, fn)
111
+ data = _safe_read_json(path)
112
+ if not data:
113
+ continue
114
 
115
+ app_id = data.get("id") or _slug(data.get("name", fn.replace(".json", "")))
116
+ name = data.get("name") or fn.replace(".json", "")
117
+ desc = (data.get("metadata", {}).get("description") or data.get("description") or "").strip()
118
+ repo = data.get("github_repo") or data.get("repo") or ""
119
+ version = data.get("version") or data.get("metadata", {}).get("version") or "v1.0.0"
120
+ color = data.get("color") or DEFAULT_CATEGORY_COLORS.get(category, "#334155")
121
+
122
+ tags = data.get("metadata", {}).get("tags") or data.get("tags") or []
123
+ if isinstance(tags, str):
124
+ tags = [tags]
125
+
126
+ icon_label = (data.get("icon_label") or data.get("icon") or name[:2]).upper()
127
+ icon_label = icon_label[:2] if len(icon_label) > 2 else icon_label
128
+
129
+ apps.append(
130
+ AppItem(
131
+ id=app_id,
132
+ name=name,
133
+ category=category,
134
+ description=desc,
135
+ repo=repo,
136
+ version=version if str(version).startswith("v") else f"v{version}",
137
+ color=color,
138
+ icon_label=icon_label,
139
+ tags=tags[:12],
140
+ )
141
+ )
142
+ if len(apps) >= MAX_APPS:
143
+ break
144
+
145
+ # Load tutorials (optional)
146
+ if os.path.isdir(tutorials_root):
147
+ for fn in sorted(os.listdir(tutorials_root)):
148
+ if fn.endswith(".json"):
149
+ t = _safe_read_json(os.path.join(tutorials_root, fn))
150
+ if t:
151
+ tutorials.append(t)
152
+
153
+ source = "snapshot_download"
154
+ return apps, tutorials, source
155
+
156
+
157
+ def group_by_category(apps: List[AppItem]) -> Dict[str, List[AppItem]]:
158
+ grouped: Dict[str, List[AppItem]] = {}
159
+ for a in apps:
160
+ grouped.setdefault(a.category, []).append(a)
161
+ # apply preferred order
162
+ ordered: Dict[str, List[AppItem]] = {}
163
+ for cat in CATEGORY_ORDER:
164
+ if cat in grouped:
165
+ ordered[cat] = grouped[cat]
166
+ for cat, items in grouped.items():
167
+ if cat not in ordered:
168
+ ordered[cat] = items
169
+ return ordered
170
 
171
 
172
+ # =========================
173
+ # UI Renderers
174
+ # =========================
175
+ def render_launcher(apps: List[AppItem], query: str = "", category: str = "all") -> str:
176
  q = (query or "").strip().lower()
177
+
178
  filtered = []
179
  for a in apps:
180
+ if category != "all" and a.category != category:
181
  continue
182
+ if q:
183
+ blob = " ".join([a.name, a.id, a.description, " ".join(a.tags)]).lower()
184
+ if q not in blob:
185
+ continue
186
+ filtered.append(a)
187
+
188
+ grouped = group_by_category(filtered)
189
+
190
+ # Build launcher sections
191
+ sections_html = []
 
 
 
 
 
 
 
 
 
192
  for cat, items in grouped.items():
193
+ label = CATEGORY_LABELS.get(cat, cat.upper())
194
+ sections_html.append(
195
+ f"""
196
+ <div class="section">
197
+ <div class="section-title">{label} <span class="count">({len(items)})</span></div>
198
+ <div class="grid">
199
+ {''.join([render_app_tile(a) for a in items])}
 
 
 
200
  </div>
201
+ </div>
202
  """
203
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
  return f"""
206
+ <div class="phone">
207
+ <div class="statusbar">
208
+ <div class="sb-left">{_now_hhmm()}</div>
209
+ <div class="sb-mid">MindsEye</div>
210
+ <div class="sb-right">
211
+ <span class="sb-signal">▮▮▮▯</span>
212
+ <span class="sb-batt">87%</span>
213
+ </div>
214
+ </div>
215
+
216
+ <div class="hero">
217
+ <div class="hero-title">MindsEye OS</div>
218
+ <div class="hero-sub">35+ Cognitive Automation Modules</div>
219
+ </div>
220
+
221
+ <div class="searchwrap">
222
+ <div class="searchicon">⌕</div>
223
+ <div class="searchtext">{escape_html(query) if query else "Search apps..."}</div>
224
+ </div>
225
+
226
+ <div class="scroll">
227
+ {''.join(sections_html) if sections_html else '<div class="empty">No apps match your search.</div>'}
228
+ </div>
229
+
230
+ <div class="navbar">
231
+ <div class="navbtn" data-nav="back">‹</div>
232
+ <div class="navbtn home" data-nav="home">●</div>
233
+ <div class="navbtn" data-nav="recents"></div>
234
+ </div>
235
+ </div>
236
+ """
237
 
238
 
239
+ def render_app_tile(app: AppItem) -> str:
240
+ # tile click triggers JS to press hidden gradio button
241
+ return f"""
242
+ <div class="tile" onclick="window.__ME_LAUNCH_APP('{escape_attr(app.id)}')">
243
+ <div class="icon" style="border-color:{escape_attr(app.color)}; box-shadow: 0 0 0 2px rgba(255,255,255,0.04) inset;">
244
+ <div class="icon-inner" style="background: color-mix(in srgb, {escape_attr(app.color)} 35%, #0b1220);">
245
+ {escape_html(app.icon_label)}
246
+ </div>
247
+ </div>
248
+ <div class="label">{escape_html(app.name)}</div>
249
+ </div>
250
+ """
251
 
252
 
253
+ def render_app_detail(app: AppItem) -> str:
254
+ tags = "".join([f"<span class='tag'>{escape_html(t)}</span>" for t in (app.tags or [])])
255
+ repo = escape_html(app.repo) if app.repo else "—"
 
 
 
 
 
256
 
257
+ return f"""
258
+ <div class="phone">
259
+ <div class="statusbar">
260
+ <div class="sb-left">{_now_hhmm()}</div>
261
+ <div class="sb-mid">{escape_html(app.name)}</div>
262
+ <div class="sb-right">
263
+ <span class="sb-signal">▮▮▮▯</span>
264
+ <span class="sb-batt">87%</span>
265
+ </div>
266
+ </div>
267
+
268
+ <div class="detail">
269
+ <div class="detail-head">
270
+ <div class="icon big" style="border-color:{escape_attr(app.color)};">
271
+ <div class="icon-inner" style="background: color-mix(in srgb, {escape_attr(app.color)} 35%, #0b1220);">
272
+ {escape_html(app.icon_label)}
273
+ </div>
274
+ </div>
275
+ <div class="detail-meta">
276
+ <div class="detail-title">{escape_html(app.name)}</div>
277
+ <div class="detail-sub">{escape_html(app.category)} • {escape_html(app.version)}</div>
278
+ </div>
279
+ </div>
280
+
281
+ <div class="card">
282
+ <div class="card-title">Description</div>
283
+ <div class="card-body">{escape_html(app.description or "No description provided yet.")}</div>
284
+ </div>
285
+
286
+ <div class="card">
287
+ <div class="card-title">Repository</div>
288
+ <div class="card-body mono">{repo}</div>
289
+ </div>
290
+
291
+ <div class="card">
292
+ <div class="card-title">Tags</div>
293
+ <div class="card-body tags">{tags if tags else "—"}</div>
294
+ </div>
295
+
296
+ <div class="card subtle">
297
+ <div class="card-title">Educational Mode</div>
298
+ <div class="card-body">
299
+ This app is a documented module in the MindsEye ecosystem. In the portfolio version,
300
+ modules are presented as learnable “apps” rather than live backend services.
301
+ </div>
302
+ </div>
303
+ </div>
304
+
305
+ <div class="navbar">
306
+ <div class="navbtn" data-nav="back">‹</div>
307
+ <div class="navbtn home" data-nav="home">●</div>
308
+ <div class="navbtn" data-nav="recents">▢</div>
309
+ </div>
310
+ </div>
311
+ """
312
 
313
 
314
+ def render_recents(recents: List[AppItem]) -> str:
315
+ tiles = "".join([render_app_tile(a) for a in recents]) if recents else "<div class='empty'>No recent apps yet.</div>"
316
+ return f"""
317
+ <div class="phone">
318
+ <div class="statusbar">
319
+ <div class="sb-left">{_now_hhmm()}</div>
320
+ <div class="sb-mid">Recents</div>
321
+ <div class="sb-right">
322
+ <span class="sb-signal">▮▮▮▯</span>
323
+ <span class="sb-batt">87%</span>
324
+ </div>
325
+ </div>
326
+
327
+ <div class="hero" style="padding-top:14px;">
328
+ <div class="hero-title">Recent Apps</div>
329
+ <div class="hero-sub">Your latest opened modules</div>
330
+ </div>
331
+
332
+ <div class="scroll">
333
+ <div class="section">
334
+ <div class="grid">{tiles}</div>
335
+ </div>
336
+ </div>
337
+
338
+ <div class="navbar">
339
+ <div class="navbtn" data-nav="back">‹</div>
340
+ <div class="navbtn home" data-nav="home">●</div>
341
+ <div class="navbtn" data-nav="recents">▢</div>
342
+ </div>
343
+ </div>
344
+ """
345
 
346
 
347
+ def escape_html(s: str) -> str:
348
+ if s is None:
349
+ return ""
350
+ return (
351
+ str(s)
352
+ .replace("&", "&amp;")
353
+ .replace("<", "&lt;")
354
+ .replace(">", "&gt;")
355
+ .replace('"', "&quot;")
356
+ .replace("'", "&#39;")
357
+ )
358
 
359
 
360
+ def escape_attr(s: str) -> str:
361
+ return escape_html(s).replace("\n", " ")
362
 
363
 
364
+ # =========================
365
+ # App Logic
366
+ # =========================
367
+ def refresh_data():
368
+ apps, tutorials, source = load_dataset_repo_apps()
369
+ return apps, tutorials, source
 
 
 
370
 
371
 
372
+ def find_app(apps: List[AppItem], app_id: str) -> Optional[AppItem]:
373
+ for a in apps:
374
+ if a.id == app_id:
375
+ return a
376
+ return None
377
 
 
 
 
378
 
379
+ # =========================
380
+ # Gradio App
381
+ # =========================
382
+ CSS = r"""
383
+ /* Hide Gradio chrome */
384
+ footer { display: none !important; }
385
+ #root > .wrap { max-width: 1200px; }
386
 
387
+ /* Full background */
388
+ body, .gradio-container {
389
+ background: radial-gradient(1200px 800px at 30% 40%, rgba(124,58,237,0.25), transparent 60%),
390
+ radial-gradient(900px 700px at 70% 60%, rgba(34,211,238,0.18), transparent 60%),
391
+ #050814 !important;
392
+ }
393
 
394
+ /* Remove default component borders */
395
+ .gr-box, .gr-form, .gr-panel { border: none !important; background: transparent !important; }
396
 
397
+ /* Phone mock */
398
+ .phone {
399
+ width: 420px;
400
+ height: 840px;
401
+ margin: 16px auto;
402
+ border-radius: 34px;
403
+ background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.03));
404
+ box-shadow: 0 30px 80px rgba(0,0,0,0.55);
405
+ border: 1px solid rgba(255,255,255,0.08);
406
+ overflow: hidden;
407
+ position: relative;
408
+ }
409
 
410
+ .statusbar {
411
+ height: 52px;
412
+ padding: 0 18px;
413
+ display: flex;
414
+ align-items: center;
415
+ justify-content: space-between;
416
+ color: rgba(255,255,255,0.85);
417
+ font-family: ui-sans-serif, system-ui;
418
+ font-size: 14px;
419
+ background: rgba(0,0,0,0.12);
420
+ }
421
 
422
+ .sb-mid { font-weight: 600; letter-spacing: 0.3px; opacity: 0.9; }
423
+ .sb-right { display: flex; gap: 10px; align-items: center; opacity: 0.85; }
 
 
 
 
 
 
 
 
 
 
 
424
 
425
+ .hero {
426
+ padding: 18px 20px 10px 20px;
427
+ text-align: center;
428
+ color: rgba(255,255,255,0.92);
429
+ font-family: ui-sans-serif, system-ui;
430
+ }
431
+ .hero-title { font-size: 32px; font-weight: 800; letter-spacing: 0.2px; }
432
+ .hero-sub { font-size: 14px; opacity: 0.75; margin-top: 2px; }
433
+
434
+ .searchwrap {
435
+ margin: 10px 18px 10px 18px;
436
+ height: 48px;
437
+ border-radius: 16px;
438
+ border: 1px solid rgba(255,255,255,0.08);
439
+ background: rgba(0,0,0,0.20);
440
+ display: flex;
441
+ align-items: center;
442
+ gap: 10px;
443
+ padding: 0 14px;
444
+ color: rgba(255,255,255,0.70);
445
+ font-family: ui-sans-serif, system-ui;
446
+ }
447
+ .searchicon { opacity: 0.85; }
448
+ .searchtext { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
449
+
450
+ .scroll {
451
+ position: absolute;
452
+ top: 52px;
453
+ left: 0;
454
+ right: 0;
455
+ bottom: 78px;
456
+ padding: 186px 18px 18px 18px; /* leaves room for hero+search */
457
+ overflow-y: auto;
458
+ }
459
 
460
+ .section { margin-top: 14px; }
461
+ .section-title {
462
+ font-family: ui-sans-serif, system-ui;
463
+ font-size: 12px;
464
+ letter-spacing: 1.2px;
465
+ text-transform: uppercase;
466
+ color: rgba(255,255,255,0.55);
467
+ margin: 6px 0 10px 2px;
468
+ }
469
+ .section-title .count { opacity: 0.6; margin-left: 6px; }
470
 
471
+ .grid {
472
+ display: grid;
473
+ grid-template-columns: repeat(4, 1fr);
474
+ gap: 14px 10px;
475
+ }
476
 
477
+ .tile { cursor: pointer; user-select: none; text-align: center; }
478
+ .icon {
479
+ width: 66px;
480
+ height: 66px;
481
+ border-radius: 18px;
482
+ border: 2px solid rgba(255,255,255,0.08);
483
+ margin: 0 auto;
484
+ display: grid;
485
+ place-items: center;
486
+ }
487
+ .icon.big { width: 78px; height: 78px; border-radius: 22px; }
488
+ .icon-inner {
489
+ width: 58px;
490
+ height: 58px;
491
+ border-radius: 16px;
492
+ display: grid;
493
+ place-items: center;
494
+ font-family: ui-sans-serif, system-ui;
495
+ font-weight: 800;
496
+ color: rgba(255,255,255,0.88);
497
+ letter-spacing: 0.8px;
498
+ }
499
+ .icon.big .icon-inner { width: 70px; height: 70px; border-radius: 20px; font-size: 18px; }
500
 
501
+ .label {
502
+ margin-top: 8px;
503
+ font-family: ui-sans-serif, system-ui;
504
+ font-size: 12px;
505
+ line-height: 1.1;
506
+ color: rgba(255,255,255,0.80);
507
+ padding: 0 2px;
508
+ }
509
 
510
+ /* Detail screen */
511
+ .detail { padding: 18px; padding-bottom: 92px; color: rgba(255,255,255,0.9); }
512
+ .detail-head { display: flex; gap: 14px; align-items: center; margin-bottom: 14px; }
513
+ .detail-title { font-size: 20px; font-weight: 800; }
514
+ .detail-sub { font-size: 12px; opacity: 0.65; margin-top: 2px; }
515
+
516
+ .card {
517
+ margin-top: 12px;
518
+ padding: 12px 12px;
519
+ border-radius: 16px;
520
+ border: 1px solid rgba(255,255,255,0.08);
521
+ background: rgba(0,0,0,0.22);
522
+ }
523
+ .card.subtle { background: rgba(0,0,0,0.16); opacity: 0.95; }
524
+ .card-title { font-size: 12px; opacity: 0.70; letter-spacing: 0.8px; text-transform: uppercase; }
525
+ .card-body { margin-top: 8px; font-size: 13px; line-height: 1.45; opacity: 0.88; }
526
+ .card-body.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono"; font-size: 12px; }
527
+ .tags { display: flex; flex-wrap: wrap; gap: 8px; }
528
+ .tag {
529
+ font-size: 12px;
530
+ padding: 6px 10px;
531
+ border-radius: 999px;
532
+ border: 1px solid rgba(255,255,255,0.10);
533
+ background: rgba(255,255,255,0.04);
534
+ opacity: 0.9;
535
+ }
536
 
537
+ /* Navbar */
538
+ .navbar {
539
+ position: absolute;
540
+ left: 0; right: 0; bottom: 0;
541
+ height: 78px;
542
+ background: rgba(0,0,0,0.24);
543
+ border-top: 1px solid rgba(255,255,255,0.08);
544
+ display: flex;
545
+ align-items: center;
546
+ justify-content: space-around;
547
+ }
548
+ .navbtn {
549
+ width: 90px;
550
+ height: 44px;
551
+ border-radius: 999px;
552
+ display: grid;
553
+ place-items: center;
554
+ color: rgba(255,255,255,0.80);
555
+ background: rgba(255,255,255,0.04);
556
+ border: 1px solid rgba(255,255,255,0.08);
557
+ font-size: 20px;
558
+ cursor: pointer;
559
+ user-select: none;
560
+ }
561
+ .navbtn.home { font-size: 16px; }
562
+ .empty {
563
+ color: rgba(255,255,255,0.65);
564
+ font-family: ui-sans-serif, system-ui;
565
+ padding: 14px;
566
+ }
567
+ """
568
 
 
 
 
 
 
569
 
570
+ JS = r"""
571
+ // Hooks from HTML tiles -> hidden Gradio button
572
+ window.__ME_LAUNCH_APP = (app_id) => {
573
+ const el = document.querySelector("button#me-hidden-launch");
574
+ const inp = document.querySelector("input#me-hidden-appid");
575
+ if (inp) inp.value = app_id;
576
+ if (el) el.click();
577
+ };
578
+
579
+ // Navbar nav -> hidden buttons
580
+ window.__ME_NAV = (target) => {
581
+ const btn = document.querySelector(`button#me-hidden-nav-${target}`);
582
+ if (btn) btn.click();
583
+ };
584
+
585
+ document.addEventListener("click", (e) => {
586
+ const nav = e.target?.closest?.("[data-nav]")?.getAttribute("data-nav");
587
+ if (nav) window.__ME_NAV(nav);
588
+ });
589
+ """
590
 
 
 
591
 
592
+ def build_ui():
593
+ with gr.Blocks(css=CSS, js=JS, theme=gr.themes.Base()) as demo:
594
+ apps_state = gr.State([]) # List[AppItem]
595
+ tutorials_state = gr.State([]) # List[dict]
596
+ source_state = gr.State("—")
597
+
598
+ screen_state = gr.State("launcher") # launcher | detail | recents
599
+ selected_app_state = gr.State("") # app_id
600
+ history_state = gr.State([]) # stack of screen tuples
601
+ recents_state = gr.State([]) # List[str] of app_ids
602
+
603
+ query_state = gr.State("")
604
+ category_state = gr.State("all")
605
+
606
+ # Main rendered phone
607
+ phone = gr.HTML(value="<div style='color:white; text-align:center; padding:20px;'>Booting…</div>")
608
+
609
+ # Hidden controls (we keep them, but user never sees them)
610
+ hidden_appid = gr.Textbox(value="", elem_id="me-hidden-appid", visible=False)
611
+ hidden_launch = gr.Button("launch", elem_id="me-hidden-launch", visible=False)
612
+
613
+ nav_back = gr.Button("back", elem_id="me-hidden-nav-back", visible=False)
614
+ nav_home = gr.Button("home", elem_id="me-hidden-nav-home", visible=False)
615
+ nav_recents = gr.Button("recents", elem_id="me-hidden-nav-recents", visible=False)
616
+
617
+ # Top “Refresh dataset” action (optional but useful)
618
+ with gr.Row():
619
+ refresh_btn = gr.Button("Refresh Dataset")
620
+ status = gr.Markdown("")
621
+
622
+ def _render(apps: List[AppItem], screen: str, selected_id: str, recents_ids: List[str], q: str, cat: str):
623
+ if screen == "detail" and selected_id:
624
+ a = find_app(apps, selected_id)
625
+ if a:
626
+ return render_app_detail(a)
627
+ return render_launcher(apps, q, cat)
628
+ if screen == "recents":
629
+ recent_apps = []
630
+ for rid in recents_ids[-20:][::-1]:
631
+ a = find_app(apps, rid)
632
+ if a:
633
+ recent_apps.append(a)
634
+ return render_recents(recent_apps)
635
+ return render_launcher(apps, q, cat)
636
+
637
+ def do_refresh():
638
+ apps, tutorials, source = refresh_data()
639
+ html = _render(apps, "launcher", "", [], "", "all")
640
+ return (
641
+ apps, tutorials, source,
642
+ "launcher", "", [], [],
643
+ "", "all",
644
+ html,
645
+ f"Loaded **{len(apps)} apps** • **{len(tutorials)} tutorials** • source: `{source}`"
646
+ )
647
+
648
+ refresh_btn.click(
649
+ do_refresh,
650
+ inputs=[],
651
+ outputs=[
652
+ apps_state, tutorials_state, source_state,
653
+ screen_state, selected_app_state, history_state, recents_state,
654
+ query_state, category_state,
655
+ phone,
656
+ status
657
+ ],
658
  )
659
 
660
+ def launch_app(apps: List[AppItem], screen: str, selected_id: str, history: List[Any], recents: List[str], app_id: str):
661
+ # push current state to history
662
+ history = list(history or [])
663
+ history.append({"screen": screen, "selected": selected_id})
664
+
665
+ # update recents
666
+ recents = list(recents or [])
667
+ if app_id:
668
+ if app_id in recents:
669
+ recents.remove(app_id)
670
+ recents.append(app_id)
671
+
672
+ html = _render(apps, "detail", app_id, recents, "", "all")
673
+ return "detail", app_id, history, recents, html
674
+
675
+ hidden_launch.click(
676
+ launch_app,
677
+ inputs=[apps_state, screen_state, selected_app_state, history_state, recents_state, hidden_appid],
678
+ outputs=[screen_state, selected_app_state, history_state, recents_state, phone],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
679
  )
680
 
681
+ def nav_home_fn(apps: List[AppItem], recents: List[str], q: str, cat: str):
682
+ return "launcher", "", [], _render(apps, "launcher", "", recents or [], q or "", cat or "all")
 
 
 
 
 
 
 
 
 
 
 
683
 
684
+ nav_home.click(
685
+ nav_home_fn,
686
+ inputs=[apps_state, recents_state, query_state, category_state],
687
+ outputs=[screen_state, selected_app_state, history_state, phone],
688
+ )
689
 
690
+ def nav_back_fn(apps: List[AppItem], history: List[Any], recents: List[str], q: str, cat: str):
691
+ history = list(history or [])
692
+ if not history:
693
+ # if no history, stay launcher
694
+ return "launcher", "", history, _render(apps, "launcher", "", recents or [], q or "", cat or "all")
695
+
696
+ prev = history.pop()
697
+ prev_screen = prev.get("screen", "launcher")
698
+ prev_selected = prev.get("selected", "")
699
+ html = _render(apps, prev_screen, prev_selected, recents or [], q or "", cat or "all")
700
+ return prev_screen, prev_selected, history, html
701
+
702
+ nav_back.click(
703
+ nav_back_fn,
704
+ inputs=[apps_state, history_state, recents_state, query_state, category_state],
705
+ outputs=[screen_state, selected_app_state, history_state, phone],
706
+ )
707
 
708
+ def nav_recents_fn(apps: List[AppItem], recents: List[str], history: List[Any], screen: str, selected: str):
709
+ history = list(history or [])
710
+ history.append({"screen": screen, "selected": selected})
711
+ html = _render(apps, "recents", "", recents or [], "", "all")
712
+ return "recents", "", history, html
713
 
714
+ nav_recents.click(
715
+ nav_recents_fn,
716
+ inputs=[apps_state, recents_state, history_state, screen_state, selected_app_state],
717
+ outputs=[screen_state, selected_app_state, history_state, phone],
718
+ )
 
 
 
 
719
 
720
+ # Auto-refresh on load
721
+ demo.load(
722
+ do_refresh,
723
+ inputs=[],
724
+ outputs=[
725
+ apps_state, tutorials_state, source_state,
726
+ screen_state, selected_app_state, history_state, recents_state,
727
+ query_state, category_state,
728
+ phone,
729
+ status
730
+ ],
731
+ )
732
 
733
+ return demo
 
734
 
735
 
736
  if __name__ == "__main__":
737
+ build_ui().launch()