import { useState, useEffect, useCallback, useRef } from 'react' import { api } from './api' import styles from './Platform.module.css' const HEARTBEAT_MS = 25000 // §15 — "browser heartbeat every 20-30 seconds" const POLL_MS = 5000 const HELP_CATEGORIES = [ { value: 'lab_problem', label: 'Lab problem' }, { value: 'dont_understand', label: "Don't understand the task" }, { value: 'validation_failing', label: 'Validation failing' }, { value: 'other', label: 'Other' }, ] // Phase 5A §22 — the live classroom experience. No live classroom -> a quiet "nothing live // right now" state. Live -> title, current activity (guidance only — a link into the student's // own Lab, never an auto-redirect), announcements, Need Help, and the student's own Lab status // (reusing the exact same /api/environment/* the My Learning tab uses — no second Lab-status // system). Heartbeats fire only while this tab is mounted and a classroom is actually joined. export default function ClassroomTab() { const [live, setLive] = useState(null) const [detail, setDetail] = useState(null) const [envStatus, setEnvStatus] = useState(null) const [joined, setJoined] = useState(false) const [helpCategory, setHelpCategory] = useState('lab_problem') const [helpMessage, setHelpMessage] = useState('') const [helpSent, setHelpSent] = useState(false) const [error, setError] = useState(null) const heartbeatRef = useRef(null) const pollRef = useRef(null) const loadLive = useCallback(() => { api.liveClassroom().then((r) => setLive(r.classroom)).catch((e) => setError(e.message)) }, []) useEffect(() => { loadLive() }, [loadLive]) const loadDetail = useCallback((id) => { api.classroom(id).then(setDetail).catch((e) => setError(e.message)) api.environmentStatus().then(setEnvStatus).catch(() => {}) }, []) useEffect(() => { if (!live) return loadDetail(live.classroom_session_id) pollRef.current = setInterval(() => loadDetail(live.classroom_session_id), POLL_MS) return () => clearInterval(pollRef.current) }, [live, loadDetail]) useEffect(() => { if (!joined || !live) return heartbeatRef.current = setInterval(() => api.classroomHeartbeat(live.classroom_session_id).catch(() => {}), HEARTBEAT_MS) api.classroomHeartbeat(live.classroom_session_id).catch(() => {}) // one immediately on join, not just after the first interval return () => clearInterval(heartbeatRef.current) }, [joined, live]) const join = async () => { try { await api.joinClassroom(live.classroom_session_id); setJoined(true) } catch (e) { setError(e.message) } } const sendHelp = async (e) => { e.preventDefault() try { await api.requestHelp(live.classroom_session_id, helpCategory, helpMessage) setHelpSent(true); setHelpMessage('') } catch (e) { setError(e.message) } } const launchLab = async () => { try { setEnvStatus(await api.launchEnvironment()) } catch (e) { setError(e.message) } } if (!live) { return (

Classroom

No live class right now. Check back at your scheduled class time.

) } return (

LIVE NOW

{live.title}

{live.description &&

{live.description}

} {live.meeting_url && (

Join Video Class

)} {!joined ? ( ) : (

You're in this classroom.

)}
{joined && detail && ( <> {detail.classroom.current_scenario_id && (

Current class activity

{detail.classroom.current_scenario_id}

Open it from the My Learning tab when you're ready — nothing here jumps you there automatically.

)}

Your Lab

{envStatus?.status === 'running' ?

Open Lab

: }

Announcements

{!detail.announcements.length ?

None yet.

: ( )}

Need help?

{helpSent ?

Help requested — your instructor has been notified.

: (
setHelpMessage(e.target.value)} />
)}
)} {error &&
{error}
}
) }