VibecoderMcSwaggins commited on
Commit
491d824
·
unverified ·
1 Parent(s): 2dc189d

fix(ui): add gradio prop to NiiVue component, fix StatusTracker i18n (#24) (#31)

Browse files

* feat(docs): add comprehensive specifications for NiiVue integration and JavaScript loading issues

- Introduced multiple new documentation files detailing the proposed Gradio Custom Component for NiiVue, addressing the "Loading..." issue on HuggingFace Spaces, and providing a root cause analysis of JavaScript loading failures.
- Included a detailed audit of JavaScript loading issues, diagnostic steps for the "Loading..." bug, and an analysis of Gradio's compatibility with WebGL.
- Documented the decision-making process and next steps for implementing the custom component approach, emphasizing the need for a structured solution to integrate NiiVue effectively.

These additions aim to enhance understanding and provide a clear path forward for developers working with Gradio and NiiVue.

* fix(docs): update technical debt documentation and add root cause analysis for NiiVue loading issue

- Revised the TECHNICAL_DEBT.md to reflect the current status of the NiiVue/WebGL integration, marking it as a P0 BLOCKER due to issues on HuggingFace Spaces.
- Added detailed sections outlining the critical issue with NiiVue not loading, including root causes and proposed solutions.
- Introduced new specifications for the Gradio Custom Component approach to resolve the loading issue, emphasizing the need for a structured solution.
- Documented the findings from the recent audits and analyses, providing a clear path forward for developers addressing the integration challenges.

These updates aim to enhance clarity and provide actionable insights for future development efforts.

* docs: deep audit of HF Spaces UI freeze issue

Comprehensive analysis of why UI loads but is completely frozen:
1. StatusTracker missing required i18n prop (likely cause)
2. Svelte 5 runes may have compatibility issues
3. Loading state never transitions to complete
4. NiiVue canvas stuck on loading text
5. demo.load() timing issues

See HF_SPACES_UI_BROKEN_AUDIT.md for full analysis and recommended fixes.

* docs: consolidated audit - ROOT CAUSE IDENTIFIED

Missing `gradio` prop which provides `i18n` to StatusTracker.
All Gradio custom components MUST declare `export let gradio: Gradio<...>`
and pass `gradio.i18n` to StatusTracker.

Validated against:
- Gradio Frontend Guide
- Gradio PDF Component Example
- StatusTracker npm package

Fix plan included with correct Index.svelte pattern.

* docs: add 'Why Was This Missed' analysis and correct prompting approach

Root cause of root cause: Spec #28 didn't thoroughly read official Gradio
Frontend Guide. The `gradio` prop requirement is clearly documented but was
missed because we focused on NiiVue/WebGL integration, not Gradio component
contracts.

Adds:
- Analysis of how spec #28 was incomplete
- The correct prompting approach for future custom components
- 5 lessons learned for AI agent development
- Verification checklist to prevent this class of error

* fix(ui): add gradio prop to NiiVue component, fix StatusTracker i18n (#24)

Root causes (externally validated by 2 independent agents):
1. Missing `gradio` prop - StatusTracker requires gradio.i18n to render
2. .gitignore excluded templates/ - rebuilt assets weren't being committed

Frontend fixes (Index.svelte):
- Add `gradio: Gradio<{ change: never }>` to Props interface
- Destructure `gradio` from $props()
- Pass `gradio.i18n` and `gradio.autoscroll` to StatusTracker
- Add try/catch to onMount for error handling
- Add `initialized` guard to prevent double loadVolumes call

Build fixes:
- Remove `backend/**/templates/` from .gitignore
- Rebuilt templates now tracked in git

External validation refuted:
- Svelte 5 runes are supported (Gradio 6 ships Svelte 5.43.14)
- EVENTS not required for output-only components

All 133 tests pass. Lint and typecheck clean.

* refactor(ui): apply CodeRabbit feedback - fix double loadVolumes call

Per CodeRabbit review on PR #31:

1. Remove loadVolumes() from onMount, let $effect handle it reactively
- onMount now only initializes NiiVue and sets initialized = true
- $effect triggers when initialized && value !== undefined
- Prevents duplicate volume loading on mount

2. Fix markdown formatting in AUDIT document
- Add language specs to fenced code blocks (lines 42, 429)
- Add period after "etc" (line 113)
- Wrap bare URL in angle brackets (line 332)

All 133 tests pass. Lint clean.

HF_SPACES_UI_BROKEN_AUDIT.md ADDED
@@ -0,0 +1,536 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HF Spaces UI Completely Broken - Consolidated Audit
2
+
3
+ **Date:** 2025-12-10
4
+ **Branch:** `debug/hf-spaces-ui-completely-broken`
5
+ **Status:** P0 CRITICAL - ROOT CAUSE IDENTIFIED + EXTERNALLY VALIDATED
6
+
7
+ ---
8
+
9
+ ## TL;DR - THE FIXES (2 Critical Issues)
10
+
11
+ ### Issue 1: Missing `gradio` Prop (Causes UI Freeze)
12
+
13
+ Our custom component is missing the **`gradio` prop** which provides `i18n` and `autoscroll`. This is 100% required for StatusTracker to work.
14
+
15
+ **Current broken code:**
16
+ ```svelte
17
+ // WRONG - missing gradio prop
18
+ let { value, loading_status, ... }: Props = $props();
19
+
20
+ <StatusTracker {...loading_status} /> // CRASHES - no i18n!
21
+ ```
22
+
23
+ **Correct pattern:**
24
+ ```svelte
25
+ // CORRECT - includes gradio prop (can use $props() OR export let)
26
+ import type { Gradio } from "@gradio/utils";
27
+
28
+ let { gradio, loading_status, ... }: Props = $props();
29
+
30
+ <StatusTracker
31
+ autoscroll={gradio.autoscroll}
32
+ i18n={gradio.i18n}
33
+ {...loading_status}
34
+ />
35
+ ```
36
+
37
+ ### Issue 2: .gitignore Excludes Compiled Templates (Causes Missing Assets on HF Spaces)
38
+
39
+ **CRITICAL NEW FINDING from external validation:**
40
+
41
+ `packages/niivueviewer/.gitignore` line 12 has:
42
+ ```text
43
+ backend/**/templates/
44
+ ```
45
+
46
+ This means **any rebuilt component templates will NOT be committed**. Even though the current templates are tracked (added before the rule), a `gradio cc build` will create new files that Git ignores.
47
+
48
+ **Fix:** Remove `backend/**/templates/` from `.gitignore`
49
+
50
+ ---
51
+
52
+ ## Root Cause (CONFIRMED)
53
+
54
+ ### Primary Issue: Missing `gradio` Prop
55
+
56
+ The custom component **does not declare the `gradio` prop**, which is injected by the parent Gradio application and contains:
57
+ - `gradio.i18n` - **REQUIRED** by StatusTracker for text formatting
58
+ - `gradio.autoscroll` - Controls scroll behavior
59
+ - `gradio.dispatch()` - For emitting events
60
+
61
+ **Evidence:**
62
+ - [Gradio Frontend Guide](https://www.gradio.app/guides/frontend) explicitly shows `gradio` prop is required
63
+ - [PDF Component Example](https://www.gradio.app/guides/pdf-component-example) shows working pattern
64
+ - [StatusTracker npm](https://www.npmjs.com/package/@gradio/statustracker) confirms `i18n` is required
65
+
66
+ ### Secondary Issue: Svelte 5 Runes vs Svelte 4 Syntax - **REFUTED**
67
+
68
+ **External validation clarified:** Svelte 5 runes are NOT inherently risky.
69
+
70
+ Gradio 6 ships with **Svelte 5.43.14**, and `@gradio` packages peer-depend on `svelte ^5.43.4`. Svelte 5 runes (`$props()`, `$effect()`) are supported.
71
+
72
+ | File | Syntax | Status |
73
+ |------|--------|--------|
74
+ | Index.svelte | `$props()` (Svelte 5) | **OK** (if gradio prop is added) |
75
+ | Example.svelte | `export let` (Svelte 4) | OK (legacy syntax) |
76
+ | Gradio PDF example | `export let` (Svelte 4) | Reference (uses older syntax) |
77
+
78
+ **Verdict:** We can keep `$props()` and `$effect()`. The issue is the missing `gradio` prop, not the syntax choice. However, mixing paradigms within a single package (Index uses runes, Example uses legacy) is inconsistent and should be unified.
79
+
80
+ ---
81
+
82
+ ## All Issues Found (Priority Order) - EXTERNALLY VALIDATED
83
+
84
+ ### P0 - Critical (Will Definitely Break)
85
+
86
+ | # | Issue | File:Line | Fix |
87
+ |---|-------|-----------|-----|
88
+ | 1 | **Missing `gradio` prop** | Index.svelte:8-20 | Add `gradio: Gradio<{...}>` to Props interface and destructure |
89
+ | 2 | **Missing `i18n` to StatusTracker** | Index.svelte:120-123 | Add `i18n={gradio.i18n}` |
90
+ | 3 | **Missing `autoscroll` to StatusTracker** | Index.svelte:120-123 | Replace `autoscroll={false}` with `autoscroll={gradio.autoscroll}` |
91
+ | 4 | **`.gitignore` excludes templates/** | `.gitignore:12` | Remove `backend/**/templates/` line |
92
+
93
+ ### P1 - Likely Breaking
94
+
95
+ | # | Issue | File:Line | Fix |
96
+ |---|-------|-----------|-----|
97
+ | 5 | No error handling in onMount | Index.svelte:40-50 | Wrap NiiVue init in try/catch |
98
+ | 6 | Double loadVolumes call (onMount + $effect) | Index.svelte:49,98-103 | Remove $effect's loadVolumes() or add guard |
99
+
100
+ ### P2 - Code Quality (Should Fix)
101
+
102
+ | # | Issue | File:Line | Fix |
103
+ |---|-------|-----------|-----|
104
+ | 7 | Unused template dependencies | frontend/package.json | Remove cropperjs, lazy-brush, resize-observer-polyfill |
105
+ | 8 | Example.svelte value shape mismatch | Example.svelte | Update to use {background_url, overlay_url} |
106
+ | 9 | Mixed Svelte syntax (Index=runes, Example=legacy) | Both files | Unify to one paradigm |
107
+
108
+ ### P3 - Documentation
109
+
110
+ | # | Issue | File:Line | Fix |
111
+ |---|-------|-----------|-----|
112
+ | 10 | TECHNICAL_DEBT.md says P0 resolved | docs/TECHNICAL_DEBT.md:13-33 | Update status to reflect unresolved |
113
+ | 11 | Stale diagnostic docs | ROOT_CAUSE_ANALYSIS.md, etc. | Archive or update |
114
+
115
+ ### NOT an Issue (External Validation Refuted)
116
+
117
+ | # | Previous Claim | Status | Reason |
118
+ |---|----------------|--------|--------|
119
+ | - | Svelte 5 runes are risky | **REFUTED** | Gradio 6 ships Svelte 5.43.14; runes are supported |
120
+ | - | Missing EVENTS breaks component | **LOW PRIORITY** | Output components can work without EVENTS declaration |
121
+
122
+ ---
123
+
124
+ ## Correct Index.svelte Pattern
125
+
126
+ Based on [Gradio's PDF Component Example](https://www.gradio.app/guides/pdf-component-example) + external validation that Svelte 5 runes are OK:
127
+
128
+ ```svelte
129
+ <script lang="ts">
130
+ import { onMount, onDestroy } from 'svelte';
131
+ import { Niivue } from '@niivue/niivue';
132
+ import { Block } from "@gradio/atoms";
133
+ import { StatusTracker } from "@gradio/statustracker";
134
+ import type { LoadingStatus } from "@gradio/statustracker";
135
+ import type { Gradio } from "@gradio/utils";
136
+
137
+ // Props interface - MUST include gradio
138
+ interface Props {
139
+ value?: { background_url: string | null; overlay_url: string | null } | null;
140
+ label?: string;
141
+ show_label?: boolean;
142
+ loading_status?: LoadingStatus;
143
+ elem_id?: string;
144
+ elem_classes?: string[];
145
+ visible?: boolean;
146
+ height?: number;
147
+ container?: boolean;
148
+ scale?: number | null;
149
+ min_width?: number;
150
+ // CRITICAL: gradio prop provides i18n, autoscroll, dispatch
151
+ gradio: Gradio<{ change: never }>;
152
+ }
153
+
154
+ // Svelte 5 runes syntax (validated as OK with Gradio 6)
155
+ let {
156
+ value = null,
157
+ label,
158
+ show_label = true,
159
+ loading_status,
160
+ elem_id = "",
161
+ elem_classes = [],
162
+ visible = true,
163
+ height = 500,
164
+ container = true,
165
+ scale = null,
166
+ min_width = undefined,
167
+ gradio // CRITICAL: must destructure this
168
+ }: Props = $props();
169
+
170
+ let canvas: HTMLCanvasElement;
171
+ let nv: Niivue | null = null;
172
+ let initialized = false; // Guard against double-load
173
+
174
+ onMount(async () => {
175
+ try {
176
+ nv = new Niivue({
177
+ backColor: [0, 0, 0, 1],
178
+ show3Dcrosshair: true,
179
+ logging: false
180
+ });
181
+ await nv.attachToCanvas(canvas);
182
+ await loadVolumes();
183
+ initialized = true;
184
+ } catch (error) {
185
+ console.error('[NiiVue] Initialization failed:', error);
186
+ }
187
+ });
188
+
189
+ onDestroy(() => {
190
+ if (nv) {
191
+ nv.cleanup();
192
+ nv = null;
193
+ }
194
+ });
195
+
196
+ async function loadVolumes() {
197
+ if (!nv) return;
198
+
199
+ while (nv.volumes.length > 0) {
200
+ nv.removeVolume(nv.volumes[0]);
201
+ }
202
+
203
+ if (!value) {
204
+ nv.drawScene();
205
+ return;
206
+ }
207
+
208
+ const volumes = [];
209
+ if (value.background_url) {
210
+ volumes.push({ url: value.background_url });
211
+ }
212
+ if (value.overlay_url) {
213
+ volumes.push({
214
+ url: value.overlay_url,
215
+ colormap: 'red',
216
+ opacity: 0.5,
217
+ });
218
+ }
219
+
220
+ if (volumes.length > 0) {
221
+ await nv.loadVolumes(volumes);
222
+ } else {
223
+ nv.drawScene();
224
+ }
225
+ }
226
+
227
+ // Svelte 5 $effect - only run after initial mount to avoid double-load
228
+ $effect(() => {
229
+ if (initialized && value !== undefined) {
230
+ loadVolumes();
231
+ }
232
+ });
233
+ </script>
234
+
235
+ <Block
236
+ {visible}
237
+ variant="solid"
238
+ padding={false}
239
+ {elem_id}
240
+ {elem_classes}
241
+ {height}
242
+ allow_overflow={false}
243
+ {container}
244
+ {scale}
245
+ {min_width}
246
+ >
247
+ {#if loading_status}
248
+ <StatusTracker
249
+ autoscroll={gradio.autoscroll}
250
+ i18n={gradio.i18n}
251
+ {...loading_status}
252
+ />
253
+ {/if}
254
+
255
+ <div class="niivue-container" style="height: {height}px;">
256
+ <canvas bind:this={canvas}></canvas>
257
+ </div>
258
+ </Block>
259
+
260
+ <style>
261
+ .niivue-container {
262
+ width: 100%;
263
+ background: #000;
264
+ position: relative;
265
+ border-radius: var(--radius-lg);
266
+ overflow: hidden;
267
+ }
268
+ canvas {
269
+ width: 100%;
270
+ height: 100%;
271
+ outline: none;
272
+ display: block;
273
+ }
274
+ </style>
275
+ ```
276
+
277
+ ---
278
+
279
+ ## Python Backend Fix (LOW PRIORITY)
280
+
281
+ **External validation says:** EVENTS is not required for output-only components. The UI freeze is NOT caused by missing EVENTS.
282
+
283
+ However, if we want to emit events in the future:
284
+
285
+ ```python
286
+ from gradio.events import Events
287
+
288
+ class NiiVueViewer(Component):
289
+ """WebGL NIfTI viewer using NiiVue."""
290
+
291
+ EVENTS = [Events.change] # OPTIONAL - add if we want to emit change events
292
+
293
+ data_model = NiiVueViewerData
294
+ # ... rest unchanged
295
+ ```
296
+
297
+ **Decision:** Skip for now. Focus on P0 issues first.
298
+
299
+ ---
300
+
301
+ ## Validation Checklist (Updated per External Validation)
302
+
303
+ ### Before Fix
304
+ - [ ] `gradio` prop declared? **NO** ← P0
305
+ - [ ] `i18n` passed to StatusTracker? **NO** ← P0
306
+ - [ ] `autoscroll` passed to StatusTracker? **NO** ← P0
307
+ - [ ] `.gitignore` allows templates/? **NO** (ignored!) ← P0
308
+ - [ ] onMount has error handling? **NO** ← P1
309
+ - [ ] Double loadVolumes prevented? **NO** ← P1
310
+
311
+ ### After Fix (Target)
312
+ - [x] `gradio` prop declared? YES
313
+ - [x] `i18n` passed to StatusTracker? YES
314
+ - [x] `autoscroll` passed to StatusTracker? YES
315
+ - [x] `.gitignore` allows templates/? YES
316
+ - [x] onMount has error handling? YES
317
+ - [x] Double loadVolumes prevented? YES (via `initialized` guard)
318
+
319
+ ### Not Required (Per External Validation)
320
+ - Svelte 4 syntax? **NOT REQUIRED** - Svelte 5 runes are OK with Gradio 6
321
+ - EVENTS in Python? **NOT REQUIRED** - output components work without it
322
+
323
+ ---
324
+
325
+ ## Test Plan
326
+
327
+ ### Step 1: Local Test
328
+ ```bash
329
+ cd /Users/ray/Desktop/CLARITY-DIGITAL-TWIN/stroke-deepisles-demo
330
+ uv run python -m stroke_deepisles_demo.ui.app
331
+ ```
332
+ Open <http://localhost:7860> and verify:
333
+ - [ ] Page loads without freezing
334
+ - [ ] Dropdown is clickable
335
+ - [ ] Buttons respond
336
+ - [ ] NiiVue canvas initializes (shows black, not "loading...")
337
+
338
+ ### Step 2: Rebuild Component
339
+ ```bash
340
+ cd packages/niivueviewer
341
+ gradio cc build
342
+ ```
343
+
344
+ ### Step 3: HF Spaces Deploy
345
+ Push to HF Spaces only after local test passes.
346
+
347
+ ---
348
+
349
+ ## External Audit Validation (Two Independent Agents Confirmed)
350
+
351
+ | Claim | Validated | Evidence |
352
+ |-------|-----------|----------|
353
+ | StatusTracker requires `i18n` | **TRUE** | @gradio/statustracker npm package requires i18n with no default; compiled JS dereferences `e.i18n()` |
354
+ | `gradio` prop provides `i18n` | **TRUE** | Official PDF Component Example shows `gradio.i18n` pattern |
355
+ | Svelte 5 runes cause issues | **REFUTED** | Gradio 6 ships Svelte 5.43.14; @gradio packages peer-depend on svelte ^5.43.4 |
356
+ | Missing EVENTS breaks component | **REFUTED** | Output components can work without EVENTS; not relevant to UI freeze |
357
+ | `.gitignore` excludes templates/ | **TRUE** | `packages/niivueviewer/.gitignore:12` has `backend/**/templates/` |
358
+ | `demo.load()` blocks hydration | **FALSE** | Runs after render, not the cause |
359
+
360
+ ### Web Search Validation Sources
361
+ - Gradio GitHub Issue #6609: Hydration freezes when frontend errors occur before StatusTracker renders
362
+ - Gradio GitHub Issue #7649: WebGL must be handled via custom component
363
+ - Gradio GitHub Issue #11319: Spaces failures when templates not packaged
364
+ - @gradio/statustracker npm tarball: Props interface confirms `i18n` required with no default
365
+
366
+ ---
367
+
368
+ ## References
369
+
370
+ - [Gradio Frontend Guide](https://www.gradio.app/guides/frontend) - Shows `gradio` prop pattern
371
+ - [Gradio PDF Component Example](https://www.gradio.app/guides/pdf-component-example) - Complete working example
372
+ - [Gradio Backend Guide](https://www.gradio.app/guides/backend) - EVENTS documentation
373
+ - [Gradio StatusTracker npm](https://www.npmjs.com/package/@gradio/statustracker) - Package details
374
+ - [Gradio Custom Components](https://www.gradio.app/guides/custom-components-in-five-minutes) - Getting started
375
+ - [Gradio i18n](https://www.gradio.app/guides/internationalization) - Internationalization docs
376
+
377
+ ---
378
+
379
+ ## Files to Modify
380
+
381
+ | File | Change |
382
+ |------|--------|
383
+ | `packages/niivueviewer/frontend/Index.svelte` | Rewrite with `gradio` prop, Svelte 4 syntax |
384
+ | `packages/niivueviewer/backend/gradio_niivueviewer/niivueviewer.py` | Add `EVENTS = [Events.change]` |
385
+
386
+ ---
387
+
388
+ ## WHY WAS THIS MISSED? (Root Cause of Root Cause)
389
+
390
+ ### The Spec Itself Was Incomplete
391
+
392
+ **Spec #28** (`docs/specs/28-gradio-custom-component-niivue.md`) provided implementation guidance that **did not include the `gradio` prop**. Looking at lines 176-296 of the spec:
393
+
394
+ ```svelte
395
+ // What the spec showed (WRONG):
396
+ export let value: {...} | null = null;
397
+ // ... custom loading/error divs
398
+ // NO gradio prop
399
+ // NO StatusTracker
400
+ ```
401
+
402
+ The spec **did NOT read the official Gradio Frontend Guide thoroughly**. It showed custom loading/error handling instead of using Gradio's official `StatusTracker` component.
403
+
404
+ ### The Implementation Improved On The Spec (But Still Wrong)
405
+
406
+ The actual implementation in `Index.svelte` **correctly** chose to use:
407
+ - `Block` component wrapper (correct)
408
+ - `StatusTracker` component (correct approach)
409
+
410
+ But then **didn't research what StatusTracker requires**:
411
+ - Missing `gradio` prop declaration
412
+ - Missing `i18n={gradio.i18n}`
413
+ - Missing `autoscroll={gradio.autoscroll}`
414
+
415
+ ### The Fundamental Failure
416
+
417
+ | Stage | What Happened | What Should Have Happened |
418
+ |-------|---------------|---------------------------|
419
+ | Spec Writing | Read "Custom Components in 5 Minutes" | Read FULL Frontend Guide + PDF Example |
420
+ | Spec Review | Focused on NiiVue/WebGL integration | Verified against working Gradio components |
421
+ | Implementation | Used StatusTracker without research | Checked StatusTracker npm package requirements |
422
+ | Testing | Tested locally (worked) | Should have checked browser console for errors |
423
+
424
+ ---
425
+
426
+ ## The Correct Prompting Approach (For Future Reference)
427
+
428
+ ### What We Asked
429
+ ```text
430
+ Create a Gradio Custom Component for NiiVue WebGL viewer
431
+ ```
432
+
433
+ ### What We Should Have Asked
434
+
435
+ ```markdown
436
+ ## Task: Create a Gradio Custom Component for NiiVue
437
+
438
+ ### BEFORE writing ANY code:
439
+
440
+ 1. **READ these official Gradio docs IN FULL** (not skimming):
441
+ - https://www.gradio.app/guides/frontend (CRITICAL - shows gradio prop)
442
+ - https://www.gradio.app/guides/pdf-component-example (working reference)
443
+ - https://www.gradio.app/guides/backend (EVENTS documentation)
444
+
445
+ 2. **IDENTIFY all required props** that Gradio passes to components:
446
+ - `value` - component's data
447
+ - `loading_status` - if using StatusTracker
448
+ - `gradio` - **CRITICAL**: provides i18n, autoscroll, dispatch()
449
+ - Other props: elem_id, elem_classes, visible, etc.
450
+
451
+ 3. **If using StatusTracker**, VERIFY what props it requires:
452
+ - Check @gradio/statustracker npm package
453
+ - Confirm `i18n` is REQUIRED (no default value)
454
+ - Confirm `autoscroll` is expected
455
+
456
+ 4. **Use Svelte 4 syntax** (`export let`) NOT Svelte 5 runes (`$props`, `$effect`)
457
+ - ALL Gradio examples use Svelte 4
458
+ - Svelte 5 runes are undocumented with Gradio components
459
+
460
+ 5. **For Python backend**, check if EVENTS are needed:
461
+ - Components that emit events need `EVENTS = [Events.change]`
462
+
463
+ ### VERIFICATION before claiming "done":
464
+ - [ ] Compare Index.svelte line-by-line against PDF Component Example
465
+ - [ ] Confirm gradio prop is declared
466
+ - [ ] Confirm i18n is passed to StatusTracker
467
+ - [ ] Check browser console for JavaScript errors
468
+ - [ ] Test on fresh browser (not cached)
469
+ ```
470
+
471
+ ---
472
+
473
+ ## Lessons Learned (Updated Post-Validation)
474
+
475
+ ### 1. Official Documentation Is Not Optional
476
+
477
+ The agent assumed it knew Gradio patterns from general knowledge. It did NOT thoroughly read the official docs. **The `gradio` prop is clearly documented** in the Frontend Guide.
478
+
479
+ ### 2. "Works Locally" ≠ "Works Correctly"
480
+
481
+ The component loaded locally because the browser cached state or errors were swallowed. A fresh test or HF Spaces deployment revealed the crash.
482
+
483
+ ### 3. Using Components Requires Understanding Their Contracts
484
+
485
+ `StatusTracker` is a black box. The agent used it without checking:
486
+ - What props it requires
487
+ - What happens if `i18n` is undefined
488
+ - Whether it has default values
489
+
490
+ ### 4. Spec Review Must Validate Against Official Examples
491
+
492
+ The spec was reviewed for:
493
+ - ✅ File structure
494
+ - ✅ Build process
495
+ - ✅ HF Spaces deployment
496
+ - ❌ **Component prop requirements** (MISSED)
497
+ - ❌ **StatusTracker requirements** (MISSED)
498
+ - ❌ **`.gitignore` patterns** (MISSED - templates were ignored!)
499
+
500
+ ### 5. ~~Svelte 5 Runes Are Risky With Gradio~~ **REFUTED**
501
+
502
+ **Correction:** External validation proved Gradio 6 ships with Svelte 5.43.14 and supports runes. The issue was the missing `gradio` prop, not the syntax choice.
503
+
504
+ ### 6. NEW: Check .gitignore Before Assuming Files Are Committed
505
+
506
+ The templates/ directory was ignored by `.gitignore`. Even though existing files were tracked, any rebuild would create new files that Git ignores. **Always verify .gitignore patterns when dealing with build artifacts.**
507
+
508
+ ### 7. NEW: External Validation Is Critical for Complex Issues
509
+
510
+ Two independent external agents validated our findings and caught:
511
+ - The `.gitignore` issue we missed
512
+ - Refuted the Svelte 5 concern
513
+ - Confirmed the exact mechanism (i18n deref crash)
514
+
515
+ ---
516
+
517
+ ## Summary (Post External Validation)
518
+
519
+ **Root Cause #1:** The UI freeze is caused by StatusTracker crashing due to missing `i18n` prop. The `i18n` is provided by the `gradio` prop, which our component never declares. The compiled JS dereferences `e.i18n()` which throws a TypeError, blocking Svelte hydration.
520
+
521
+ **Root Cause #2:** The `.gitignore` file ignores `backend/**/templates/`, meaning any rebuilt component assets won't be committed. This is a deployment time bomb.
522
+
523
+ **Why it was missed:** The spec didn't thoroughly read official Gradio documentation OR check `.gitignore` patterns.
524
+
525
+ **The fix is straightforward (P0 only):**
526
+ 1. Add `gradio: Gradio<{...}>` to Props interface in Index.svelte
527
+ 2. Pass `gradio.i18n` and `gradio.autoscroll` to StatusTracker
528
+ 3. Remove `backend/**/templates/` from `.gitignore`
529
+ 4. Rebuild with `gradio cc build`
530
+ 5. Commit the new templates
531
+
532
+ **NOT required (per external validation):**
533
+ - Converting to Svelte 4 syntax (Svelte 5 runes are fine)
534
+ - Adding EVENTS to Python backend (output components don't need it)
535
+
536
+ **For future custom components:** Always read the official Frontend Guide and PDF Component Example FIRST, verify your implementation matches, AND check `.gitignore` patterns for build artifacts.
packages/niivueviewer/.gitignore CHANGED
@@ -9,4 +9,3 @@ __tmp/*
9
  .mypycache
10
  .ruff_cache
11
  node_modules
12
- backend/**/templates/
 
9
  .mypycache
10
  .ruff_cache
11
  node_modules
 
packages/niivueviewer/backend/gradio_niivueviewer/templates/component/index.js CHANGED
The diff for this file is too large to render. See raw diff
 
packages/niivueviewer/frontend/Index.svelte CHANGED
@@ -4,6 +4,7 @@
4
  import { Block } from "@gradio/atoms";
5
  import { StatusTracker } from "@gradio/statustracker";
6
  import type { LoadingStatus } from "@gradio/statustracker";
 
7
 
8
  interface Props {
9
  value?: { background_url: string | null; overlay_url: string | null } | null;
@@ -17,6 +18,8 @@
17
  container?: boolean;
18
  scale?: number;
19
  min_width?: number;
 
 
20
  }
21
 
22
  let {
@@ -30,23 +33,30 @@
30
  height = 500,
31
  container = true,
32
  scale = null,
33
- min_width = undefined
 
34
  }: Props = $props();
35
 
36
  let div_container: HTMLDivElement;
37
  let nv: Niivue | null = null;
38
  let canvas: HTMLCanvasElement;
 
39
 
40
  onMount(async () => {
41
- // Initialize NiiVue
42
- nv = new Niivue({
43
- backColor: [0, 0, 0, 1],
44
- show3Dcrosshair: true,
45
- logging: false // Reduce noise
46
- });
47
-
48
- await nv.attachToCanvas(canvas);
49
- await loadVolumes();
 
 
 
 
 
50
  });
51
 
52
  onDestroy(() => {
@@ -94,10 +104,9 @@
94
  }
95
  }
96
 
97
- // Reactive effect: Re-load volumes when `value` changes
98
  $effect(() => {
99
- // Dependence on value
100
- if (value || value === null) {
101
  loadVolumes();
102
  }
103
  });
@@ -118,7 +127,8 @@
118
  >
119
  {#if loading_status}
120
  <StatusTracker
121
- autoscroll={false}
 
122
  {...loading_status}
123
  />
124
  {/if}
 
4
  import { Block } from "@gradio/atoms";
5
  import { StatusTracker } from "@gradio/statustracker";
6
  import type { LoadingStatus } from "@gradio/statustracker";
7
+ import type { Gradio } from "@gradio/utils";
8
 
9
  interface Props {
10
  value?: { background_url: string | null; overlay_url: string | null } | null;
 
18
  container?: boolean;
19
  scale?: number;
20
  min_width?: number;
21
+ // CRITICAL: gradio prop provides i18n, autoscroll, dispatch
22
+ gradio: Gradio<{ change: never }>;
23
  }
24
 
25
  let {
 
33
  height = 500,
34
  container = true,
35
  scale = null,
36
+ min_width = undefined,
37
+ gradio
38
  }: Props = $props();
39
 
40
  let div_container: HTMLDivElement;
41
  let nv: Niivue | null = null;
42
  let canvas: HTMLCanvasElement;
43
+ let initialized = false;
44
 
45
  onMount(async () => {
46
+ try {
47
+ nv = new Niivue({
48
+ backColor: [0, 0, 0, 1],
49
+ show3Dcrosshair: true,
50
+ logging: false
51
+ });
52
+
53
+ await nv.attachToCanvas(canvas);
54
+ // Don't call loadVolumes() here - let $effect handle it reactively
55
+ // Setting initialized = true triggers $effect which calls loadVolumes()
56
+ initialized = true;
57
+ } catch (error) {
58
+ console.error('[NiiVue] Initialization failed:', error);
59
+ }
60
  });
61
 
62
  onDestroy(() => {
 
104
  }
105
  }
106
 
107
+ // Reactive effect: Re-load volumes when `value` changes (only after init)
108
  $effect(() => {
109
+ if (initialized && value !== undefined) {
 
110
  loadVolumes();
111
  }
112
  });
 
127
  >
128
  {#if loading_status}
129
  <StatusTracker
130
+ autoscroll={gradio.autoscroll}
131
+ i18n={gradio.i18n}
132
  {...loading_status}
133
  />
134
  {/if}