# UI review — issues and betterments Scope: the Gradio shell (`app.py`) and the embedded dual-pane viewer (`viewer/index.html` + `viewer/app.js`) as it renders inside the Space. Ordered by impact. Each item names the file and the concrete change. --- ## P0 — broken or misleading behaviour ### 1. `Running…` is never cleared when the run fails `app.py` (`run.click(...).then(...)`) — the chain writes `"Running…"` into `status`, and `predict` raises `gr.Error` on every failure path. Gradio shows the error toast, but `status` is an *output of the raising function*, so it is never overwritten: the page keeps claiming the job is in flight, permanently. Fix: return errors as values instead of raising for the recoverable cases (quota exhausted, no image, non-finite output), or add a trailing cleanup step that resets `status` regardless of outcome. ### 2. Stale results stay on screen during and after a new run Uploading a second image leaves the previous run's viewer iframe, gallery, 16-bit image and download bundle visible. A user who changes the GSD and re-runs sees the *old* 3D scene for the whole run and has no way to tell which result the downloads belong to. Fix: in the first `.then` step, clear every output (`gr.update(value=None)` for gallery/height/downloads, `""` for the viewer) alongside `"Running…"`. ### 3. The run button is not disabled while a run is in flight Two clicks = two ZeroGPU reservations = double the quota burn, which is the single most common failure mode for this Space (see the quota error text in `app.py`). Nothing in the UI prevents it. Fix: `run.click(lambda: (gr.update(interactive=False), "Running…"), ...)` and re-enable in a trailing `.then`. Setting `concurrency_limit=1` on the event is worth doing too. ### 4. No empty state — the viewer is a 560 px void on first load `_CSS` reserves the height so the layout does not collapse, but nothing is drawn there. A first-time visitor sees a large blank panel and no signal that it will become a 3D scene. Fix: render a placeholder into `gr.HTML` at startup (a dashed frame with "Your 3D terrain will appear here" plus the one-line workflow), replaced on the first result. ### 5. No examples There is no `gr.Examples`. A visitor with no aerial imagery to hand — the majority — cannot try the demo at all. `viewer/sample/` already exists. Fix: `gr.Examples([[sample_png, 0.5, False]], inputs=[image, gsd, tta])` with two or three scenes at different GSDs. ### 6. Silent downsampling is never surfaced `read_scene(..., max_side=1600)` resamples any large scene, and the viewer payload is capped again at `_VIEWER_MAX_SIDE = 1024`. `meta.json` carries the note; the UI says nothing. A user who uploads an 8000 px tile gets a quarter-resolution product and no warning. Fix: when the input was resampled, add a line to `status` — "Scene resampled 8000 → 1600 px (GSD adjusted 0.15 → 0.75 m/px)." --- ## P1 — comprehension: the result cannot be read ### 7. No colour legend anywhere The turbo height colormap is the primary output — in the gallery, the 2D map pane and the 3D drape — and there is no colorbar in any of the three. Colour cannot be converted back to metres by eye. `viewer/app.js` has `turbo()`; nothing draws a scale from it. Fix: draw a vertical colorbar with min/mid/max metre ticks into the 2D pane and the 3D pane corner, and bake one into `height_colormap.png` in `_write_artifacts`. ### 8. No scale bar or north arrow on the 2D map The pane reports nothing about ground extent. `S.gsd` is known, so a 100 m / 500 m bar is a few lines of canvas. ### 9. The elevation profile is unlabelled `drawProfile` in `viewer/app.js` prints only the max height. No y-axis minimum, no x-axis ground distance, no units on the axis. The numbers exist in `reportProbe` but are not tied to the plot. Fix: label both ends of the y range and the x extent in metres. ### 10. The low-ground warning is buried The `frac_below_1m < 0.15` warning is appended to the bottom of a four-line markdown blob in the sidebar. It is the most important correctness signal the app produces — it usually means the declared GSD is wrong. Fix: give it its own `gr.Markdown` with a visible warning style, above the stats, hidden when not triggered. ### 11. Two product names in one page The Gradio header says "Single View Height Estimator"; the iframe sidebar says "DepthWizard · single-view DSM · SIH 2026 PS 26175". Inside one viewport that reads as an embedded third-party tool. Fix: pick one name; hide or rewrite the viewer's `

`/`.tag` under `body.embedded`. ### 12. Dead UI in embedded mode The viewer hides `#loadSection` when embedded, but leaves: - the **Validation** section telling the user to "load `gt_ndsm_m.npy` alongside" — impossible, the file picker is gone; - permanently-disabled `Reference height` / `Error vs reference` options in both layer dropdowns. Fix: `body.embedded` hides the Validation section and both reference options; keep them for the standalone `viewer/index.html`. ### 13. "Mesh detail" options are misleading in the Space The embedded payload is capped at 1024 px (~1.05 M pixels), so "Full (1.5M)" and "Balanced (500k)" differ little and "Full" can never be reached. `_VIEWER_MAX_SIDE` and the `#quality` options are set independently and disagree. Fix: derive the option list from the actual grid size, or relabel to Fast / Balanced / Full without vertex counts. ### 14. Downloads are an unlabelled pile of temp paths `gr.File(file_count="multiple")` shows `dw_a1b2c3_result.zip`, `ndsm_m.npy`, `terrain.glb`… with no explanation of what to do with each. The zip name is a temp directory id. Fix: rename the archive after the uploaded file, and put a one-line legend under the component — "`.npy` = raw metres · `.glb` = textured mesh · `meta.json` = encoding + scene metadata". --- ## P2 — input ergonomics ### 15. The GSD field is the app's main footgun and is under-supported `minimum=0.01`, no maximum, and `0` silently means "guess from the checkpoint's canonical GSD" (`float(gsd or 0)` → `read_scene` falls back to `spec.canonical_gsd_m`). Nothing tells the user that, before or after. Fix: add a preset radio (0.3 / 0.5 / 1.0 m/px + Custom + "I don't know"), make the "don't know" path explicit, and echo the *effective* GSD and its source in the status line (`scene_meta.gsd_source` is already computed). ### 16. TTA does not disclose its cost "Higher quality (slower)" hides the fact that it asks for a 120 s ZeroGPU window instead of 45 s — and that this is the usual cause of the quota error. The user only learns it from the failure message. Fix: `info="~4× the compute; reserves 120 s of GPU quota instead of 45 s"`. ### 17. No expected runtime anywhere Nothing tells the user whether to wait 10 s or 3 minutes before the first run. ### 18. No reset / clear Once a result is on screen there is no way back to the empty state. --- ## P3 — layout, theme, accessibility ### 19. The Gradio shell is light, the viewer is hard-coded dark `viewer/index.html` sets `color-scheme: dark` and `--bg:#0d1013`. In default (light) Gradio that is a 760 px black slab in a white page. Fix: set a dark `gr.themes` on the Blocks, or make the viewer's palette follow `prefers-color-scheme` and pass the host theme in through the payload. ### 20. Fixed 760 px iframe height Hard-coded in `_viewer()`. On a 768 px-tall laptop the viewer needs page scrolling, while the viewer itself is `overflow:hidden` — so its sidebar scrolls independently inside a frame that is already clipped. Two nested scroll contexts. Fix: size the iframe in viewport units (`height:min(760px, 78vh)`) with a floor, and add a fullscreen toggle (the frame already has `allowfullscreen`). ### 21. The viewer's mobile breakpoint fires on desktop `@media (max-width:900px)` is evaluated against the *iframe* width. In the 3:1 column split the frame is often under 900 px on a normal laptop, so the 2D pane disappears and the sidebar stacks — the mobile layout on a desktop, which reads as a bug. Fix: switch to a container-driven breakpoint (or lower it and set a `min_width` on the Gradio column so the frame stays wide). ### 22. The 322 px fixed sidebar squeezes the 2D pane Inside an already-narrow frame, `322px | 38% | 1fr` leaves the map pane a sliver. Consider a collapsible sidebar. ### 23. No `