VibecoderMcSwaggins commited on
Commit
d8cfaa8
·
1 Parent(s): 55a71b1

fix(ui): simplify NiiVue loading, add diagnostic logging (#24)

Browse files

Root cause analysis from external agent suggests the "Loading..." hang
is caused by JavaScript failing to load due to path resolution issues
or silent errors.

Changes based on external agent feedback:

1. **Use `head` param instead of `head_paths`**
- Eliminates file I/O that could fail on HF Spaces
- Inline script injection is simpler and more reliable

2. **Add diagnostic logging at startup**
- Log asset directory paths at module load time
- Log whether assets exist for debugging HF Spaces issues

3. **Improve error handling in NiiVue loader**
- Add try/catch around ES module import
- Store errors in window.NIIVUE_LOAD_ERROR for debugging
- Update js_on_load to check for loader errors

4. **Clean up root app.py**
- Clarify it's NOT used by HF Spaces Docker (CMD uses ui/app.py)
- Update to use same `head` approach as ui/app.py

5. **Add DIAGNOSTIC_HF_LOADING.md**
- Documents all research and hypotheses
- Lists external agent's analysis and recommendations

All 136 tests pass. Lint and type checks clean.

DIAGNOSTIC_HF_LOADING.md ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Diagnostic: HuggingFace Spaces "Loading..." Forever Bug
2
+
3
+ **Date**: 2025-12-10
4
+ **Status**: UNRESOLVED - App stuck on "Loading..." despite backend running
5
+ **Symptom**: HF Spaces shows "Running on T4", logs show successful startup, but UI never renders
6
+
7
+ ## Observed Behavior
8
+
9
+ ```
10
+ ===== Application Startup at 2025-12-10 02:10:47 =====
11
+ * Running on local URL: http://0.0.0.0:7860
12
+ ```
13
+
14
+ - Backend Python starts successfully
15
+ - Gradio server binds to `0.0.0.0:7860` (correct for Docker)
16
+ - Frontend shows Gradio "Loading..." spinner indefinitely
17
+ - No error messages in container logs
18
+
19
+ ---
20
+
21
+ ## Research Findings
22
+
23
+ ### 1. Known Gradio Issues with Custom JavaScript
24
+
25
+ #### Issue #11649: Custom JS via `head` fails with 404
26
+ **Source**: [GitHub Issue #11649](https://github.com/gradio-app/gradio/issues/11649)
27
+
28
+ Users report custom JavaScript files failing to load even with `allowed_paths` configured.
29
+
30
+ **Solution found**: Use `head_paths` parameter instead of `head`:
31
+ ```python
32
+ with gr.Blocks(head_paths=["custom.html"]) as demo:
33
+ ```
34
+
35
+ Alternative URL format that works: `/gradio_api/file=static/custom.js` instead of `/file/static/custom.js`
36
+
37
+ #### Issue #10250: JavaScript in `head` param not executing
38
+ **Source**: [GitHub Issue #10250](https://github.com/gradio-app/gradio/issues/10250)
39
+
40
+ JavaScript occasionally executes after extended delays (5-10+ minutes) or not at all.
41
+
42
+ **Key insight**: `gr.HTML()` components intentionally do NOT execute JavaScript for security. Only `head` parameter supports JS execution.
43
+
44
+ #### Issue #6426: gr.Blocks head argument not working
45
+ **Source**: [GitHub Issue #6426](https://github.com/gradio-app/gradio/issues/6426)
46
+
47
+ Two critical bugs:
48
+ 1. Only the FIRST script tag from `head` is applied
49
+ 2. Script tags are injected AFTER page loads, preventing execution
50
+
51
+ **Fixed in PR #6639** - but may require specific Gradio version.
52
+
53
+ ### 2. ES Module Script Behavior
54
+
55
+ **Source**: [ES Modules Explainer](https://gist.github.com/jakub-g/385ee6b41085303a53ad92c7c8afd7a6)
56
+
57
+ Key facts about `<script type="module">`:
58
+ - **Always deferred by default** - does NOT block HTML parsing
59
+ - **No way to make it blocking** - even without async/defer
60
+ - If import fails, script silently fails but shouldn't block page
61
+
62
+ **Implication**: Our NiiVue loader (`<script type="module">`) should NOT be blocking Gradio's rendering. The issue is elsewhere.
63
+
64
+ ### 3. HuggingFace Spaces Known Issues
65
+
66
+ #### Origin Mismatch Bug (Issue #10893)
67
+ **Source**: [HF Forum Thread](https://discuss.huggingface.co/t/gradio-space-javascript-not-executing-fields-not-populating-persistent-syntaxerror-in-browser-console/163689)
68
+
69
+ Gradio's `postMessage` calls fail due to origin mismatch between `https://huggingface.co` and the actual Space URL. This can prevent JavaScript execution entirely.
70
+
71
+ #### Docker "Running" But "Loading..." Forever
72
+ **Source**: [HF Forum Thread](https://discuss.huggingface.co/t/space-stuck-in-building-despite-gradio-welcome-port/36890)
73
+
74
+ Common causes:
75
+ - Binding to `127.0.0.1` instead of `0.0.0.0` (**we already fixed this**)
76
+ - Incorrect port configuration (**we use 7860**)
77
+ - Private Space authentication issues (**our Space is public**)
78
+
79
+ #### Interface Not Showing Despite Running
80
+ **Source**: [HF Forum Thread](https://discuss.huggingface.co/t/bug-free-gradio-interface-not-loading-on-hfs/25102)
81
+
82
+ Assigning Interface to a variable before launching can cause this:
83
+ ```python
84
+ # WRONG
85
+ iface = gr.Interface(...).launch()
86
+
87
+ # RIGHT
88
+ gr.Interface(...).launch()
89
+ ```
90
+
91
+ **Our code**: We use `get_demo().launch()` which SHOULD be correct.
92
+
93
+ ### 4. Gradio set_static_paths Requirements
94
+
95
+ **Source**: [Gradio Docs](https://www.gradio.app/docs/gradio/set_static_paths)
96
+
97
+ - Must be called BEFORE creating Blocks
98
+ - Affects ALL Gradio apps in the same interpreter session
99
+ - Exposes entire directories to network (security consideration)
100
+
101
+ **Our code**: We call `gr.set_static_paths()` at module level before imports. ✅
102
+
103
+ ### 5. Browser Cache Issues
104
+
105
+ **Source**: [HF Forum Thread](https://discuss.huggingface.co/t/issue-with-perpetual-loading-on-the-space/35684)
106
+
107
+ Some "Loading..." issues resolved by clearing browser cache. Works in one browser but not another.
108
+
109
+ **Unlikely cause**: This is a fresh deployment, not a cache issue.
110
+
111
+ ---
112
+
113
+ ## Our Current Implementation
114
+
115
+ ### JavaScript Loading Flow
116
+
117
+ 1. `app.py` (root entry point for HF Spaces Docker):
118
+ - Calls `gr.set_static_paths(paths=[str(_ASSETS_DIR)])` before imports
119
+ - Imports `get_demo()` and `get_niivue_loader_path()`
120
+ - Launches with `head_paths=[str(niivue_loader)]`
121
+
122
+ 2. `ui/app.py` (also calls set_static_paths):
123
+ - Module-level `gr.set_static_paths()` before imports
124
+ - Creates demo with `js_on_load=NIIVUE_ON_LOAD_JS` on gr.HTML component
125
+
126
+ 3. `viewer.py`:
127
+ - Generates `niivue-loader.html` at runtime with absolute path
128
+ - Content: `<script type="module">import { Niivue } from '/gradio_api/file=...'</script>`
129
+
130
+ ### Files Involved
131
+
132
+ | File | Purpose | Status |
133
+ |------|---------|--------|
134
+ | `app.py` (root) | HF Spaces entry point | Uses head_paths ✅ |
135
+ | `src/.../ui/app.py` | Main UI module | Uses js_on_load ✅ |
136
+ | `src/.../ui/viewer.py` | NiiVue loader generation | Generates at runtime ✅ |
137
+ | `src/.../ui/components.py` | UI components | Uses NIIVUE_ON_LOAD_JS ✅ |
138
+ | `src/.../ui/assets/niivue.js` | Vendored NiiVue library | 2.9MB, tracked ✅ |
139
+ | `src/.../ui/assets/niivue-loader.html` | Generated loader | gitignored ✅ |
140
+
141
+ ### Dockerfile CMD
142
+
143
+ ```dockerfile
144
+ CMD ["python", "-m", "stroke_deepisles_demo.ui.app"]
145
+ ```
146
+
147
+ This runs `ui/app.py` as `__main__`, which should execute our launch() with head_paths.
148
+
149
+ ---
150
+
151
+ ## Hypotheses
152
+
153
+ ### H1: `js_on_load` Breaking Gradio Initialization
154
+
155
+ **Theory**: The `js_on_load` parameter on `gr.HTML` might be executing before Gradio fully initializes, causing a crash.
156
+
157
+ **Evidence**: Our code has `js_on_load=NIIVUE_ON_LOAD_JS` which is a complex async IIFE.
158
+
159
+ **Test**: Remove `js_on_load` parameter and see if app loads.
160
+
161
+ ### H2: `head_paths` Not Being Applied on HF Spaces
162
+
163
+ **Theory**: The `head_paths` parameter might not be reaching the frontend on HF Spaces due to Docker networking or Gradio configuration.
164
+
165
+ **Evidence**: Issue #11649 shows head-related parameters have bugs.
166
+
167
+ **Test**: Check browser Network tab for niivue.js 404 or missing script.
168
+
169
+ ### H3: demo.load() Blocking Initial Render
170
+
171
+ **Theory**: The `demo.load(initialize_case_selector, ...)` call might be blocking the initial UI render.
172
+
173
+ **Evidence**: `initialize_case_selector()` calls `list_case_ids()` which loads HuggingFace dataset.
174
+
175
+ **Test**: Remove demo.load() and see if app loads.
176
+
177
+ ### H4: Double set_static_paths Causing Conflict
178
+
179
+ **Theory**: Both `app.py` (root) and `ui/app.py` call `gr.set_static_paths()`. This might cause conflicts.
180
+
181
+ **Evidence**: Gradio docs say "affects ALL Gradio apps in same interpreter session".
182
+
183
+ **Test**: Remove one of the set_static_paths calls.
184
+
185
+ ### H5: Module Import Order Issue
186
+
187
+ **Theory**: The order of imports and set_static_paths calls might matter on HF Spaces but not locally.
188
+
189
+ **Evidence**: We have `noqa: E402` comments indicating non-standard import order.
190
+
191
+ **Test**: Trace exact import order and when set_static_paths is effective.
192
+
193
+ ### H6: Path Resolution Different in Docker
194
+
195
+ **Theory**: `Path(__file__).resolve()` might resolve to different paths in Docker vs local.
196
+
197
+ **Evidence**: We use absolute paths for NIIVUE_JS_URL computed at import time.
198
+
199
+ **Test**: Log the actual paths being computed in Docker.
200
+
201
+ ---
202
+
203
+ ## Diagnostic Steps to Try
204
+
205
+ 1. **Minimal Test**: Create a branch that removes ALL custom JS and test if basic Gradio loads
206
+ 2. **Log Paths**: Add logging to show exactly what paths are computed in Docker
207
+ 3. **Browser DevTools**: Check Network tab and Console for errors (if accessible)
208
+ 4. **Gradio Version**: Verify we're using a version with all relevant fixes
209
+ 5. **HF Spaces Logs**: Check full container logs for any Python errors not shown in UI
210
+
211
+ ---
212
+
213
+ ## Related Documentation
214
+
215
+ - [AUDIT_JS_LOADING_ISSUES.md](./AUDIT_JS_LOADING_ISSUES.md) - Previous audit of JavaScript loading issues
216
+ - [docs/specs/24-bug-hf-spaces-loading-forever.md](./docs/specs/24-bug-hf-spaces-loading-forever.md) - Original bug specification
217
+
218
+ ---
219
+
220
+ ## External Resources
221
+
222
+ - [Gradio Custom CSS and JS Guide](https://www.gradio.app/guides/custom-CSS-and-JS)
223
+ - [Gradio File Access Guide](https://www.gradio.app/guides/file-access)
224
+ - [Gradio set_static_paths Docs](https://www.gradio.app/docs/gradio/set_static_paths)
225
+ - [Gradio Issue #11649](https://github.com/gradio-app/gradio/issues/11649) - head_paths solution
226
+ - [Gradio Issue #10250](https://github.com/gradio-app/gradio/issues/10250) - head JS not executing
227
+ - [Gradio Issue #6426](https://github.com/gradio-app/gradio/issues/6426) - head argument bugs
228
+ - [HF Spaces Docker Guide](https://huggingface.co/docs/hub/spaces-sdks-docker)
app.py CHANGED
@@ -1,12 +1,9 @@
1
- """Entry point for Hugging Face Spaces deployment.
2
 
3
- This module provides the entry point for deploying the stroke-deepisles-demo
4
- application to Hugging Face Spaces. It handles environment detection and
5
- configures Gradio appropriately for the deployment environment.
6
 
7
- See:
8
- - docs/specs/07-hf-spaces-deployment.md
9
- - https://huggingface.co/docs/hub/spaces-sdks-docker
10
  """
11
 
12
  from pathlib import Path
@@ -14,15 +11,16 @@ from pathlib import Path
14
  import gradio as gr
15
 
16
  # CRITICAL: Allow direct file serving for local assets (niivue.js)
17
- # This fixes the P0 "Loading..." bug on HF Spaces (Issue #11649)
18
  # Must be called BEFORE creating any Blocks
19
  _ASSETS_DIR = Path(__file__).parent / "src" / "stroke_deepisles_demo" / "ui" / "assets"
20
  gr.set_static_paths(paths=[str(_ASSETS_DIR)])
21
 
22
  from stroke_deepisles_demo.core.config import get_settings # noqa: E402
23
- from stroke_deepisles_demo.core.logging import setup_logging # noqa: E402
24
  from stroke_deepisles_demo.ui.app import get_demo # noqa: E402
25
- from stroke_deepisles_demo.ui.viewer import get_niivue_loader_path # noqa: E402
 
 
26
 
27
  # Initialize logging
28
  settings = get_settings()
@@ -32,14 +30,15 @@ setup_logging(settings.log_level, format_style=settings.log_format)
32
  demo = get_demo()
33
 
34
  if __name__ == "__main__":
35
- # Launch configuration
36
- # - server_name: 0.0.0.0 required for HF Spaces (Docker)
37
- # - server_port: 7860 is HF Spaces default
38
- # - theme: Gradio 6 uses launch() for theme
39
- # - css: Hide footer for cleaner look
 
40
 
41
- # Generate the NiiVue loader HTML file (creates if needed)
42
- niivue_loader = get_niivue_loader_path()
43
 
44
  demo.launch(
45
  server_name=settings.gradio_server_name,
@@ -48,5 +47,5 @@ if __name__ == "__main__":
48
  theme=gr.themes.Soft(),
49
  css="footer {visibility: hidden}",
50
  allowed_paths=[str(_ASSETS_DIR)],
51
- head_paths=[str(niivue_loader)], # Official Gradio approach (Issue #11649)
52
  )
 
1
+ """Alternative entry point for local development.
2
 
3
+ NOTE: HuggingFace Spaces Docker deployment uses `python -m stroke_deepisles_demo.ui.app`
4
+ (see Dockerfile CMD). This file is for local development convenience only.
 
5
 
6
+ For HF Spaces deployment, see: src/stroke_deepisles_demo/ui/app.py
 
 
7
  """
8
 
9
  from pathlib import Path
 
11
  import gradio as gr
12
 
13
  # CRITICAL: Allow direct file serving for local assets (niivue.js)
 
14
  # Must be called BEFORE creating any Blocks
15
  _ASSETS_DIR = Path(__file__).parent / "src" / "stroke_deepisles_demo" / "ui" / "assets"
16
  gr.set_static_paths(paths=[str(_ASSETS_DIR)])
17
 
18
  from stroke_deepisles_demo.core.config import get_settings # noqa: E402
19
+ from stroke_deepisles_demo.core.logging import get_logger, setup_logging # noqa: E402
20
  from stroke_deepisles_demo.ui.app import get_demo # noqa: E402
21
+ from stroke_deepisles_demo.ui.viewer import get_niivue_head_html # noqa: E402
22
+
23
+ logger = get_logger(__name__)
24
 
25
  # Initialize logging
26
  settings = get_settings()
 
30
  demo = get_demo()
31
 
32
  if __name__ == "__main__":
33
+ # Log startup info for debugging
34
+ logger.info("=" * 60)
35
+ logger.info("STARTUP: stroke-deepisles-demo (root app.py)")
36
+ logger.info("Assets directory: %s", _ASSETS_DIR.resolve())
37
+ logger.info("Assets exists: %s", _ASSETS_DIR.exists())
38
+ logger.info("=" * 60)
39
 
40
+ # Get the NiiVue loader HTML (inline script, no file I/O needed)
41
+ niivue_head = get_niivue_head_html()
42
 
43
  demo.launch(
44
  server_name=settings.gradio_server_name,
 
47
  theme=gr.themes.Soft(),
48
  css="footer {visibility: hidden}",
49
  allowed_paths=[str(_ASSETS_DIR)],
50
+ head=niivue_head, # Inject NiiVue loader directly
51
  )
src/stroke_deepisles_demo/ui/app.py CHANGED
@@ -27,7 +27,7 @@ from stroke_deepisles_demo.ui.components import ( # noqa: E402
27
  from stroke_deepisles_demo.ui.viewer import ( # noqa: E402
28
  NIIVUE_UPDATE_JS,
29
  create_niivue_html,
30
- get_niivue_loader_path,
31
  nifti_to_gradio_url,
32
  render_3panel_view,
33
  render_slice_comparison,
@@ -281,8 +281,16 @@ if __name__ == "__main__":
281
  settings = get_settings()
282
  setup_logging(settings.log_level, format_style=settings.log_format)
283
 
284
- # Generate the NiiVue loader HTML file (creates if needed)
285
- niivue_loader = get_niivue_loader_path()
 
 
 
 
 
 
 
 
286
 
287
  get_demo().launch(
288
  server_name=settings.gradio_server_name,
@@ -292,5 +300,5 @@ if __name__ == "__main__":
292
  css="footer {visibility: hidden}",
293
  show_error=True, # Show full Python tracebacks in UI for debugging
294
  allowed_paths=[str(_ASSETS_DIR)],
295
- head_paths=[str(niivue_loader)], # Official Gradio approach (Issue #11649)
296
  )
 
27
  from stroke_deepisles_demo.ui.viewer import ( # noqa: E402
28
  NIIVUE_UPDATE_JS,
29
  create_niivue_html,
30
+ get_niivue_head_html,
31
  nifti_to_gradio_url,
32
  render_3panel_view,
33
  render_slice_comparison,
 
281
  settings = get_settings()
282
  setup_logging(settings.log_level, format_style=settings.log_format)
283
 
284
+ # Log startup info for debugging HF Spaces issues
285
+ logger.info("=" * 60)
286
+ logger.info("STARTUP: stroke-deepisles-demo")
287
+ logger.info("Assets directory: %s", _ASSETS_DIR.resolve())
288
+ logger.info("Assets exists: %s", _ASSETS_DIR.exists())
289
+ logger.info("=" * 60)
290
+
291
+ # Get the NiiVue loader HTML (inline script, no file I/O needed)
292
+ # Using `head` param directly is simpler than `head_paths` with file generation
293
+ niivue_head = get_niivue_head_html()
294
 
295
  get_demo().launch(
296
  server_name=settings.gradio_server_name,
 
300
  css="footer {visibility: hidden}",
301
  show_error=True, # Show full Python tracebacks in UI for debugging
302
  allowed_paths=[str(_ASSETS_DIR)],
303
+ head=niivue_head, # Inject NiiVue loader directly (simpler than head_paths)
304
  )
src/stroke_deepisles_demo/ui/viewer.py CHANGED
@@ -32,46 +32,66 @@ _ASSET_DIR = Path(__file__).parent / "assets"
32
  _NIIVUE_JS_PATH = _ASSET_DIR / "niivue.js"
33
 
34
  # Ensure absolute path for Gradio serving
35
- # NOTE: This path must be added to allowed_paths in demo.launch()
36
  NIIVUE_JS_URL = f"/gradio_api/file={_NIIVUE_JS_PATH.resolve()}"
37
 
 
 
 
 
 
38
 
39
- def get_niivue_loader_path() -> Path:
40
- """
41
- Get path to the NiiVue loader HTML file, creating it if needed.
42
 
43
- This function generates an HTML file that loads NiiVue as a global.
44
- Using head_paths with a file is the official Gradio-recommended approach
45
- for loading custom JavaScript (see GitHub issue #11649).
46
 
47
- The HTML file is generated at runtime because the niivue.js path
48
- is dynamic (depends on installation location).
 
49
 
50
  Returns:
51
- Path to the niivue-loader.html file
52
 
53
  Note:
54
- The returned path must be included in allowed_paths during launch().
 
55
  """
56
- loader_path = _ASSET_DIR / "niivue-loader.html"
 
57
 
58
- # Generate the loader HTML with the correct absolute path
59
- loader_content = f"""<!--
60
- NiiVue Loader for Gradio (auto-generated)
61
- Loads NiiVue library and exposes it globally for js_on_load handlers.
62
- See: docs/specs/24-bug-hf-spaces-loading-forever.md
63
- -->
64
  <script type="module">
65
- import {{ Niivue }} from '{NIIVUE_JS_URL}';
66
- window.Niivue = Niivue;
67
- console.log('[NiiVue Loader] Loaded globally:', typeof window.Niivue);
 
 
 
 
 
 
 
 
68
  </script>
69
  """
70
 
71
- # Write/update the loader file (idempotent)
72
- # This ensures the path is always correct for the current installation
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  try:
74
- # Check if file exists and has correct content
75
  if loader_path.exists():
76
  existing_content = loader_path.read_text()
77
  if existing_content == loader_content:
@@ -80,8 +100,6 @@ def get_niivue_loader_path() -> Path:
80
  loader_path.write_text(loader_content)
81
  logger.debug("Generated NiiVue loader at %s", loader_path)
82
  except OSError as e:
83
- # If we can't write (e.g., read-only filesystem), the file should
84
- # already exist from the build process
85
  logger.warning("Could not write loader file at %s: %s", loader_path, e)
86
  if not loader_path.exists():
87
  raise RuntimeError(
@@ -457,6 +475,10 @@ NIIVUE_ON_LOAD_JS = """
457
  const waitForNiivue = async () => {
458
  for (let i = 0; i < 50; i++) {
459
  if (window.Niivue) return window.Niivue;
 
 
 
 
460
  await new Promise(r => setTimeout(r, 100));
461
  }
462
  return null;
@@ -464,7 +486,9 @@ NIIVUE_ON_LOAD_JS = """
464
 
465
  const Niivue = await waitForNiivue();
466
  if (!Niivue) {
467
- throw new Error('NiiVue not loaded after 5s. Ensure head_paths includes niivue-loader.html and gr.set_static_paths is called.');
 
 
468
  }
469
 
470
  // Initialize NiiVue
@@ -567,6 +591,10 @@ NIIVUE_UPDATE_JS = """
567
  const waitForNiivue = async () => {
568
  for (let i = 0; i < 50; i++) {
569
  if (window.Niivue) return window.Niivue;
 
 
 
 
570
  await new Promise(r => setTimeout(r, 100));
571
  }
572
  return null;
@@ -574,7 +602,9 @@ NIIVUE_UPDATE_JS = """
574
 
575
  const Niivue = await waitForNiivue();
576
  if (!Niivue) {
577
- throw new Error('NiiVue not loaded after 5s. Ensure head_paths includes niivue-loader.html and gr.set_static_paths is called.');
 
 
578
  }
579
 
580
  // Initialize NiiVue
 
32
  _NIIVUE_JS_PATH = _ASSET_DIR / "niivue.js"
33
 
34
  # Ensure absolute path for Gradio serving
35
+ # NOTE: This path must be added to allowed_paths AND set_static_paths in demo.launch()
36
  NIIVUE_JS_URL = f"/gradio_api/file={_NIIVUE_JS_PATH.resolve()}"
37
 
38
+ # Log the computed paths at module load time for debugging HF Spaces issues
39
+ logger.info("NiiVue assets directory: %s", _ASSET_DIR.resolve())
40
+ logger.info("NiiVue JS path: %s", _NIIVUE_JS_PATH.resolve())
41
+ logger.info("NiiVue JS URL: %s", NIIVUE_JS_URL)
42
+ logger.info("NiiVue JS exists: %s", _NIIVUE_JS_PATH.exists())
43
 
 
 
 
44
 
45
+ def get_niivue_head_html() -> str:
46
+ """
47
+ Get HTML content to inject into page <head> for NiiVue loading.
48
 
49
+ This returns an inline script that loads NiiVue as a global variable.
50
+ Using the `head` parameter directly (instead of `head_paths` with a file)
51
+ is simpler and avoids file I/O issues on HF Spaces.
52
 
53
  Returns:
54
+ HTML string containing the NiiVue loader script
55
 
56
  Note:
57
+ The niivue.js path must be registered with gr.set_static_paths()
58
+ and included in allowed_paths during launch().
59
  """
60
+ # Log for debugging path resolution issues on HF Spaces
61
+ logger.info("Generating NiiVue head HTML with URL: %s", NIIVUE_JS_URL)
62
 
63
+ return f"""<!-- NiiVue Loader: Exposes window.Niivue for js_on_load handlers -->
 
 
 
 
 
64
  <script type="module">
65
+ try {{
66
+ const niivueUrl = '{NIIVUE_JS_URL}';
67
+ console.log('[NiiVue Loader] Attempting to load from:', niivueUrl);
68
+ const {{ Niivue }} = await import(niivueUrl);
69
+ window.Niivue = Niivue;
70
+ console.log('[NiiVue Loader] Successfully loaded, window.Niivue:', typeof window.Niivue);
71
+ }} catch (error) {{
72
+ console.error('[NiiVue Loader] FAILED to load:', error);
73
+ // Surface the error visibly so we can debug on HF Spaces
74
+ window.NIIVUE_LOAD_ERROR = error.message;
75
+ }}
76
  </script>
77
  """
78
 
79
+
80
+ def get_niivue_loader_path() -> Path:
81
+ """
82
+ DEPRECATED: Use get_niivue_head_html() with the `head` parameter instead.
83
+
84
+ Get path to the NiiVue loader HTML file, creating it if needed.
85
+ This file-based approach is kept for backwards compatibility but
86
+ the direct `head` parameter approach is preferred.
87
+
88
+ Returns:
89
+ Path to the niivue-loader.html file
90
+ """
91
+ loader_path = _ASSET_DIR / "niivue-loader.html"
92
+ loader_content = get_niivue_head_html()
93
+
94
  try:
 
95
  if loader_path.exists():
96
  existing_content = loader_path.read_text()
97
  if existing_content == loader_content:
 
100
  loader_path.write_text(loader_content)
101
  logger.debug("Generated NiiVue loader at %s", loader_path)
102
  except OSError as e:
 
 
103
  logger.warning("Could not write loader file at %s: %s", loader_path, e)
104
  if not loader_path.exists():
105
  raise RuntimeError(
 
475
  const waitForNiivue = async () => {
476
  for (let i = 0; i < 50; i++) {
477
  if (window.Niivue) return window.Niivue;
478
+ // Check if loader script failed (error stored in global)
479
+ if (window.NIIVUE_LOAD_ERROR) {
480
+ throw new Error('NiiVue loader failed: ' + window.NIIVUE_LOAD_ERROR);
481
+ }
482
  await new Promise(r => setTimeout(r, 100));
483
  }
484
  return null;
 
486
 
487
  const Niivue = await waitForNiivue();
488
  if (!Niivue) {
489
+ // Provide diagnostic info about what might be wrong
490
+ const loadErr = window.NIIVUE_LOAD_ERROR || 'unknown';
491
+ throw new Error('NiiVue not loaded after 5s. Check browser console for errors. Load error: ' + loadErr);
492
  }
493
 
494
  // Initialize NiiVue
 
591
  const waitForNiivue = async () => {
592
  for (let i = 0; i < 50; i++) {
593
  if (window.Niivue) return window.Niivue;
594
+ // Check if loader script failed (error stored in global)
595
+ if (window.NIIVUE_LOAD_ERROR) {
596
+ throw new Error('NiiVue loader failed: ' + window.NIIVUE_LOAD_ERROR);
597
+ }
598
  await new Promise(r => setTimeout(r, 100));
599
  }
600
  return null;
 
602
 
603
  const Niivue = await waitForNiivue();
604
  if (!Niivue) {
605
+ // Provide diagnostic info about what might be wrong
606
+ const loadErr = window.NIIVUE_LOAD_ERROR || 'unknown';
607
+ throw new Error('NiiVue not loaded after 5s. Check browser console for errors. Load error: ' + loadErr);
608
  }
609
 
610
  // Initialize NiiVue