# Bug #11: NiiVue js_on_load Doesn't Re-run on Value Update ## Status: FIXED **Date:** 2025-12-09 **Branch:** `fix/niivue-js-rerun` **Fixed By:** Implementing `.then(fn=None, js=NIIVUE_UPDATE_JS)` pattern with correct `document.querySelector` context. **Related:** Bug #10 (Fixed) --- ## TL;DR - ROOT CAUSE **Gradio's `js_on_load` only runs ONCE when the component first mounts.** When we update the `gr.HTML` value with new content (after segmentation), the `js_on_load` code does NOT re-execute. The HTML updates, but the JavaScript initialization never runs. --- ## Symptom After successful DeepISLES inference on HF Spaces: - Viewer shows "Loading viewer..." (initial HTML state) - Status never changes to "Checking WebGL2..." or "Loading NiiVue..." - No error message displayed - No brain scan visible **What IS working:** - DeepISLES inference completes (~36 seconds) - Slice Comparison (matplotlib 2D view) renders correctly - Metrics JSON displays correctly - Download button provides the prediction mask - Initial HTML renders with data-* attributes **What is NOT working:** - js_on_load JavaScript doesn't re-run when value updates - NiiVue never initializes after segmentation --- ## Evidence ### Gradio Documentation From [Custom HTML Components Guide](https://www.gradio.app/guides/custom_HTML_components): > "Event listeners attached in `js_on_load` are only attached **once** when the component is first rendered. If your component creates new elements dynamically that need event listeners, attach the event listener to a parent element..." ### Observed Behavior 1. Page loads → js_on_load runs → No volumeUrl → Shows "Waiting for segmentation..." 2. User clicks "Run Segmentation" 3. DeepISLES runs successfully 4. `run_segmentation()` returns new HTML with data-volume-url attribute 5. gr.HTML value updates with new HTML 6. **js_on_load does NOT re-run** ← THE BUG 7. Viewer shows "Loading viewer..." (static HTML, no JS executed) ### Server Logs (Working) ```text INFO: Running segmentation for sub-stroke0001 INFO: DeepISLES subprocess completed in 35.73s ``` Inference works. The problem is client-side JavaScript execution. --- ## Code Flow Analysis ### Current Implementation (BROKEN) ```python # components.py - js_on_load set once at component creation niivue_viewer = gr.HTML( label="Interactive 3D Viewer", js_on_load=NIIVUE_ON_LOAD_JS, # Runs ONCE on mount ) # app.py - returns new HTML value after segmentation def run_segmentation(...): # ... inference ... niivue_html = create_niivue_html(dwi_url, mask_url) return niivue_html, ... # Value updates, but js_on_load doesn't re-run ``` ### Why It Fails 1. Component mounts → js_on_load runs (no data yet) 2. Value updates → HTML re-renders, js_on_load SKIPPED 3. New HTML has data-* attributes but no JS execution --- ## Proposed Solutions (Ranked) ### Solution 1: Use `js` Parameter on Event Handler (Recommended) Gradio allows running JavaScript after an event completes: ```python run_btn.click( fn=run_segmentation, inputs=[...], outputs=[results["niivue_viewer"], ...], ).then( fn=None, # MUST be explicit! js=NIIVUE_UPDATE_JS, # ⚠️ CANNOT reuse NIIVUE_ON_LOAD_JS - different context! ) ``` **Pros:** Native Gradio pattern, runs after each update **Cons:** Requires separate JS constant (see "Different JS Context" section below) **⚠️ CRITICAL:** The `js` param does NOT have access to `element`. You must use `document.querySelector()` instead. See the corrected JavaScript in the "Recommended Implementation" section. ### Solution 2: MutationObserver in js_on_load Watch for DOM changes and re-initialize. This approach IS valid because `js_on_load` has access to `element`: ```javascript // In js_on_load - 'element' IS available here const initNiiVue = async () => { const container = element.querySelector('.niivue-viewer') || element; const volumeUrl = container.dataset.volumeUrl; if (!volumeUrl) return; // ... NiiVue initialization code ... }; // Watch for attribute changes (when Python updates data-volume-url) const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'attributes' && mutation.attributeName.startsWith('data-')) { initNiiVue(); break; } } }); // Observe the element for attribute changes observer.observe(element, { attributes: true, subtree: true, attributeFilter: ['data-volume-url', 'data-mask-url'] }); // Initial check initNiiVue(); ``` **Pros:** Self-contained in js_on_load, no separate event wiring needed **Cons:** More complex, relies on Gradio updating DOM attributes (may not work if Gradio replaces the entire element instead of updating attributes) ### Solution 3: Use gradio-iframe Component The `gradio-iframe` package allows JavaScript to execute normally: ```python from gradio_iframe import iFrame niivue_viewer = iFrame( value=create_niivue_html_with_script(...), # Scripts execute in iframe ) ``` **Pros:** Scripts execute normally inside iframe **Cons:** Additional dependency, iframe quirks ### Solution 4: Embed JS in HTML via data: URL iframe Self-contained iframe with script: ```python def create_niivue_html(...): html_with_script = f'''...''' encoded = base64.b64encode(html_with_script.encode()).decode() return f'' ``` **Pros:** No external dependency, scripts execute **Cons:** Complex, potential CSP issues ### Solution 5: Custom Gradio Component Build a proper `gradio_niivue` Svelte component: ```bash gradio cc create NiiVue --template HTML ``` **Pros:** Most robust, proper lifecycle hooks **Cons:** Significant development effort --- ## Investigation Steps ### Step 1: Test Solution 1 (js param on .then()) ```python run_btn.click( fn=run_segmentation, inputs=[...], outputs=[...], ).then( fn=None, js="console.log('then JS ran'); console.log(document.querySelector('.niivue-viewer'));" ) ``` Verify: - Does `js` run after value update? - Does it have access to the updated DOM? ### Step 2: Test Solution 2 (MutationObserver) Add observer to js_on_load and check if it triggers on value change. ### Step 3: Check Browser Console Open DevTools and look for: - JavaScript errors - Console logs from js_on_load - Network requests to NiiVue CDN --- ## Temporary Workaround The 2D Slice Comparison view works correctly and provides adequate visualization for evaluation purposes while we fix the 3D viewer. --- ## Priority Assessment **Severity:** P1 (High) - 3D viewer is a key feature for the demo - The fix we deployed doesn't fully work - Blocks demo usability for 3D visualization **Impact:** - Users see "Loading viewer..." indefinitely - 2D fallback still works - Demo is partially functional --- ## Deep Web Research (2025-12-09) ### Relationship Between Bug #10 and Bug #11 **They are the SAME underlying issue with two symptoms:** 1. **Bug #10**: Gradio strips `