import { useState, useEffect, useRef, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { Box, Button, Typography, useMediaQuery, useTheme } from '@mui/material'; import ProgressBar from '../components/ProgressBar'; import SwipeCard from '../components/SwipeCard'; export default function Trial() { const navigate = useNavigate(); const theme = useTheme(); const isDesktop = useMediaQuery(theme.breakpoints.up('md')); const [trials, setTrials] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); const [sessionId, setSessionId] = useState(''); const [imageLoaded, setImageLoaded] = useState(false); const imageShownAt = useRef(Date.now()); // Load session from localStorage useEffect(() => { const sid = localStorage.getItem('session_id'); const trialsJson = localStorage.getItem('trials'); const idx = parseInt(localStorage.getItem('current_index') || '0', 10); if (!sid || !trialsJson) { navigate('/'); return; } const parsedTrials = JSON.parse(trialsJson); setSessionId(sid); setTrials(parsedTrials); setCurrentIndex(idx); }, [navigate]); // Reset timer when trial changes useEffect(() => { imageShownAt.current = Date.now(); setImageLoaded(false); }, [currentIndex]); // Preload next 2 images useEffect(() => { if (trials.length === 0) return; for (let offset = 1; offset <= 2; offset++) { const nextIdx = currentIndex + offset; if (nextIdx < trials.length) { const img = new Image(); img.src = `/images/${trials[nextIdx].path}`; } } }, [currentIndex, trials]); const handleResponse = useCallback((response) => { if (trials.length === 0 || currentIndex >= trials.length) return; const trial = trials[currentIndex]; const responseTimeMs = Date.now() - imageShownAt.current; // Fire and forget fetch('/api/session/respond', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sessionId, trial_index: currentIndex, image_id: trial.id, label: trial.label, response, response_time_ms: responseTimeMs, }), }).catch(console.error); const nextIndex = currentIndex + 1; if (nextIndex >= trials.length) { // Complete session fetch('/api/session/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sessionId }), }).catch(console.error); localStorage.setItem('current_index', String(nextIndex)); navigate('/done'); } else { setCurrentIndex(nextIndex); localStorage.setItem('current_index', String(nextIndex)); } }, [trials, currentIndex, sessionId, navigate]); // Keyboard shortcuts useEffect(() => { const handleKey = (e) => { if (e.key === 'ArrowLeft') handleResponse('real'); else if (e.key === 'ArrowRight') handleResponse('fake'); }; window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); }, [handleResponse]); if (trials.length === 0) return null; if (currentIndex >= trials.length) return null; const currentTrial = trials[currentIndex]; return ( {/* Top bar */} Color Turing Test {currentIndex + 1} / {trials.length} {/* Main image area */} handleResponse('real')} onSwipeRight={() => handleResponse('fake')} > {/* Fixed-size container — all images render at the same visual footprint */} { setImageLoaded(true); imageShownAt.current = Date.now(); }} sx={{ width: '100%', height: '100%', objectFit: 'contain', opacity: imageLoaded ? 1 : 0, transition: 'opacity 0.15s ease-in', }} /> {/* Bottom area */} {/* Mobile: swipe hint + smaller tap buttons as fallback */} {!isDesktop && ( absent ← swipe → present )} {/* Desktop: keyboard hint */} {isDesktop && ( ← Absent      Present → )} ); }