Spaces:
Sleeping
Sleeping
Commit ·
4eb4118
1
Parent(s): dacf755
docs: add implementation plan for planning review gate & guided wizard
Browse files
docs/superpowers/plans/2026-04-12-planning-review-gate.md
ADDED
|
@@ -0,0 +1,1755 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Planning Review Gate & Guided Wizard Implementation Plan
|
| 2 |
+
|
| 3 |
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
| 4 |
+
|
| 5 |
+
**Goal:** Add a planning phase with user approval before CAD generation, a guided wizard UI, 3MF export, and organized downloads.
|
| 6 |
+
|
| 7 |
+
**Architecture:** `DesignState` gains `phase` and `plan` fields. A scoring function triggers plan presentation when completeness crosses a configurable threshold. Two new API endpoints handle approve/reject. The frontend adds a Chat/Guided tab switcher with a 7-step wizard and inline plan cards.
|
| 8 |
+
|
| 9 |
+
**Tech Stack:** Python/Pydantic (backend), FastAPI (API), vanilla JS/HTML (frontend), CadQuery (3MF export)
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
### Task 1: PlanningConfig in settings
|
| 14 |
+
|
| 15 |
+
**Files:**
|
| 16 |
+
- Modify: `config/settings.py:82-96` (insert before `AgentConfig`)
|
| 17 |
+
- Modify: `config/settings.py:121-123` (add `planning` field to `Settings`)
|
| 18 |
+
- Modify: `config.yaml:72-73` (add `planning` section before `agents`)
|
| 19 |
+
- Test: `tests/test_settings.py`
|
| 20 |
+
|
| 21 |
+
- [ ] **Step 1: Write failing test**
|
| 22 |
+
|
| 23 |
+
```python
|
| 24 |
+
# tests/test_settings.py — add to existing file
|
| 25 |
+
|
| 26 |
+
class TestPlanningConfig:
|
| 27 |
+
def test_planning_defaults(self):
|
| 28 |
+
from config.settings import Settings
|
| 29 |
+
s = Settings()
|
| 30 |
+
assert s.planning.threshold == 8.0
|
| 31 |
+
assert s.planning.weights["material"] == 3
|
| 32 |
+
assert s.planning.caps["dimension"] == 4
|
| 33 |
+
assert "plan" in s.planning.trigger_keywords
|
| 34 |
+
assert "cad" in s.planning.approved_agents
|
| 35 |
+
|
| 36 |
+
def test_planning_threshold_from_yaml(self):
|
| 37 |
+
from config.settings import settings
|
| 38 |
+
assert settings.planning.threshold == 8.0
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
- [ ] **Step 2: Run test to verify it fails**
|
| 42 |
+
|
| 43 |
+
Run: `cd /home/daniel/NeuralCAD && python -m pytest tests/test_settings.py::TestPlanningConfig -v`
|
| 44 |
+
Expected: FAIL with `AttributeError: 'Settings' object has no attribute 'planning'`
|
| 45 |
+
|
| 46 |
+
- [ ] **Step 3: Add PlanningConfig model to settings.py**
|
| 47 |
+
|
| 48 |
+
Add after `RoutingConfig` class (line 96):
|
| 49 |
+
|
| 50 |
+
```python
|
| 51 |
+
class PlanningConfig(BaseModel):
|
| 52 |
+
threshold: float = 8.0
|
| 53 |
+
weights: dict[str, float] = Field(default_factory=lambda: {
|
| 54 |
+
"material": 3, "dimension": 1, "feature": 1,
|
| 55 |
+
"constraint": 1, "part_name": 1, "description": 1,
|
| 56 |
+
"axis_recommendation": 2,
|
| 57 |
+
})
|
| 58 |
+
caps: dict[str, int] = Field(default_factory=lambda: {
|
| 59 |
+
"dimension": 4, "feature": 4, "constraint": 2,
|
| 60 |
+
})
|
| 61 |
+
trigger_keywords: list[str] = Field(default_factory=lambda: [
|
| 62 |
+
"plan", "review", "ready", "show plan", "summarize", "what do we have",
|
| 63 |
+
])
|
| 64 |
+
approved_agents: list[str] = Field(default_factory=lambda: ["cad", "cnc"])
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
Add to `Settings` class after `cam` field (line 121):
|
| 68 |
+
|
| 69 |
+
```python
|
| 70 |
+
planning: PlanningConfig = Field(default_factory=PlanningConfig)
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
- [ ] **Step 4: Add planning section to config.yaml**
|
| 74 |
+
|
| 75 |
+
Add before the `agents:` line (line 73 in config.yaml):
|
| 76 |
+
|
| 77 |
+
```yaml
|
| 78 |
+
planning:
|
| 79 |
+
threshold: 8
|
| 80 |
+
weights:
|
| 81 |
+
material: 3
|
| 82 |
+
dimension: 1
|
| 83 |
+
feature: 1
|
| 84 |
+
constraint: 1
|
| 85 |
+
part_name: 1
|
| 86 |
+
description: 1
|
| 87 |
+
axis_recommendation: 2
|
| 88 |
+
caps:
|
| 89 |
+
dimension: 4
|
| 90 |
+
feature: 4
|
| 91 |
+
constraint: 2
|
| 92 |
+
trigger_keywords:
|
| 93 |
+
- "plan"
|
| 94 |
+
- "review"
|
| 95 |
+
- "ready"
|
| 96 |
+
- "show plan"
|
| 97 |
+
- "summarize"
|
| 98 |
+
- "what do we have"
|
| 99 |
+
approved_agents:
|
| 100 |
+
- "cad"
|
| 101 |
+
- "cnc"
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
- [ ] **Step 5: Run test to verify it passes**
|
| 105 |
+
|
| 106 |
+
Run: `cd /home/daniel/NeuralCAD && python -m pytest tests/test_settings.py::TestPlanningConfig -v`
|
| 107 |
+
Expected: PASS
|
| 108 |
+
|
| 109 |
+
- [ ] **Step 6: Run full test suite**
|
| 110 |
+
|
| 111 |
+
Run: `cd /home/daniel/NeuralCAD && python -m pytest tests/ -v --tb=short`
|
| 112 |
+
Expected: All existing tests pass
|
| 113 |
+
|
| 114 |
+
- [ ] **Step 7: Commit**
|
| 115 |
+
|
| 116 |
+
```bash
|
| 117 |
+
git add config/settings.py config.yaml tests/test_settings.py
|
| 118 |
+
git commit -m "feat: add PlanningConfig to settings with threshold, weights, caps"
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
---
|
| 122 |
+
|
| 123 |
+
### Task 2: DesignPlan model and compute_score()
|
| 124 |
+
|
| 125 |
+
**Files:**
|
| 126 |
+
- Modify: `agents/design_state.py:1-10` (add imports)
|
| 127 |
+
- Modify: `agents/design_state.py:40-49` (add fields to DesignState)
|
| 128 |
+
- Create new class and function in `agents/design_state.py`
|
| 129 |
+
- Test: `tests/test_design_state.py`
|
| 130 |
+
|
| 131 |
+
- [ ] **Step 1: Write failing tests**
|
| 132 |
+
|
| 133 |
+
```python
|
| 134 |
+
# tests/test_design_state.py — add to existing file
|
| 135 |
+
|
| 136 |
+
from agents.design_state import DesignPlan, compute_score
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
class TestDesignPlan:
|
| 140 |
+
def test_create_from_state(self):
|
| 141 |
+
state = DesignState(
|
| 142 |
+
part_name="bracket",
|
| 143 |
+
description="mounting bracket",
|
| 144 |
+
material="aluminum 6061",
|
| 145 |
+
dimensions={"width": 60.0, "height": 40.0, "depth": 20.0},
|
| 146 |
+
features=["4x M6 holes"],
|
| 147 |
+
constraints=["min wall 3mm"],
|
| 148 |
+
axis_recommendation="3-axis",
|
| 149 |
+
decisions=["use aluminum"],
|
| 150 |
+
)
|
| 151 |
+
plan = DesignPlan.from_state(state, confidence_score=9.0)
|
| 152 |
+
assert plan.part_name == "bracket"
|
| 153 |
+
assert plan.material == "aluminum 6061"
|
| 154 |
+
assert plan.dimensions == {"width": 60.0, "height": 40.0, "depth": 20.0}
|
| 155 |
+
assert plan.confidence_score == 9.0
|
| 156 |
+
assert plan.machining_notes == []
|
| 157 |
+
|
| 158 |
+
def test_plan_render(self):
|
| 159 |
+
plan = DesignPlan(
|
| 160 |
+
part_name="bracket",
|
| 161 |
+
description="test",
|
| 162 |
+
material="aluminum 6061",
|
| 163 |
+
dimensions={"width": 60.0},
|
| 164 |
+
features=["4x M6 holes"],
|
| 165 |
+
constraints=[],
|
| 166 |
+
axis_recommendation="3-axis",
|
| 167 |
+
machining_notes=["No undercuts"],
|
| 168 |
+
confidence_score=9.0,
|
| 169 |
+
)
|
| 170 |
+
rendered = plan.render_approved()
|
| 171 |
+
assert "APPROVED DESIGN PLAN" in rendered
|
| 172 |
+
assert "aluminum 6061" in rendered
|
| 173 |
+
assert "No undercuts" in rendered
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
class TestComputeScore:
|
| 177 |
+
def test_empty_state_scores_zero(self):
|
| 178 |
+
assert compute_score(DesignState()) == 0.0
|
| 179 |
+
|
| 180 |
+
def test_material_scores_3(self):
|
| 181 |
+
state = DesignState(material="aluminum")
|
| 182 |
+
assert compute_score(state) == 3.0
|
| 183 |
+
|
| 184 |
+
def test_full_state_above_threshold(self):
|
| 185 |
+
state = DesignState(
|
| 186 |
+
part_name="bracket",
|
| 187 |
+
description="test bracket",
|
| 188 |
+
material="aluminum 6061",
|
| 189 |
+
dimensions={"width": 60.0, "height": 40.0, "depth": 20.0},
|
| 190 |
+
features=["4x M6 holes"],
|
| 191 |
+
constraints=["min wall 3mm"],
|
| 192 |
+
axis_recommendation="3-axis",
|
| 193 |
+
)
|
| 194 |
+
score = compute_score(state)
|
| 195 |
+
assert score >= 8.0 # threshold
|
| 196 |
+
|
| 197 |
+
def test_dimension_cap_at_4(self):
|
| 198 |
+
state = DesignState(dimensions={
|
| 199 |
+
"width": 60, "height": 40, "depth": 20,
|
| 200 |
+
"length": 100, "diameter": 10, "radius": 5,
|
| 201 |
+
})
|
| 202 |
+
score = compute_score(state)
|
| 203 |
+
assert score == 4.0 # 6 dims but capped at 4
|
| 204 |
+
|
| 205 |
+
def test_feature_cap_at_4(self):
|
| 206 |
+
state = DesignState(features=["a", "b", "c", "d", "e", "f"])
|
| 207 |
+
score = compute_score(state)
|
| 208 |
+
assert score == 4.0 # 6 features but capped at 4
|
| 209 |
+
|
| 210 |
+
def test_constraint_cap_at_2(self):
|
| 211 |
+
state = DesignState(constraints=["a", "b", "c", "d"])
|
| 212 |
+
score = compute_score(state)
|
| 213 |
+
assert score == 2.0 # 4 constraints but capped at 2
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
class TestDesignStatePhase:
|
| 217 |
+
def test_default_phase_exploring(self):
|
| 218 |
+
state = DesignState()
|
| 219 |
+
assert state.phase == "exploring"
|
| 220 |
+
assert state.plan is None
|
| 221 |
+
|
| 222 |
+
def test_phase_serialization(self):
|
| 223 |
+
plan = DesignPlan(
|
| 224 |
+
part_name="b", description="", material="steel",
|
| 225 |
+
dimensions={}, features=[], constraints=[],
|
| 226 |
+
axis_recommendation="", machining_notes=[],
|
| 227 |
+
confidence_score=5.0,
|
| 228 |
+
)
|
| 229 |
+
state = DesignState(phase="planning", plan=plan)
|
| 230 |
+
d = state.model_dump()
|
| 231 |
+
assert d["phase"] == "planning"
|
| 232 |
+
assert d["plan"]["material"] == "steel"
|
| 233 |
+
|
| 234 |
+
def test_roundtrip_from_dict(self):
|
| 235 |
+
state = DesignState(phase="approved", material="brass")
|
| 236 |
+
d = state.model_dump()
|
| 237 |
+
restored = DesignState(**d)
|
| 238 |
+
assert restored.phase == "approved"
|
| 239 |
+
assert restored.material == "brass"
|
| 240 |
+
```
|
| 241 |
+
|
| 242 |
+
- [ ] **Step 2: Run tests to verify they fail**
|
| 243 |
+
|
| 244 |
+
Run: `cd /home/daniel/NeuralCAD && python -m pytest tests/test_design_state.py::TestDesignPlan -v`
|
| 245 |
+
Expected: FAIL with `ImportError: cannot import name 'DesignPlan'`
|
| 246 |
+
|
| 247 |
+
- [ ] **Step 3: Add DesignPlan model, compute_score(), and new DesignState fields**
|
| 248 |
+
|
| 249 |
+
In `agents/design_state.py`, add `DesignPlan` class before `DesignState`:
|
| 250 |
+
|
| 251 |
+
```python
|
| 252 |
+
class DesignPlan(BaseModel):
|
| 253 |
+
"""Structured plan presented to user for review/editing before generation."""
|
| 254 |
+
part_name: str = ""
|
| 255 |
+
description: str = ""
|
| 256 |
+
material: str = ""
|
| 257 |
+
dimensions: dict[str, float] = Field(default_factory=dict)
|
| 258 |
+
features: list[str] = Field(default_factory=list)
|
| 259 |
+
constraints: list[str] = Field(default_factory=list)
|
| 260 |
+
axis_recommendation: str = ""
|
| 261 |
+
machining_notes: list[str] = Field(default_factory=list)
|
| 262 |
+
confidence_score: float = 0.0
|
| 263 |
+
|
| 264 |
+
@classmethod
|
| 265 |
+
def from_state(cls, state: "DesignState", confidence_score: float) -> "DesignPlan":
|
| 266 |
+
"""Create a plan snapshot from the current design state."""
|
| 267 |
+
return cls(
|
| 268 |
+
part_name=state.part_name,
|
| 269 |
+
description=state.description,
|
| 270 |
+
material=state.material,
|
| 271 |
+
dimensions=dict(state.dimensions),
|
| 272 |
+
features=list(state.features),
|
| 273 |
+
constraints=list(state.constraints),
|
| 274 |
+
axis_recommendation=state.axis_recommendation,
|
| 275 |
+
machining_notes=[],
|
| 276 |
+
confidence_score=confidence_score,
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
def render_approved(self) -> str:
|
| 280 |
+
"""Render as an approved plan context block for LLM agents."""
|
| 281 |
+
lines = ["## APPROVED DESIGN PLAN (user-confirmed)"]
|
| 282 |
+
if self.part_name:
|
| 283 |
+
lines.append(f"Part: {self.part_name}")
|
| 284 |
+
if self.description:
|
| 285 |
+
lines.append(f"Description: {self.description}")
|
| 286 |
+
if self.material:
|
| 287 |
+
lines.append(f"Material: {self.material}")
|
| 288 |
+
if self.dimensions:
|
| 289 |
+
dims = ", ".join(f"{k}={v}mm" for k, v in self.dimensions.items())
|
| 290 |
+
lines.append(f"Dimensions: {dims}")
|
| 291 |
+
if self.features:
|
| 292 |
+
lines.append(f"Features: {'; '.join(self.features)}")
|
| 293 |
+
if self.constraints:
|
| 294 |
+
lines.append(f"Constraints: {'; '.join(self.constraints)}")
|
| 295 |
+
if self.axis_recommendation:
|
| 296 |
+
lines.append(f"Axis: {self.axis_recommendation}")
|
| 297 |
+
if self.machining_notes:
|
| 298 |
+
lines.append(f"Machining Notes: {'; '.join(self.machining_notes)}")
|
| 299 |
+
lines.append("")
|
| 300 |
+
lines.append("This plan has been reviewed and approved by the user.")
|
| 301 |
+
lines.append("Generate the model according to these specifications.")
|
| 302 |
+
return "\n".join(lines)
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
Add two new fields to `DesignState` (after `axis_recommendation`):
|
| 306 |
+
|
| 307 |
+
```python
|
| 308 |
+
phase: str = "exploring"
|
| 309 |
+
plan: DesignPlan | None = None
|
| 310 |
+
```
|
| 311 |
+
|
| 312 |
+
Add `compute_score()` function after the `DesignState` class:
|
| 313 |
+
|
| 314 |
+
```python
|
| 315 |
+
def compute_score(state: DesignState) -> float:
|
| 316 |
+
"""Compute completeness score for a design state using configured weights and caps."""
|
| 317 |
+
cfg = settings.planning
|
| 318 |
+
score = 0.0
|
| 319 |
+
if state.material:
|
| 320 |
+
score += cfg.weights.get("material", 3)
|
| 321 |
+
if state.part_name:
|
| 322 |
+
score += cfg.weights.get("part_name", 1)
|
| 323 |
+
if state.description:
|
| 324 |
+
score += cfg.weights.get("description", 1)
|
| 325 |
+
if state.axis_recommendation:
|
| 326 |
+
score += cfg.weights.get("axis_recommendation", 2)
|
| 327 |
+
dim_cap = cfg.caps.get("dimension", 4)
|
| 328 |
+
dim_count = min(len(state.dimensions), dim_cap)
|
| 329 |
+
score += dim_count * cfg.weights.get("dimension", 1)
|
| 330 |
+
feat_cap = cfg.caps.get("feature", 4)
|
| 331 |
+
feat_count = min(len(state.features), feat_cap)
|
| 332 |
+
score += feat_count * cfg.weights.get("feature", 1)
|
| 333 |
+
const_cap = cfg.caps.get("constraint", 2)
|
| 334 |
+
const_count = min(len(state.constraints), const_cap)
|
| 335 |
+
score += const_count * cfg.weights.get("constraint", 1)
|
| 336 |
+
return score
|
| 337 |
+
```
|
| 338 |
+
|
| 339 |
+
- [ ] **Step 4: Run tests to verify they pass**
|
| 340 |
+
|
| 341 |
+
Run: `cd /home/daniel/NeuralCAD && python -m pytest tests/test_design_state.py -v`
|
| 342 |
+
Expected: All pass (existing + new)
|
| 343 |
+
|
| 344 |
+
- [ ] **Step 5: Run full test suite**
|
| 345 |
+
|
| 346 |
+
Run: `cd /home/daniel/NeuralCAD && python -m pytest tests/ -v --tb=short`
|
| 347 |
+
Expected: All pass
|
| 348 |
+
|
| 349 |
+
- [ ] **Step 6: Commit**
|
| 350 |
+
|
| 351 |
+
```bash
|
| 352 |
+
git add agents/design_state.py tests/test_design_state.py
|
| 353 |
+
git commit -m "feat: add DesignPlan model, compute_score(), phase/plan fields to DesignState"
|
| 354 |
+
```
|
| 355 |
+
|
| 356 |
+
---
|
| 357 |
+
|
| 358 |
+
### Task 3: Plan approve/reject API endpoints
|
| 359 |
+
|
| 360 |
+
**Files:**
|
| 361 |
+
- Modify: `server/routes.py:1-10` (add imports)
|
| 362 |
+
- Modify: `server/routes.py:38-39` (add request models)
|
| 363 |
+
- Add new endpoints at end of `server/routes.py`
|
| 364 |
+
- Test: `tests/test_api_routes.py`
|
| 365 |
+
|
| 366 |
+
- [ ] **Step 1: Write failing tests**
|
| 367 |
+
|
| 368 |
+
```python
|
| 369 |
+
# tests/test_api_routes.py — add to existing file
|
| 370 |
+
|
| 371 |
+
class TestPlanApproveEndpoint:
|
| 372 |
+
def test_approve_sets_phase(self):
|
| 373 |
+
resp = client.post("/api/plan/approve", json={
|
| 374 |
+
"plan": {
|
| 375 |
+
"part_name": "bracket",
|
| 376 |
+
"description": "test",
|
| 377 |
+
"material": "aluminum 6061",
|
| 378 |
+
"dimensions": {"width": 60},
|
| 379 |
+
"features": ["4x M6 holes"],
|
| 380 |
+
"constraints": ["min wall 3mm"],
|
| 381 |
+
"axis_recommendation": "3-axis",
|
| 382 |
+
"machining_notes": [],
|
| 383 |
+
"confidence_score": 9.0,
|
| 384 |
+
},
|
| 385 |
+
"design_state": {
|
| 386 |
+
"part_name": "bracket",
|
| 387 |
+
"material": "steel",
|
| 388 |
+
"dimensions": {"width": 50},
|
| 389 |
+
"phase": "planning",
|
| 390 |
+
},
|
| 391 |
+
})
|
| 392 |
+
assert resp.status_code == 200
|
| 393 |
+
data = resp.json()
|
| 394 |
+
assert data["design_state"]["phase"] == "approved"
|
| 395 |
+
assert data["design_state"]["material"] == "aluminum 6061"
|
| 396 |
+
assert data["design_state"]["dimensions"]["width"] == 60
|
| 397 |
+
|
| 398 |
+
def test_approve_merges_plan_into_state(self):
|
| 399 |
+
resp = client.post("/api/plan/approve", json={
|
| 400 |
+
"plan": {
|
| 401 |
+
"part_name": "gear",
|
| 402 |
+
"description": "spur gear",
|
| 403 |
+
"material": "brass",
|
| 404 |
+
"dimensions": {"diameter": 40},
|
| 405 |
+
"features": [],
|
| 406 |
+
"constraints": [],
|
| 407 |
+
"axis_recommendation": "3-axis",
|
| 408 |
+
"machining_notes": ["No undercuts"],
|
| 409 |
+
"confidence_score": 8.0,
|
| 410 |
+
},
|
| 411 |
+
"design_state": {"phase": "planning"},
|
| 412 |
+
})
|
| 413 |
+
data = resp.json()
|
| 414 |
+
assert data["design_state"]["part_name"] == "gear"
|
| 415 |
+
assert data["design_state"]["plan"]["material"] == "brass"
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
class TestPlanRejectEndpoint:
|
| 419 |
+
def test_reject_resets_phase(self):
|
| 420 |
+
resp = client.post("/api/plan/reject", json={
|
| 421 |
+
"design_state": {
|
| 422 |
+
"phase": "planning",
|
| 423 |
+
"material": "aluminum",
|
| 424 |
+
"plan": {"part_name": "x", "description": "", "material": "aluminum",
|
| 425 |
+
"dimensions": {}, "features": [], "constraints": [],
|
| 426 |
+
"axis_recommendation": "", "machining_notes": [],
|
| 427 |
+
"confidence_score": 5.0},
|
| 428 |
+
},
|
| 429 |
+
})
|
| 430 |
+
assert resp.status_code == 200
|
| 431 |
+
data = resp.json()
|
| 432 |
+
assert data["design_state"]["phase"] == "exploring"
|
| 433 |
+
assert data["design_state"]["plan"] is None
|
| 434 |
+
assert data["design_state"]["material"] == "aluminum"
|
| 435 |
+
```
|
| 436 |
+
|
| 437 |
+
- [ ] **Step 2: Run tests to verify they fail**
|
| 438 |
+
|
| 439 |
+
Run: `cd /home/daniel/NeuralCAD && python -m pytest tests/test_api_routes.py::TestPlanApproveEndpoint -v`
|
| 440 |
+
Expected: FAIL with 404 (endpoint doesn't exist)
|
| 441 |
+
|
| 442 |
+
- [ ] **Step 3: Add endpoints to routes.py**
|
| 443 |
+
|
| 444 |
+
Add imports at top of `server/routes.py`:
|
| 445 |
+
|
| 446 |
+
```python
|
| 447 |
+
from agents.design_state import DesignState, DesignPlan
|
| 448 |
+
```
|
| 449 |
+
|
| 450 |
+
Add request models after `ReportRequest`:
|
| 451 |
+
|
| 452 |
+
```python
|
| 453 |
+
class PlanApproveRequest(BaseModel):
|
| 454 |
+
plan: dict = Field(...)
|
| 455 |
+
design_state: dict = Field(default_factory=dict)
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
class PlanRejectRequest(BaseModel):
|
| 459 |
+
design_state: dict = Field(default_factory=dict)
|
| 460 |
+
```
|
| 461 |
+
|
| 462 |
+
Add endpoints at end of file:
|
| 463 |
+
|
| 464 |
+
```python
|
| 465 |
+
@router.post("/api/plan/approve")
|
| 466 |
+
async def plan_approve(body: PlanApproveRequest):
|
| 467 |
+
"""Approve (possibly edited) design plan, merge into state."""
|
| 468 |
+
plan = DesignPlan(**body.plan)
|
| 469 |
+
state = DesignState(**body.design_state)
|
| 470 |
+
# Merge plan edits back into state
|
| 471 |
+
state.part_name = plan.part_name
|
| 472 |
+
state.description = plan.description
|
| 473 |
+
state.material = plan.material
|
| 474 |
+
state.dimensions = dict(plan.dimensions)
|
| 475 |
+
state.features = list(plan.features)
|
| 476 |
+
state.constraints = list(plan.constraints)
|
| 477 |
+
state.axis_recommendation = plan.axis_recommendation
|
| 478 |
+
state.phase = "approved"
|
| 479 |
+
state.plan = plan
|
| 480 |
+
return JSONResponse({"design_state": state.model_dump()})
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
@router.post("/api/plan/reject")
|
| 484 |
+
async def plan_reject(body: PlanRejectRequest):
|
| 485 |
+
"""Reject plan, reset to exploring."""
|
| 486 |
+
state = DesignState(**body.design_state)
|
| 487 |
+
state.phase = "exploring"
|
| 488 |
+
state.plan = None
|
| 489 |
+
return JSONResponse({"design_state": state.model_dump()})
|
| 490 |
+
```
|
| 491 |
+
|
| 492 |
+
- [ ] **Step 4: Run tests to verify they pass**
|
| 493 |
+
|
| 494 |
+
Run: `cd /home/daniel/NeuralCAD && python -m pytest tests/test_api_routes.py -v`
|
| 495 |
+
Expected: All pass (existing + new)
|
| 496 |
+
|
| 497 |
+
- [ ] **Step 5: Commit**
|
| 498 |
+
|
| 499 |
+
```bash
|
| 500 |
+
git add server/routes.py tests/test_api_routes.py
|
| 501 |
+
git commit -m "feat: add /api/plan/approve and /api/plan/reject endpoints"
|
| 502 |
+
```
|
| 503 |
+
|
| 504 |
+
---
|
| 505 |
+
|
| 506 |
+
### Task 4: Orchestrator phase branching
|
| 507 |
+
|
| 508 |
+
**Files:**
|
| 509 |
+
- Modify: `agents/crew_orchestrator.py:17` (add imports)
|
| 510 |
+
- Modify: `agents/crew_orchestrator.py:30-55` (update `_build_agent_context`)
|
| 511 |
+
- Modify: `agents/crew_orchestrator.py:93-105` (update `chat_turn` entry)
|
| 512 |
+
- Modify: `agents/crew_orchestrator.py:122-135` (update `_run_crew` top)
|
| 513 |
+
- Test: `tests/test_crew_orchestrator.py`
|
| 514 |
+
|
| 515 |
+
- [ ] **Step 1: Read existing crew orchestrator tests**
|
| 516 |
+
|
| 517 |
+
Read `tests/test_crew_orchestrator.py` to understand the current test patterns and mock setup before writing new tests.
|
| 518 |
+
|
| 519 |
+
- [ ] **Step 2: Write failing tests**
|
| 520 |
+
|
| 521 |
+
```python
|
| 522 |
+
# tests/test_crew_orchestrator.py — add to existing file
|
| 523 |
+
|
| 524 |
+
class TestPlanningPhase:
|
| 525 |
+
"""Tests for planning phase in CrewOrchestrator."""
|
| 526 |
+
|
| 527 |
+
def test_manual_plan_trigger(self):
|
| 528 |
+
"""User typing a trigger keyword returns plan without running crew."""
|
| 529 |
+
orch = CrewOrchestrator(backend_name="mock")
|
| 530 |
+
state = DesignState(
|
| 531 |
+
part_name="bracket",
|
| 532 |
+
material="aluminum 6061",
|
| 533 |
+
dimensions={"width": 60, "height": 40, "depth": 20},
|
| 534 |
+
axis_recommendation="3-axis",
|
| 535 |
+
)
|
| 536 |
+
result = orch.chat_turn(
|
| 537 |
+
message="show plan",
|
| 538 |
+
history=[],
|
| 539 |
+
design_state=state.model_dump(),
|
| 540 |
+
)
|
| 541 |
+
assert result["design_state"]["phase"] == "planning"
|
| 542 |
+
assert result["design_state"]["plan"] is not None
|
| 543 |
+
assert result["design_state"]["plan"]["material"] == "aluminum 6061"
|
| 544 |
+
|
| 545 |
+
def test_auto_plan_trigger_on_threshold(self):
|
| 546 |
+
"""Score crossing threshold auto-triggers planning phase."""
|
| 547 |
+
orch = CrewOrchestrator(backend_name="mock")
|
| 548 |
+
# State that's above default threshold of 8:
|
| 549 |
+
# material(3) + 3 dims(3) + axis(2) = 8
|
| 550 |
+
state = DesignState(
|
| 551 |
+
material="aluminum 6061",
|
| 552 |
+
dimensions={"width": 60, "height": 40, "depth": 20},
|
| 553 |
+
axis_recommendation="3-axis",
|
| 554 |
+
)
|
| 555 |
+
result = orch.chat_turn(
|
| 556 |
+
message="looks good",
|
| 557 |
+
history=[],
|
| 558 |
+
design_state=state.model_dump(),
|
| 559 |
+
)
|
| 560 |
+
# After extract_decisions + score check, should transition to planning
|
| 561 |
+
ds = result["design_state"]
|
| 562 |
+
# State already has enough score, so after this turn it should trigger
|
| 563 |
+
assert ds["phase"] in ("planning", "exploring")
|
| 564 |
+
# If mock fallback runs, it might not trigger — check score directly
|
| 565 |
+
from agents.design_state import compute_score
|
| 566 |
+
assert compute_score(DesignState(**ds)) >= 8.0 or ds["phase"] == "planning"
|
| 567 |
+
|
| 568 |
+
def test_approved_phase_not_reset_by_normal_message(self):
|
| 569 |
+
"""When phase is approved, orchestrator keeps it approved."""
|
| 570 |
+
orch = CrewOrchestrator(backend_name="mock")
|
| 571 |
+
from agents.design_state import DesignPlan
|
| 572 |
+
plan = DesignPlan(
|
| 573 |
+
part_name="bracket", description="test", material="aluminum",
|
| 574 |
+
dimensions={"width": 60}, features=[], constraints=[],
|
| 575 |
+
axis_recommendation="3-axis", machining_notes=[],
|
| 576 |
+
confidence_score=9.0,
|
| 577 |
+
)
|
| 578 |
+
state = DesignState(
|
| 579 |
+
phase="approved",
|
| 580 |
+
plan=plan,
|
| 581 |
+
material="aluminum",
|
| 582 |
+
dimensions={"width": 60},
|
| 583 |
+
)
|
| 584 |
+
result = orch.chat_turn(
|
| 585 |
+
message="Generate the approved design",
|
| 586 |
+
history=[],
|
| 587 |
+
design_state=state.model_dump(),
|
| 588 |
+
)
|
| 589 |
+
# Should stay in approved or transition based on CAD result
|
| 590 |
+
assert "responses" in result
|
| 591 |
+
```
|
| 592 |
+
|
| 593 |
+
- [ ] **Step 3: Run tests to verify they fail**
|
| 594 |
+
|
| 595 |
+
Run: `cd /home/daniel/NeuralCAD && python -m pytest tests/test_crew_orchestrator.py::TestPlanningPhase -v`
|
| 596 |
+
Expected: FAIL (planning logic not implemented yet)
|
| 597 |
+
|
| 598 |
+
- [ ] **Step 4: Add planning imports to crew_orchestrator.py**
|
| 599 |
+
|
| 600 |
+
Add to imports at top:
|
| 601 |
+
|
| 602 |
+
```python
|
| 603 |
+
from agents.design_state import DesignState, DesignPlan, extract_decisions, compute_score
|
| 604 |
+
```
|
| 605 |
+
|
| 606 |
+
Remove the existing import:
|
| 607 |
+
```python
|
| 608 |
+
from agents.design_state import DesignState, extract_decisions
|
| 609 |
+
```
|
| 610 |
+
|
| 611 |
+
- [ ] **Step 5: Add trigger keyword detection function**
|
| 612 |
+
|
| 613 |
+
Add after `_build_agent_context` function:
|
| 614 |
+
|
| 615 |
+
```python
|
| 616 |
+
def _is_plan_trigger(message: str) -> bool:
|
| 617 |
+
"""Check if user message is requesting a plan review."""
|
| 618 |
+
lower = message.lower().strip()
|
| 619 |
+
for keyword in settings.planning.trigger_keywords:
|
| 620 |
+
if keyword in lower:
|
| 621 |
+
return True
|
| 622 |
+
return False
|
| 623 |
+
```
|
| 624 |
+
|
| 625 |
+
- [ ] **Step 6: Update _build_agent_context for approved phase**
|
| 626 |
+
|
| 627 |
+
Modify `_build_agent_context` to accept an optional `DesignPlan` and render it differently when approved:
|
| 628 |
+
|
| 629 |
+
```python
|
| 630 |
+
def _build_agent_context(
|
| 631 |
+
message: str,
|
| 632 |
+
history: list[dict],
|
| 633 |
+
design_state: DesignState,
|
| 634 |
+
max_history: int = 20,
|
| 635 |
+
approved_plan: DesignPlan | None = None,
|
| 636 |
+
) -> str:
|
| 637 |
+
"""Build a shared context string that each CrewAI agent receives."""
|
| 638 |
+
parts = []
|
| 639 |
+
|
| 640 |
+
if approved_plan:
|
| 641 |
+
parts.append(approved_plan.render_approved())
|
| 642 |
+
else:
|
| 643 |
+
spec = design_state.render()
|
| 644 |
+
if spec:
|
| 645 |
+
parts.append(f"## Current Design Spec\n{spec}")
|
| 646 |
+
|
| 647 |
+
recent = history[-max_history:] if len(history) > max_history else history
|
| 648 |
+
if recent:
|
| 649 |
+
lines = []
|
| 650 |
+
for msg in recent:
|
| 651 |
+
if msg.get("role") == "user":
|
| 652 |
+
lines.append(f"USER: {msg.get('content', '')}")
|
| 653 |
+
else:
|
| 654 |
+
aid = msg.get("agent_id", "unknown")
|
| 655 |
+
name = AGENTS.get(aid, AGENTS["design"]).name
|
| 656 |
+
lines.append(f"{name.upper()}: {msg.get('content', '')}")
|
| 657 |
+
parts.append("## Recent conversation\n" + "\n".join(lines))
|
| 658 |
+
|
| 659 |
+
parts.append(f"## User's latest message\n{message}")
|
| 660 |
+
return "\n\n".join(parts)
|
| 661 |
+
```
|
| 662 |
+
|
| 663 |
+
- [ ] **Step 7: Add phase branching to chat_turn/_run_crew**
|
| 664 |
+
|
| 665 |
+
At the start of `_run_crew`, before the existing agent selection logic, add phase handling:
|
| 666 |
+
|
| 667 |
+
```python
|
| 668 |
+
state = DesignState(**(design_state_dict or {}))
|
| 669 |
+
|
| 670 |
+
# Phase: manual plan trigger
|
| 671 |
+
if state.phase == "exploring" and _is_plan_trigger(message):
|
| 672 |
+
score = compute_score(state)
|
| 673 |
+
plan = DesignPlan.from_state(state, confidence_score=score)
|
| 674 |
+
state.phase = "planning"
|
| 675 |
+
state.plan = plan
|
| 676 |
+
return {
|
| 677 |
+
"responses": [],
|
| 678 |
+
"preview": None,
|
| 679 |
+
"design_state": state.model_dump(),
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
# Phase: if somehow in planning and user sends a message, reset to exploring
|
| 683 |
+
if state.phase == "planning":
|
| 684 |
+
state.phase = "exploring"
|
| 685 |
+
state.plan = None
|
| 686 |
+
|
| 687 |
+
# Phase: approved — override routing to approved_agents
|
| 688 |
+
if state.phase == "approved" and state.plan:
|
| 689 |
+
active_ids = list(settings.planning.approved_agents)
|
| 690 |
+
approved_plan = state.plan
|
| 691 |
+
else:
|
| 692 |
+
approved_plan = None
|
| 693 |
+
```
|
| 694 |
+
|
| 695 |
+
Then pass `approved_plan` to `_build_agent_context`:
|
| 696 |
+
|
| 697 |
+
```python
|
| 698 |
+
context = _build_agent_context(message, history, state, max_history, approved_plan=approved_plan)
|
| 699 |
+
```
|
| 700 |
+
|
| 701 |
+
For the approved phase, skip the normal routing (the `active_ids` were already set above). Wrap the existing routing block with a condition:
|
| 702 |
+
|
| 703 |
+
```python
|
| 704 |
+
if not (state.phase == "approved" and state.plan):
|
| 705 |
+
# Select which agents should respond
|
| 706 |
+
if mentions:
|
| 707 |
+
active_ids = list(mentions)
|
| 708 |
+
else:
|
| 709 |
+
active_ids = _router.route(message)
|
| 710 |
+
|
| 711 |
+
# Check CAD trigger
|
| 712 |
+
if "cad" not in active_ids and _router.has_cad_trigger(message):
|
| 713 |
+
active_ids.append("cad")
|
| 714 |
+
```
|
| 715 |
+
|
| 716 |
+
After the crew result processing, before `return`, add auto-trigger for exploring phase:
|
| 717 |
+
|
| 718 |
+
```python
|
| 719 |
+
# Auto-trigger plan if score crosses threshold
|
| 720 |
+
if updated_state.phase == "exploring":
|
| 721 |
+
score = compute_score(updated_state)
|
| 722 |
+
if score >= settings.planning.threshold:
|
| 723 |
+
plan = DesignPlan.from_state(updated_state, confidence_score=score)
|
| 724 |
+
updated_state.phase = "planning"
|
| 725 |
+
updated_state.plan = plan
|
| 726 |
+
|
| 727 |
+
# If approved and CAD said NOT READY, reset
|
| 728 |
+
if state.phase == "approved":
|
| 729 |
+
for r in responses:
|
| 730 |
+
if r.get("agent_id") == "cad" and r.get("message", "").upper().startswith("NOT READY:"):
|
| 731 |
+
updated_state.phase = "exploring"
|
| 732 |
+
updated_state.plan = None
|
| 733 |
+
break
|
| 734 |
+
```
|
| 735 |
+
|
| 736 |
+
- [ ] **Step 8: Run tests to verify they pass**
|
| 737 |
+
|
| 738 |
+
Run: `cd /home/daniel/NeuralCAD && python -m pytest tests/test_crew_orchestrator.py -v`
|
| 739 |
+
Expected: All pass
|
| 740 |
+
|
| 741 |
+
- [ ] **Step 9: Run full test suite**
|
| 742 |
+
|
| 743 |
+
Run: `cd /home/daniel/NeuralCAD && python -m pytest tests/ -v --tb=short`
|
| 744 |
+
Expected: All pass
|
| 745 |
+
|
| 746 |
+
- [ ] **Step 10: Commit**
|
| 747 |
+
|
| 748 |
+
```bash
|
| 749 |
+
git add agents/crew_orchestrator.py tests/test_crew_orchestrator.py
|
| 750 |
+
git commit -m "feat: add planning phase branching to CrewOrchestrator"
|
| 751 |
+
```
|
| 752 |
+
|
| 753 |
+
---
|
| 754 |
+
|
| 755 |
+
### Task 5: 3MF export and endpoint
|
| 756 |
+
|
| 757 |
+
**Files:**
|
| 758 |
+
- Modify: `core/executor.py:183-190` (add `export_3mf`, update `export_all`)
|
| 759 |
+
- Modify: `server/web.py:205-206` (add 3MF endpoint after gcode endpoint)
|
| 760 |
+
- Test: `tests/test_executor.py`
|
| 761 |
+
|
| 762 |
+
- [ ] **Step 1: Write failing test**
|
| 763 |
+
|
| 764 |
+
```python
|
| 765 |
+
# tests/test_executor.py — add to existing file
|
| 766 |
+
|
| 767 |
+
class TestExport3MF:
|
| 768 |
+
def test_export_3mf(self, tmp_path):
|
| 769 |
+
import cadquery as cq
|
| 770 |
+
from core.executor import export_3mf
|
| 771 |
+
shape = cq.Workplane("XY").box(10, 10, 10)
|
| 772 |
+
path = tmp_path / "test.3mf"
|
| 773 |
+
result = export_3mf(shape, path)
|
| 774 |
+
assert result.exists()
|
| 775 |
+
assert result.suffix == ".3mf"
|
| 776 |
+
assert result.stat().st_size > 0
|
| 777 |
+
|
| 778 |
+
def test_export_all_includes_3mf(self, tmp_path):
|
| 779 |
+
import cadquery as cq
|
| 780 |
+
from core.executor import export_all
|
| 781 |
+
shape = cq.Workplane("XY").box(10, 10, 10)
|
| 782 |
+
base = tmp_path / "part"
|
| 783 |
+
files = export_all(shape, base)
|
| 784 |
+
assert "3mf" in files
|
| 785 |
+
assert files["3mf"].exists()
|
| 786 |
+
assert files["step"].exists()
|
| 787 |
+
assert files["stl"].exists()
|
| 788 |
+
```
|
| 789 |
+
|
| 790 |
+
- [ ] **Step 2: Run test to verify it fails**
|
| 791 |
+
|
| 792 |
+
Run: `cd /home/daniel/NeuralCAD && python -m pytest tests/test_executor.py::TestExport3MF -v`
|
| 793 |
+
Expected: FAIL with `ImportError: cannot import name 'export_3mf'`
|
| 794 |
+
|
| 795 |
+
- [ ] **Step 3: Add export_3mf and update export_all**
|
| 796 |
+
|
| 797 |
+
In `core/executor.py`, add after `export_stl` function:
|
| 798 |
+
|
| 799 |
+
```python
|
| 800 |
+
def export_3mf(result: cq.Workplane, path: str | Path) -> Path:
|
| 801 |
+
"""Export a CadQuery workplane to 3MF format (slicer-ready)."""
|
| 802 |
+
path = Path(path)
|
| 803 |
+
cq.exporters.export(result, str(path), exportType="3MF")
|
| 804 |
+
return path
|
| 805 |
+
```
|
| 806 |
+
|
| 807 |
+
Update `export_all` to include 3MF:
|
| 808 |
+
|
| 809 |
+
```python
|
| 810 |
+
def export_all(result: cq.Workplane, base_path: str | Path) -> dict[str, Path]:
|
| 811 |
+
"""Export to STEP, STL, and 3MF."""
|
| 812 |
+
base = Path(base_path)
|
| 813 |
+
return {
|
| 814 |
+
"step": export_step(result, base.with_suffix(".step")),
|
| 815 |
+
"stl": export_stl(result, base.with_suffix(".stl")),
|
| 816 |
+
"3mf": export_3mf(result, base.with_suffix(".3mf")),
|
| 817 |
+
}
|
| 818 |
+
```
|
| 819 |
+
|
| 820 |
+
- [ ] **Step 4: Run test to verify it passes**
|
| 821 |
+
|
| 822 |
+
Run: `cd /home/daniel/NeuralCAD && python -m pytest tests/test_executor.py::TestExport3MF -v`
|
| 823 |
+
Expected: PASS
|
| 824 |
+
|
| 825 |
+
- [ ] **Step 5: Add 3MF serving endpoint to web.py**
|
| 826 |
+
|
| 827 |
+
In `server/web.py`, add after the `get_gcode` endpoint (line 205):
|
| 828 |
+
|
| 829 |
+
```python
|
| 830 |
+
@app.get("/api/models/{name}.3mf")
|
| 831 |
+
async def get_3mf(name: str):
|
| 832 |
+
path = OUTPUT_DIR / f"{name}.3mf"
|
| 833 |
+
if not path.exists():
|
| 834 |
+
return JSONResponse({"error": f"3MF not found: {name}"}, status_code=404)
|
| 835 |
+
return FileResponse(path, media_type="model/3mf", filename=f"{name}.3mf")
|
| 836 |
+
```
|
| 837 |
+
|
| 838 |
+
- [ ] **Step 6: Run full test suite**
|
| 839 |
+
|
| 840 |
+
Run: `cd /home/daniel/NeuralCAD && python -m pytest tests/ -v --tb=short`
|
| 841 |
+
Expected: All pass
|
| 842 |
+
|
| 843 |
+
- [ ] **Step 7: Commit**
|
| 844 |
+
|
| 845 |
+
```bash
|
| 846 |
+
git add core/executor.py server/web.py tests/test_executor.py
|
| 847 |
+
git commit -m "feat: add 3MF export and /api/models/{name}.3mf endpoint"
|
| 848 |
+
```
|
| 849 |
+
|
| 850 |
+
---
|
| 851 |
+
|
| 852 |
+
### Task 6: 3MF in orchestrator preview response
|
| 853 |
+
|
| 854 |
+
**Files:**
|
| 855 |
+
- Modify: `agents/crew_orchestrator.py:296-310` (add `threemf_url` to preview dict)
|
| 856 |
+
|
| 857 |
+
- [ ] **Step 1: Update preview dict in _run_crew**
|
| 858 |
+
|
| 859 |
+
In `agents/crew_orchestrator.py`, find the block where `preview` dict is created (around line 303) and add `threemf_url`:
|
| 860 |
+
|
| 861 |
+
```python
|
| 862 |
+
preview = {
|
| 863 |
+
"success": True,
|
| 864 |
+
"part_name": part_name,
|
| 865 |
+
"stl_url": f"/api/models/{part_name}.stl",
|
| 866 |
+
"step_url": f"/api/models/{part_name}.step",
|
| 867 |
+
"threemf_url": f"/api/models/{part_name}.3mf",
|
| 868 |
+
"execution": {"success": True},
|
| 869 |
+
"validation": validation.model_dump(),
|
| 870 |
+
}
|
| 871 |
+
```
|
| 872 |
+
|
| 873 |
+
- [ ] **Step 2: Run full test suite**
|
| 874 |
+
|
| 875 |
+
Run: `cd /home/daniel/NeuralCAD && python -m pytest tests/ -v --tb=short`
|
| 876 |
+
Expected: All pass
|
| 877 |
+
|
| 878 |
+
- [ ] **Step 3: Commit**
|
| 879 |
+
|
| 880 |
+
```bash
|
| 881 |
+
git add agents/crew_orchestrator.py
|
| 882 |
+
git commit -m "feat: include threemf_url in preview response"
|
| 883 |
+
```
|
| 884 |
+
|
| 885 |
+
---
|
| 886 |
+
|
| 887 |
+
### Task 7: Frontend tab switcher and guided wizard
|
| 888 |
+
|
| 889 |
+
**Files:**
|
| 890 |
+
- Modify: `web/index.html:1256-1283` (chat panel header, add tabs)
|
| 891 |
+
- Modify: `web/index.html` (add wizard HTML, CSS, JS)
|
| 892 |
+
|
| 893 |
+
This is the largest frontend task. It adds the tab bar, guided wizard panel, and state sync logic.
|
| 894 |
+
|
| 895 |
+
- [ ] **Step 1: Add tab switcher CSS**
|
| 896 |
+
|
| 897 |
+
In `web/index.html`, add CSS after the existing chat panel styles (find the `#chat-panel` CSS block):
|
| 898 |
+
|
| 899 |
+
```css
|
| 900 |
+
/* ---- TAB SWITCHER ---- */
|
| 901 |
+
.chat-tabs {
|
| 902 |
+
display: flex;
|
| 903 |
+
border-bottom: 1px solid var(--border);
|
| 904 |
+
background: var(--bg-panel);
|
| 905 |
+
flex-shrink: 0;
|
| 906 |
+
}
|
| 907 |
+
.chat-tab {
|
| 908 |
+
flex: 1;
|
| 909 |
+
padding: 8px 0;
|
| 910 |
+
text-align: center;
|
| 911 |
+
font-family: var(--font-mono);
|
| 912 |
+
font-size: 11px;
|
| 913 |
+
font-weight: 500;
|
| 914 |
+
color: var(--text-secondary);
|
| 915 |
+
background: none;
|
| 916 |
+
border: none;
|
| 917 |
+
cursor: pointer;
|
| 918 |
+
border-bottom: 2px solid transparent;
|
| 919 |
+
transition: color 0.2s, border-color 0.2s;
|
| 920 |
+
}
|
| 921 |
+
.chat-tab:hover { color: var(--text-primary); }
|
| 922 |
+
.chat-tab.active {
|
| 923 |
+
color: var(--accent);
|
| 924 |
+
border-bottom-color: var(--accent);
|
| 925 |
+
}
|
| 926 |
+
#guided-panel { display: none; overflow-y: auto; flex: 1; padding: 12px; }
|
| 927 |
+
#guided-panel.active { display: flex; flex-direction: column; gap: 12px; }
|
| 928 |
+
#chat-messages.hidden { display: none; }
|
| 929 |
+
|
| 930 |
+
/* ---- WIZARD STEPS ---- */
|
| 931 |
+
.wizard-step {
|
| 932 |
+
background: var(--bg-surface);
|
| 933 |
+
border: 1px solid var(--border);
|
| 934 |
+
border-radius: 8px;
|
| 935 |
+
padding: 12px;
|
| 936 |
+
}
|
| 937 |
+
.wizard-step.completed { border-color: var(--success); }
|
| 938 |
+
.wizard-step-header {
|
| 939 |
+
display: flex; justify-content: space-between; align-items: center;
|
| 940 |
+
margin-bottom: 8px;
|
| 941 |
+
}
|
| 942 |
+
.wizard-step-title {
|
| 943 |
+
font-family: var(--font-mono); font-size: 11px;
|
| 944 |
+
color: var(--text-secondary); font-weight: 600;
|
| 945 |
+
}
|
| 946 |
+
.wizard-step-check { color: var(--success); font-size: 14px; }
|
| 947 |
+
.wizard-chips {
|
| 948 |
+
display: flex; flex-wrap: wrap; gap: 6px;
|
| 949 |
+
}
|
| 950 |
+
.wizard-chip {
|
| 951 |
+
padding: 5px 12px; border-radius: 14px;
|
| 952 |
+
font-family: var(--font-mono); font-size: 11px;
|
| 953 |
+
background: var(--bg-input); border: 1px solid var(--border);
|
| 954 |
+
color: var(--text-primary); cursor: pointer;
|
| 955 |
+
transition: border-color 0.15s, background 0.15s;
|
| 956 |
+
}
|
| 957 |
+
.wizard-chip:hover { border-color: var(--accent); }
|
| 958 |
+
.wizard-chip.selected {
|
| 959 |
+
border-color: var(--accent); background: var(--accent-glow);
|
| 960 |
+
color: var(--accent);
|
| 961 |
+
}
|
| 962 |
+
.wizard-input {
|
| 963 |
+
width: 100%; padding: 6px 10px; margin-top: 6px;
|
| 964 |
+
background: var(--bg-input); border: 1px solid var(--border);
|
| 965 |
+
border-radius: 6px; color: var(--text-primary);
|
| 966 |
+
font-family: var(--font-mono); font-size: 12px;
|
| 967 |
+
}
|
| 968 |
+
.wizard-input:focus { outline: none; border-color: var(--accent); }
|
| 969 |
+
.wizard-dim-row {
|
| 970 |
+
display: flex; gap: 8px; align-items: center; margin-top: 6px;
|
| 971 |
+
}
|
| 972 |
+
.wizard-dim-label {
|
| 973 |
+
font-family: var(--font-mono); font-size: 11px;
|
| 974 |
+
color: var(--text-secondary); min-width: 50px;
|
| 975 |
+
}
|
| 976 |
+
.wizard-dim-input {
|
| 977 |
+
width: 80px; padding: 4px 8px;
|
| 978 |
+
background: var(--bg-input); border: 1px solid var(--border);
|
| 979 |
+
border-radius: 4px; color: var(--text-primary);
|
| 980 |
+
font-family: var(--font-mono); font-size: 12px;
|
| 981 |
+
}
|
| 982 |
+
.wizard-dim-unit {
|
| 983 |
+
font-family: var(--font-mono); font-size: 10px;
|
| 984 |
+
color: var(--text-muted);
|
| 985 |
+
}
|
| 986 |
+
.wizard-review-field {
|
| 987 |
+
display: flex; justify-content: space-between; align-items: center;
|
| 988 |
+
padding: 4px 0; border-bottom: 1px solid var(--border);
|
| 989 |
+
}
|
| 990 |
+
.wizard-review-label {
|
| 991 |
+
font-family: var(--font-mono); font-size: 10px;
|
| 992 |
+
color: var(--text-secondary);
|
| 993 |
+
}
|
| 994 |
+
.wizard-review-value {
|
| 995 |
+
font-family: var(--font-mono); font-size: 11px;
|
| 996 |
+
color: var(--text-primary);
|
| 997 |
+
}
|
| 998 |
+
.wizard-btn-row { display: flex; gap: 8px; margin-top: 10px; }
|
| 999 |
+
.wizard-btn {
|
| 1000 |
+
flex: 1; padding: 8px; border-radius: 6px;
|
| 1001 |
+
font-family: var(--font-mono); font-size: 11px;
|
| 1002 |
+
font-weight: 600; cursor: pointer; border: 1px solid var(--border);
|
| 1003 |
+
transition: background 0.15s;
|
| 1004 |
+
}
|
| 1005 |
+
.wizard-btn-primary {
|
| 1006 |
+
background: var(--accent); color: var(--bg-void); border-color: var(--accent);
|
| 1007 |
+
}
|
| 1008 |
+
.wizard-btn-secondary {
|
| 1009 |
+
background: var(--bg-surface); color: var(--text-secondary);
|
| 1010 |
+
}
|
| 1011 |
+
```
|
| 1012 |
+
|
| 1013 |
+
- [ ] **Step 2: Add tab bar HTML**
|
| 1014 |
+
|
| 1015 |
+
Replace the `chat-header` div (lines 1260-1271) with:
|
| 1016 |
+
|
| 1017 |
+
```html
|
| 1018 |
+
<div class="chat-tabs">
|
| 1019 |
+
<button class="chat-tab active" id="tab-chat" onclick="switchTab('chat')">Chat</button>
|
| 1020 |
+
<button class="chat-tab" id="tab-guided" onclick="switchTab('guided')">Guided</button>
|
| 1021 |
+
</div>
|
| 1022 |
+
|
| 1023 |
+
<div class="chat-header">
|
| 1024 |
+
<div class="chat-header-left">
|
| 1025 |
+
<span class="chat-header-title" data-i18n="designChat">Design Chat</span>
|
| 1026 |
+
<button onclick="newDesign()" title="New Design" style="background:none;border:1px solid var(--border);border-radius:4px;color:var(--text-secondary);padding:2px 8px;font-size:10px;cursor:pointer;margin-left:8px;" data-i18n="newBtn">NEW</button>
|
| 1027 |
+
<div class="agent-dots">
|
| 1028 |
+
<div class="agent-dot" style="background: var(--agent-design);" title="Design Agent"></div>
|
| 1029 |
+
<div class="agent-dot" style="background: var(--agent-engineering);" title="Engineering Agent"></div>
|
| 1030 |
+
<div class="agent-dot" style="background: var(--agent-cnc);" title="CNC Agent"></div>
|
| 1031 |
+
<div class="agent-dot" style="background: var(--agent-cad);" title="CAD Coder Agent"></div>
|
| 1032 |
+
</div>
|
| 1033 |
+
</div>
|
| 1034 |
+
</div>
|
| 1035 |
+
```
|
| 1036 |
+
|
| 1037 |
+
- [ ] **Step 3: Add guided panel HTML after chat-messages div**
|
| 1038 |
+
|
| 1039 |
+
After the closing `</div>` of `#chat-messages` (line 1283), add:
|
| 1040 |
+
|
| 1041 |
+
```html
|
| 1042 |
+
<div id="guided-panel">
|
| 1043 |
+
<!-- Step 1: Part Type -->
|
| 1044 |
+
<div class="wizard-step" id="wiz-step-1">
|
| 1045 |
+
<div class="wizard-step-header">
|
| 1046 |
+
<span class="wizard-step-title">1. PART TYPE</span>
|
| 1047 |
+
<span class="wizard-step-check" id="wiz-check-1"></span>
|
| 1048 |
+
</div>
|
| 1049 |
+
<div class="wizard-chips">
|
| 1050 |
+
<button class="wizard-chip" onclick="wizSetPart('bracket','Mounting bracket')">Bracket</button>
|
| 1051 |
+
<button class="wizard-chip" onclick="wizSetPart('enclosure','Enclosure housing')">Enclosure</button>
|
| 1052 |
+
<button class="wizard-chip" onclick="wizSetPart('plate','Flat plate')">Plate</button>
|
| 1053 |
+
<button class="wizard-chip" onclick="wizSetPart('shaft','Cylindrical shaft')">Shaft</button>
|
| 1054 |
+
<button class="wizard-chip" onclick="wizSetPart('gear','Spur gear')">Gear</button>
|
| 1055 |
+
<button class="wizard-chip" onclick="wizSetPart('flange','Pipe flange')">Flange</button>
|
| 1056 |
+
</div>
|
| 1057 |
+
<input class="wizard-input" placeholder="Or type custom part name..." onchange="wizSetPart(this.value, this.value)">
|
| 1058 |
+
</div>
|
| 1059 |
+
|
| 1060 |
+
<!-- Step 2: Material -->
|
| 1061 |
+
<div class="wizard-step" id="wiz-step-2">
|
| 1062 |
+
<div class="wizard-step-header">
|
| 1063 |
+
<span class="wizard-step-title">2. MATERIAL</span>
|
| 1064 |
+
<span class="wizard-step-check" id="wiz-check-2"></span>
|
| 1065 |
+
</div>
|
| 1066 |
+
<div class="wizard-chips">
|
| 1067 |
+
<button class="wizard-chip" onclick="wizSetMaterial('aluminum 6061')">Aluminum 6061</button>
|
| 1068 |
+
<button class="wizard-chip" onclick="wizSetMaterial('aluminum 7075')">Aluminum 7075</button>
|
| 1069 |
+
<button class="wizard-chip" onclick="wizSetMaterial('stainless steel 304')">Steel 304</button>
|
| 1070 |
+
<button class="wizard-chip" onclick="wizSetMaterial('stainless steel 316')">Steel 316</button>
|
| 1071 |
+
<button class="wizard-chip" onclick="wizSetMaterial('brass')">Brass</button>
|
| 1072 |
+
<button class="wizard-chip" onclick="wizSetMaterial('titanium')">Titanium</button>
|
| 1073 |
+
<button class="wizard-chip" onclick="wizSetMaterial('nylon')">Nylon</button>
|
| 1074 |
+
<button class="wizard-chip" onclick="wizSetMaterial('delrin')">Delrin</button>
|
| 1075 |
+
</div>
|
| 1076 |
+
<input class="wizard-input" placeholder="Or type custom material..." onchange="wizSetMaterial(this.value)">
|
| 1077 |
+
</div>
|
| 1078 |
+
|
| 1079 |
+
<!-- Step 3: Dimensions -->
|
| 1080 |
+
<div class="wizard-step" id="wiz-step-3">
|
| 1081 |
+
<div class="wizard-step-header">
|
| 1082 |
+
<span class="wizard-step-title">3. DIMENSIONS (mm)</span>
|
| 1083 |
+
<span class="wizard-step-check" id="wiz-check-3"></span>
|
| 1084 |
+
</div>
|
| 1085 |
+
<div class="wizard-dim-row">
|
| 1086 |
+
<span class="wizard-dim-label">Width</span>
|
| 1087 |
+
<input class="wizard-dim-input" id="wiz-dim-width" type="number" onchange="wizSetDim('width', this.value)">
|
| 1088 |
+
<span class="wizard-dim-unit">mm</span>
|
| 1089 |
+
</div>
|
| 1090 |
+
<div class="wizard-dim-row">
|
| 1091 |
+
<span class="wizard-dim-label">Height</span>
|
| 1092 |
+
<input class="wizard-dim-input" id="wiz-dim-height" type="number" onchange="wizSetDim('height', this.value)">
|
| 1093 |
+
<span class="wizard-dim-unit">mm</span>
|
| 1094 |
+
</div>
|
| 1095 |
+
<div class="wizard-dim-row">
|
| 1096 |
+
<span class="wizard-dim-label">Depth</span>
|
| 1097 |
+
<input class="wizard-dim-input" id="wiz-dim-depth" type="number" onchange="wizSetDim('depth', this.value)">
|
| 1098 |
+
<span class="wizard-dim-unit">mm</span>
|
| 1099 |
+
</div>
|
| 1100 |
+
</div>
|
| 1101 |
+
|
| 1102 |
+
<!-- Step 4: Features -->
|
| 1103 |
+
<div class="wizard-step" id="wiz-step-4">
|
| 1104 |
+
<div class="wizard-step-header">
|
| 1105 |
+
<span class="wizard-step-title">4. FEATURES</span>
|
| 1106 |
+
<span class="wizard-step-check" id="wiz-check-4"></span>
|
| 1107 |
+
</div>
|
| 1108 |
+
<div class="wizard-chips">
|
| 1109 |
+
<button class="wizard-chip" id="wiz-feat-holes" onclick="wizToggleFeature(this, 'holes')">Mounting Holes</button>
|
| 1110 |
+
<button class="wizard-chip" id="wiz-feat-fillets" onclick="wizToggleFeature(this, 'fillets')">Fillets</button>
|
| 1111 |
+
<button class="wizard-chip" id="wiz-feat-chamfers" onclick="wizToggleFeature(this, 'chamfers')">Chamfers</button>
|
| 1112 |
+
<button class="wizard-chip" id="wiz-feat-pockets" onclick="wizToggleFeature(this, 'pockets')">Pockets</button>
|
| 1113 |
+
<button class="wizard-chip" id="wiz-feat-slots" onclick="wizToggleFeature(this, 'slots')">Slots</button>
|
| 1114 |
+
</div>
|
| 1115 |
+
<div id="wiz-holes-config" style="display:none;margin-top:8px;">
|
| 1116 |
+
<div class="wizard-dim-row">
|
| 1117 |
+
<span class="wizard-dim-label">Count</span>
|
| 1118 |
+
<input class="wizard-dim-input" id="wiz-hole-count" type="number" value="4" min="1" onchange="wizUpdateHoles()">
|
| 1119 |
+
</div>
|
| 1120 |
+
<div class="wizard-chips" style="margin-top:6px;">
|
| 1121 |
+
<button class="wizard-chip" id="wiz-hole-m3" onclick="wizSetHoleSize('M3')">M3</button>
|
| 1122 |
+
<button class="wizard-chip" id="wiz-hole-m4" onclick="wizSetHoleSize('M4')">M4</button>
|
| 1123 |
+
<button class="wizard-chip selected" id="wiz-hole-m6" onclick="wizSetHoleSize('M6')">M6</button>
|
| 1124 |
+
<button class="wizard-chip" id="wiz-hole-m8" onclick="wizSetHoleSize('M8')">M8</button>
|
| 1125 |
+
</div>
|
| 1126 |
+
</div>
|
| 1127 |
+
<input class="wizard-input" placeholder="Or type custom feature..." onkeydown="if(event.key==='Enter'){wizAddCustomFeature(this.value);this.value='';}">
|
| 1128 |
+
</div>
|
| 1129 |
+
|
| 1130 |
+
<!-- Step 5: Constraints -->
|
| 1131 |
+
<div class="wizard-step" id="wiz-step-5">
|
| 1132 |
+
<div class="wizard-step-header">
|
| 1133 |
+
<span class="wizard-step-title">5. CONSTRAINTS</span>
|
| 1134 |
+
<span class="wizard-step-check" id="wiz-check-5"></span>
|
| 1135 |
+
</div>
|
| 1136 |
+
<div class="wizard-dim-row">
|
| 1137 |
+
<span class="wizard-dim-label">Min wall</span>
|
| 1138 |
+
<input class="wizard-dim-input" id="wiz-min-wall" type="number" value="3" step="0.5" onchange="wizUpdateConstraints()">
|
| 1139 |
+
<span class="wizard-dim-unit">mm</span>
|
| 1140 |
+
</div>
|
| 1141 |
+
<div class="wizard-dim-row">
|
| 1142 |
+
<span class="wizard-dim-label">Max size</span>
|
| 1143 |
+
<input class="wizard-dim-input" id="wiz-max-size" type="number" value="500" onchange="wizUpdateConstraints()">
|
| 1144 |
+
<span class="wizard-dim-unit">mm</span>
|
| 1145 |
+
</div>
|
| 1146 |
+
</div>
|
| 1147 |
+
|
| 1148 |
+
<!-- Step 6: Machining -->
|
| 1149 |
+
<div class="wizard-step" id="wiz-step-6">
|
| 1150 |
+
<div class="wizard-step-header">
|
| 1151 |
+
<span class="wizard-step-title">6. MACHINING</span>
|
| 1152 |
+
<span class="wizard-step-check" id="wiz-check-6"></span>
|
| 1153 |
+
</div>
|
| 1154 |
+
<div class="wizard-chips">
|
| 1155 |
+
<button class="wizard-chip" onclick="wizSetAxis('3-axis', this)">3-axis</button>
|
| 1156 |
+
<button class="wizard-chip" onclick="wizSetAxis('3+2-axis', this)">3+2-axis</button>
|
| 1157 |
+
<button class="wizard-chip" onclick="wizSetAxis('5-axis', this)">5-axis</button>
|
| 1158 |
+
<button class="wizard-chip" onclick="wizSetAxis('', this)">Auto</button>
|
| 1159 |
+
</div>
|
| 1160 |
+
</div>
|
| 1161 |
+
|
| 1162 |
+
<!-- Step 7: Review -->
|
| 1163 |
+
<div class="wizard-step" id="wiz-step-7">
|
| 1164 |
+
<div class="wizard-step-header">
|
| 1165 |
+
<span class="wizard-step-title">7. REVIEW</span>
|
| 1166 |
+
<span class="wizard-step-check" id="wiz-check-7"></span>
|
| 1167 |
+
</div>
|
| 1168 |
+
<div id="wiz-review-content"></div>
|
| 1169 |
+
<div id="wiz-score" style="font-family:var(--font-mono);font-size:11px;color:var(--text-secondary);margin-top:8px;"></div>
|
| 1170 |
+
<div class="wizard-btn-row">
|
| 1171 |
+
<button class="wizard-btn wizard-btn-primary" onclick="wizApprove()">Approve & Generate</button>
|
| 1172 |
+
<button class="wizard-btn wizard-btn-secondary" onclick="switchTab('chat')">Back to Chat</button>
|
| 1173 |
+
</div>
|
| 1174 |
+
</div>
|
| 1175 |
+
</div>
|
| 1176 |
+
```
|
| 1177 |
+
|
| 1178 |
+
- [ ] **Step 4: Add wizard JavaScript**
|
| 1179 |
+
|
| 1180 |
+
Add to the `<script>` section, after the existing state variables:
|
| 1181 |
+
|
| 1182 |
+
```javascript
|
| 1183 |
+
// ── WIZARD STATE ──────────────────────────────────────
|
| 1184 |
+
let activeTab = 'chat';
|
| 1185 |
+
let wizHoleSize = 'M6';
|
| 1186 |
+
let wizFeatures = new Set();
|
| 1187 |
+
|
| 1188 |
+
function switchTab(tab) {
|
| 1189 |
+
activeTab = tab;
|
| 1190 |
+
document.getElementById('tab-chat').classList.toggle('active', tab === 'chat');
|
| 1191 |
+
document.getElementById('tab-guided').classList.toggle('active', tab === 'guided');
|
| 1192 |
+
document.getElementById('chat-messages').classList.toggle('hidden', tab === 'guided');
|
| 1193 |
+
document.querySelector('.chat-header').style.display = tab === 'chat' ? 'flex' : 'none';
|
| 1194 |
+
document.getElementById('guided-panel').classList.toggle('active', tab === 'guided');
|
| 1195 |
+
if (tab === 'guided') syncWizardFromState();
|
| 1196 |
+
}
|
| 1197 |
+
|
| 1198 |
+
function wizSetPart(name, desc) {
|
| 1199 |
+
designState.part_name = name;
|
| 1200 |
+
designState.description = desc;
|
| 1201 |
+
wizMarkStep(1); saveState();
|
| 1202 |
+
document.querySelectorAll('#wiz-step-1 .wizard-chip').forEach(c => c.classList.remove('selected'));
|
| 1203 |
+
event.target.classList.add('selected');
|
| 1204 |
+
}
|
| 1205 |
+
|
| 1206 |
+
function wizSetMaterial(mat) {
|
| 1207 |
+
designState.material = mat;
|
| 1208 |
+
wizMarkStep(2); saveState();
|
| 1209 |
+
document.querySelectorAll('#wiz-step-2 .wizard-chip').forEach(c => c.classList.remove('selected'));
|
| 1210 |
+
if (event && event.target) event.target.classList.add('selected');
|
| 1211 |
+
}
|
| 1212 |
+
|
| 1213 |
+
function wizSetDim(name, val) {
|
| 1214 |
+
if (!designState.dimensions) designState.dimensions = {};
|
| 1215 |
+
const v = parseFloat(val);
|
| 1216 |
+
if (v > 0) designState.dimensions[name] = v;
|
| 1217 |
+
else delete designState.dimensions[name];
|
| 1218 |
+
wizMarkStep(3); saveState();
|
| 1219 |
+
}
|
| 1220 |
+
|
| 1221 |
+
function wizToggleFeature(el, feat) {
|
| 1222 |
+
if (wizFeatures.has(feat)) {
|
| 1223 |
+
wizFeatures.delete(feat);
|
| 1224 |
+
el.classList.remove('selected');
|
| 1225 |
+
} else {
|
| 1226 |
+
wizFeatures.add(feat);
|
| 1227 |
+
el.classList.add('selected');
|
| 1228 |
+
}
|
| 1229 |
+
if (feat === 'holes') {
|
| 1230 |
+
document.getElementById('wiz-holes-config').style.display = wizFeatures.has('holes') ? 'block' : 'none';
|
| 1231 |
+
}
|
| 1232 |
+
wizRebuildFeatures();
|
| 1233 |
+
wizMarkStep(4); saveState();
|
| 1234 |
+
}
|
| 1235 |
+
|
| 1236 |
+
function wizSetHoleSize(size) {
|
| 1237 |
+
wizHoleSize = size;
|
| 1238 |
+
document.querySelectorAll('#wiz-holes-config .wizard-chip').forEach(c => c.classList.remove('selected'));
|
| 1239 |
+
document.getElementById('wiz-hole-' + size.toLowerCase()).classList.add('selected');
|
| 1240 |
+
wizRebuildFeatures();
|
| 1241 |
+
saveState();
|
| 1242 |
+
}
|
| 1243 |
+
|
| 1244 |
+
function wizUpdateHoles() { wizRebuildFeatures(); saveState(); }
|
| 1245 |
+
|
| 1246 |
+
function wizRebuildFeatures() {
|
| 1247 |
+
if (!designState.features) designState.features = [];
|
| 1248 |
+
// Remove auto-generated features, keep custom ones
|
| 1249 |
+
designState.features = designState.features.filter(f => f.startsWith('custom:'));
|
| 1250 |
+
if (wizFeatures.has('holes')) {
|
| 1251 |
+
const count = document.getElementById('wiz-hole-count')?.value || 4;
|
| 1252 |
+
designState.features.push(count + 'x ' + wizHoleSize + ' holes');
|
| 1253 |
+
}
|
| 1254 |
+
if (wizFeatures.has('fillets')) designState.features.push('fillets');
|
| 1255 |
+
if (wizFeatures.has('chamfers')) designState.features.push('chamfers');
|
| 1256 |
+
if (wizFeatures.has('pockets')) designState.features.push('pockets');
|
| 1257 |
+
if (wizFeatures.has('slots')) designState.features.push('slots');
|
| 1258 |
+
// Re-add custom features without prefix
|
| 1259 |
+
designState.features = designState.features.map(f => f.replace('custom:', ''));
|
| 1260 |
+
}
|
| 1261 |
+
|
| 1262 |
+
function wizAddCustomFeature(val) {
|
| 1263 |
+
if (!val.trim()) return;
|
| 1264 |
+
if (!designState.features) designState.features = [];
|
| 1265 |
+
designState.features.push(val.trim());
|
| 1266 |
+
wizMarkStep(4); saveState();
|
| 1267 |
+
}
|
| 1268 |
+
|
| 1269 |
+
function wizUpdateConstraints() {
|
| 1270 |
+
designState.constraints = [];
|
| 1271 |
+
const wall = document.getElementById('wiz-min-wall')?.value;
|
| 1272 |
+
const size = document.getElementById('wiz-max-size')?.value;
|
| 1273 |
+
if (wall) designState.constraints.push('min wall ' + wall + 'mm');
|
| 1274 |
+
if (size && parseFloat(size) < 500) designState.constraints.push('max size ' + size + 'mm');
|
| 1275 |
+
wizMarkStep(5); saveState();
|
| 1276 |
+
}
|
| 1277 |
+
|
| 1278 |
+
function wizSetAxis(axis, el) {
|
| 1279 |
+
designState.axis_recommendation = axis;
|
| 1280 |
+
document.querySelectorAll('#wiz-step-6 .wizard-chip').forEach(c => c.classList.remove('selected'));
|
| 1281 |
+
if (el) el.classList.add('selected');
|
| 1282 |
+
wizMarkStep(6); saveState();
|
| 1283 |
+
}
|
| 1284 |
+
|
| 1285 |
+
function wizMarkStep(n) {
|
| 1286 |
+
const check = document.getElementById('wiz-check-' + n);
|
| 1287 |
+
const step = document.getElementById('wiz-step-' + n);
|
| 1288 |
+
if (check) check.textContent = '\u2713';
|
| 1289 |
+
if (step) step.classList.add('completed');
|
| 1290 |
+
if (n <= 6) wizUpdateReview();
|
| 1291 |
+
}
|
| 1292 |
+
|
| 1293 |
+
function wizUpdateReview() {
|
| 1294 |
+
const el = document.getElementById('wiz-review-content');
|
| 1295 |
+
if (!el) return;
|
| 1296 |
+
let html = '';
|
| 1297 |
+
const fields = [
|
| 1298 |
+
['Part', designState.part_name || ''],
|
| 1299 |
+
['Material', designState.material || ''],
|
| 1300 |
+
['Dimensions', Object.entries(designState.dimensions || {}).map(([k,v]) => k + '=' + v + 'mm').join(', ') || ''],
|
| 1301 |
+
['Features', (designState.features || []).join(', ') || ''],
|
| 1302 |
+
['Constraints', (designState.constraints || []).join(', ') || ''],
|
| 1303 |
+
['Machining', designState.axis_recommendation || 'Auto'],
|
| 1304 |
+
];
|
| 1305 |
+
for (const [label, value] of fields) {
|
| 1306 |
+
html += '<div class="wizard-review-field"><span class="wizard-review-label">' + label + '</span><span class="wizard-review-value">' + (value || '\u2014') + '</span></div>';
|
| 1307 |
+
}
|
| 1308 |
+
el.innerHTML = html;
|
| 1309 |
+
|
| 1310 |
+
// Show score
|
| 1311 |
+
const score = wizComputeScore();
|
| 1312 |
+
const scoreEl = document.getElementById('wiz-score');
|
| 1313 |
+
if (scoreEl) scoreEl.textContent = 'Score: ' + score.toFixed(0) + '/8 ' + (score >= 8 ? '\u2713 Ready' : '\u2717 Need more info');
|
| 1314 |
+
}
|
| 1315 |
+
|
| 1316 |
+
function wizComputeScore() {
|
| 1317 |
+
let s = 0;
|
| 1318 |
+
if (designState.material) s += 3;
|
| 1319 |
+
if (designState.part_name) s += 1;
|
| 1320 |
+
if (designState.description) s += 1;
|
| 1321 |
+
if (designState.axis_recommendation) s += 2;
|
| 1322 |
+
s += Math.min(Object.keys(designState.dimensions || {}).length, 4);
|
| 1323 |
+
s += Math.min((designState.features || []).length, 4);
|
| 1324 |
+
s += Math.min((designState.constraints || []).length, 2);
|
| 1325 |
+
return s;
|
| 1326 |
+
}
|
| 1327 |
+
|
| 1328 |
+
async function wizApprove() {
|
| 1329 |
+
const plan = {
|
| 1330 |
+
part_name: designState.part_name || '',
|
| 1331 |
+
description: designState.description || '',
|
| 1332 |
+
material: designState.material || '',
|
| 1333 |
+
dimensions: designState.dimensions || {},
|
| 1334 |
+
features: designState.features || [],
|
| 1335 |
+
constraints: designState.constraints || [],
|
| 1336 |
+
axis_recommendation: designState.axis_recommendation || '',
|
| 1337 |
+
machining_notes: [],
|
| 1338 |
+
confidence_score: wizComputeScore(),
|
| 1339 |
+
};
|
| 1340 |
+
try {
|
| 1341 |
+
const resp = await fetch('/api/plan/approve', {
|
| 1342 |
+
method: 'POST',
|
| 1343 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1344 |
+
body: JSON.stringify({ plan: plan, design_state: designState }),
|
| 1345 |
+
});
|
| 1346 |
+
const data = await resp.json();
|
| 1347 |
+
designState = data.design_state;
|
| 1348 |
+
saveState();
|
| 1349 |
+
switchTab('chat');
|
| 1350 |
+
await sendMessage('Generate the approved design');
|
| 1351 |
+
} catch (err) {
|
| 1352 |
+
console.error('Plan approve failed:', err);
|
| 1353 |
+
}
|
| 1354 |
+
}
|
| 1355 |
+
|
| 1356 |
+
function syncWizardFromState() {
|
| 1357 |
+
// Sync dimension inputs
|
| 1358 |
+
const dims = designState.dimensions || {};
|
| 1359 |
+
for (const key of ['width', 'height', 'depth']) {
|
| 1360 |
+
const el = document.getElementById('wiz-dim-' + key);
|
| 1361 |
+
if (el && dims[key]) el.value = dims[key];
|
| 1362 |
+
}
|
| 1363 |
+
// Sync step checks
|
| 1364 |
+
if (designState.part_name) wizMarkStep(1);
|
| 1365 |
+
if (designState.material) wizMarkStep(2);
|
| 1366 |
+
if (Object.keys(dims).length > 0) wizMarkStep(3);
|
| 1367 |
+
if ((designState.features || []).length > 0) wizMarkStep(4);
|
| 1368 |
+
if ((designState.constraints || []).length > 0) wizMarkStep(5);
|
| 1369 |
+
if (designState.axis_recommendation) wizMarkStep(6);
|
| 1370 |
+
wizUpdateReview();
|
| 1371 |
+
}
|
| 1372 |
+
```
|
| 1373 |
+
|
| 1374 |
+
- [ ] **Step 5: Test manually**
|
| 1375 |
+
|
| 1376 |
+
Run: `cd /home/daniel/NeuralCAD && python -m server.web --port 5000`
|
| 1377 |
+
Open browser at `http://localhost:5000`. Verify:
|
| 1378 |
+
- Tab switcher shows "Chat" and "Guided" tabs
|
| 1379 |
+
- Clicking "Guided" shows 7 wizard steps
|
| 1380 |
+
- Selecting chips updates designState
|
| 1381 |
+
- Dimension inputs work
|
| 1382 |
+
- Review step shows accumulated values and score
|
| 1383 |
+
- "Back to Chat" switches back
|
| 1384 |
+
- State syncs between tabs
|
| 1385 |
+
|
| 1386 |
+
- [ ] **Step 6: Commit**
|
| 1387 |
+
|
| 1388 |
+
```bash
|
| 1389 |
+
git add web/index.html
|
| 1390 |
+
git commit -m "feat: add Chat/Guided tab switcher with 7-step wizard"
|
| 1391 |
+
```
|
| 1392 |
+
|
| 1393 |
+
---
|
| 1394 |
+
|
| 1395 |
+
### Task 8: Plan card in chat mode
|
| 1396 |
+
|
| 1397 |
+
**Files:**
|
| 1398 |
+
- Modify: `web/index.html` (add plan card rendering in `sendMessage`)
|
| 1399 |
+
|
| 1400 |
+
- [ ] **Step 1: Add plan card CSS**
|
| 1401 |
+
|
| 1402 |
+
Add to the CSS section:
|
| 1403 |
+
|
| 1404 |
+
```css
|
| 1405 |
+
/* ---- PLAN CARD ---- */
|
| 1406 |
+
.plan-card {
|
| 1407 |
+
background: var(--bg-surface);
|
| 1408 |
+
border: 1px solid var(--accent);
|
| 1409 |
+
border-radius: 8px;
|
| 1410 |
+
padding: 14px;
|
| 1411 |
+
margin: 8px 0;
|
| 1412 |
+
}
|
| 1413 |
+
.plan-card-title {
|
| 1414 |
+
font-family: var(--font-mono);
|
| 1415 |
+
font-size: 11px;
|
| 1416 |
+
font-weight: 700;
|
| 1417 |
+
color: var(--accent);
|
| 1418 |
+
margin-bottom: 10px;
|
| 1419 |
+
}
|
| 1420 |
+
.plan-card .wizard-review-field { padding: 3px 0; }
|
| 1421 |
+
.plan-card-score {
|
| 1422 |
+
font-family: var(--font-mono); font-size: 11px;
|
| 1423 |
+
color: var(--text-secondary); margin-top: 8px;
|
| 1424 |
+
}
|
| 1425 |
+
.plan-card-actions { display: flex; gap: 8px; margin-top: 10px; }
|
| 1426 |
+
.plan-card-btn {
|
| 1427 |
+
flex: 1; padding: 7px; border-radius: 5px;
|
| 1428 |
+
font-family: var(--font-mono); font-size: 11px;
|
| 1429 |
+
font-weight: 600; cursor: pointer; border: 1px solid var(--border);
|
| 1430 |
+
}
|
| 1431 |
+
.plan-card-approve {
|
| 1432 |
+
background: var(--success); color: var(--bg-void); border-color: var(--success);
|
| 1433 |
+
}
|
| 1434 |
+
.plan-card-reject {
|
| 1435 |
+
background: var(--bg-surface); color: var(--text-secondary);
|
| 1436 |
+
}
|
| 1437 |
+
```
|
| 1438 |
+
|
| 1439 |
+
- [ ] **Step 2: Add plan card rendering function**
|
| 1440 |
+
|
| 1441 |
+
Add to the JavaScript section:
|
| 1442 |
+
|
| 1443 |
+
```javascript
|
| 1444 |
+
function renderPlanCard(plan) {
|
| 1445 |
+
const fields = [
|
| 1446 |
+
['Part', plan.part_name],
|
| 1447 |
+
['Material', plan.material],
|
| 1448 |
+
['Dimensions', Object.entries(plan.dimensions || {}).map(([k,v]) => k + '=' + v + 'mm').join(', ')],
|
| 1449 |
+
['Features', (plan.features || []).join(', ')],
|
| 1450 |
+
['Constraints', (plan.constraints || []).join(', ')],
|
| 1451 |
+
['Axis', plan.axis_recommendation || 'Auto'],
|
| 1452 |
+
];
|
| 1453 |
+
let html = '<div class="plan-card" id="active-plan-card">';
|
| 1454 |
+
html += '<div class="plan-card-title">\u25c6 PLAN READY FOR REVIEW</div>';
|
| 1455 |
+
for (const [label, value] of fields) {
|
| 1456 |
+
html += '<div class="wizard-review-field"><span class="wizard-review-label">' + label + '</span><span class="wizard-review-value">' + (value || '\u2014') + '</span></div>';
|
| 1457 |
+
}
|
| 1458 |
+
if (plan.machining_notes && plan.machining_notes.length) {
|
| 1459 |
+
html += '<div class="wizard-review-field"><span class="wizard-review-label">Notes</span><span class="wizard-review-value">' + plan.machining_notes.join('; ') + '</span></div>';
|
| 1460 |
+
}
|
| 1461 |
+
html += '<div class="plan-card-score">Score: ' + (plan.confidence_score || 0).toFixed(0) + '/8</div>';
|
| 1462 |
+
html += '<div class="plan-card-actions">';
|
| 1463 |
+
html += '<button class="plan-card-btn plan-card-approve" onclick="approvePlanCard()">Approve</button>';
|
| 1464 |
+
html += '<button class="plan-card-btn plan-card-reject" onclick="rejectPlanCard()">Reject</button>';
|
| 1465 |
+
html += '</div></div>';
|
| 1466 |
+
return html;
|
| 1467 |
+
}
|
| 1468 |
+
|
| 1469 |
+
async function approvePlanCard() {
|
| 1470 |
+
const plan = designState.plan;
|
| 1471 |
+
if (!plan) return;
|
| 1472 |
+
try {
|
| 1473 |
+
const resp = await fetch('/api/plan/approve', {
|
| 1474 |
+
method: 'POST',
|
| 1475 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1476 |
+
body: JSON.stringify({ plan: plan, design_state: designState }),
|
| 1477 |
+
});
|
| 1478 |
+
const data = await resp.json();
|
| 1479 |
+
designState = data.design_state;
|
| 1480 |
+
saveState();
|
| 1481 |
+
const card = document.getElementById('active-plan-card');
|
| 1482 |
+
if (card) card.remove();
|
| 1483 |
+
await sendMessage('Generate the approved design');
|
| 1484 |
+
} catch (err) {
|
| 1485 |
+
console.error('Plan approve failed:', err);
|
| 1486 |
+
}
|
| 1487 |
+
}
|
| 1488 |
+
|
| 1489 |
+
async function rejectPlanCard() {
|
| 1490 |
+
try {
|
| 1491 |
+
const resp = await fetch('/api/plan/reject', {
|
| 1492 |
+
method: 'POST',
|
| 1493 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1494 |
+
body: JSON.stringify({ design_state: designState }),
|
| 1495 |
+
});
|
| 1496 |
+
const data = await resp.json();
|
| 1497 |
+
designState = data.design_state;
|
| 1498 |
+
saveState();
|
| 1499 |
+
const card = document.getElementById('active-plan-card');
|
| 1500 |
+
if (card) card.remove();
|
| 1501 |
+
} catch (err) {
|
| 1502 |
+
console.error('Plan reject failed:', err);
|
| 1503 |
+
}
|
| 1504 |
+
}
|
| 1505 |
+
```
|
| 1506 |
+
|
| 1507 |
+
- [ ] **Step 3: Update sendMessage to render plan card**
|
| 1508 |
+
|
| 1509 |
+
In the `sendMessage` function, after `if (data.design_state) { designState = data.design_state; }`, add:
|
| 1510 |
+
|
| 1511 |
+
```javascript
|
| 1512 |
+
// If phase transitioned to planning, show plan card
|
| 1513 |
+
if (designState.phase === 'planning' && designState.plan) {
|
| 1514 |
+
const old = document.getElementById('active-plan-card');
|
| 1515 |
+
if (old) old.remove();
|
| 1516 |
+
const msgs = document.getElementById('chat-messages');
|
| 1517 |
+
const cardDiv = document.createElement('div');
|
| 1518 |
+
cardDiv.innerHTML = renderPlanCard(designState.plan);
|
| 1519 |
+
msgs.appendChild(cardDiv.firstChild);
|
| 1520 |
+
msgs.scrollTop = msgs.scrollHeight;
|
| 1521 |
+
}
|
| 1522 |
+
```
|
| 1523 |
+
|
| 1524 |
+
- [ ] **Step 4: Test manually**
|
| 1525 |
+
|
| 1526 |
+
Run the dev server and test:
|
| 1527 |
+
- Chat enough info to cross the scoring threshold (material + dimensions + axis)
|
| 1528 |
+
- Verify plan card appears inline in chat
|
| 1529 |
+
- Click "Approve" — should trigger generation
|
| 1530 |
+
- On a fresh conversation, click "Reject" — card should disappear, chat continues
|
| 1531 |
+
|
| 1532 |
+
- [ ] **Step 5: Commit**
|
| 1533 |
+
|
| 1534 |
+
```bash
|
| 1535 |
+
git add web/index.html
|
| 1536 |
+
git commit -m "feat: add inline plan card in chat with approve/reject"
|
| 1537 |
+
```
|
| 1538 |
+
|
| 1539 |
+
---
|
| 1540 |
+
|
| 1541 |
+
### Task 9: Download panel with 3MF
|
| 1542 |
+
|
| 1543 |
+
**Files:**
|
| 1544 |
+
- Modify: `web/index.html:1230-1234` (add 3MF button to download panel)
|
| 1545 |
+
- Modify: `web/index.html:2257-2274` (update `updateDownloads` function)
|
| 1546 |
+
- Modify: `web/index.html:2352-2355` (update gallery card downloads)
|
| 1547 |
+
|
| 1548 |
+
- [ ] **Step 1: Add 3MF download button**
|
| 1549 |
+
|
| 1550 |
+
In the download buttons div (line 1230-1234), add the 3MF button:
|
| 1551 |
+
|
| 1552 |
+
```html
|
| 1553 |
+
<div id="download-btns">
|
| 1554 |
+
<a class="dl-btn" id="dl-step" download>STEP</a>
|
| 1555 |
+
<a class="dl-btn" id="dl-stl" download>STL</a>
|
| 1556 |
+
<a class="dl-btn" id="dl-3mf" download>3MF</a>
|
| 1557 |
+
<a id="dl-gcode" class="dl-btn" download style="display:none"><span class="dl-icon">↧</span> G-CODE</a>
|
| 1558 |
+
<a class="dl-btn" id="dl-report" download>REPORT</a>
|
| 1559 |
+
</div>
|
| 1560 |
+
```
|
| 1561 |
+
|
| 1562 |
+
- [ ] **Step 2: Update updateDownloads function**
|
| 1563 |
+
|
| 1564 |
+
In the `updateDownloads` function, add 3MF URL:
|
| 1565 |
+
|
| 1566 |
+
```javascript
|
| 1567 |
+
function updateDownloads(partName) {
|
| 1568 |
+
const el = document.getElementById('download-btns');
|
| 1569 |
+
if (!partName) { el.classList.remove('visible'); return; }
|
| 1570 |
+
el.classList.add('visible');
|
| 1571 |
+
|
| 1572 |
+
document.getElementById('dl-step').href = '/api/models/' + partName + '.step';
|
| 1573 |
+
document.getElementById('dl-stl').href = '/api/models/' + partName + '.stl';
|
| 1574 |
+
document.getElementById('dl-3mf').href = '/api/models/' + partName + '.3mf';
|
| 1575 |
+
document.getElementById('dl-report').href = '/api/models/' + partName + '_report.json';
|
| 1576 |
+
|
| 1577 |
+
const dlGcode = document.getElementById('dl-gcode');
|
| 1578 |
+
if (dlGcode) {
|
| 1579 |
+
const gcodePath = '/api/models/' + partName + '.gcode';
|
| 1580 |
+
fetch(gcodePath, { method: 'HEAD' }).then(r => {
|
| 1581 |
+
dlGcode.style.display = r.ok ? 'inline-flex' : 'none';
|
| 1582 |
+
dlGcode.href = gcodePath;
|
| 1583 |
+
}).catch(() => { dlGcode.style.display = 'none'; });
|
| 1584 |
+
}
|
| 1585 |
+
}
|
| 1586 |
+
```
|
| 1587 |
+
|
| 1588 |
+
- [ ] **Step 3: Update gallery card downloads**
|
| 1589 |
+
|
| 1590 |
+
Find the gallery card download section (around line 2352) and add 3MF:
|
| 1591 |
+
|
| 1592 |
+
```javascript
|
| 1593 |
+
html += '<div class="gallery-card-downloads">';
|
| 1594 |
+
html += '<a class="gallery-dl" href="/api/models/' + name + '.step" download>STEP</a>';
|
| 1595 |
+
html += '<a class="gallery-dl" href="/api/models/' + name + '.stl" download>STL</a>';
|
| 1596 |
+
html += '<a class="gallery-dl" href="/api/models/' + name + '.3mf" download>3MF</a>';
|
| 1597 |
+
html += '<a class="gallery-dl" href="/api/models/' + name + '.gcode" download>GCODE</a>';
|
| 1598 |
+
```
|
| 1599 |
+
|
| 1600 |
+
- [ ] **Step 4: Test manually**
|
| 1601 |
+
|
| 1602 |
+
Run dev server, generate a model, verify:
|
| 1603 |
+
- STEP, STL, 3MF buttons all visible after generation
|
| 1604 |
+
- G-CODE button only visible when CAM has run
|
| 1605 |
+
- Gallery cards show all 4 download options
|
| 1606 |
+
- All downloads work (correct file served)
|
| 1607 |
+
|
| 1608 |
+
- [ ] **Step 5: Commit**
|
| 1609 |
+
|
| 1610 |
+
```bash
|
| 1611 |
+
git add web/index.html
|
| 1612 |
+
git commit -m "feat: add 3MF download button and update gallery cards"
|
| 1613 |
+
```
|
| 1614 |
+
|
| 1615 |
+
---
|
| 1616 |
+
|
| 1617 |
+
### Task 10: i18n entries for new UI
|
| 1618 |
+
|
| 1619 |
+
**Files:**
|
| 1620 |
+
- Modify: `web/index.html` (add translations to `I18N` object)
|
| 1621 |
+
|
| 1622 |
+
- [ ] **Step 1: Add i18n keys**
|
| 1623 |
+
|
| 1624 |
+
Add to the `I18N.en` object:
|
| 1625 |
+
|
| 1626 |
+
```javascript
|
| 1627 |
+
tabChat: 'Chat',
|
| 1628 |
+
tabGuided: 'Guided',
|
| 1629 |
+
wizPartType: '1. PART TYPE',
|
| 1630 |
+
wizMaterial: '2. MATERIAL',
|
| 1631 |
+
wizDimensions: '3. DIMENSIONS (mm)',
|
| 1632 |
+
wizFeatures: '4. FEATURES',
|
| 1633 |
+
wizConstraints: '5. CONSTRAINTS',
|
| 1634 |
+
wizMachining: '6. MACHINING',
|
| 1635 |
+
wizReview: '7. REVIEW',
|
| 1636 |
+
wizApprove: 'Approve & Generate',
|
| 1637 |
+
wizBackToChat: 'Back to Chat',
|
| 1638 |
+
planReady: 'PLAN READY FOR REVIEW',
|
| 1639 |
+
planApprove: 'Approve',
|
| 1640 |
+
planReject: 'Reject',
|
| 1641 |
+
planScoreReady: 'Ready',
|
| 1642 |
+
planScoreNeed: 'Need more info',
|
| 1643 |
+
```
|
| 1644 |
+
|
| 1645 |
+
Add corresponding entries to `I18N['zh-TW']`:
|
| 1646 |
+
|
| 1647 |
+
```javascript
|
| 1648 |
+
tabChat: '\u5c0d\u8a71',
|
| 1649 |
+
tabGuided: '\u5f15\u5c0e',
|
| 1650 |
+
wizPartType: '1. \u96f6\u4ef6\u985e\u578b',
|
| 1651 |
+
wizMaterial: '2. \u6750\u6599',
|
| 1652 |
+
wizDimensions: '3. \u5c3a\u5bf8 (mm)',
|
| 1653 |
+
wizFeatures: '4. \u7279\u5fb5',
|
| 1654 |
+
wizConstraints: '5. \u7d04\u675f',
|
| 1655 |
+
wizMachining: '6. \u52a0\u5de5',
|
| 1656 |
+
wizReview: '7. \u5be9\u67e5',
|
| 1657 |
+
wizApprove: '\u6279\u51c6\u4e26\u751f\u6210',
|
| 1658 |
+
wizBackToChat: '\u8fd4\u56de\u5c0d\u8a71',
|
| 1659 |
+
planReady: '\u8a08\u756b\u5df2\u6e96\u5099\u5be9\u67e5',
|
| 1660 |
+
planApprove: '\u6279\u51c6',
|
| 1661 |
+
planReject: '\u62d2\u7d55',
|
| 1662 |
+
planScoreReady: '\u5df2\u5c31\u7dd2',
|
| 1663 |
+
planScoreNeed: '\u9700\u8981\u66f4\u591a\u8cc7\u8a0a',
|
| 1664 |
+
```
|
| 1665 |
+
|
| 1666 |
+
Add corresponding entries to `I18N.vi`:
|
| 1667 |
+
|
| 1668 |
+
```javascript
|
| 1669 |
+
tabChat: 'Tr\u00f2 Chuy\u1ec7n',
|
| 1670 |
+
tabGuided: 'H\u01b0\u1edbng D\u1eabn',
|
| 1671 |
+
wizPartType: '1. LO\u1ea0I CHI TI\u1ebET',
|
| 1672 |
+
wizMaterial: '2. V\u1eacT LI\u1ec6U',
|
| 1673 |
+
wizDimensions: '3. K\u00cdCH TH\u01af\u1edaC (mm)',
|
| 1674 |
+
wizFeatures: '4. T\u00cdNH N\u0102NG',
|
| 1675 |
+
wizConstraints: '5. R\u00c0NG BU\u1ed8C',
|
| 1676 |
+
wizMachining: '6. GIA C\u00d4NG',
|
| 1677 |
+
wizReview: '7. XEM L\u1ea0I',
|
| 1678 |
+
wizApprove: 'Duy\u1ec7t & T\u1ea1o',
|
| 1679 |
+
wizBackToChat: 'V\u1ec1 Tr\u00f2 Chuy\u1ec7n',
|
| 1680 |
+
planReady: 'K\u1ebe HO\u1ea0CH S\u1eb4N S\u00c0NG',
|
| 1681 |
+
planApprove: 'Duy\u1ec7t',
|
| 1682 |
+
planReject: 'T\u1eeb Ch\u1ed1i',
|
| 1683 |
+
planScoreReady: 'S\u1eb5n s\u00e0ng',
|
| 1684 |
+
planScoreNeed: 'C\u1ea7n th\u00eam th\u00f4ng tin',
|
| 1685 |
+
```
|
| 1686 |
+
|
| 1687 |
+
- [ ] **Step 2: Apply `data-i18n` attributes to new HTML elements**
|
| 1688 |
+
|
| 1689 |
+
Add `data-i18n` attributes to the tab buttons and wizard step titles so `applyTranslations()` picks them up.
|
| 1690 |
+
|
| 1691 |
+
- [ ] **Step 3: Test i18n**
|
| 1692 |
+
|
| 1693 |
+
Run dev server, switch language to zh-TW and VI, verify wizard labels translate.
|
| 1694 |
+
|
| 1695 |
+
- [ ] **Step 4: Commit**
|
| 1696 |
+
|
| 1697 |
+
```bash
|
| 1698 |
+
git add web/index.html
|
| 1699 |
+
git commit -m "feat: add i18n translations for wizard and plan card UI"
|
| 1700 |
+
```
|
| 1701 |
+
|
| 1702 |
+
---
|
| 1703 |
+
|
| 1704 |
+
### Task 11: Full integration test
|
| 1705 |
+
|
| 1706 |
+
**Files:**
|
| 1707 |
+
- Run all tests and manual verification
|
| 1708 |
+
|
| 1709 |
+
- [ ] **Step 1: Run full test suite**
|
| 1710 |
+
|
| 1711 |
+
Run: `cd /home/daniel/NeuralCAD && python -m pytest tests/ -v --tb=short`
|
| 1712 |
+
Expected: All pass
|
| 1713 |
+
|
| 1714 |
+
- [ ] **Step 2: Manual end-to-end test — Chat flow**
|
| 1715 |
+
|
| 1716 |
+
Run: `cd /home/daniel/NeuralCAD && python -m server.web --port 5000`
|
| 1717 |
+
|
| 1718 |
+
1. Open `http://localhost:5000`
|
| 1719 |
+
2. Chat: "I need a servo bracket, 60mm wide, 40mm high, 20mm deep, aluminum 6061, 4x M6 holes, 3-axis"
|
| 1720 |
+
3. Verify plan card appears (score should be >= 8)
|
| 1721 |
+
4. Click "Approve" on the plan card
|
| 1722 |
+
5. Verify CAD generation triggers
|
| 1723 |
+
6. Verify 3D preview loads
|
| 1724 |
+
7. Verify downloads: STEP, STL, 3MF all work
|
| 1725 |
+
8. Verify G-CODE appears if CAM agent ran
|
| 1726 |
+
|
| 1727 |
+
- [ ] **Step 3: Manual end-to-end test — Guided flow**
|
| 1728 |
+
|
| 1729 |
+
1. Click "NEW" to reset
|
| 1730 |
+
2. Switch to "Guided" tab
|
| 1731 |
+
3. Step through: Bracket → Aluminum 6061 → 60/40/20 → M6 holes → min wall 3mm → 3-axis
|
| 1732 |
+
4. Review step shows all values, score >= 8
|
| 1733 |
+
5. Click "Approve & Generate"
|
| 1734 |
+
6. Verify switches to Chat, generation message sent
|
| 1735 |
+
7. Verify 3D preview and downloads
|
| 1736 |
+
|
| 1737 |
+
- [ ] **Step 4: Manual test — Reject flow**
|
| 1738 |
+
|
| 1739 |
+
1. In chat, accumulate enough info for plan card to appear
|
| 1740 |
+
2. Click "Reject"
|
| 1741 |
+
3. Verify card disappears, phase resets to "exploring"
|
| 1742 |
+
4. Continue chatting normally
|
| 1743 |
+
|
| 1744 |
+
- [ ] **Step 5: Manual test — Language switching**
|
| 1745 |
+
|
| 1746 |
+
1. Switch to zh-TW
|
| 1747 |
+
2. Open Guided tab — verify labels are in Chinese
|
| 1748 |
+
3. Switch to VI — verify Vietnamese translations
|
| 1749 |
+
|
| 1750 |
+
- [ ] **Step 6: Commit any fixes**
|
| 1751 |
+
|
| 1752 |
+
```bash
|
| 1753 |
+
git add -A
|
| 1754 |
+
git commit -m "fix: integration test fixes for planning review gate"
|
| 1755 |
+
```
|