'use client' import { useState, useEffect, useCallback, useRef } from 'react' import { LessonSession, LessonPhase, TimerState, PhaseDurations } from '@/lib/companion/types' import { PHASE_ORDER, PHASE_DISPLAY_NAMES, PHASE_COLORS, DEFAULT_PHASE_DURATIONS, SYSTEM_TEMPLATES, getTimerColorStatus, STORAGE_KEYS, } from '@/lib/companion/constants' interface UseLessonSessionOptions { onPhaseComplete?: (phaseIndex: number) => void onLessonComplete?: (session: LessonSession) => void onOvertimeStart?: () => void } interface UseLessonSessionReturn { session: LessonSession | null timerState: TimerState | null startLesson: (data: { classId: string className?: string subject: string topic?: string templateId?: string }) => void endLesson: () => void clearSession: () => void pauseLesson: () => void resumeLesson: () => void extendTime: (minutes: number) => void skipPhase: () => void saveReflection: (rating: number, notes: string, nextSteps: string) => void addHomework: (title: string, dueDate: string) => void removeHomework: (id: string) => void isRunning: boolean isPaused: boolean } function createInitialPhases(durations: PhaseDurations): LessonPhase[] { return PHASE_ORDER.map((phaseId) => ({ phase: phaseId, duration: durations[phaseId], status: 'planned', actualTime: 0, })) } function generateSessionId(): string { return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` } export function useLessonSession( options: UseLessonSessionOptions = {} ): UseLessonSessionReturn { const { onPhaseComplete, onLessonComplete, onOvertimeStart } = options const [session, setSession] = useState(null) const [timerState, setTimerState] = useState(null) const timerRef = useRef(null) const lastTickRef = useRef(Date.now()) const hasTriggeredOvertimeRef = useRef(false) // Calculate timer state from session const calculateTimerState = useCallback((sess: LessonSession): TimerState | null => { if (!sess || sess.status === 'completed') return null const currentPhase = sess.phases[sess.currentPhaseIndex] if (!currentPhase) return null const phaseDurationSeconds = currentPhase.duration * 60 const elapsedInPhase = currentPhase.actualTime const remainingSeconds = phaseDurationSeconds - elapsedInPhase const progress = Math.min(elapsedInPhase / phaseDurationSeconds, 1) const isOvertime = remainingSeconds < 0 return { isRunning: sess.status === 'in_progress' && !sess.isPaused, isPaused: sess.isPaused, elapsedSeconds: elapsedInPhase, remainingSeconds: Math.max(remainingSeconds, -999), totalSeconds: phaseDurationSeconds, progress, colorStatus: getTimerColorStatus(remainingSeconds, isOvertime), currentPhase, } }, []) // Timer tick function const tick = useCallback(() => { if (!session || session.isPaused || session.status !== 'in_progress') return const now = Date.now() const delta = Math.floor((now - lastTickRef.current) / 1000) if (delta <= 0) return lastTickRef.current = now setSession((prev) => { if (!prev) return null const updatedPhases = [...prev.phases] const currentPhase = updatedPhases[prev.currentPhaseIndex] if (!currentPhase) return prev currentPhase.actualTime += delta // Check for overtime const phaseDurationSeconds = currentPhase.duration * 60 if ( currentPhase.actualTime > phaseDurationSeconds && !hasTriggeredOvertimeRef.current ) { hasTriggeredOvertimeRef.current = true onOvertimeStart?.() } // Update total elapsed time const totalElapsed = prev.elapsedTime + delta return { ...prev, phases: updatedPhases, elapsedTime: totalElapsed, overtimeMinutes: Math.max( 0, Math.floor((currentPhase.actualTime - phaseDurationSeconds) / 60) ), } }) }, [session, onOvertimeStart]) // Start/stop timer based on session state useEffect(() => { if (session?.status === 'in_progress' && !session.isPaused) { lastTickRef.current = Date.now() timerRef.current = setInterval(tick, 100) } else { if (timerRef.current) { clearInterval(timerRef.current) timerRef.current = null } } return () => { if (timerRef.current) { clearInterval(timerRef.current) } } }, [session?.status, session?.isPaused, tick]) // Update timer state when session changes useEffect(() => { if (session) { setTimerState(calculateTimerState(session)) } else { setTimerState(null) } }, [session, calculateTimerState]) // Persist session to localStorage useEffect(() => { if (session) { localStorage.setItem(STORAGE_KEYS.CURRENT_SESSION, JSON.stringify(session)) } }, [session]) // Restore session from localStorage on mount useEffect(() => { const stored = localStorage.getItem(STORAGE_KEYS.CURRENT_SESSION) if (stored) { try { const parsed = JSON.parse(stored) as LessonSession const sessionTime = new Date(parsed.startTime).getTime() const isRecent = Date.now() - sessionTime < 24 * 60 * 60 * 1000 if (parsed.status !== 'completed' && isRecent) { setSession({ ...parsed, isPaused: true }) } } catch { // Invalid stored session, ignore } } }, []) const startLesson = useCallback( (data: { classId: string className?: string subject: string topic?: string templateId?: string }) => { let durations = DEFAULT_PHASE_DURATIONS if (data.templateId) { const template = SYSTEM_TEMPLATES.find((t) => t.templateId === data.templateId) if (template) { durations = template.durations as PhaseDurations } } const phases = createInitialPhases(durations) phases[0].status = 'active' phases[0].startedAt = new Date().toISOString() const newSession: LessonSession = { sessionId: generateSessionId(), classId: data.classId, className: data.className || data.classId, subject: data.subject, topic: data.topic, startTime: new Date().toISOString(), phases, totalPlannedDuration: Object.values(durations).reduce((a, b) => a + b, 0), currentPhaseIndex: 0, elapsedTime: 0, isPaused: false, pauseDuration: 0, overtimeMinutes: 0, status: 'in_progress', homeworkList: [], materials: [], } hasTriggeredOvertimeRef.current = false setSession(newSession) }, [] ) const endLesson = useCallback(() => { if (!session) return const completedSession: LessonSession = { ...session, status: 'completed', endTime: new Date().toISOString(), phases: session.phases.map((p, i) => ({ ...p, status: i <= session.currentPhaseIndex ? 'completed' : 'skipped', completedAt: i <= session.currentPhaseIndex ? new Date().toISOString() : undefined, })), } setSession(completedSession) onLessonComplete?.(completedSession) }, [session, onLessonComplete]) const clearSession = useCallback(() => { setSession(null) localStorage.removeItem(STORAGE_KEYS.CURRENT_SESSION) }, []) const pauseLesson = useCallback(() => { if (!session || session.isPaused) return setSession((prev) => prev ? { ...prev, isPaused: true, pausedAt: new Date().toISOString(), status: 'paused', } : null ) }, [session]) const resumeLesson = useCallback(() => { if (!session || !session.isPaused) return const pausedAt = session.pausedAt ? new Date(session.pausedAt).getTime() : Date.now() const pauseDelta = Math.floor((Date.now() - pausedAt) / 1000) setSession((prev) => prev ? { ...prev, isPaused: false, pausedAt: undefined, pauseDuration: prev.pauseDuration + pauseDelta, status: 'in_progress', } : null ) lastTickRef.current = Date.now() }, [session]) const extendTime = useCallback( (minutes: number) => { if (!session) return setSession((prev) => { if (!prev) return null const updatedPhases = [...prev.phases] const currentPhase = updatedPhases[prev.currentPhaseIndex] if (!currentPhase) return prev currentPhase.duration += minutes if (hasTriggeredOvertimeRef.current) { const phaseDurationSeconds = currentPhase.duration * 60 if (currentPhase.actualTime < phaseDurationSeconds) { hasTriggeredOvertimeRef.current = false } } return { ...prev, phases: updatedPhases, totalPlannedDuration: prev.totalPlannedDuration + minutes, } }) }, [session] ) const skipPhase = useCallback(() => { if (!session) return const nextPhaseIndex = session.currentPhaseIndex + 1 if (nextPhaseIndex >= session.phases.length) { endLesson() return } setSession((prev) => { if (!prev) return null const updatedPhases = [...prev.phases] updatedPhases[prev.currentPhaseIndex] = { ...updatedPhases[prev.currentPhaseIndex], status: 'completed', completedAt: new Date().toISOString(), } updatedPhases[nextPhaseIndex] = { ...updatedPhases[nextPhaseIndex], status: 'active', startedAt: new Date().toISOString(), } return { ...prev, phases: updatedPhases, currentPhaseIndex: nextPhaseIndex, overtimeMinutes: 0, } }) hasTriggeredOvertimeRef.current = false onPhaseComplete?.(session.currentPhaseIndex) }, [session, endLesson, onPhaseComplete]) const saveReflection = useCallback( (rating: number, notes: string, nextSteps: string) => { if (!session) return setSession((prev) => prev ? { ...prev, reflection: { rating, notes, nextSteps, savedAt: new Date().toISOString(), }, } : null ) }, [session] ) const addHomework = useCallback( (title: string, dueDate: string) => { if (!session) return const newHomework = { id: `hw-${Date.now()}`, title, dueDate, completed: false, } setSession((prev) => prev ? { ...prev, homeworkList: [...prev.homeworkList, newHomework], } : null ) }, [session] ) const removeHomework = useCallback( (id: string) => { if (!session) return setSession((prev) => prev ? { ...prev, homeworkList: prev.homeworkList.filter((hw) => hw.id !== id), } : null ) }, [session] ) return { session, timerState, startLesson, endLesson, clearSession, pauseLesson, resumeLesson, extendTime, skipPhase, saveReflection, addHomework, removeHomework, isRunning: session?.status === 'in_progress' && !session?.isPaused, isPaused: session?.isPaused ?? false, } }