msg encrypted ai Cursor commited on
Commit
a7832f0
·
1 Parent(s): 9939b9d

Feat/sunday sprint 2 (#15)

Browse files

* Fix HF Gradio SDK build: drop editable requirements.

HF pip installs requirements.txt before copying the repo, so -e ./libs/*
paths fail. Load monorepo packages via sys.path in app.py instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Skip llama-cpp-python on HF Space build.

Compiling llama-cpp-python blocks HF builds for 10+ minutes. The Space
uses transformers presets; lazy-import llama_cpp only when selected.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Disable Gradio SSR on HF Space so app binds to 7860.

HF enables SSR by default; without a working Node proxy Gradio falls
back to port 7861 and the Space container crashes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* css layout

* origin fix

* Use ?classic query param for Classic UI on HF Spaces.

Root-relative /classic links resolve to http://hf.space under the Hub
embed and trigger mixed-content errors. Studio links use ?classic; the
server redirects to a relative classic path on the same HTTPS origin.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

app.py CHANGED
@@ -1,5 +1,25 @@
1
  """Hugging Face Gradio SDK entry point (ZeroGPU / Gradio Spaces)."""
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from gradio_space.server import main
4
 
5
  if __name__ == "__main__":
 
1
  """Hugging Face Gradio SDK entry point (ZeroGPU / Gradio Spaces)."""
2
 
3
+ import os
4
+
5
+ # HF Spaces default SSR to True; Node proxy then binds 7860 and Python falls back to 7861.
6
+ os.environ["GRADIO_SSR_MODE"] = "False"
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ _ROOT = Path(__file__).resolve().parent
12
+ for _src in (
13
+ "apps/gradio-space/src",
14
+ "libs/inference/src",
15
+ "libs/researchmind/src",
16
+ "libs/agent/src",
17
+ "libs/echocoach/src",
18
+ ):
19
+ _path = str(_ROOT / _src)
20
+ if _path not in sys.path:
21
+ sys.path.insert(0, _path)
22
+
23
  from gradio_space.server import main
24
 
25
  if __name__ == "__main__":
apps/gradio-space/src/gradio_space/app.py CHANGED
@@ -24,7 +24,7 @@ def build_demo() -> gr.Blocks:
24
  <div class="brand-block">
25
  <h1>Build Small</h1>
26
  <p>Local lesson slides, research, voice coaching — offline on small models.
27
- <a href="/">Studio UI</a> ·
28
  <a href="https://huggingface.co/build-small-hackathon" target="_blank">Hackathon</a></p>
29
  </div>
30
  """
 
24
  <div class="brand-block">
25
  <h1>Build Small</h1>
26
  <p>Local lesson slides, research, voice coaching — offline on small models.
27
+ <a href="../">Studio UI</a> ·
28
  <a href="https://huggingface.co/build-small-hackathon" target="_blank">Hackathon</a></p>
29
  </div>
30
  """
apps/gradio-space/src/gradio_space/server.py CHANGED
@@ -4,7 +4,8 @@ import os
4
  from pathlib import Path
5
 
6
  import gradio as gr
7
- from fastapi.responses import FileResponse
 
8
  from fastapi.staticfiles import StaticFiles
9
 
10
  from gradio import mount_gradio_app
@@ -36,8 +37,24 @@ def _all_allowed_paths() -> list[str]:
36
  return list(dict.fromkeys(paths))
37
 
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  def create_server() -> gr.Server:
40
  server = gr.Server(title="Build Small Studio")
 
41
 
42
  register_studio_apis(server)
43
 
@@ -45,11 +62,16 @@ def create_server() -> gr.Server:
45
  server.mount("/static/studio", StaticFiles(directory=str(_STATIC_DIR)), name="studio_static")
46
 
47
  @server.get("/")
48
- async def studio_index() -> FileResponse:
 
 
 
49
  return FileResponse(_STATIC_DIR / "index.html")
50
 
51
  @server.get("/studio")
52
- async def studio_alias() -> FileResponse:
 
 
53
  return FileResponse(_STATIC_DIR / "index.html")
54
 
55
  demo = build_demo()
@@ -83,6 +105,7 @@ def main() -> None:
83
  footer_links=[],
84
  allowed_paths=_all_allowed_paths(),
85
  show_error=True,
 
86
  )
87
 
88
 
 
4
  from pathlib import Path
5
 
6
  import gradio as gr
7
+ from fastapi import Request
8
+ from fastapi.responses import FileResponse, RedirectResponse
9
  from fastapi.staticfiles import StaticFiles
10
 
11
  from gradio import mount_gradio_app
 
37
  return list(dict.fromkeys(paths))
38
 
39
 
40
+ def _register_hf_https_middleware(server: gr.Server) -> None:
41
+ """HF terminates TLS; the app sees HTTP and Gradio may emit http:// asset URLs."""
42
+ if not os.environ.get("SPACE_ID"):
43
+ return
44
+
45
+ @server.middleware("http")
46
+ async def force_https_scheme(request, call_next):
47
+ request.scope["scheme"] = "https"
48
+ return await call_next(request)
49
+
50
+
51
+ def _wants_classic_ui(request: Request) -> bool:
52
+ return "classic" in request.query_params
53
+
54
+
55
  def create_server() -> gr.Server:
56
  server = gr.Server(title="Build Small Studio")
57
+ _register_hf_https_middleware(server)
58
 
59
  register_studio_apis(server)
60
 
 
62
  server.mount("/static/studio", StaticFiles(directory=str(_STATIC_DIR)), name="studio_static")
63
 
64
  @server.get("/")
65
+ async def studio_index(request: Request):
66
+ if _wants_classic_ui(request):
67
+ # Relative path keeps huggingface.co/spaces/.../classic on HTTPS (not http hf.space).
68
+ return RedirectResponse(url="classic", status_code=302)
69
  return FileResponse(_STATIC_DIR / "index.html")
70
 
71
  @server.get("/studio")
72
+ async def studio_alias(request: Request):
73
+ if _wants_classic_ui(request):
74
+ return RedirectResponse(url="classic", status_code=302)
75
  return FileResponse(_STATIC_DIR / "index.html")
76
 
77
  demo = build_demo()
 
105
  footer_links=[],
106
  allowed_paths=_all_allowed_paths(),
107
  show_error=True,
108
+ ssr_mode=False,
109
  )
110
 
111
 
apps/gradio-space/static/studio/index.html CHANGED
@@ -21,6 +21,8 @@
21
  <div id="studio-error" class="studio-banner studio-banner-error hidden" role="alert"></div>
22
  <div id="studio-loading" class="studio-banner studio-banner-loading hidden">Working…</div>
23
 
 
 
24
  <aside id="sidebar" class="sidebar">
25
  <button type="button" id="sidebar-close" class="sidebar-close material-symbols-outlined" aria-label="Close menu">close</button>
26
  <div class="sidebar-brand">
@@ -37,11 +39,11 @@
37
  <button type="button" class="nav-item" data-view="language-lessons"><span class="material-symbols-outlined">translate</span>Language lessons</button>
38
  <button type="button" class="nav-item" data-view="debug"><span class="material-symbols-outlined">bug_report</span>Debug</button>
39
  <button type="button" id="btn-open-settings" class="nav-item"><span class="material-symbols-outlined">settings</span>Settings</button>
40
- <a href="/classic" class="nav-item nav-link"><span class="material-symbols-outlined">open_in_new</span>Classic UI</a>
41
  </nav>
42
  <div class="sidebar-footer">
43
  <p class="sidebar-foot-label">Powered by local small models</p>
44
- <a href="/classic">Open Classic UI</a>
45
  </div>
46
  </aside>
47
 
@@ -56,31 +58,41 @@
56
  <button type="button" id="theme-toggle-btn" class="btn btn-ghost btn-icon" aria-label="Toggle light/dark theme" title="Toggle theme">
57
  <span class="material-symbols-outlined" id="theme-icon">dark_mode</span>
58
  </button>
59
- <a href="/classic" class="btn btn-ghost">Classic UI</a>
60
- <button type="button" id="btn-export" class="btn btn-primary" disabled>Export</button>
 
 
 
61
  </div>
62
  </header>
63
 
64
  <div class="workspace-context-bar" id="workspace-context-bar">
65
- <div class="workspace-context-inner">
66
- <label class="field ws-field">
67
- <span>Workspace topic</span>
68
- <input id="workspace-topic" type="text" class="input" value="small model finetuning" placeholder="e.g. Small language model finetuning" />
69
- </label>
70
- <label class="field ws-field">
71
- <span>ResearchMind session</span>
72
- <select id="workspace-session" class="input">
73
- <option value="">New session (on ingest)</option>
74
- </select>
75
- </label>
76
- <button type="button" id="workspace-refresh-sessions" class="btn btn-ghost btn-icon" title="Refresh sessions">↻</button>
77
- <details class="workspace-docs-details" id="workspace-docs-details">
78
- <summary>Source scope (RAG)</summary>
79
- <div id="workspace-doc-list" class="workspace-doc-list"></div>
80
- <p id="workspace-rag-hint" class="status-text"></p>
81
- <p id="workspace-memory" class="status-text workspace-memory"></p>
82
- </details>
83
- </div>
 
 
 
 
 
 
 
84
  </div>
85
 
86
  <main class="workspace" data-view="slides">
@@ -396,7 +408,7 @@
396
  </div>
397
  <p class="lessons-classic-link status-text">
398
  Pitch metrics and monologue analysis live in
399
- <a href="/classic">Classic UI → EchoCoach</a>.
400
  </p>
401
  </div>
402
  </div>
@@ -478,7 +490,7 @@
478
  <summary>Paths &amp; files</summary>
479
  <pre id="settings-paths" class="settings-pre"></pre>
480
  </details>
481
- <a href="/classic" class="btn btn-ghost btn-block">Open Classic UI</a>
482
  </aside>
483
  </div>
484
 
 
21
  <div id="studio-error" class="studio-banner studio-banner-error hidden" role="alert"></div>
22
  <div id="studio-loading" class="studio-banner studio-banner-loading hidden">Working…</div>
23
 
24
+ <button type="button" id="sidebar-backdrop" class="sidebar-backdrop hidden" aria-label="Close menu"></button>
25
+
26
  <aside id="sidebar" class="sidebar">
27
  <button type="button" id="sidebar-close" class="sidebar-close material-symbols-outlined" aria-label="Close menu">close</button>
28
  <div class="sidebar-brand">
 
39
  <button type="button" class="nav-item" data-view="language-lessons"><span class="material-symbols-outlined">translate</span>Language lessons</button>
40
  <button type="button" class="nav-item" data-view="debug"><span class="material-symbols-outlined">bug_report</span>Debug</button>
41
  <button type="button" id="btn-open-settings" class="nav-item"><span class="material-symbols-outlined">settings</span>Settings</button>
42
+ <a href="?classic" class="nav-item nav-link"><span class="material-symbols-outlined">open_in_new</span>Classic UI</a>
43
  </nav>
44
  <div class="sidebar-footer">
45
  <p class="sidebar-foot-label">Powered by local small models</p>
46
+ <a href="?classic">Open Classic UI</a>
47
  </div>
48
  </aside>
49
 
 
58
  <button type="button" id="theme-toggle-btn" class="btn btn-ghost btn-icon" aria-label="Toggle light/dark theme" title="Toggle theme">
59
  <span class="material-symbols-outlined" id="theme-icon">dark_mode</span>
60
  </button>
61
+ <a href="?classic" class="btn btn-ghost topbar-classic-link" aria-label="Classic UI" title="Classic UI">
62
+ <span class="material-symbols-outlined">open_in_new</span>
63
+ <span class="topbar-classic-label">Classic UI</span>
64
+ </a>
65
+ <button type="button" id="btn-export" class="btn btn-primary topbar-export" disabled>Export</button>
66
  </div>
67
  </header>
68
 
69
  <div class="workspace-context-bar" id="workspace-context-bar">
70
+ <details class="workspace-context-mobile" id="workspace-context-mobile">
71
+ <summary class="workspace-context-summary">
72
+ <span class="material-symbols-outlined workspace-context-summary-icon">tune</span>
73
+ <span id="workspace-context-summary-text" class="workspace-context-summary-text">Workspace</span>
74
+ <span class="material-symbols-outlined workspace-context-chevron">expand_more</span>
75
+ </summary>
76
+ <div class="workspace-context-inner">
77
+ <label class="field ws-field">
78
+ <span>Workspace topic</span>
79
+ <input id="workspace-topic" type="text" class="input" value="small model finetuning" placeholder="e.g. Small language model finetuning" />
80
+ </label>
81
+ <label class="field ws-field">
82
+ <span>ResearchMind session</span>
83
+ <select id="workspace-session" class="input">
84
+ <option value="">New session (on ingest)</option>
85
+ </select>
86
+ </label>
87
+ <button type="button" id="workspace-refresh-sessions" class="btn btn-ghost btn-icon" title="Refresh sessions">↻</button>
88
+ <details class="workspace-docs-details" id="workspace-docs-details">
89
+ <summary>Source scope (RAG)</summary>
90
+ <div id="workspace-doc-list" class="workspace-doc-list"></div>
91
+ <p id="workspace-rag-hint" class="status-text"></p>
92
+ <p id="workspace-memory" class="status-text workspace-memory"></p>
93
+ </details>
94
+ </div>
95
+ </details>
96
  </div>
97
 
98
  <main class="workspace" data-view="slides">
 
408
  </div>
409
  <p class="lessons-classic-link status-text">
410
  Pitch metrics and monologue analysis live in
411
+ <a href="?classic">Classic UI → EchoCoach</a>.
412
  </p>
413
  </div>
414
  </div>
 
490
  <summary>Paths &amp; files</summary>
491
  <pre id="settings-paths" class="settings-pre"></pre>
492
  </details>
493
+ <a href="?classic" class="btn btn-ghost btn-block">Open Classic UI</a>
494
  </aside>
495
  </div>
496
 
apps/gradio-space/static/studio/studio.css CHANGED
@@ -165,8 +165,10 @@ body {
165
  display: flex;
166
  align-items: center;
167
  justify-content: space-between;
 
168
  padding: 0 2rem;
169
  z-index: 40;
 
170
  }
171
 
172
  .breadcrumb {
@@ -175,17 +177,67 @@ body {
175
  gap: 0.5rem;
176
  font-size: 0.9rem;
177
  color: var(--secondary);
 
 
178
  }
179
 
180
  .breadcrumb strong {
181
  color: var(--on-surface);
182
  border-bottom: 2px solid var(--primary);
183
  padding-bottom: 0.35rem;
 
 
 
 
184
  }
185
 
186
  .crumb-sep { font-size: 1rem; }
187
 
188
- .topbar-actions { display: flex; gap: 0.75rem; align-items: center; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
  .workspace-context-bar {
191
  position: fixed;
@@ -1282,19 +1334,178 @@ body {
1282
  }
1283
  }
1284
 
 
 
 
 
 
 
1285
  @media (max-width: 768px) {
1286
- :root { --sidebar-w: 0px; }
 
 
 
 
 
1287
  .sidebar {
1288
  transform: translateX(-100%);
1289
- transition: transform 0.2s ease;
1290
  width: min(280px, 85vw);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1291
  }
1292
- .sidebar.open { transform: translateX(0); }
1293
- .sidebar-close, .topbar-icon { display: inline-flex; background: none; border: none; cursor: pointer; color: var(--secondary); }
1294
- .topbar { left: 0; padding: 0 1rem; }
1295
- .workspace-context-bar { left: 0; padding: 0.5rem 1rem; }
1296
- .workspace { margin-left: 0; padding-top: calc(var(--topbar-h) + var(--context-bar-h) + 1rem); }
1297
- .studio-banner { left: 0; }
1298
  }
1299
 
1300
  /* Parity additions */
 
165
  display: flex;
166
  align-items: center;
167
  justify-content: space-between;
168
+ gap: 0.75rem;
169
  padding: 0 2rem;
170
  z-index: 40;
171
+ min-width: 0;
172
  }
173
 
174
  .breadcrumb {
 
177
  gap: 0.5rem;
178
  font-size: 0.9rem;
179
  color: var(--secondary);
180
+ min-width: 0;
181
+ flex: 1;
182
  }
183
 
184
  .breadcrumb strong {
185
  color: var(--on-surface);
186
  border-bottom: 2px solid var(--primary);
187
  padding-bottom: 0.35rem;
188
+ min-width: 0;
189
+ overflow: hidden;
190
+ text-overflow: ellipsis;
191
+ white-space: nowrap;
192
  }
193
 
194
  .crumb-sep { font-size: 1rem; }
195
 
196
+ .topbar-actions { display: flex; gap: 0.5rem; align-items: center; flex-shrink: 0; }
197
+
198
+ .topbar-classic-link .material-symbols-outlined {
199
+ font-size: 1.15rem;
200
+ }
201
+
202
+ .topbar-classic-label {
203
+ margin-left: 0.15rem;
204
+ }
205
+
206
+ .sidebar-backdrop {
207
+ position: fixed;
208
+ inset: 0;
209
+ z-index: 45;
210
+ background: rgba(0, 0, 0, 0.45);
211
+ opacity: 0;
212
+ transition: opacity 0.2s ease;
213
+ border: none;
214
+ padding: 0;
215
+ cursor: pointer;
216
+ }
217
+
218
+ .sidebar-backdrop:not(.hidden) {
219
+ opacity: 1;
220
+ }
221
+
222
+ body.sidebar-open {
223
+ overflow: hidden;
224
+ }
225
+
226
+ .workspace-context-mobile {
227
+ margin: 0;
228
+ }
229
+
230
+ .workspace-context-mobile > summary {
231
+ list-style: none;
232
+ }
233
+
234
+ .workspace-context-mobile > summary::-webkit-details-marker {
235
+ display: none;
236
+ }
237
+
238
+ .workspace-context-summary {
239
+ display: none;
240
+ }
241
 
242
  .workspace-context-bar {
243
  position: fixed;
 
1334
  }
1335
  }
1336
 
1337
+ @media (min-width: 769px) {
1338
+ .workspace-context-mobile .workspace-context-inner {
1339
+ display: flex !important;
1340
+ }
1341
+ }
1342
+
1343
  @media (max-width: 768px) {
1344
+ :root {
1345
+ --sidebar-w: 0px;
1346
+ --topbar-h: 52px;
1347
+ --context-bar-h: 44px;
1348
+ }
1349
+
1350
  .sidebar {
1351
  transform: translateX(-100%);
1352
+ transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
1353
  width: min(280px, 85vw);
1354
+ z-index: 60;
1355
+ box-shadow: 4px 0 24px rgba(0, 0, 0, 0.15);
1356
+ }
1357
+
1358
+ .sidebar.open {
1359
+ transform: translateX(0);
1360
+ }
1361
+
1362
+ .sidebar-close,
1363
+ .topbar-icon {
1364
+ display: inline-flex;
1365
+ align-items: center;
1366
+ justify-content: center;
1367
+ min-width: 2.5rem;
1368
+ min-height: 2.5rem;
1369
+ background: none;
1370
+ border: none;
1371
+ cursor: pointer;
1372
+ color: var(--secondary);
1373
+ border-radius: 8px;
1374
+ flex-shrink: 0;
1375
+ }
1376
+
1377
+ .topbar-icon:active,
1378
+ .sidebar-close:active {
1379
+ background: var(--surface-container-low);
1380
+ }
1381
+
1382
+ .topbar {
1383
+ left: 0;
1384
+ right: 0;
1385
+ display: grid;
1386
+ grid-template-columns: auto minmax(0, 1fr) auto;
1387
+ gap: 0.35rem 0.5rem;
1388
+ padding: 0 0.75rem;
1389
+ padding-top: env(safe-area-inset-top, 0);
1390
+ height: auto;
1391
+ min-height: var(--topbar-h);
1392
+ }
1393
+
1394
+ .breadcrumb > span:first-child,
1395
+ .breadcrumb .crumb-sep {
1396
+ display: none;
1397
+ }
1398
+
1399
+ .breadcrumb strong {
1400
+ border-bottom: none;
1401
+ padding-bottom: 0;
1402
+ font-size: 0.95rem;
1403
+ font-weight: 600;
1404
+ }
1405
+
1406
+ .topbar-classic-link,
1407
+ .topbar-export {
1408
+ display: none !important;
1409
+ }
1410
+
1411
+ body[data-view="slides"] .topbar-export:not(:disabled) {
1412
+ display: inline-flex !important;
1413
+ }
1414
+
1415
+ .topbar-actions {
1416
+ gap: 0.25rem;
1417
+ }
1418
+
1419
+ .topbar-actions .btn-icon {
1420
+ min-width: 2.5rem;
1421
+ min-height: 2.5rem;
1422
+ padding: 0.4rem;
1423
+ }
1424
+
1425
+ .workspace-context-bar {
1426
+ left: 0;
1427
+ padding: 0;
1428
+ }
1429
+
1430
+ .workspace-context-summary {
1431
+ display: flex;
1432
+ align-items: center;
1433
+ gap: 0.5rem;
1434
+ padding: 0.55rem 0.75rem;
1435
+ cursor: pointer;
1436
+ font-size: 0.85rem;
1437
+ font-weight: 500;
1438
+ color: var(--on-surface);
1439
+ user-select: none;
1440
+ -webkit-tap-highlight-color: transparent;
1441
+ }
1442
+
1443
+ .workspace-context-summary-icon {
1444
+ font-size: 1.1rem;
1445
+ color: var(--secondary);
1446
+ flex-shrink: 0;
1447
+ }
1448
+
1449
+ .workspace-context-summary-text {
1450
+ flex: 1;
1451
+ min-width: 0;
1452
+ overflow: hidden;
1453
+ text-overflow: ellipsis;
1454
+ white-space: nowrap;
1455
+ }
1456
+
1457
+ .workspace-context-chevron {
1458
+ font-size: 1.25rem;
1459
+ color: var(--secondary);
1460
+ flex-shrink: 0;
1461
+ transition: transform 0.2s ease;
1462
+ }
1463
+
1464
+ .workspace-context-mobile[open] .workspace-context-chevron {
1465
+ transform: rotate(180deg);
1466
+ }
1467
+
1468
+ .workspace-context-mobile[open] .workspace-context-inner {
1469
+ padding: 0 0.75rem 0.75rem;
1470
+ border-top: 1px solid var(--outline-variant);
1471
+ }
1472
+
1473
+ .workspace-context-mobile:not([open]) .workspace-context-inner {
1474
+ display: none;
1475
+ }
1476
+
1477
+ .workspace-context-inner .ws-field {
1478
+ flex: 1 1 100%;
1479
+ min-width: 0;
1480
+ }
1481
+
1482
+ .workspace {
1483
+ margin-left: 0;
1484
+ padding: calc(var(--topbar-h) + var(--context-bar-h) + 0.75rem) 0.75rem 1rem;
1485
+ }
1486
+
1487
+ .studio-banner {
1488
+ left: 0;
1489
+ }
1490
+
1491
+ .workspace-context-bar,
1492
+ .topbar {
1493
+ padding-left: max(0.75rem, env(safe-area-inset-left));
1494
+ padding-right: max(0.75rem, env(safe-area-inset-right));
1495
+ }
1496
+
1497
+ .card {
1498
+ padding: 0.85rem;
1499
+ border-radius: var(--radius-lg);
1500
+ }
1501
+
1502
+ .controls-grid {
1503
+ grid-template-columns: 1fr;
1504
+ }
1505
+
1506
+ .section-label {
1507
+ font-size: 0.68rem;
1508
  }
 
 
 
 
 
 
1509
  }
1510
 
1511
  /* Parity additions */
apps/gradio-space/static/studio/studio.js CHANGED
@@ -25,6 +25,12 @@ function toggleTheme() {
25
 
26
  applyTheme(getPreferredTheme());
27
 
 
 
 
 
 
 
28
  const SLIDE_PIPELINE_STEPS = [
29
  "Load language model",
30
  "Gather lesson sources",
@@ -745,7 +751,70 @@ async function refreshDebugDocuments() {
745
  function updateProjectTitle() {
746
  const topic = state.workspaceTopic || "";
747
  const short = topic.split(" for ")[0] || topic || "Project";
748
- $("#project-title").textContent = short.slice(0, 40);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
  }
750
 
751
  function updateWorkspaceRagHint() {
@@ -763,7 +832,7 @@ function updateWorkspaceRagHint() {
763
 
764
  async function getClient() {
765
  if (!state.client) {
766
- state.client = await Client.connect(window.location.origin);
767
  }
768
  return state.client;
769
  }
@@ -942,7 +1011,7 @@ async function callApi(name, args = []) {
942
  return data;
943
  } catch (err) {
944
  const message = err?.message || String(err);
945
- showError(`${message} — try Classic UI at /classic`);
946
  throw err;
947
  } finally {
948
  setLoading(false);
@@ -1012,6 +1081,7 @@ async function refreshWorkspaceSessions(selectId) {
1012
  }
1013
  }
1014
  await refreshDebugSessions();
 
1015
  }
1016
 
1017
  async function refreshDocuments() {
@@ -1104,6 +1174,7 @@ async function reloadModelFromSettings() {
1104
  async function initWorkspace() {
1105
  $("#workspace-topic").value = state.workspaceTopic;
1106
  syncResearchLayout();
 
1107
  updateProjectTitle();
1108
  updateResearchRagBadge();
1109
  await refreshWorkspaceSessions();
@@ -1115,6 +1186,7 @@ async function initWorkspace() {
1115
  await refreshDebugDocuments();
1116
  const recStatus = await callApi("recording_status", []);
1117
  state.useBrowserMic = !recStatus.backend || /unavailable|no capture/i.test(recStatus.message || "");
 
1118
  }
1119
 
1120
  async function ingestUrl() {
@@ -1211,6 +1283,7 @@ async function generateSlides() {
1211
  <a href="${fileUrl(data.downloads.docx)}" download>DOCX</a>
1212
  <a href="${fileUrl(data.downloads.html)}" download>HTML</a>`;
1213
  $("#btn-export").disabled = false;
 
1214
  }
1215
 
1216
  const outlineDetails = $("#slide-outline-details");
@@ -1455,11 +1528,15 @@ function bindUi() {
1455
  btn.classList.add("active");
1456
  $(".workspace").dataset.view = btn.dataset.view;
1457
  syncResearchLayout();
1458
- $("#sidebar").classList.remove("open");
 
1459
  });
1460
  });
1461
 
1462
- $("#btn-open-settings")?.addEventListener("click", openSettingsDrawer);
 
 
 
1463
  $("#btn-close-settings")?.addEventListener("click", closeSettingsDrawer);
1464
  $("#settings-backdrop")?.addEventListener("click", closeSettingsDrawer);
1465
  $("#theme-toggle")?.addEventListener("change", toggleTheme);
@@ -1467,8 +1544,9 @@ function bindUi() {
1467
  $("#btn-reload-model")?.addEventListener("click", () => reloadModelFromSettings().catch(() => {}));
1468
 
1469
  $("#btn-open-research-view")?.addEventListener("click", openResearchView);
1470
- $("#sidebar-open")?.addEventListener("click", () => $("#sidebar").classList.add("open"));
1471
- $("#sidebar-close")?.addEventListener("click", () => $("#sidebar").classList.remove("open"));
 
1472
 
1473
  $("#workspace-topic").addEventListener("input", (e) => {
1474
  state.workspaceTopic = e.target.value.trim();
@@ -1477,6 +1555,7 @@ function bindUi() {
1477
 
1478
  $("#workspace-session").addEventListener("change", (e) => {
1479
  state.workspaceSessionId = e.target.value;
 
1480
  refreshDocuments().catch(() => {});
1481
  refreshDebugDocuments().catch(() => {});
1482
  });
@@ -1597,10 +1676,11 @@ function bindUi() {
1597
  });
1598
 
1599
  syncLessonsModeUi();
 
1600
  }
1601
 
1602
  bindUi();
1603
  initWorkspace().catch((err) => {
1604
  console.error(err);
1605
- showError("Could not connect to Studio API. Open /classic for full Gradio UI.");
1606
  });
 
25
 
26
  applyTheme(getPreferredTheme());
27
 
28
+ function appOrigin() {
29
+ const { protocol, hostname } = window.location;
30
+ const secureProto = protocol === "http:" ? "https:" : protocol;
31
+ return `${secureProto}//${hostname}`;
32
+ }
33
+
34
  const SLIDE_PIPELINE_STEPS = [
35
  "Load language model",
36
  "Gather lesson sources",
 
751
  function updateProjectTitle() {
752
  const topic = state.workspaceTopic || "";
753
  const short = topic.split(" for ")[0] || topic || "Project";
754
+ const title = short.slice(0, 40);
755
+ const el = $("#project-title");
756
+ if (el) {
757
+ el.textContent = title;
758
+ el.title = topic || title;
759
+ }
760
+ updateWorkspaceContextSummary();
761
+ }
762
+
763
+ function updateWorkspaceContextSummary() {
764
+ const el = $("#workspace-context-summary-text");
765
+ if (!el) return;
766
+ const topic = (state.workspaceTopic || "Workspace").trim();
767
+ const shortTopic = (topic.split(" for ")[0] || topic || "Workspace").slice(0, 32);
768
+ const sessionSel = $("#workspace-session");
769
+ let sessionLabel = "New session";
770
+ if (sessionSel?.value) {
771
+ const label = sessionSel.selectedOptions[0]?.textContent?.trim() || "Session";
772
+ sessionLabel = label.length > 22 ? `${label.slice(0, 19)}…` : label;
773
+ }
774
+ el.textContent = `${shortTopic} · ${sessionLabel}`;
775
+ el.title = topic ? `${topic} · ${sessionLabel}` : sessionLabel;
776
+ }
777
+
778
+ function syncViewChrome(view) {
779
+ const active = view || $(".workspace")?.dataset.view || "slides";
780
+ document.body.dataset.view = active;
781
+ }
782
+
783
+ function openSidebar() {
784
+ $("#sidebar")?.classList.add("open");
785
+ $("#sidebar-backdrop")?.classList.remove("hidden");
786
+ document.body.classList.add("sidebar-open");
787
+ }
788
+
789
+ function closeSidebar() {
790
+ $("#sidebar")?.classList.remove("open");
791
+ $("#sidebar-backdrop")?.classList.add("hidden");
792
+ document.body.classList.remove("sidebar-open");
793
+ }
794
+
795
+ function syncLayoutOffsets() {
796
+ const topbar = $(".topbar");
797
+ const ctxBar = $("#workspace-context-bar");
798
+ if (!topbar || !ctxBar) return;
799
+ document.documentElement.style.setProperty("--topbar-h", `${topbar.offsetHeight}px`);
800
+ document.documentElement.style.setProperty("--context-bar-h", `${ctxBar.offsetHeight}px`);
801
+ }
802
+
803
+ function bindLayoutSync() {
804
+ syncLayoutOffsets();
805
+ window.addEventListener("resize", syncLayoutOffsets);
806
+ const ctxBar = $("#workspace-context-bar");
807
+ if (ctxBar && typeof ResizeObserver !== "undefined") {
808
+ const ro = new ResizeObserver(() => syncLayoutOffsets());
809
+ ro.observe(ctxBar);
810
+ if ($(".topbar")) ro.observe($(".topbar"));
811
+ }
812
+ $("#workspace-context-mobile")?.addEventListener("toggle", syncLayoutOffsets);
813
+ document.addEventListener("keydown", (e) => {
814
+ if (e.key === "Escape" && $("#sidebar")?.classList.contains("open")) {
815
+ closeSidebar();
816
+ }
817
+ });
818
  }
819
 
820
  function updateWorkspaceRagHint() {
 
832
 
833
  async function getClient() {
834
  if (!state.client) {
835
+ state.client = await Client.connect(appOrigin());
836
  }
837
  return state.client;
838
  }
 
1011
  return data;
1012
  } catch (err) {
1013
  const message = err?.message || String(err);
1014
+ showError(`${message} — try Classic UI (?classic)`);
1015
  throw err;
1016
  } finally {
1017
  setLoading(false);
 
1081
  }
1082
  }
1083
  await refreshDebugSessions();
1084
+ updateWorkspaceContextSummary();
1085
  }
1086
 
1087
  async function refreshDocuments() {
 
1174
  async function initWorkspace() {
1175
  $("#workspace-topic").value = state.workspaceTopic;
1176
  syncResearchLayout();
1177
+ syncViewChrome();
1178
  updateProjectTitle();
1179
  updateResearchRagBadge();
1180
  await refreshWorkspaceSessions();
 
1186
  await refreshDebugDocuments();
1187
  const recStatus = await callApi("recording_status", []);
1188
  state.useBrowserMic = !recStatus.backend || /unavailable|no capture/i.test(recStatus.message || "");
1189
+ syncLayoutOffsets();
1190
  }
1191
 
1192
  async function ingestUrl() {
 
1283
  <a href="${fileUrl(data.downloads.docx)}" download>DOCX</a>
1284
  <a href="${fileUrl(data.downloads.html)}" download>HTML</a>`;
1285
  $("#btn-export").disabled = false;
1286
+ syncLayoutOffsets();
1287
  }
1288
 
1289
  const outlineDetails = $("#slide-outline-details");
 
1528
  btn.classList.add("active");
1529
  $(".workspace").dataset.view = btn.dataset.view;
1530
  syncResearchLayout();
1531
+ syncViewChrome(btn.dataset.view);
1532
+ closeSidebar();
1533
  });
1534
  });
1535
 
1536
+ $("#btn-open-settings")?.addEventListener("click", () => {
1537
+ closeSidebar();
1538
+ openSettingsDrawer();
1539
+ });
1540
  $("#btn-close-settings")?.addEventListener("click", closeSettingsDrawer);
1541
  $("#settings-backdrop")?.addEventListener("click", closeSettingsDrawer);
1542
  $("#theme-toggle")?.addEventListener("change", toggleTheme);
 
1544
  $("#btn-reload-model")?.addEventListener("click", () => reloadModelFromSettings().catch(() => {}));
1545
 
1546
  $("#btn-open-research-view")?.addEventListener("click", openResearchView);
1547
+ $("#sidebar-open")?.addEventListener("click", openSidebar);
1548
+ $("#sidebar-close")?.addEventListener("click", closeSidebar);
1549
+ $("#sidebar-backdrop")?.addEventListener("click", closeSidebar);
1550
 
1551
  $("#workspace-topic").addEventListener("input", (e) => {
1552
  state.workspaceTopic = e.target.value.trim();
 
1555
 
1556
  $("#workspace-session").addEventListener("change", (e) => {
1557
  state.workspaceSessionId = e.target.value;
1558
+ updateWorkspaceContextSummary();
1559
  refreshDocuments().catch(() => {});
1560
  refreshDebugDocuments().catch(() => {});
1561
  });
 
1676
  });
1677
 
1678
  syncLessonsModeUi();
1679
+ bindLayoutSync();
1680
  }
1681
 
1682
  bindUi();
1683
  initWorkspace().catch((err) => {
1684
  console.error(err);
1685
+ showError("Could not connect to Studio API. Open ?classic for full Gradio UI.");
1686
  });
libs/inference/src/inference/factory.py CHANGED
@@ -1,6 +1,5 @@
1
  from inference.base import InferenceBackend
2
  from inference.config import ModelConfig, get_model_config
3
- from inference.llama_cpp import LlamaCppBackend
4
 
5
  _backend: InferenceBackend | None = None
6
  _backend_key: tuple | None = None
@@ -8,6 +7,8 @@ _backend_key: tuple | None = None
8
 
9
  def _create_backend(config: ModelConfig) -> InferenceBackend:
10
  if config.backend == "llama_cpp":
 
 
11
  return LlamaCppBackend(config)
12
 
13
  if config.backend == "transformers":
 
1
  from inference.base import InferenceBackend
2
  from inference.config import ModelConfig, get_model_config
 
3
 
4
  _backend: InferenceBackend | None = None
5
  _backend_key: tuple | None = None
 
7
 
8
  def _create_backend(config: ModelConfig) -> InferenceBackend:
9
  if config.backend == "llama_cpp":
10
+ from inference.llama_cpp import LlamaCppBackend
11
+
12
  return LlamaCppBackend(config)
13
 
14
  if config.backend == "transformers":
requirements.txt CHANGED
@@ -1,9 +1,5 @@
1
- # Workspace packages (HF clones full repo)
2
- -e ./libs/inference
3
- -e ./libs/researchmind
4
- -e ./libs/agent
5
- -e ./libs/echocoach[piper,whisper]
6
- -e ./apps/gradio-space
7
 
8
  # Pinned runtime deps (do not pin gradio, spaces, or huggingface_hub — HF preinstalls them)
9
  accelerate==1.13.0
@@ -11,7 +7,8 @@ torch==2.12.0
11
  torchvision==0.27.0
12
  transformers==5.10.2
13
  peft==0.19.1
14
- llama-cpp-python==0.3.26
 
15
  sentence-transformers==5.5.1
16
  pydantic>=2.0.0
17
  pyyaml>=6.0.2
 
1
+ # Monorepo workspace packages are loaded via PYTHONPATH in app.py (HF installs
2
+ # requirements before copying the repo, so -e ./libs/* editable installs fail).
 
 
 
 
3
 
4
  # Pinned runtime deps (do not pin gradio, spaces, or huggingface_hub — HF preinstalls them)
5
  accelerate==1.13.0
 
7
  torchvision==0.27.0
8
  transformers==5.10.2
9
  peft==0.19.1
10
+ # llama-cpp-python omitted — compiles from source on HF (10+ min / timeout).
11
+ # Space uses transformers presets (minicpm5-1b). Install locally for GGUF presets.
12
  sentence-transformers==5.5.1
13
  pydantic>=2.0.0
14
  pyyaml>=6.0.2