Spaces:
Running
Running
Commit ·
9221fb1
1
Parent(s): c416092
feat(frontend): replay mode, camera angle fix, endpoint alignment
Browse files- App.js: add REPLAY_EP_ID from ?ep= URL param, fetchReplayEpisode(),
replay-mode header with REPLAY badge and ← GRID button
- App.css: .replay-badge and .back-to-grid-btn styles
- Fold3DCanvas: fix camera pitch/yaw (0.62/-0.52) so flat paper
renders as square not diamond; render from server paper_state FOLD data
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- src/App.css +24 -0
- src/App.js +45 -8
- src/components/Fold3DCanvas.js +2 -2
src/App.css
CHANGED
|
@@ -67,6 +67,30 @@
|
|
| 67 |
margin-left: auto;
|
| 68 |
}
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
.api-status {
|
| 71 |
font-size: 11px;
|
| 72 |
font-family: var(--font-display);
|
|
|
|
| 67 |
margin-left: auto;
|
| 68 |
}
|
| 69 |
|
| 70 |
+
.replay-badge {
|
| 71 |
+
font-size: 10px;
|
| 72 |
+
font-family: var(--font-display);
|
| 73 |
+
letter-spacing: 0.1em;
|
| 74 |
+
color: #38bdf8;
|
| 75 |
+
background: rgba(56, 189, 248, 0.1);
|
| 76 |
+
border: 1px solid rgba(56, 189, 248, 0.3);
|
| 77 |
+
padding: 3px 8px;
|
| 78 |
+
border-radius: 3px;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.back-to-grid-btn {
|
| 82 |
+
font-size: 10px;
|
| 83 |
+
font-family: var(--font-display);
|
| 84 |
+
letter-spacing: 0.08em;
|
| 85 |
+
color: #64748b;
|
| 86 |
+
background: transparent;
|
| 87 |
+
border: 1px solid #1e2a3a;
|
| 88 |
+
padding: 3px 10px;
|
| 89 |
+
border-radius: 3px;
|
| 90 |
+
cursor: pointer;
|
| 91 |
+
}
|
| 92 |
+
.back-to-grid-btn:hover { color: #e2e8f0; border-color: #64748b; }
|
| 93 |
+
|
| 94 |
.api-status {
|
| 95 |
font-size: 11px;
|
| 96 |
font-family: var(--font-display);
|
src/App.js
CHANGED
|
@@ -10,16 +10,22 @@ import Fold3DCanvas from './components/Fold3DCanvas';
|
|
| 10 |
|
| 11 |
const API_BASE = '';
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
function App() {
|
| 14 |
const [targets, setTargets] = useState({});
|
| 15 |
const [selectedTarget, setSelectedTarget] = useState('half_fold');
|
| 16 |
const [episode, setEpisode] = useState(null);
|
| 17 |
const [currentStep, setCurrentStep] = useState(0);
|
| 18 |
const [playing, setPlaying] = useState(false);
|
| 19 |
-
const [apiStatus, setApiStatus] = useState('connecting');
|
| 20 |
const [episodeLoading, setEpisodeLoading] = useState(false);
|
| 21 |
const intervalRef = useRef(null);
|
| 22 |
|
|
|
|
|
|
|
| 23 |
const fetchTargets = useCallback(async () => {
|
| 24 |
try {
|
| 25 |
const res = await fetch(`${API_BASE}/targets`);
|
|
@@ -50,13 +56,35 @@ function App() {
|
|
| 50 |
}
|
| 51 |
}, []);
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
useEffect(() => {
|
| 54 |
fetchTargets();
|
| 55 |
}, [fetchTargets]);
|
| 56 |
|
| 57 |
useEffect(() => {
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
const totalSteps = episode ? episode.steps.length : 0;
|
| 62 |
|
|
@@ -106,11 +134,20 @@ function App() {
|
|
| 106 |
OPTI<span className="title-accent">GAMI</span> RL
|
| 107 |
</span>
|
| 108 |
<div className="header-sep" />
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
<div className="header-sep" />
|
| 115 |
<PlayerControls
|
| 116 |
playing={playing}
|
|
|
|
| 10 |
|
| 11 |
const API_BASE = '';
|
| 12 |
|
| 13 |
+
// Read ?ep=<episode_id> from URL — set when navigating from training grid
|
| 14 |
+
const _urlParams = new URLSearchParams(window.location.search);
|
| 15 |
+
const REPLAY_EP_ID = _urlParams.get('ep') || null;
|
| 16 |
+
|
| 17 |
function App() {
|
| 18 |
const [targets, setTargets] = useState({});
|
| 19 |
const [selectedTarget, setSelectedTarget] = useState('half_fold');
|
| 20 |
const [episode, setEpisode] = useState(null);
|
| 21 |
const [currentStep, setCurrentStep] = useState(0);
|
| 22 |
const [playing, setPlaying] = useState(false);
|
| 23 |
+
const [apiStatus, setApiStatus] = useState('connecting');
|
| 24 |
const [episodeLoading, setEpisodeLoading] = useState(false);
|
| 25 |
const intervalRef = useRef(null);
|
| 26 |
|
| 27 |
+
const isReplayMode = REPLAY_EP_ID !== null;
|
| 28 |
+
|
| 29 |
const fetchTargets = useCallback(async () => {
|
| 30 |
try {
|
| 31 |
const res = await fetch(`${API_BASE}/targets`);
|
|
|
|
| 56 |
}
|
| 57 |
}, []);
|
| 58 |
|
| 59 |
+
const fetchReplayEpisode = useCallback(async (epId) => {
|
| 60 |
+
setEpisodeLoading(true);
|
| 61 |
+
setPlaying(false);
|
| 62 |
+
setCurrentStep(0);
|
| 63 |
+
try {
|
| 64 |
+
const res = await fetch(`${API_BASE}/episode/replay/${epId}`);
|
| 65 |
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
| 66 |
+
const data = await res.json();
|
| 67 |
+
setEpisode(data);
|
| 68 |
+
setApiStatus('ok');
|
| 69 |
+
} catch {
|
| 70 |
+
setEpisode(null);
|
| 71 |
+
setApiStatus('err');
|
| 72 |
+
} finally {
|
| 73 |
+
setEpisodeLoading(false);
|
| 74 |
+
}
|
| 75 |
+
}, []);
|
| 76 |
+
|
| 77 |
useEffect(() => {
|
| 78 |
fetchTargets();
|
| 79 |
}, [fetchTargets]);
|
| 80 |
|
| 81 |
useEffect(() => {
|
| 82 |
+
if (isReplayMode) {
|
| 83 |
+
fetchReplayEpisode(REPLAY_EP_ID);
|
| 84 |
+
} else {
|
| 85 |
+
fetchDemoEpisode(selectedTarget);
|
| 86 |
+
}
|
| 87 |
+
}, [isReplayMode, selectedTarget, fetchDemoEpisode, fetchReplayEpisode]);
|
| 88 |
|
| 89 |
const totalSteps = episode ? episode.steps.length : 0;
|
| 90 |
|
|
|
|
| 134 |
OPTI<span className="title-accent">GAMI</span> RL
|
| 135 |
</span>
|
| 136 |
<div className="header-sep" />
|
| 137 |
+
{isReplayMode ? (
|
| 138 |
+
<>
|
| 139 |
+
<span className="replay-badge">REPLAY — {REPLAY_EP_ID}</span>
|
| 140 |
+
<button className="back-to-grid-btn" onClick={() => window.history.back()}>
|
| 141 |
+
← GRID
|
| 142 |
+
</button>
|
| 143 |
+
</>
|
| 144 |
+
) : (
|
| 145 |
+
<TargetSelector
|
| 146 |
+
targets={targets}
|
| 147 |
+
selected={selectedTarget}
|
| 148 |
+
onChange={name => setSelectedTarget(name)}
|
| 149 |
+
/>
|
| 150 |
+
)}
|
| 151 |
<div className="header-sep" />
|
| 152 |
<PlayerControls
|
| 153 |
playing={playing}
|
src/components/Fold3DCanvas.js
CHANGED
|
@@ -54,8 +54,8 @@ function projectVertex(vertex, dim) {
|
|
| 54 |
let y = vertex[1] - 0.5;
|
| 55 |
let z = vertex[2] || 0;
|
| 56 |
|
| 57 |
-
const pitch =
|
| 58 |
-
const yaw = -0.
|
| 59 |
|
| 60 |
const cp = Math.cos(pitch);
|
| 61 |
const sp = Math.sin(pitch);
|
|
|
|
| 54 |
let y = vertex[1] - 0.5;
|
| 55 |
let z = vertex[2] || 0;
|
| 56 |
|
| 57 |
+
const pitch = 0.62;
|
| 58 |
+
const yaw = -0.52;
|
| 59 |
|
| 60 |
const cp = Math.cos(pitch);
|
| 61 |
const sp = Math.sin(pitch);
|