Merge pull request #27 from The-Obstacle-Is-The-Way/main
Browse filesfix(ui): NiiVue self-loading + prediction overlay visibility (#24)
- app.py +3 -4
- pyproject.toml +1 -0
- src/stroke_deepisles_demo/data/adapter.py +1 -1
- src/stroke_deepisles_demo/ui/app.py +4 -5
- src/stroke_deepisles_demo/ui/viewer.py +78 -57
app.py
CHANGED
|
@@ -18,7 +18,6 @@ gr.set_static_paths(paths=[str(_ASSETS_DIR)])
|
|
| 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 |
|
|
@@ -37,8 +36,9 @@ if __name__ == "__main__":
|
|
| 37 |
logger.info("Assets exists: %s", _ASSETS_DIR.exists())
|
| 38 |
logger.info("=" * 60)
|
| 39 |
|
| 40 |
-
#
|
| 41 |
-
|
|
|
|
| 42 |
|
| 43 |
demo.launch(
|
| 44 |
server_name=settings.gradio_server_name,
|
|
@@ -47,5 +47,4 @@ if __name__ == "__main__":
|
|
| 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 |
)
|
|
|
|
| 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 |
|
| 22 |
logger = get_logger(__name__)
|
| 23 |
|
|
|
|
| 36 |
logger.info("Assets exists: %s", _ASSETS_DIR.exists())
|
| 37 |
logger.info("=" * 60)
|
| 38 |
|
| 39 |
+
# NOTE: No `head=` parameter needed!
|
| 40 |
+
# NiiVue is loaded directly by js_on_load from data-niivue-url attribute.
|
| 41 |
+
# This fixes the HF Spaces "Loading..." forever bug (Issue #24).
|
| 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 |
)
|
pyproject.toml
CHANGED
|
@@ -111,6 +111,7 @@ module = [
|
|
| 111 |
"datasets.*",
|
| 112 |
"niivue.*",
|
| 113 |
"numpy.*",
|
|
|
|
| 114 |
"pytest.*",
|
| 115 |
# DeepISLES modules (only available in DeepISLES Docker image)
|
| 116 |
"src.isles22_ensemble",
|
|
|
|
| 111 |
"datasets.*",
|
| 112 |
"niivue.*",
|
| 113 |
"numpy.*",
|
| 114 |
+
"pyarrow.*",
|
| 115 |
"pytest.*",
|
| 116 |
# DeepISLES modules (only available in DeepISLES Docker image)
|
| 117 |
"src.isles22_ensemble",
|
src/stroke_deepisles_demo/data/adapter.py
CHANGED
|
@@ -269,7 +269,7 @@ class HuggingFaceDataset:
|
|
| 269 |
Returns:
|
| 270 |
Dict with dwi_bytes, adc_bytes, and optionally mask_bytes
|
| 271 |
"""
|
| 272 |
-
import pyarrow.parquet as pq
|
| 273 |
from huggingface_hub import HfFileSystem
|
| 274 |
|
| 275 |
from stroke_deepisles_demo.data.constants import ISLES24_NUM_FILES
|
|
|
|
| 269 |
Returns:
|
| 270 |
Dict with dwi_bytes, adc_bytes, and optionally mask_bytes
|
| 271 |
"""
|
| 272 |
+
import pyarrow.parquet as pq
|
| 273 |
from huggingface_hub import HfFileSystem
|
| 274 |
|
| 275 |
from stroke_deepisles_demo.data.constants import ISLES24_NUM_FILES
|
src/stroke_deepisles_demo/ui/app.py
CHANGED
|
@@ -27,7 +27,6 @@ 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_head_html,
|
| 31 |
nifti_to_gradio_url,
|
| 32 |
render_3panel_view,
|
| 33 |
render_slice_comparison,
|
|
@@ -288,9 +287,10 @@ if __name__ == "__main__":
|
|
| 288 |
logger.info("Assets exists: %s", _ASSETS_DIR.exists())
|
| 289 |
logger.info("=" * 60)
|
| 290 |
|
| 291 |
-
#
|
| 292 |
-
#
|
| 293 |
-
|
|
|
|
| 294 |
|
| 295 |
get_demo().launch(
|
| 296 |
server_name=settings.gradio_server_name,
|
|
@@ -300,5 +300,4 @@ if __name__ == "__main__":
|
|
| 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 |
)
|
|
|
|
| 27 |
from stroke_deepisles_demo.ui.viewer import ( # noqa: E402
|
| 28 |
NIIVUE_UPDATE_JS,
|
| 29 |
create_niivue_html,
|
|
|
|
| 30 |
nifti_to_gradio_url,
|
| 31 |
render_3panel_view,
|
| 32 |
render_slice_comparison,
|
|
|
|
| 287 |
logger.info("Assets exists: %s", _ASSETS_DIR.exists())
|
| 288 |
logger.info("=" * 60)
|
| 289 |
|
| 290 |
+
# NOTE: No `head=` parameter needed!
|
| 291 |
+
# NiiVue is loaded directly by js_on_load from data-niivue-url attribute.
|
| 292 |
+
# This fixes the HF Spaces "Loading..." forever bug (Issue #24) by removing
|
| 293 |
+
# the dependency on head scripts that could block Gradio initialization.
|
| 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 |
)
|
src/stroke_deepisles_demo/ui/viewer.py
CHANGED
|
@@ -390,12 +390,16 @@ def create_niivue_html(
|
|
| 390 |
Create HTML for NiiVue viewer (static content only).
|
| 391 |
|
| 392 |
This function generates an HTML snippet with data attributes containing
|
| 393 |
-
volume URLs. The actual NiiVue initialization
|
| 394 |
-
in the gr.HTML component (see NIIVUE_ON_LOAD_JS).
|
| 395 |
|
| 396 |
IMPORTANT: Gradio's gr.HTML strips <script> tags for security.
|
| 397 |
JavaScript must be passed via the js_on_load parameter instead.
|
| 398 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
Args:
|
| 400 |
volume_url: Gradio file URL (e.g., /gradio_api/file=/path/to/file.nii.gz)
|
| 401 |
mask_url: Optional Gradio file URL to mask NIfTI file
|
|
@@ -416,12 +420,16 @@ def create_niivue_html(
|
|
| 416 |
# Using json.dumps ensures proper escaping
|
| 417 |
volume_attr = f"data-volume-url={json.dumps(volume_url)}"
|
| 418 |
mask_attr = f"data-mask-url={json.dumps(mask_url)}" if mask_url else 'data-mask-url=""'
|
|
|
|
|
|
|
|
|
|
| 419 |
|
| 420 |
return f"""<div
|
| 421 |
id="niivue-container-{viewer_id}"
|
| 422 |
class="niivue-viewer"
|
| 423 |
{volume_attr}
|
| 424 |
{mask_attr}
|
|
|
|
| 425 |
style="width:100%; height:{height}px; background:#000; border-radius:8px; position:relative;"
|
| 426 |
>
|
| 427 |
<canvas style="width:100%; height:100%;"></canvas>
|
|
@@ -435,13 +443,12 @@ def create_niivue_html(
|
|
| 435 |
# This runs when the gr.HTML component FIRST loads (mounts)
|
| 436 |
# Variables available: element, props, trigger
|
| 437 |
#
|
| 438 |
-
#
|
| 439 |
-
#
|
| 440 |
-
#
|
| 441 |
#
|
| 442 |
-
#
|
| 443 |
-
#
|
| 444 |
-
# escaping of all JS braces. The 6-line duplication is acceptable for readability.
|
| 445 |
NIIVUE_ON_LOAD_JS = """
|
| 446 |
(async () => {
|
| 447 |
const container = element.querySelector('.niivue-viewer') || element;
|
|
@@ -451,6 +458,7 @@ NIIVUE_ON_LOAD_JS = """
|
|
| 451 |
// Get URLs from data attributes
|
| 452 |
const volumeUrl = container.dataset.volumeUrl;
|
| 453 |
const maskUrl = container.dataset.maskUrl;
|
|
|
|
| 454 |
|
| 455 |
// Skip if no volume URL (initial empty state)
|
| 456 |
if (!volumeUrl) {
|
|
@@ -468,28 +476,36 @@ NIIVUE_ON_LOAD_JS = """
|
|
| 468 |
return;
|
| 469 |
}
|
| 470 |
|
| 471 |
-
if (status) status.innerText = 'Loading NiiVue...';
|
| 472 |
-
|
| 473 |
-
//
|
| 474 |
-
//
|
| 475 |
-
const
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
}
|
| 484 |
-
return null;
|
| 485 |
};
|
| 486 |
|
| 487 |
-
const Niivue = await
|
| 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
|
| 495 |
const nv = new Niivue({
|
|
@@ -529,10 +545,10 @@ NIIVUE_ON_LOAD_JS = """
|
|
| 529 |
nv.setRenderAzimuthElevation(120, 10);
|
| 530 |
nv.drawScene();
|
| 531 |
|
| 532 |
-
console.log('NiiVue
|
| 533 |
|
| 534 |
} catch (error) {
|
| 535 |
-
console.error('NiiVue
|
| 536 |
// Use textContent instead of innerHTML to prevent XSS
|
| 537 |
const errorDiv = document.createElement('div');
|
| 538 |
errorDiv.style.cssText = 'color:#f66;padding:20px;text-align:center;';
|
|
@@ -547,20 +563,15 @@ NIIVUE_ON_LOAD_JS = """
|
|
| 547 |
# This runs after Python updates the HTML value.
|
| 548 |
# ⚠️ CRITICAL: 'element' is NOT available here! Must use document.querySelector
|
| 549 |
#
|
| 550 |
-
#
|
| 551 |
-
#
|
| 552 |
-
# as it breaks Gradio on HF Spaces.
|
| 553 |
-
#
|
| 554 |
-
# NOTE: waitForNiivue() is duplicated from NIIVUE_ON_LOAD_JS above. This is
|
| 555 |
-
# intentional - extracting to a shared constant would require complex f-string
|
| 556 |
-
# escaping of all JS braces. The 6-line duplication is acceptable for readability.
|
| 557 |
NIIVUE_UPDATE_JS = """
|
| 558 |
(async () => {
|
| 559 |
// We must find the container globally since 'element' is not available in event handlers
|
| 560 |
const container = document.querySelector('.niivue-viewer');
|
| 561 |
|
| 562 |
if (!container) {
|
| 563 |
-
console.error('NiiVue
|
| 564 |
return;
|
| 565 |
}
|
| 566 |
|
|
@@ -570,6 +581,7 @@ NIIVUE_UPDATE_JS = """
|
|
| 570 |
// Get URLs from data attributes
|
| 571 |
const volumeUrl = container.dataset.volumeUrl;
|
| 572 |
const maskUrl = container.dataset.maskUrl;
|
|
|
|
| 573 |
|
| 574 |
// Skip if no volume URL
|
| 575 |
if (!volumeUrl) {
|
|
@@ -577,7 +589,10 @@ NIIVUE_UPDATE_JS = """
|
|
| 577 |
}
|
| 578 |
|
| 579 |
try {
|
| 580 |
-
if (status)
|
|
|
|
|
|
|
|
|
|
| 581 |
|
| 582 |
// Check WebGL2 support
|
| 583 |
const gl = canvas.getContext('webgl2');
|
|
@@ -586,26 +601,32 @@ NIIVUE_UPDATE_JS = """
|
|
| 586 |
return;
|
| 587 |
}
|
| 588 |
|
| 589 |
-
//
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 599 |
}
|
| 600 |
-
return null;
|
| 601 |
};
|
| 602 |
|
| 603 |
-
const Niivue = await
|
| 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
|
| 611 |
const nv = new Niivue({
|
|
@@ -645,10 +666,10 @@ NIIVUE_UPDATE_JS = """
|
|
| 645 |
nv.setRenderAzimuthElevation(120, 10);
|
| 646 |
nv.drawScene();
|
| 647 |
|
| 648 |
-
console.log('NiiVue
|
| 649 |
|
| 650 |
} catch (error) {
|
| 651 |
-
console.error('NiiVue
|
| 652 |
const errorDiv = document.createElement('div');
|
| 653 |
errorDiv.style.cssText = 'color:#f66;padding:20px;text-align:center;';
|
| 654 |
errorDiv.textContent = 'Error reloading viewer: ' + error.message;
|
|
|
|
| 390 |
Create HTML for NiiVue viewer (static content only).
|
| 391 |
|
| 392 |
This function generates an HTML snippet with data attributes containing
|
| 393 |
+
volume URLs AND the NiiVue library URL. The actual NiiVue initialization
|
| 394 |
+
is handled by js_on_load in the gr.HTML component (see NIIVUE_ON_LOAD_JS).
|
| 395 |
|
| 396 |
IMPORTANT: Gradio's gr.HTML strips <script> tags for security.
|
| 397 |
JavaScript must be passed via the js_on_load parameter instead.
|
| 398 |
|
| 399 |
+
The NiiVue library URL is embedded in data-niivue-url so that js_on_load
|
| 400 |
+
can load the library on-demand. This removes the dependency on the `head=`
|
| 401 |
+
parameter working correctly, which has been problematic on HF Spaces.
|
| 402 |
+
|
| 403 |
Args:
|
| 404 |
volume_url: Gradio file URL (e.g., /gradio_api/file=/path/to/file.nii.gz)
|
| 405 |
mask_url: Optional Gradio file URL to mask NIfTI file
|
|
|
|
| 420 |
# Using json.dumps ensures proper escaping
|
| 421 |
volume_attr = f"data-volume-url={json.dumps(volume_url)}"
|
| 422 |
mask_attr = f"data-mask-url={json.dumps(mask_url)}" if mask_url else 'data-mask-url=""'
|
| 423 |
+
# Embed NiiVue library URL so js_on_load can load it directly
|
| 424 |
+
# This removes dependency on head= script working on HF Spaces
|
| 425 |
+
niivue_url_attr = f"data-niivue-url={json.dumps(NIIVUE_JS_URL)}"
|
| 426 |
|
| 427 |
return f"""<div
|
| 428 |
id="niivue-container-{viewer_id}"
|
| 429 |
class="niivue-viewer"
|
| 430 |
{volume_attr}
|
| 431 |
{mask_attr}
|
| 432 |
+
{niivue_url_attr}
|
| 433 |
style="width:100%; height:{height}px; background:#000; border-radius:8px; position:relative;"
|
| 434 |
>
|
| 435 |
<canvas style="width:100%; height:100%;"></canvas>
|
|
|
|
| 443 |
# This runs when the gr.HTML component FIRST loads (mounts)
|
| 444 |
# Variables available: element, props, trigger
|
| 445 |
#
|
| 446 |
+
# CRITICAL FIX (Issue #24): This code loads NiiVue DIRECTLY via dynamic import()
|
| 447 |
+
# from the data-niivue-url attribute. This removes the dependency on the `head=`
|
| 448 |
+
# parameter which was blocking Gradio initialization on HF Spaces.
|
| 449 |
#
|
| 450 |
+
# The old approach used window.Niivue from a head script, but ES module failures
|
| 451 |
+
# in <head> can prevent Gradio's Svelte app from hydrating, causing "Loading..." forever.
|
|
|
|
| 452 |
NIIVUE_ON_LOAD_JS = """
|
| 453 |
(async () => {
|
| 454 |
const container = element.querySelector('.niivue-viewer') || element;
|
|
|
|
| 458 |
// Get URLs from data attributes
|
| 459 |
const volumeUrl = container.dataset.volumeUrl;
|
| 460 |
const maskUrl = container.dataset.maskUrl;
|
| 461 |
+
const niivueUrl = container.dataset.niivueUrl;
|
| 462 |
|
| 463 |
// Skip if no volume URL (initial empty state)
|
| 464 |
if (!volumeUrl) {
|
|
|
|
| 476 |
return;
|
| 477 |
}
|
| 478 |
|
| 479 |
+
if (status) status.innerText = 'Loading NiiVue library...';
|
| 480 |
+
|
| 481 |
+
// Load NiiVue directly (self-sufficient, no head= dependency)
|
| 482 |
+
// This fixes the HF Spaces "Loading..." forever bug (Issue #24)
|
| 483 |
+
const loadNiivue = async () => {
|
| 484 |
+
// Return cached if already loaded
|
| 485 |
+
if (window.Niivue) {
|
| 486 |
+
console.log('[NiiVue] Using cached window.Niivue');
|
| 487 |
+
return window.Niivue;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
// Load directly from the URL embedded in data attribute
|
| 491 |
+
if (!niivueUrl) {
|
| 492 |
+
throw new Error('No NiiVue URL provided in data-niivue-url attribute');
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
console.log('[NiiVue] Loading from:', niivueUrl);
|
| 496 |
+
try {
|
| 497 |
+
const module = await import(niivueUrl);
|
| 498 |
+
window.Niivue = module.Niivue;
|
| 499 |
+
console.log('[NiiVue] Successfully loaded and cached');
|
| 500 |
+
return module.Niivue;
|
| 501 |
+
} catch (e) {
|
| 502 |
+
// Provide detailed error for debugging
|
| 503 |
+
console.error('[NiiVue] Import failed:', e);
|
| 504 |
+
throw new Error('Failed to load NiiVue from ' + niivueUrl + ': ' + e.message);
|
| 505 |
}
|
|
|
|
| 506 |
};
|
| 507 |
|
| 508 |
+
const Niivue = await loadNiivue();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
|
| 510 |
// Initialize NiiVue
|
| 511 |
const nv = new Niivue({
|
|
|
|
| 545 |
nv.setRenderAzimuthElevation(120, 10);
|
| 546 |
nv.drawScene();
|
| 547 |
|
| 548 |
+
console.log('[NiiVue] Viewer initialized successfully');
|
| 549 |
|
| 550 |
} catch (error) {
|
| 551 |
+
console.error('[NiiVue] Initialization error:', error);
|
| 552 |
// Use textContent instead of innerHTML to prevent XSS
|
| 553 |
const errorDiv = document.createElement('div');
|
| 554 |
errorDiv.style.cssText = 'color:#f66;padding:20px;text-align:center;';
|
|
|
|
| 563 |
# This runs after Python updates the HTML value.
|
| 564 |
# ⚠️ CRITICAL: 'element' is NOT available here! Must use document.querySelector
|
| 565 |
#
|
| 566 |
+
# CRITICAL FIX (Issue #24): This code loads NiiVue DIRECTLY via dynamic import()
|
| 567 |
+
# from the data-niivue-url attribute. Same pattern as NIIVUE_ON_LOAD_JS.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
NIIVUE_UPDATE_JS = """
|
| 569 |
(async () => {
|
| 570 |
// We must find the container globally since 'element' is not available in event handlers
|
| 571 |
const container = document.querySelector('.niivue-viewer');
|
| 572 |
|
| 573 |
if (!container) {
|
| 574 |
+
console.error('[NiiVue] Container not found');
|
| 575 |
return;
|
| 576 |
}
|
| 577 |
|
|
|
|
| 581 |
// Get URLs from data attributes
|
| 582 |
const volumeUrl = container.dataset.volumeUrl;
|
| 583 |
const maskUrl = container.dataset.maskUrl;
|
| 584 |
+
const niivueUrl = container.dataset.niivueUrl;
|
| 585 |
|
| 586 |
// Skip if no volume URL
|
| 587 |
if (!volumeUrl) {
|
|
|
|
| 589 |
}
|
| 590 |
|
| 591 |
try {
|
| 592 |
+
if (status) {
|
| 593 |
+
status.style.display = 'block';
|
| 594 |
+
status.innerText = 'Reloading viewer...';
|
| 595 |
+
}
|
| 596 |
|
| 597 |
// Check WebGL2 support
|
| 598 |
const gl = canvas.getContext('webgl2');
|
|
|
|
| 601 |
return;
|
| 602 |
}
|
| 603 |
|
| 604 |
+
// Load NiiVue directly (self-sufficient, no head= dependency)
|
| 605 |
+
const loadNiivue = async () => {
|
| 606 |
+
// Return cached if already loaded
|
| 607 |
+
if (window.Niivue) {
|
| 608 |
+
console.log('[NiiVue] Using cached window.Niivue');
|
| 609 |
+
return window.Niivue;
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
// Load directly from the URL embedded in data attribute
|
| 613 |
+
if (!niivueUrl) {
|
| 614 |
+
throw new Error('No NiiVue URL provided in data-niivue-url attribute');
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
console.log('[NiiVue] Loading from:', niivueUrl);
|
| 618 |
+
try {
|
| 619 |
+
const module = await import(niivueUrl);
|
| 620 |
+
window.Niivue = module.Niivue;
|
| 621 |
+
console.log('[NiiVue] Successfully loaded and cached');
|
| 622 |
+
return module.Niivue;
|
| 623 |
+
} catch (e) {
|
| 624 |
+
console.error('[NiiVue] Import failed:', e);
|
| 625 |
+
throw new Error('Failed to load NiiVue from ' + niivueUrl + ': ' + e.message);
|
| 626 |
}
|
|
|
|
| 627 |
};
|
| 628 |
|
| 629 |
+
const Niivue = await loadNiivue();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 630 |
|
| 631 |
// Initialize NiiVue
|
| 632 |
const nv = new Niivue({
|
|
|
|
| 666 |
nv.setRenderAzimuthElevation(120, 10);
|
| 667 |
nv.drawScene();
|
| 668 |
|
| 669 |
+
console.log('[NiiVue] Viewer re-initialized successfully via event handler');
|
| 670 |
|
| 671 |
} catch (error) {
|
| 672 |
+
console.error('[NiiVue] Re-initialization error:', error);
|
| 673 |
const errorDiv = document.createElement('div');
|
| 674 |
errorDiv.style.cssText = 'color:#f66;padding:20px;text-align:center;';
|
| 675 |
errorDiv.textContent = 'Error reloading viewer: ' + error.message;
|