import { useState, useEffect, useCallback, useRef } from 'react'; import './App.css'; import CreaseCanvas from './components/CreaseCanvas'; import RewardPanel from './components/RewardPanel'; import StepFeed from './components/StepFeed'; import InfoBadges from './components/InfoBadges'; import TargetSelector from './components/TargetSelector'; import PlayerControls from './components/PlayerControls'; import Fold3DCanvas from './components/Fold3DCanvas'; const API_BASE = ''; // Read ?ep= from URL — set when navigating from training grid const _urlParams = new URLSearchParams(window.location.search); const REPLAY_EP_ID = _urlParams.get('ep') || null; function App() { const [targets, setTargets] = useState({}); const [selectedTarget, setSelectedTarget] = useState('half_fold'); const [episode, setEpisode] = useState(null); const [currentStep, setCurrentStep] = useState(0); const [playing, setPlaying] = useState(false); const [apiStatus, setApiStatus] = useState('connecting'); const [episodeLoading, setEpisodeLoading] = useState(false); const intervalRef = useRef(null); const isReplayMode = REPLAY_EP_ID !== null; const fetchTargets = useCallback(async () => { try { const res = await fetch(`${API_BASE}/targets`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setTargets(data); setApiStatus('ok'); } catch { setApiStatus('err'); } }, []); const fetchDemoEpisode = useCallback(async (targetName) => { setEpisodeLoading(true); setPlaying(false); setCurrentStep(0); try { const res = await fetch(`${API_BASE}/episode/demo?target=${targetName}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setEpisode(data); setApiStatus('ok'); } catch { setEpisode(null); setApiStatus('err'); } finally { setEpisodeLoading(false); } }, []); const fetchReplayEpisode = useCallback(async (epId) => { setEpisodeLoading(true); setPlaying(false); setCurrentStep(0); try { const res = await fetch(`${API_BASE}/episode/replay/${epId}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setEpisode(data); setApiStatus('ok'); } catch { setEpisode(null); setApiStatus('err'); } finally { setEpisodeLoading(false); } }, []); useEffect(() => { fetchTargets(); }, [fetchTargets]); useEffect(() => { if (isReplayMode) { fetchReplayEpisode(REPLAY_EP_ID); } else { fetchDemoEpisode(selectedTarget); } }, [isReplayMode, selectedTarget, fetchDemoEpisode, fetchReplayEpisode]); const totalSteps = episode ? episode.steps.length : 0; // currentStep is 1-indexed for display (0 = "empty paper before any folds") // steps array is 0-indexed: steps[0] = result of fold 1 const activeStepData = episode && currentStep > 0 ? episode.steps[currentStep - 1] : null; useEffect(() => { if (playing) { intervalRef.current = setInterval(() => { setCurrentStep(prev => { if (prev >= totalSteps) { setPlaying(false); return prev; } return prev + 1; }); }, 1500); } return () => clearInterval(intervalRef.current); }, [playing, totalSteps]); const handlePlay = () => { if (currentStep >= totalSteps) setCurrentStep(0); setPlaying(true); }; const handlePause = () => setPlaying(false); const handleNext = () => { setPlaying(false); setCurrentStep(prev => Math.min(prev + 1, totalSteps)); }; const handlePrev = () => { setPlaying(false); setCurrentStep(prev => Math.max(prev - 1, 0)); }; const handleReset = () => { setPlaying(false); setCurrentStep(0); }; const targetDef = targets[selectedTarget] || null; return (
OPTIGAMI RL
{isReplayMode ? ( <> REPLAY — {REPLAY_EP_ID} ) : ( setSelectedTarget(name)} /> )}
{apiStatus === 'ok' ? 'API OK' : apiStatus === 'err' ? 'API ERR' : 'CONNECTING'}
TASK — {targetDef ? targetDef.name.replace(/_/g, ' ').toUpperCase() : '—'}
{currentStep === 0 ? 'INITIAL STATE' : `STEP ${currentStep} / ${totalSteps}`}
3D FOLD PREVIEW
FOLD SEQUENCE
{episodeLoading ? (
FETCHING EPISODE...
) : ( )}
METRICS
EPISODE INFO
); } export default App;