ianalin123 Claude Sonnet 4.6 commited on
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>

Files changed (3) hide show
  1. src/App.css +24 -0
  2. src/App.js +45 -8
  3. 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'); // 'connecting' | 'ok' | 'err'
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
- fetchDemoEpisode(selectedTarget);
59
- }, [selectedTarget, fetchDemoEpisode]);
 
 
 
 
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
- <TargetSelector
110
- targets={targets}
111
- selected={selectedTarget}
112
- onChange={name => setSelectedTarget(name)}
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 = 1.04;
58
- const yaw = -0.78;
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);