fffiloni commited on
Commit
6707b93
·
verified ·
1 Parent(s): c4ac2fb

Upload 3 files

Browse files
Files changed (3) hide show
  1. CHANGELOG.md +9 -0
  2. README.md +8 -16
  3. app.py +58 -34
CHANGELOG.md CHANGED
@@ -9,6 +9,15 @@
9
 
10
  # Changelog
11
 
 
 
 
 
 
 
 
 
 
12
  ## v31 — Functional feedback verification
13
 
14
  - Added stronger Custom UI feedback states so bucket checks, build launch, validation launch, and polling always show visible status.
 
9
 
10
  # Changelog
11
 
12
+ ## V33 — Root custom dashboard
13
+
14
+ - Serve the custom dashboard directly at `/` and `/custom`.
15
+ - Move the legacy Gradio controls behind `/_gradio` for OAuth/debug only.
16
+ - Remove the product-facing Gradio iframe/preview experience.
17
+ - Keep OAuth-backed API routes and custom dashboard behavior intact.
18
+ - Add `uvicorn` as an explicit runtime dependency.
19
+
20
+
21
  ## v31 — Functional feedback verification
22
 
23
  - Added stronger Custom UI feedback states so bucket checks, build launch, validation launch, and polling always show visible status.
README.md CHANGED
@@ -26,7 +26,9 @@ model card → agentic job → generated private Space → hardware if available
26
 
27
  ## Custom UI
28
 
29
- A custom dashboard is available at `/custom`. It is wired to the same OAuth-backed backend as the Gradio fallback and supports:
 
 
30
 
31
  - bucket onboarding, including check/create before build;
32
  - build launch with explicit ZeroGPU-first and fixed-GPU fallback controls;
@@ -35,7 +37,11 @@ A custom dashboard is available at `/custom`. It is wired to the same OAuth-back
35
  - recent run exploration from the signed-in user's Bucket;
36
  - quick links to the HF Job, generated Space, Space settings, and run artifacts.
37
 
38
- The Gradio controls remain available as a conservative fallback while the custom UI is validated across more models. The custom UI now follows the validated dashboard layout: Build from model card, live progress, validation metrics, recent events, Run Explorer, selected run details, report preview, and quick links are visible from the main cockpit. It prevents build launches until the user bucket is ready, validates required fields before launch, shows explicit busy/error/success states, and keeps retrying live progress with a visible warning if polling is temporarily unavailable. This avoids leaving users unsure whether the Job is still running.
 
 
 
 
39
 
40
  ## V26 custom Run Explorer with selected-run actions
41
 
@@ -117,20 +123,6 @@ Generated Spaces no longer pin `huggingface_hub<1.0.0`; modern model cards may r
117
  Hardware decisions are written to `hardware_strategy.json` and `hardware_attempts.json` in the user run bucket.
118
 
119
 
120
- ## V24 custom UI shell
121
-
122
- V24 introduces the first custom-frontend pass without replacing the validated Gradio controls yet. The custom dashboard is served at `/custom` and embedded in the app as **Custom UI preview**. It includes:
123
-
124
- - a product-style Build from model card screen,
125
- - a live progress card and timeline model,
126
- - quick links placeholders,
127
- - a Run Explorer with selected-run actions shell for the connected bucket,
128
- - static frontend assets under `web/static/`,
129
- - a small API skeleton for app info, progress projection, and future run listing.
130
-
131
- The OAuth-backed actions remain in the functional Gradio controls for this pass. The next custom UI pass should wire the frontend actions to authenticated backend endpoints while preserving the same per-user bucket and Job security model.
132
-
133
-
134
  ### Custom UI V27
135
 
136
  The custom interface now includes run-level actions: select a previous run, open its Job/Space/Settings/Artifacts, or prepare the Validate existing Space form directly from that run. A manual hardware action panel appears when a run requires user hardware selection.
 
26
 
27
  ## Custom UI
28
 
29
+ The product root (`/`) is now the custom dashboard. `/custom` is kept as an alias for the same UI. Gradio is no longer shown in the normal product flow; a debug/auth mount exists under `/_gradio` only to preserve Hugging Face OAuth routes and internal diagnostics.
30
+
31
+ The custom dashboard supports:
32
 
33
  - bucket onboarding, including check/create before build;
34
  - build launch with explicit ZeroGPU-first and fixed-GPU fallback controls;
 
37
  - recent run exploration from the signed-in user's Bucket;
38
  - quick links to the HF Job, generated Space, Space settings, and run artifacts.
39
 
40
+ The UI follows the validated dashboard layout: Build from model card, live progress, validation metrics, recent events, Run Explorer, selected run details, report preview, and quick links are visible from the main cockpit. It prevents build launches until the user bucket is ready, validates required fields before launch, shows explicit busy/error/success states, and keeps retrying live progress with a visible warning if polling is temporarily unavailable. This avoids leaving users unsure whether the Job is still running.
41
+
42
+ ### V33 root custom dashboard
43
+
44
+ V33 removes the Gradio iframe/product shell problem: the public root is the custom dashboard itself. The old Gradio controls are not part of the normal UI anymore.
45
 
46
  ## V26 custom Run Explorer with selected-run actions
47
 
 
123
  Hardware decisions are written to `hardware_strategy.json` and `hardware_attempts.json` in the user run bucket.
124
 
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  ### Custom UI V27
127
 
128
  The custom interface now includes run-level actions: select a previous run, open its Job/Space/Settings/Artifacts, or prepare the Validate existing Space form directly from that run. A manual hardware action panel appears when a run requires user hardware selection.
app.py CHANGED
@@ -5,8 +5,9 @@ from pathlib import Path
5
  from typing import Any
6
 
7
  import gradio as gr
8
- from fastapi import HTTPException, Request
9
- from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
 
10
 
11
  from src.bucket import check_user_bucket, create_user_bucket, read_run_bundle, list_recent_runs
12
  from src.config import settings, user_bucket_source
@@ -99,17 +100,18 @@ def _api_links(*, run_id: str | None, bucket_source: str | None, target_space: s
99
  "artifacts_url": _run_artifacts_url(run_id, bucket_source),
100
  }
101
 
102
- def register_custom_routes(demo: gr.Blocks) -> None:
103
- """Register the custom UI and OAuth-backed JSON endpoints."""
104
 
105
- @demo.app.get("/custom", response_class=HTMLResponse)
 
106
  async def custom_index(): # type: ignore[no-untyped-def]
107
  index_path = WEB_DIR / "index.html"
108
  if not index_path.exists():
109
  raise HTTPException(status_code=404, detail="Custom UI index not found")
110
  return HTMLResponse(index_path.read_text(encoding="utf-8"))
111
 
112
- @demo.app.get("/custom-static/{asset_path:path}")
113
  async def custom_static(asset_path: str): # type: ignore[no-untyped-def]
114
  path = (STATIC_DIR / asset_path).resolve()
115
  if STATIC_DIR.resolve() not in path.parents and path != STATIC_DIR.resolve():
@@ -118,7 +120,15 @@ def register_custom_routes(demo: gr.Blocks) -> None:
118
  raise HTTPException(status_code=404, detail="Asset not found")
119
  return FileResponse(path)
120
 
121
- @demo.app.get("/api/app-info")
 
 
 
 
 
 
 
 
122
  async def api_app_info(request: Request): # type: ignore[no-untyped-def]
123
  ctx: dict[str, Any] | None = None
124
  try:
@@ -128,17 +138,17 @@ def register_custom_routes(demo: gr.Blocks) -> None:
128
  return JSONResponse(
129
  {
130
  "name": "Agentic Space Factory",
131
- "version": "v31-functional-feedback",
132
  "bucket_default": settings.bucket_name,
133
  "workflows": ["build_from_model_card", "validate_existing_space", "runs_explorer"],
134
- "custom_ui_status": "oauth_verified_bridge",
135
  "user": {"username": ctx["username"], "missing_scopes": ctx.get("missing_scopes", []), "warnings": ctx.get("warnings", [])} if ctx else None,
136
- "login_url": "/login/huggingface",
137
- "logout_url": "/logout",
138
  }
139
  )
140
 
141
- @demo.app.get("/api/me")
142
  async def api_me(request: Request): # type: ignore[no-untyped-def]
143
  ctx = _oauth_context_from_request(request)
144
  return JSONResponse(
@@ -155,29 +165,29 @@ def register_custom_routes(demo: gr.Blocks) -> None:
155
  "missing_scopes": ctx.get("missing_scopes", []),
156
  "warnings": ctx.get("warnings", []),
157
  "expires_at": ctx.get("expires_at"),
158
- "login_url": "/login/huggingface",
159
- "logout_url": "/logout",
160
  }
161
  )
162
 
163
- @demo.app.get("/api/oauth/diagnostics")
164
  async def api_oauth_diagnostics(request: Request): # type: ignore[no-untyped-def]
165
  ctx = extract_oauth_context(request)
166
  return JSONResponse({"user": public_oauth_context(ctx), "token_identity": verify_token_identity(ctx)})
167
 
168
- @demo.app.get("/api/bucket/status")
169
  async def api_bucket_status(request: Request, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
170
  ctx = _oauth_context_from_request(request)
171
  return JSONResponse(check_user_bucket(username=ctx["username"], bucket_name=bucket_name, token=ctx["token"]))
172
 
173
- @demo.app.post("/api/bucket/create")
174
  async def api_bucket_create(request: Request, payload: dict[str, Any] | None = None): # type: ignore[no-untyped-def]
175
  ctx = _oauth_context_from_request(request)
176
  payload = payload or {}
177
  bucket_name = str(payload.get("bucket_name") or settings.bucket_name)
178
  return JSONResponse(create_user_bucket(username=ctx["username"], bucket_name=bucket_name, token=ctx["token"]))
179
 
180
- @demo.app.post("/api/build")
181
  async def api_build(request: Request, payload: dict[str, Any]): # type: ignore[no-untyped-def]
182
  ctx = _oauth_context_from_request(request)
183
  bucket_name = payload.get("bucket_name") or settings.bucket_name
@@ -208,7 +218,7 @@ def register_custom_routes(demo: gr.Blocks) -> None:
208
  )
209
  return JSONResponse(result)
210
 
211
- @demo.app.post("/api/validate")
212
  async def api_validate(request: Request, payload: dict[str, Any]): # type: ignore[no-untyped-def]
213
  ctx = _oauth_context_from_request(request)
214
  bucket_name = payload.get("bucket_name") or settings.bucket_name
@@ -241,7 +251,7 @@ def register_custom_routes(demo: gr.Blocks) -> None:
241
  )
242
  return JSONResponse(result)
243
 
244
- @demo.app.post("/api/progress/from-events")
245
  async def api_progress_from_events(payload: dict[str, Any]): # type: ignore[no-untyped-def]
246
  events = payload.get("events") or []
247
  state = payload.get("state") or {}
@@ -251,7 +261,7 @@ def register_custom_routes(demo: gr.Blocks) -> None:
251
  raise HTTPException(status_code=400, detail="state must be an object")
252
  return JSONResponse(progress_from_events(events, state=state))
253
 
254
- @demo.app.get("/api/runs")
255
  async def api_runs( # type: ignore[no-untyped-def]
256
  request: Request,
257
  bucket_name: str = settings.bucket_name,
@@ -270,7 +280,7 @@ def register_custom_routes(demo: gr.Blocks) -> None:
270
  }
271
  )
272
 
273
- @demo.app.get("/api/runs/{run_id}")
274
  async def api_run_detail(request: Request, run_id: str, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
275
  ctx = _oauth_context_from_request(request)
276
  run_id = validate_run_id(run_id)
@@ -278,7 +288,7 @@ def register_custom_routes(demo: gr.Blocks) -> None:
278
  bundle = read_run_bundle(run_id, bucket_source=bucket_source, token=ctx["token"])
279
  return JSONResponse({"run_id": run_id, "bucket_source": bucket_source, **bundle})
280
 
281
- @demo.app.get("/api/runs/{run_id}/progress")
282
  async def api_run_progress(request: Request, run_id: str, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
283
  ctx = _oauth_context_from_request(request)
284
  run_id = validate_run_id(run_id)
@@ -556,15 +566,6 @@ def build_demo() -> gr.Blocks:
556
  login_status = gr.Markdown()
557
  demo.load(fn=get_login_status, inputs=None, outputs=login_status)
558
 
559
- with gr.Tab("Custom UI preview"):
560
- gr.Markdown(
561
- """
562
- ## Custom UI shell
563
-
564
- This is the first custom-frontend pass. The shell, CSS, progress timeline, and API skeleton are available below. OAuth-backed actions remain in the functional Gradio tabs while the custom auth bridge is completed.
565
- """
566
- )
567
- gr.HTML('<iframe src="/custom" style="width:100%; min-height:900px; border:1px solid #e5e7eb; border-radius:16px; background:white;"></iframe>')
568
 
569
  gr.Markdown("## Run storage")
570
  gr.Markdown(
@@ -751,9 +752,32 @@ It cannot guarantee that every model card becomes a working Space. It cannot byp
751
  """
752
  )
753
 
754
- register_custom_routes(demo)
755
  return demo
756
 
757
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
758
  if __name__ == "__main__":
759
- build_demo().launch()
 
5
  from typing import Any
6
 
7
  import gradio as gr
8
+ import uvicorn
9
+ from fastapi import FastAPI, HTTPException, Request
10
+ from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, RedirectResponse
11
 
12
  from src.bucket import check_user_bucket, create_user_bucket, read_run_bundle, list_recent_runs
13
  from src.config import settings, user_bucket_source
 
100
  "artifacts_url": _run_artifacts_url(run_id, bucket_source),
101
  }
102
 
103
+ def register_custom_routes(fastapi_app: FastAPI) -> None:
104
+ """Register the root custom UI and OAuth-backed JSON endpoints."""
105
 
106
+ @fastapi_app.get("/", response_class=HTMLResponse)
107
+ @fastapi_app.get("/custom", response_class=HTMLResponse)
108
  async def custom_index(): # type: ignore[no-untyped-def]
109
  index_path = WEB_DIR / "index.html"
110
  if not index_path.exists():
111
  raise HTTPException(status_code=404, detail="Custom UI index not found")
112
  return HTMLResponse(index_path.read_text(encoding="utf-8"))
113
 
114
+ @fastapi_app.get("/custom-static/{asset_path:path}")
115
  async def custom_static(asset_path: str): # type: ignore[no-untyped-def]
116
  path = (STATIC_DIR / asset_path).resolve()
117
  if STATIC_DIR.resolve() not in path.parents and path != STATIC_DIR.resolve():
 
120
  raise HTTPException(status_code=404, detail="Asset not found")
121
  return FileResponse(path)
122
 
123
+ @fastapi_app.get("/login/huggingface")
124
+ async def login_redirect(): # type: ignore[no-untyped-def]
125
+ return RedirectResponse("/_gradio/login/huggingface")
126
+
127
+ @fastapi_app.get("/logout")
128
+ async def logout_redirect(): # type: ignore[no-untyped-def]
129
+ return RedirectResponse("/_gradio/logout")
130
+
131
+ @fastapi_app.get("/api/app-info")
132
  async def api_app_info(request: Request): # type: ignore[no-untyped-def]
133
  ctx: dict[str, Any] | None = None
134
  try:
 
138
  return JSONResponse(
139
  {
140
  "name": "Agentic Space Factory",
141
+ "version": "v33-custom-root-ui",
142
  "bucket_default": settings.bucket_name,
143
  "workflows": ["build_from_model_card", "validate_existing_space", "runs_explorer"],
144
+ "custom_ui_status": "root_custom_ui",
145
  "user": {"username": ctx["username"], "missing_scopes": ctx.get("missing_scopes", []), "warnings": ctx.get("warnings", [])} if ctx else None,
146
+ "login_url": "/_gradio/login/huggingface",
147
+ "logout_url": "/_gradio/logout",
148
  }
149
  )
150
 
151
+ @fastapi_app.get("/api/me")
152
  async def api_me(request: Request): # type: ignore[no-untyped-def]
153
  ctx = _oauth_context_from_request(request)
154
  return JSONResponse(
 
165
  "missing_scopes": ctx.get("missing_scopes", []),
166
  "warnings": ctx.get("warnings", []),
167
  "expires_at": ctx.get("expires_at"),
168
+ "login_url": "/_gradio/login/huggingface",
169
+ "logout_url": "/_gradio/logout",
170
  }
171
  )
172
 
173
+ @fastapi_app.get("/api/oauth/diagnostics")
174
  async def api_oauth_diagnostics(request: Request): # type: ignore[no-untyped-def]
175
  ctx = extract_oauth_context(request)
176
  return JSONResponse({"user": public_oauth_context(ctx), "token_identity": verify_token_identity(ctx)})
177
 
178
+ @fastapi_app.get("/api/bucket/status")
179
  async def api_bucket_status(request: Request, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
180
  ctx = _oauth_context_from_request(request)
181
  return JSONResponse(check_user_bucket(username=ctx["username"], bucket_name=bucket_name, token=ctx["token"]))
182
 
183
+ @fastapi_app.post("/api/bucket/create")
184
  async def api_bucket_create(request: Request, payload: dict[str, Any] | None = None): # type: ignore[no-untyped-def]
185
  ctx = _oauth_context_from_request(request)
186
  payload = payload or {}
187
  bucket_name = str(payload.get("bucket_name") or settings.bucket_name)
188
  return JSONResponse(create_user_bucket(username=ctx["username"], bucket_name=bucket_name, token=ctx["token"]))
189
 
190
+ @fastapi_app.post("/api/build")
191
  async def api_build(request: Request, payload: dict[str, Any]): # type: ignore[no-untyped-def]
192
  ctx = _oauth_context_from_request(request)
193
  bucket_name = payload.get("bucket_name") or settings.bucket_name
 
218
  )
219
  return JSONResponse(result)
220
 
221
+ @fastapi_app.post("/api/validate")
222
  async def api_validate(request: Request, payload: dict[str, Any]): # type: ignore[no-untyped-def]
223
  ctx = _oauth_context_from_request(request)
224
  bucket_name = payload.get("bucket_name") or settings.bucket_name
 
251
  )
252
  return JSONResponse(result)
253
 
254
+ @fastapi_app.post("/api/progress/from-events")
255
  async def api_progress_from_events(payload: dict[str, Any]): # type: ignore[no-untyped-def]
256
  events = payload.get("events") or []
257
  state = payload.get("state") or {}
 
261
  raise HTTPException(status_code=400, detail="state must be an object")
262
  return JSONResponse(progress_from_events(events, state=state))
263
 
264
+ @fastapi_app.get("/api/runs")
265
  async def api_runs( # type: ignore[no-untyped-def]
266
  request: Request,
267
  bucket_name: str = settings.bucket_name,
 
280
  }
281
  )
282
 
283
+ @fastapi_app.get("/api/runs/{run_id}")
284
  async def api_run_detail(request: Request, run_id: str, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
285
  ctx = _oauth_context_from_request(request)
286
  run_id = validate_run_id(run_id)
 
288
  bundle = read_run_bundle(run_id, bucket_source=bucket_source, token=ctx["token"])
289
  return JSONResponse({"run_id": run_id, "bucket_source": bucket_source, **bundle})
290
 
291
+ @fastapi_app.get("/api/runs/{run_id}/progress")
292
  async def api_run_progress(request: Request, run_id: str, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
293
  ctx = _oauth_context_from_request(request)
294
  run_id = validate_run_id(run_id)
 
566
  login_status = gr.Markdown()
567
  demo.load(fn=get_login_status, inputs=None, outputs=login_status)
568
 
 
 
 
 
 
 
 
 
 
569
 
570
  gr.Markdown("## Run storage")
571
  gr.Markdown(
 
752
  """
753
  )
754
 
 
755
  return demo
756
 
757
 
758
+ def create_app() -> FastAPI:
759
+ """Create the product app.
760
+
761
+ The public root path is the custom dashboard. A Gradio fallback/debug UI is
762
+ mounted under `/_gradio` only to preserve Hugging Face OAuth routes and for
763
+ internal diagnostics; users should not see Gradio in the normal product flow.
764
+ """
765
+ fastapi_app = FastAPI(title="Agentic Space Factory")
766
+ register_custom_routes(fastapi_app)
767
+ try:
768
+ debug_demo = build_demo()
769
+ return gr.mount_gradio_app(fastapi_app, debug_demo, path="/_gradio")
770
+ except ValueError as exc:
771
+ # Local development without an HF token can make Gradio's mocked OAuth
772
+ # routes fail at import time. Keep the custom UI importable and testable.
773
+ # In Spaces, where HF OAuth is configured, the Gradio auth mount is used.
774
+ if "HF_TOKEN" not in str(exc) and "logged in to HF" not in str(exc):
775
+ raise
776
+ return fastapi_app
777
+
778
+
779
+ app = create_app()
780
+
781
+
782
  if __name__ == "__main__":
783
+ uvicorn.run(app, host="0.0.0.0", port=7860)