diff --git a/apps/web/src/app/practice/[studentId]/PracticeClient.tsx b/apps/web/src/app/practice/[studentId]/PracticeClient.tsx index 3d6cb23c..9f242517 100644 --- a/apps/web/src/app/practice/[studentId]/PracticeClient.tsx +++ b/apps/web/src/app/practice/[studentId]/PracticeClient.tsx @@ -17,6 +17,7 @@ import { useEndSessionEarly, useRecordSlotResult, } from '@/hooks/useSessionPlan' +import { useSessionBroadcast, type BroadcastPracticeState } from '@/hooks/useSessionBroadcast' import { css } from '../../../../styled-system/css' interface PracticeClientProps { @@ -116,6 +117,26 @@ export function PracticeClient({ studentId, player, initialSession }: PracticeCl router.push(`/practice/${studentId}/summary`, { scroll: false }) }, [studentId, router]) + // Build broadcast state for session observation + // This broadcasts the student's practice to teachers observing in real-time + const currentSlot = currentPart?.slots[currentPlan.currentSlotIndex] + const broadcastState: BroadcastPracticeState | null = useMemo(() => { + if (!currentSlot?.problem || !timingData) return null + return { + currentProblem: { + terms: currentSlot.problem.terms, + answer: currentSlot.problem.answer, + }, + phase: isPaused ? 'feedback' : 'problem', // Use 'feedback' when paused as a proxy + studentAnswer: null, // We don't have access to the answer in PracticeClient + isCorrect: null, + startedAt: timingData.startTime, + } + }, [currentSlot?.problem, timingData, isPaused]) + + // Broadcast session state if student is in a classroom + useSessionBroadcast(currentPlan.id, studentId, broadcastState) + // Build session HUD data for PracticeSubNav const sessionHud: SessionHudData | undefined = currentPart ? { diff --git a/apps/web/src/components/classroom/ClassroomTab.tsx b/apps/web/src/components/classroom/ClassroomTab.tsx index b8a8f50f..e640d281 100644 --- a/apps/web/src/components/classroom/ClassroomTab.tsx +++ b/apps/web/src/components/classroom/ClassroomTab.tsx @@ -4,7 +4,13 @@ import { useCallback } from 'react' import Link from 'next/link' import type { Classroom } from '@/db/schema' import { useTheme } from '@/contexts/ThemeContext' -import { useClassroomPresence, useLeaveClassroom, type PresenceStudent } from '@/hooks/useClassroom' +import { + useActiveSessionsInClassroom, + useClassroomPresence, + useLeaveClassroom, + type ActiveSessionInfo, + type PresenceStudent, +} from '@/hooks/useClassroom' import { css } from '../../../styled-system/css' import { ClassroomCodeShare } from './ClassroomCodeShare' @@ -28,6 +34,14 @@ export function ClassroomTab({ classroom }: ClassroomTabProps) { const { data: presentStudents = [], isLoading } = useClassroomPresence(classroom.id) const leaveClassroom = useLeaveClassroom() + // Fetch active sessions to show "Practicing" indicator + const { data: activeSessions = [] } = useActiveSessionsInClassroom(classroom.id) + + // Map active sessions by playerId for quick lookup + const activeSessionsByPlayer = new Map( + activeSessions.map((session) => [session.playerId, session]) + ) + const handleRemoveStudent = useCallback( (playerId: string) => { leaveClassroom.mutate({ @@ -108,6 +122,7 @@ export function ClassroomTab({ classroom }: ClassroomTabProps) { handleRemoveStudent(student.id)} isRemoving={ leaveClassroom.isPending && leaveClassroom.variables?.playerId === student.id @@ -212,18 +227,27 @@ export function ClassroomTab({ classroom }: ClassroomTabProps) { interface PresentStudentCardProps { student: PresenceStudent + activeSession?: ActiveSessionInfo onRemove: () => void isRemoving: boolean isDark: boolean } -function PresentStudentCard({ student, onRemove, isRemoving, isDark }: PresentStudentCardProps) { +function PresentStudentCard({ + student, + activeSession, + onRemove, + isRemoving, + isDark, +}: PresentStudentCardProps) { const enteredAt = new Date(student.enteredAt) const timeAgo = getTimeAgo(enteredAt) + const isPracticing = !!activeSession return (
@@ -262,7 +290,7 @@ function PresentStudentCard({ student, onRemove, isRemoving, isDark }: PresentSt > {student.emoji} - {/* Online indicator */} + {/* Online/Practicing indicator */} + {/* Keyframes for practicing pulse animation */} + {isPracticing && ( +