Conversation
WalkthroughThis PR introduces a complete browser-based robotics playground featuring two interactive experiences: a gesture-controlled 3D maze simulator and a robot assembly simulator. Both support optional MediaPipe hand-gesture control with keyboard fallback, canvas-based rendering, physics simulation, and responsive UI overlays with tutorials. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant App as Maze App
participant Camera as Camera/Vision
participant Canvas as Canvas Renderer
participant Sim as Simulation
User->>App: Load page / Grant camera access
App->>Camera: Request MediaPipe setup & camera stream
alt Gesture Mode
Camera-->>App: Hand landmarks detected
App->>App: Classify gestures + filter stability
else Keyboard Mode
User->>App: Press W/A/S/D or Space
end
loop Animation Frame
App->>Sim: Update robot position based on gesture/input
Sim->>Sim: Check collisions, move NPCs, update state
App->>Canvas: Raycast walls for first-person view
App->>Canvas: Render objects/humans/exit with depth
Canvas->>Canvas: Draw HUD, toolbox, lidar panel
Canvas-->>User: Display interactive maze
end
User->>App: Reach exit marker
App->>Sim: Detect proximity, trigger reset
Sim->>App: Generate new maze layout
sequenceDiagram
participant User
participant App as Assembly App
participant Camera as Camera/Vision
participant Canvas as Canvas Renderer
participant Sim as Simulation
User->>App: Load page / Grant camera access
App->>Camera: Load MediaPipe & request camera stream
alt Gesture Mode
Camera->>App: Mirror video feed
Camera-->>App: Hand landmarks per frame
App->>App: Classify gestures, update cursor
else Keyboard Mode
User->>App: Press W/A/S/D, hover toolbox, pinch
end
loop Animation Frame
alt Cursor not interacting with toolbox
App->>Sim: Update robot position/angle from gesture
end
App->>Sim: Check for stable fist gesture or Space key
Sim->>Sim: Trigger pickup/drop or rotate equipped module
App->>Canvas: Draw ground, grid, objects, humans
App->>Canvas: Render robot model with equipped parts
App->>Canvas: Draw toolbox UI + camera overlay panel
Canvas-->>User: Display assembly interface
end
User->>App: Select toolbox part via pinch
App->>Sim: Equip selected module on robot
Sim->>Canvas: Update preview to show equipped part
Estimated code review effort🎯 4 (Complex) | ⏱️ ~70 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@3D/app.js`:
- Around line 179-193: Rename the confusing bgr helper to a clearer name (e.g.,
bgrToRgb or colorFromBgr) and update all usages and related helpers to match
(rename bgrAlpha to bgrToRgba or colorFromBgrAlpha if desired) so the function
intent — converting a BGR array to an RGB(A) CSS string — is explicit; modify
the function declaration for bgr (and bgrAlpha) to the new names and update any
references in the codebase, keeping the colorCache key logic and return format
unchanged.
- Around line 55-68: The MEDIAPIPE_SOURCES array includes an unpinned CDN entry
using "@latest" which risks unexpected breaks; update the fallback entry in
MEDIAPIPE_SOURCES to a specific version (e.g., match "0.10.21" or another chosen
semver like "@0.10" if supported) for both moduleUrl and wasmRoot so all sources
are version-pinned; edit the object that currently contains moduleUrl
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/vision_bundle.mjs"
and its wasmRoot to use the chosen pinned version string, keeping the other two
entries unchanged.
- Around line 2411-2413: The calls resetMazeWorld(), drawBootScreen(), and
init() run immediately at module load and can fail if DOM elements are not yet
available; wrap these calls inside a DOMContentLoaded listener (e.g.,
document.addEventListener('DOMContentLoaded', ...)) so that resetMazeWorld,
drawBootScreen, and init execute only after the DOM is ready, preserving the
current call order and behavior.
- Around line 2106-2122: The final error message in loadVisionModule currently
only shows the first collected error; update the thrown Error to include all
entries from the errors array (e.g., join them with a separator and include a
count) so callers see every CDN failure from MEDIAPIPE_SOURCES; keep existing
behavior of setting activeWasmRoot and rethrowing a descriptive Error that
combines a short summary with the joined errors for easier debugging.
- Around line 74-76: The code assumes DOM elements exist; add null checks after
calling document.getElementById for "simCanvas" and "cameraVideo" to avoid
TypeError when accessing canvas.getContext or using video; for example, verify
that const canvas and const video are non-null, and if either is null call a
clear failure path (throw an Error or log via console.error and return/disable
initialization) and only then call canvas.getContext("2d") to set ctx; reference
the existing symbols canvas, ctx, video and document.getElementById in your fix.
- Line 2060: The empty function drawModeBanner is dead code but still invoked
with a ctx argument elsewhere; either remove the drawModeBanner declaration and
its call site so there's no unused function/call, or if it's intended for future
use, add a clear TODO comment inside drawModeBanner and update its signature to
accept the ctx parameter (or actually use ctx) to match the call site; ensure
you update or remove the caller that currently passes ctx to drawModeBanner
accordingly.
- Around line 35-42: Remove the unused HAND_CONNECTIONS constant declaration
from the file (the const HAND_CONNECTIONS = [...] block) since it is not
referenced anywhere; delete that block and run the linter/tests to confirm
nothing else depends on HAND_CONNECTIONS (search for the symbol to be sure) and
then commit the change.
In `@3D/assemble-app.js`:
- Around line 1666-1684: The keydown handler must ignore auto-repeated events
for one-shot actions (pickup/drop and toolbox cycling): check event.repeat (or
maintain a pressed-key set) and return early for repeats when handling Space or
number keys; specifically, in the window.addEventListener("keydown") handler,
guard the branches that call tryPickup/tryDrop (when checking heldObject and
event.code === "Space") and the toolbox cycling branch that calls
cycleSelectedPart(TOOLBOX_CATEGORIES[...]) (when controlMode === "manual" and
event.key between "1" and "5") so they only run on the initial keydown
(non-repeat). Use event.repeat or a small map keyed by event.code to suppress
repeated firings while still allowing manualKeys tracking to behave correctly.
- Around line 46-60: MEDIAPIPE_SOURCES mixes pinned and mutable versions which
can load an incompatible bundle; update every moduleUrl and wasmRoot entry in
the MEDIAPIPE_SOURCES array to the same explicit version (e.g., replace any
"@latest" entries with "@0.10.21") and also change HAND_LANDMARKER_MODEL_URL to
point to the matching pinned model version rather than "latest" so the bundle
and model remain version-consistent across all CDNs.
In `@3D/assemble-style.css`:
- Around line 58-156: The tutorial CSS is duplicated; extract the shared rules
(.tutorial-pop, .tutorial-card, .tutorial-kicker, .tutorial-card h2,
.tutorial-card p, .tutorial-card ul, .tutorial-card li + li, .tutorial-close,
.tutorial-action and their :hover states) into a single common stylesheet (e.g.,
shared-tutorial.css) and import that from both 3D/style.css and
3D/assemble-style.css, leaving only page-specific overrides (colors, layout
widths, paddings or unique selectors) in each file so future fixes apply once.
In `@3D/assemble.html`:
- Around line 19-41: The dialog with id="assembly-tutorial" declares aria-modal
but doesn't move or trap focus; update the assemblyTutorial initialization to
save document.activeElement before opening, programmatically focus the first
focusable control inside the dialog (e.g., element ids "assembly-tutorial-close"
or "assembly-tutorial-dismiss"), install keyboard handlers to trap Tab/Shift+Tab
within the dialog and to close on Escape by calling closeAssemblyTutorial, and
when closing restore focus to the previously focused element; additionally, make
background content non-tabbable while open (e.g., set inert/aria-hidden or
disable tabbable elements outside the assemblyTutorial) so the "Move to Maze"
link (class "mode-jump") and other controls cannot be focused while the modal is
open.
In `@3D/index.html`:
- Around line 20-48: The dialog with id "maze-tutorial" needs real modal focus
management: when opened, programmatically set focus into the dialog (e.g., to
the button with id "maze-tutorial-dismiss" or the dialog container), save the
previously focused element, add a keydown handler to trap Tab/Shift+Tab within
the dialog (using the dialog's focusable elements) and ignore outside focus
movement, and on close (handlers for "maze-tutorial-close" and
"maze-tutorial-dismiss") restore focus to the saved element and remove the
keydown trap. Also mark the background content as inert/unfocusable while open
(e.g., toggle aria-hidden or inert on the nav.route-nav and main content) so
keyboard/screen‑reader users cannot reach elements behind the modal; ensure
these attributes are removed when the dialog is dismissed. Ensure all changes
target the DOM nodes referenced by mazeTutorial, "maze-tutorial-close", and
"maze-tutorial-dismiss".
In `@index.html`:
- Around line 593-626: The onClick handlers on the anchor elements (the tech-btn
anchors that call document.getElementById('page-transition') and set
window.location.href) unconditionally call preventDefault(), breaking modified
clicks and assistive navigation; modify each onClick to only call
e.preventDefault() when it's an unmodified primary click (e.button === 0 and
!e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey), then run the overlay
activation and timed navigation; also add brief accessibility attributes to the
anchors (e.g., aria-label describing the transition and aria-describedby or
aria-controls pointing to the 'page-transition' overlay) so screen readers
understand the enhanced behavior.
In `@README.md`:
- Around line 141-152: In the "Option 1: Open directly" README section, replace
the incorrect clone and cd commands referencing
alphaonelabs-virtual-robotics-playground with the correct repository (git clone
https://github.com/alphaonelabs/botlab.git and cd botlab) and update the
numbered step "2. Open `index.html`..." to "1. Open `index.html`..." so the
ordered list is sequential (this fixes the wrong repository reference and the
MD029 lint issue); ensure these changes are applied inside the existing code
block in that section.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: alphaonelabs/coderabbit/.coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: eaee2086-071f-41b2-989e-f1855ca8e5aa
⛔ Files ignored due to path filters (1)
.DS_Storeis excluded by!**/.DS_Store
📒 Files selected for processing (9)
3D/app.js3D/assemble-app.js3D/assemble-style.css3D/assemble.html3D/index.html3D/style.cssREADME.mdhome.htmlindex.html
| const HAND_CONNECTIONS = [ | ||
| [0, 1], [1, 2], [2, 3], [3, 4], | ||
| [0, 5], [5, 6], [6, 7], [7, 8], | ||
| [0, 9], [9, 10], [10, 11], [11, 12], | ||
| [0, 13], [13, 14], [14, 15], [15, 16], | ||
| [0, 17], [17, 18], [18, 19], [19, 20], | ||
| [5, 9], [9, 13], [13, 17] | ||
| ]; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for HAND_CONNECTIONS usage
rg -n "HAND_CONNECTIONS" 3D/app.jsRepository: alphaonelabs/botlab
Length of output: 92
🏁 Script executed:
rg "HAND_CONNECTIONS" --type js --type tsRepository: alphaonelabs/botlab
Length of output: 145
Remove the unused HAND_CONNECTIONS constant.
This constant defines MediaPipe hand skeleton connections but isn't referenced anywhere in the codebase. It appears to be leftover from earlier development—perhaps intended for a hand skeleton overlay feature that wasn't completed. Removing it will keep the code cleaner and easier to maintain.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@3D/app.js` around lines 35 - 42, Remove the unused HAND_CONNECTIONS constant
declaration from the file (the const HAND_CONNECTIONS = [...] block) since it is
not referenced anywhere; delete that block and run the linter/tests to confirm
nothing else depends on HAND_CONNECTIONS (search for the symbol to be sure) and
then commit the change.
| const MEDIAPIPE_SOURCES = [ | ||
| { | ||
| moduleUrl: "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.21/vision_bundle.mjs", | ||
| wasmRoot: "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.21/wasm" | ||
| }, | ||
| { | ||
| moduleUrl: "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/vision_bundle.mjs", | ||
| wasmRoot: "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm" | ||
| }, | ||
| { | ||
| moduleUrl: "https://unpkg.com/@mediapipe/tasks-vision@0.10.21/vision_bundle.mjs", | ||
| wasmRoot: "https://unpkg.com/@mediapipe/tasks-vision@0.10.21/wasm" | ||
| } | ||
| ]; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider pinning all MediaPipe CDN versions for reproducibility.
Using @latest (lines 61-62) as a fallback CDN source creates a risk of unexpected breakage if the MediaPipe team releases a version with breaking changes. While I understand this provides a fallback if the pinned version becomes unavailable, it may be better to pin to a specific version across all sources, or at least use a semver range like @0.10 if the CDN supports it.
This is a trade-off between availability and stability—just something to be aware of for production use!
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@3D/app.js` around lines 55 - 68, The MEDIAPIPE_SOURCES array includes an
unpinned CDN entry using "@latest" which risks unexpected breaks; update the
fallback entry in MEDIAPIPE_SOURCES to a specific version (e.g., match "0.10.21"
or another chosen semver like "@0.10" if supported) for both moduleUrl and
wasmRoot so all sources are version-pinned; edit the object that currently
contains moduleUrl
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/vision_bundle.mjs"
and its wasmRoot to use the chosen pinned version string, keeping the other two
entries unchanged.
| const canvas = document.getElementById("simCanvas"); | ||
| const ctx = canvas.getContext("2d"); | ||
| const video = document.getElementById("cameraVideo"); |
There was a problem hiding this comment.
Add null checks for DOM elements to prevent runtime errors.
If the expected DOM elements (#simCanvas, #cameraVideo) are missing or misspelled in the HTML, these lines will cause a TypeError when attempting to call methods on null. Adding defensive checks here provides a clearer error message and prevents cascading failures.
🛡️ Suggested defensive approach
-const canvas = document.getElementById("simCanvas");
-const ctx = canvas.getContext("2d");
-const video = document.getElementById("cameraVideo");
+const canvas = document.getElementById("simCanvas");
+if (!canvas) {
+ throw new Error("Required element `#simCanvas` not found in the DOM");
+}
+const ctx = canvas.getContext("2d");
+if (!ctx) {
+ throw new Error("Could not get 2D rendering context from canvas");
+}
+const video = document.getElementById("cameraVideo");
+if (!video) {
+ throw new Error("Required element `#cameraVideo` not found in the DOM");
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const canvas = document.getElementById("simCanvas"); | |
| const ctx = canvas.getContext("2d"); | |
| const video = document.getElementById("cameraVideo"); | |
| const canvas = document.getElementById("simCanvas"); | |
| if (!canvas) { | |
| throw new Error("Required element `#simCanvas` not found in the DOM"); | |
| } | |
| const ctx = canvas.getContext("2d"); | |
| if (!ctx) { | |
| throw new Error("Could not get 2D rendering context from canvas"); | |
| } | |
| const video = document.getElementById("cameraVideo"); | |
| if (!video) { | |
| throw new Error("Required element `#cameraVideo` not found in the DOM"); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@3D/app.js` around lines 74 - 76, The code assumes DOM elements exist; add
null checks after calling document.getElementById for "simCanvas" and
"cameraVideo" to avoid TypeError when accessing canvas.getContext or using
video; for example, verify that const canvas and const video are non-null, and
if either is null call a clear failure path (throw an Error or log via
console.error and return/disable initialization) and only then call
canvas.getContext("2d") to set ctx; reference the existing symbols canvas, ctx,
video and document.getElementById in your fix.
| function bgr(color) { | ||
| const key = color.join(","); | ||
| if (!colorCache.has(key)) { | ||
| colorCache.set(key, `rgb(${color[2]}, ${color[1]}, ${color[0]})`); | ||
| } | ||
| return colorCache.get(key); | ||
| } | ||
|
|
||
| function bgrAlpha(color, alpha) { | ||
| return `rgba(${color[2]}, ${color[1]}, ${color[0]}, ${alpha})`; | ||
| } | ||
|
|
||
| function rgb(r, g, b, alpha = 1) { | ||
| return alpha === 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${alpha})`; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Minor naming clarity: bgr() function outputs RGB.
The function bgr() accepts a color array in BGR order (common in OpenCV/image processing contexts) and converts it to an RGB CSS string—that's the correct behavior! However, the name might confuse readers who expect it to output BGR. A name like bgrToRgb() or colorFromBgr() would make the transformation explicit.
That said, if this naming convention is established elsewhere in the project (e.g., from MediaPipe or OpenCV integration), keeping it consistent is totally fine!
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@3D/app.js` around lines 179 - 193, Rename the confusing bgr helper to a
clearer name (e.g., bgrToRgb or colorFromBgr) and update all usages and related
helpers to match (rename bgrAlpha to bgrToRgba or colorFromBgrAlpha if desired)
so the function intent — converting a BGR array to an RGB(A) CSS string — is
explicit; modify the function declaration for bgr (and bgrAlpha) to the new
names and update any references in the codebase, keeping the colorCache key
logic and return format unchanged.
| fillText(ctx, "FROZEN", SIM_W / 2, 35, 36, bgr([0, 80, 255]), "center", 700); | ||
| } | ||
|
|
||
| function drawModeBanner() {} |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Empty function drawModeBanner appears to be dead code.
This function has an empty body but is still called on line 2263 with ctx as an argument (which the function doesn't accept). If this was intended functionality that's no longer needed, consider removing both the function and its call site. If it's planned for future use, a // TODO comment explaining the intent would help future maintainers.
🧹 Option 1: Remove if not needed
-function drawModeBanner() {}And remove the call on line 2263:
- drawModeBanner(ctx);📝 Option 2: Add TODO if planned
-function drawModeBanner() {}
+// TODO: Implement mode banner overlay (e.g., "GESTURE MODE" / "MANUAL MODE")
+function drawModeBanner(context) {
+ // Placeholder for future implementation
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function drawModeBanner() {} | |
| // TODO: Implement mode banner overlay (e.g., "GESTURE MODE" / "MANUAL MODE") | |
| function drawModeBanner(context) { | |
| // Placeholder for future implementation | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@3D/app.js` at line 2060, The empty function drawModeBanner is dead code but
still invoked with a ctx argument elsewhere; either remove the drawModeBanner
declaration and its call site so there's no unused function/call, or if it's
intended for future use, add a clear TODO comment inside drawModeBanner and
update its signature to accept the ctx parameter (or actually use ctx) to match
the call site; ensure you update or remove the caller that currently passes ctx
to drawModeBanner accordingly.
| .tutorial-pop { | ||
| position: fixed; | ||
| inset: 0; | ||
| z-index: 20; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| padding: 24px; | ||
| background: rgba(0, 0, 0, 0.48); | ||
| backdrop-filter: blur(4px); | ||
| } | ||
|
|
||
| .tutorial-pop.is-hidden { | ||
| display: none; | ||
| } | ||
|
|
||
| .tutorial-card { | ||
| position: relative; | ||
| width: min(460px, 100%); | ||
| padding: 22px 22px 18px; | ||
| border: 1px solid rgba(96, 165, 250, 0.55); | ||
| border-radius: 20px; | ||
| background: rgba(6, 12, 20, 0.96); | ||
| color: #e5eef8; | ||
| box-shadow: 0 24px 80px rgba(0, 0, 0, 0.45); | ||
| } | ||
|
|
||
| .tutorial-kicker { | ||
| margin: 0 0 8px; | ||
| color: #93c5fd; | ||
| font-size: 11px; | ||
| font-weight: 700; | ||
| letter-spacing: 0.16em; | ||
| text-transform: uppercase; | ||
| } | ||
|
|
||
| .tutorial-card h2 { | ||
| margin: 0 0 10px; | ||
| color: #fff; | ||
| font-size: 28px; | ||
| line-height: 1.05; | ||
| } | ||
|
|
||
| .tutorial-card p { | ||
| margin: 0 0 12px; | ||
| color: #cbd5e1; | ||
| font-size: 14px; | ||
| line-height: 1.5; | ||
| } | ||
|
|
||
| .tutorial-card ul { | ||
| margin: 0 0 16px; | ||
| padding-left: 18px; | ||
| color: #dbe7f5; | ||
| font-size: 14px; | ||
| line-height: 1.5; | ||
| } | ||
|
|
||
| .tutorial-card li + li { | ||
| margin-top: 8px; | ||
| } | ||
|
|
||
| .tutorial-close { | ||
| position: absolute; | ||
| top: 12px; | ||
| right: 12px; | ||
| width: 34px; | ||
| height: 34px; | ||
| border: 0; | ||
| border-radius: 999px; | ||
| background: rgba(30, 41, 59, 0.95); | ||
| color: #e2e8f0; | ||
| font-size: 20px; | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| .tutorial-close:hover { | ||
| background: rgba(51, 65, 85, 0.98); | ||
| } | ||
|
|
||
| .tutorial-action { | ||
| display: inline-flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| padding: 10px 16px; | ||
| border: 1px solid rgba(34, 197, 94, 0.65); | ||
| border-radius: 999px; | ||
| background: rgba(20, 83, 45, 0.95); | ||
| color: #f0fdf4; | ||
| font-size: 12px; | ||
| font-weight: 700; | ||
| letter-spacing: 0.12em; | ||
| text-transform: uppercase; | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| .tutorial-action:hover { | ||
| background: rgba(22, 101, 52, 1); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
The tutorial styling is already duplicated across both 3D entry pages.
This block is almost the same as 3D/style.css, so any future fix to the modal or button states will have to be made twice. Consider extracting the shared tutorial/card/button rules into one common stylesheet and leaving only page-specific colors/layout here.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@3D/assemble-style.css` around lines 58 - 156, The tutorial CSS is duplicated;
extract the shared rules (.tutorial-pop, .tutorial-card, .tutorial-kicker,
.tutorial-card h2, .tutorial-card p, .tutorial-card ul, .tutorial-card li + li,
.tutorial-close, .tutorial-action and their :hover states) into a single common
stylesheet (e.g., shared-tutorial.css) and import that from both 3D/style.css
and 3D/assemble-style.css, leaving only page-specific overrides (colors, layout
widths, paddings or unique selectors) in each file so future fixes apply once.
| <div class="tutorial-pop" id="assembly-tutorial" role="dialog" aria-modal="true" aria-labelledby="assembly-tutorial-title"> | ||
| <div class="tutorial-card"> | ||
| <button class="tutorial-close" id="assembly-tutorial-close" aria-label="Close tutorial">×</button> | ||
| <div class="tutorial-kicker">Quick Start</div> | ||
| <h2 id="assembly-tutorial-title">3D Assembly</h2> | ||
| <p>Use your right hand to move the robot:</p> | ||
| <ul> | ||
| <li><strong>1 finger</strong> = move forward</li> | ||
| <li><strong>2 fingers</strong> = move backward</li> | ||
| <li><strong>3 fingers</strong> = move right</li> | ||
| <li><strong>4 fingers</strong> = move left</li> | ||
| </ul> | ||
| <button class="tutorial-action" id="assembly-tutorial-dismiss">Got It</button> | ||
| </div> | ||
| </div> | ||
| <a href="./index.html" class="mode-jump" aria-label="Move to 3D maze page">Move to Maze</a> | ||
| <canvas id="simCanvas" width="1920" height="1080"></canvas> | ||
| <video id="cameraVideo" autoplay playsinline muted></video> | ||
| <script> | ||
| const assemblyTutorial = document.getElementById("assembly-tutorial"); | ||
| const closeAssemblyTutorial = () => assemblyTutorial.classList.add("is-hidden"); | ||
| document.getElementById("assembly-tutorial-close").addEventListener("click", closeAssemblyTutorial); | ||
| document.getElementById("assembly-tutorial-dismiss").addEventListener("click", closeAssemblyTutorial); |
There was a problem hiding this comment.
The tutorial is marked modal, but it does not behave like one.
aria-modal="true" is set, but focus is never moved into the dialog and the Move to Maze link remains tabbable behind it. Keyboard users can navigate outside the overlay, so the modal semantics are currently misleading. Please move initial focus into the dialog and trap/restore focus when it closes; Escape-to-dismiss would help too.
As per coding guidelines, **/*.html: Review HTML templates for accessibility (ARIA attributes, semantic elements), XSS risks from unescaped user content, and proper use of template inheritance.
🧰 Tools
🪛 HTMLHint (1.9.2)
[warning] 21-21: The type attribute must be present on elements.
(button-type-require)
[warning] 31-31: The type attribute must be present on
elements.(button-type-require)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@3D/assemble.html` around lines 19 - 41, The dialog with
id="assembly-tutorial" declares aria-modal but doesn't move or trap focus;
update the assemblyTutorial initialization to save document.activeElement before
opening, programmatically focus the first focusable control inside the dialog
(e.g., element ids "assembly-tutorial-close" or "assembly-tutorial-dismiss"),
install keyboard handlers to trap Tab/Shift+Tab within the dialog and to close
on Escape by calling closeAssemblyTutorial, and when closing restore focus to
the previously focused element; additionally, make background content
non-tabbable while open (e.g., set inert/aria-hidden or disable tabbable
elements outside the assemblyTutorial) so the "Move to Maze" link (class
"mode-jump") and other controls cannot be focused while the modal is open.
| <div class="tutorial-pop" id="maze-tutorial" role="dialog" aria-modal="true" aria-labelledby="maze-tutorial-title"> | ||
| <div class="tutorial-card"> | ||
| <button class="tutorial-close" id="maze-tutorial-close" aria-label="Close tutorial">×</button> | ||
| <div class="tutorial-kicker">Quick Start</div> | ||
| <h2 id="maze-tutorial-title">3D Maze</h2> | ||
| <p>Use your right hand to move the robot:</p> | ||
| <ul> | ||
| <li><strong>1 finger</strong> = move forward</li> | ||
| <li><strong>2 fingers</strong> = move backward</li> | ||
| <li><strong>3 fingers</strong> = move right</li> | ||
| <li><strong>4 fingers</strong> = move left</li> | ||
| <li><strong>M</strong> = new maze</li> | ||
| </ul> | ||
| <button class="tutorial-action" id="maze-tutorial-dismiss">Got It</button> | ||
| </div> | ||
| </div> | ||
| <nav class="route-nav" aria-label="Playground pages"> | ||
| <a href="../index.html">Main</a> | ||
| <a href="../home.html">2D Robotics</a> | ||
| <a href="./assemble.html">3D Assembly</a> | ||
| <a href="./index.html" class="is-active" aria-current="page">3D Maze</a> | ||
| </nav> | ||
| <canvas id="simCanvas" width="1920" height="1080"></canvas> | ||
| <video id="cameraVideo" autoplay playsinline muted></video> | ||
| <script> | ||
| const mazeTutorial = document.getElementById("maze-tutorial"); | ||
| const closeMazeTutorial = () => mazeTutorial.classList.add("is-hidden"); | ||
| document.getElementById("maze-tutorial-close").addEventListener("click", closeMazeTutorial); | ||
| document.getElementById("maze-tutorial-dismiss").addEventListener("click", closeMazeTutorial); |
There was a problem hiding this comment.
This dialog also needs real modal focus management.
While the tutorial is open, the route nav behind it is still reachable by keyboard. That makes aria-modal="true" incorrect in practice and creates a confusing first-run experience for keyboard and screen-reader users. Please move focus into the dialog on open and keep it there until the user dismisses it.
As per coding guidelines, **/*.html: Review HTML templates for accessibility (ARIA attributes, semantic elements), XSS risks from unescaped user content, and proper use of template inheritance.
🧰 Tools
🪛 HTMLHint (1.9.2)
[warning] 22-22: The type attribute must be present on elements.
(button-type-require)
[warning] 33-33: The type attribute must be present on
elements.(button-type-require)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@3D/index.html` around lines 20 - 48, The dialog with id "maze-tutorial" needs
real modal focus management: when opened, programmatically set focus into the
dialog (e.g., to the button with id "maze-tutorial-dismiss" or the dialog
container), save the previously focused element, add a keydown handler to trap
Tab/Shift+Tab within the dialog (using the dialog's focusable elements) and
ignore outside focus movement, and on close (handlers for "maze-tutorial-close"
and "maze-tutorial-dismiss") restore focus to the saved element and remove the
keydown trap. Also mark the background content as inert/unfocusable while open
(e.g., toggle aria-hidden or inert on the nav.route-nav and main content) so
keyboard/screen‑reader users cannot reach elements behind the modal; ensure
these attributes are removed when the dialog is dismissed. Ensure all changes
target the DOM nodes referenced by mazeTutorial, "maze-tutorial-close", and
"maze-tutorial-dismiss".
| <a href="3D/assemble.html" className="tech-btn" style={{ letterSpacing: '0.14em' }} onClick={(e) => { | ||
| e.preventDefault(); | ||
| const overlay = document.getElementById('page-transition'); | ||
| overlay.classList.add('active'); | ||
| setTimeout(() => { window.location.href = '3D/assemble.html'; }, 1200); | ||
| }}> | ||
| <span className="btn-corner tl"></span> | ||
| <span className="btn-corner tr"></span> | ||
| <span className="btn-corner bl"></span> | ||
| <span className="btn-corner br"></span> | ||
| <span className="btn-pulse"></span> | ||
| <span className="btn-status"></span> | ||
| <span>Assemble a robot in 3D</span> | ||
| <svg className="btn-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2"> | ||
| <path strokeLinecap="round" strokeLinejoin="round" d="M20 7.5L12 3 4 7.5m16 0v9L12 21m8-13.5L12 12m0 9-8-4.5v-9M12 21v-9m0 0L4 7.5m8 4.5 8-4.5" /> | ||
| </svg> | ||
| </a> | ||
| <a href="3D/index.html" className="tech-btn" style={{ letterSpacing: '0.14em' }} onClick={(e) => { | ||
| e.preventDefault(); | ||
| const overlay = document.getElementById('page-transition'); | ||
| overlay.classList.add('active'); | ||
| setTimeout(() => { window.location.href = '3D/index.html'; }, 1200); | ||
| }}> | ||
| <span className="btn-corner tl"></span> | ||
| <span className="btn-corner tr"></span> | ||
| <span className="btn-corner bl"></span> | ||
| <span className="btn-corner br"></span> | ||
| <span className="btn-pulse"></span> | ||
| <span className="btn-status"></span> | ||
| <span>Run the 3D maze</span> | ||
| <svg className="btn-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2"> | ||
| <path strokeLinecap="round" strokeLinejoin="round" d="M4 7h16M4 12h10M4 17h16" /> | ||
| </svg> | ||
| </a> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the index.html file
find . -name "index.html" -type f | head -20Repository: alphaonelabs/botlab
Length of output: 91
🏁 Script executed:
# Get file size to understand what we're dealing with
find . -name "index.html" -type f -exec wc -l {} \;Repository: alphaonelabs/botlab
Length of output: 98
🏁 Script executed:
# Read the specific lines mentioned in the review
sed -n '593,626p' ./index.htmlRepository: alphaonelabs/botlab
Length of output: 2152
🏁 Script executed:
# Get context around these lines to understand the structure
sed -n '580,640p' ./index.htmlRepository: alphaonelabs/botlab
Length of output: 3803
Preserve native link behavior for the new transition CTAs and add accessibility attributes.
These handlers call preventDefault() unconditionally, which blocks Cmd/Ctrl+Click (open in new tab), Shift+Click (open in new window), and middle-click navigation—affecting power users and users relying on keyboard navigation. Additionally, screen readers may not properly understand the enhanced link behavior.
Please guard the prevention to only intercept unmodified primary clicks, and add ARIA attributes to clarify the enhanced interaction:
Suggested improvements
- <a href="3D/assemble.html" className="tech-btn" style={{ letterSpacing: '0.14em' }} onClick={(e) => {
+ <a href="3D/assemble.html" className="tech-btn" style={{ letterSpacing: '0.14em' }} role="link" aria-label="Assemble a robot in 3D with animated transition" onClick={(e) => {
+ if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
+ return;
+ }
e.preventDefault();
const overlay = document.getElementById('page-transition');
overlay.classList.add('active');
setTimeout(() => { window.location.href = '3D/assemble.html'; }, 1200);
}}>
- <a href="3D/index.html" className="tech-btn" style={{ letterSpacing: '0.14em' }} onClick={(e) => {
+ <a href="3D/index.html" className="tech-btn" style={{ letterSpacing: '0.14em' }} role="link" aria-label="Run the 3D maze with animated transition" onClick={(e) => {
+ if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
+ return;
+ }
e.preventDefault();
const overlay = document.getElementById('page-transition');
overlay.classList.add('active');
setTimeout(() => { window.location.href = '3D/index.html'; }, 1200);
}}>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@index.html` around lines 593 - 626, The onClick handlers on the anchor
elements (the tech-btn anchors that call
document.getElementById('page-transition') and set window.location.href)
unconditionally call preventDefault(), breaking modified clicks and assistive
navigation; modify each onClick to only call e.preventDefault() when it's an
unmodified primary click (e.button === 0 and !e.metaKey && !e.ctrlKey &&
!e.shiftKey && !e.altKey), then run the overlay activation and timed navigation;
also add brief accessibility attributes to the anchors (e.g., aria-label
describing the transition and aria-describedby or aria-controls pointing to the
'page-transition' overlay) so screen readers understand the enhanced behavior.
| ### Option 1: Open directly | ||
|
|
||
| You can open the project directly in a browser: | ||
|
|
||
| 1. Clone the repository: | ||
|
|
||
| ```bash | ||
| git clone https://github.com/alphaonelabs/alphaonelabs-virtual-robotics-playground.git | ||
| cd alphaonelabs-virtual-robotics-playground | ||
| ``` | ||
|
|
||
| - The **shape** of the detected object — `Square`, `Circle`, or `Triangle` | ||
| - The **color** of the detected object — e.g. `Red`, `Orange`, `Green`, `Blue` | ||
| 2. Open `index.html` in a modern browser. |
There was a problem hiding this comment.
Fix the Getting Started commands in this block.
This PR is on alphaonelabs/botlab, so the current clone URL and cd target point to a different repository. The same list also trips MD029 if markdownlint is enforced.
Suggested doc fix
1. Clone the repository:
```bash
-git clone https://github.com/alphaonelabs/alphaonelabs-virtual-robotics-playground.git
-cd alphaonelabs-virtual-robotics-playground
+git clone https://github.com/alphaonelabs/botlab.git
+cd botlab@@
-2. Open index.html in a modern browser.
+1. Open index.html in a modern browser.
</details>
<details>
<summary>🧰 Tools</summary>
<details>
<summary>🪛 markdownlint-cli2 (0.22.0)</summary>
[warning] 152-152: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1
(MD029, ol-prefix)
</details>
</details>
<details>
<summary>🤖 Prompt for AI Agents</summary>
Verify each finding against the current code and only fix it if needed.
In @README.md around lines 141 - 152, In the "Option 1: Open directly" README
section, replace the incorrect clone and cd commands referencing
alphaonelabs-virtual-robotics-playground with the correct repository (git clone
https://github.com/alphaonelabs/botlab.git and cd botlab) and update the
numbered step "2. Open index.html..." to "1. Open index.html..." so the
ordered list is sequential (this fixes the wrong repository reference and the
MD029 lint issue); ensure these changes are applied inside the existing code
block in that section.
</details>
<!-- fingerprinting:phantom:medusa:grasshopper:5bcef4a9-f051-44e8-a0cd-a44f25634038 -->
<!-- This is an auto-generated comment by CodeRabbit -->
The 3D Maze app is an interactive web application that allows users to explore and navigate a virtual maze rendered in three dimensions. Users can move through the maze, experience immersive graphics, and challenge themselves to find the exit. The app is designed for both entertainment and educational purposes, showcasing 3D rendering, user controls, and real-time interaction within a browser environment.