diff --git a/.gitattributes b/.gitattributes index 33e7dae9..4d5d0dce 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,4 @@ *.stl filter=lfs diff=lfs merge=lfs -text *.npz filter=lfs diff=lfs merge=lfs -text *.onnx filter=lfs diff=lfs merge=lfs -text +src/reachy_mini/daemon/app/wasm/bin/** filter=lfs diff=lfs merge=lfs -text diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..41692988 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "src/reachy_mini/daemon/app/wasm/mujoco_web"] + path = src/reachy_mini/daemon/app/wasm/mujoco_web + url = git@github.com:pollen-robotics/mujoco_web.git + branch = mirror diff --git a/src/reachy_mini/daemon/app/dashboard/style.css b/src/reachy_mini/daemon/app/dashboard/style.css index d59674bd..5bf89468 100644 --- a/src/reachy_mini/daemon/app/dashboard/style.css +++ b/src/reachy_mini/daemon/app/dashboard/style.css @@ -34,4 +34,97 @@ ul#examples-list li a { ul#examples-list li a:hover { text-decoration: underline; +} + +/* Tab styles */ +.tab-container { + margin: 0 2em; +} + +.tab-nav { + display: flex; + border-bottom: 2px solid #ddd; + margin-bottom: 1em; +} + +.tab-button { + padding: 0.8em 1.5em; + border: none; + background: #f0f0f0; + cursor: pointer; + margin-right: 2px; + border-top-left-radius: 8px; + border-top-right-radius: 8px; + font-size: 0.9em; + transition: background-color 0.2s; +} + +.tab-button:hover { + background: #e0e0e0; +} + +.tab-button.active { + background: #2d3e50; + color: white; +} + +.tab-content { + display: none; + padding: 1em 0; +} + +.tab-content.active { + display: block; +} + +/* MuJoCo container styles */ +#mujoco-container { + background: #000; + border-radius: 8px; + overflow: hidden; + border: 1px solid #ddd; +} + +/* Daemon control styles */ +#daemon-control { + background: #fff; + padding: 1.5em; + border-radius: 8px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.07); + margin-bottom: 1em; +} + +#daemon-control h2 { + margin-top: 0; + color: #2d3e50; +} + +#daemon-control button { + padding: 0.5em 1em; + margin-right: 0.5em; + margin-bottom: 0.5em; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9em; +} + +#start-daemon { + background: #27ae60; + color: white; +} + +#stop-daemon { + background: #e74c3c; + color: white; +} + +#restart-daemon { + background: #f39c12; + color: white; +} + +#daemon-control label { + display: block; + margin: 0.5em 0; } \ No newline at end of file diff --git a/src/reachy_mini/daemon/app/main.py b/src/reachy_mini/daemon/app/main.py index 60be7a45..a83aa0f8 100644 --- a/src/reachy_mini/daemon/app/main.py +++ b/src/reachy_mini/daemon/app/main.py @@ -119,6 +119,85 @@ async def list_examples(request: Request): name="dashboard", ) + # Mount WASM files - always use bin directory + wasm_bin_dir = Path(__file__).parent / "wasm" / "bin" + original_mjcf_dir = Path(__file__).parent.parent.parent / "descriptions" / "reachy_mini" / "mjcf" + + if wasm_bin_dir.exists(): + # Mount original model files from descriptions directory + if original_mjcf_dir.exists(): + app.mount( + "/wasm/dist/examples/scenes/reachy", + StaticFiles(directory=str(original_mjcf_dir)), + name="wasm_original_models", + ) + + # Mount general path last + app.mount( + "/wasm/dist", + StaticFiles(directory=str(wasm_bin_dir)), + name="wasm", + ) + + # Add redirects for React app assets when accessed from iframe + from fastapi.responses import RedirectResponse + + @app.get("/assets/{file_path}") + async def get_asset_redirect(file_path: str): + # Handle versioned files by mapping to generic names + if file_path.startswith("mujoco_wasm-") and file_path.endswith(".wasm"): + return RedirectResponse(url="/wasm/dist/assets/mujoco_wasm.wasm") + elif file_path.startswith("mujoco_wasm-") and file_path.endswith(".js"): + return RedirectResponse(url="/wasm/dist/assets/mujoco_wasm.js") + elif file_path.startswith("index-") and file_path.endswith(".js"): + return RedirectResponse(url="/wasm/dist/assets/index.js") + elif file_path.startswith("index-") and file_path.endswith(".css"): + return RedirectResponse(url="/wasm/dist/assets/index.css") + else: + return RedirectResponse(url=f"/wasm/dist/assets/{file_path}") + + @app.get("/vite.svg") + async def get_vite_svg(): + return RedirectResponse(url="/wasm/dist/vite.svg") + + # Also add direct redirects for the full paths + @app.get("/wasm/dist/assets/mujoco_wasm-{hash}.wasm") + async def get_versioned_mujoco_wasm(hash: str): + return RedirectResponse(url="/wasm/dist/assets/mujoco_wasm.wasm") + + @app.get("/wasm/dist/assets/mujoco_wasm-{hash}.js") + async def get_versioned_mujoco_js(hash: str): + return RedirectResponse(url="/wasm/dist/assets/mujoco_wasm.js") + + @app.get("/wasm/dist/assets/index-{hash}.js") + async def get_versioned_index_js(hash: str): + return RedirectResponse(url="/wasm/dist/assets/index.js") + + @app.get("/wasm/dist/assets/index-{hash}.css") + async def get_versioned_index_css(hash: str): + return RedirectResponse(url="/wasm/dist/assets/index.css") + + # Add proper MIME types and COOP/COEP headers for WASM + @app.middleware("http") + async def add_wasm_headers(request: Request, call_next): + response = await call_next(request) + + # Add MIME types + if request.url.path.endswith('.wasm'): + response.headers["content-type"] = "application/wasm" + elif request.url.path.endswith('.js') and '/wasm/' in request.url.path: + response.headers["content-type"] = "text/javascript" + + # Add Cross-Origin headers for WASM Web Workers + response.headers["Cross-Origin-Opener-Policy"] = "same-origin" + response.headers["Cross-Origin-Embedder-Policy"] = "require-corp" + + return response + + else: + print(f"Warning: WASM bin directory not found at {wasm_bin_dir}") + print("Run the build script to generate the bin directory with generic asset names") + return app diff --git a/src/reachy_mini/daemon/app/templates/dashboard.html b/src/reachy_mini/daemon/app/templates/dashboard.html index f667c514..a35c841c 100644 --- a/src/reachy_mini/daemon/app/templates/dashboard.html +++ b/src/reachy_mini/daemon/app/templates/dashboard.html @@ -9,32 +9,141 @@