'use client'; // Learner player entry — L3 user end. // // Resolves a published courseware from the registry, materializes (or reuses // the cached copy of) its frozen bundle, then opens the classroom in pure // playback. No course generation is ever triggered: the materialized document // carries no generation outlines, so scene/media generation stays silent. The // original classroom teacher/assistant, dynamic grading, and PBL runtime remain // available and may use their normal teaching-time model calls. import { useCallback, useEffect, useRef, useState } from 'react'; import { useParams, useRouter, useSearchParams } from 'next/navigation'; import { BookOpen, Loader2, RefreshCw } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useI18n } from '@/lib/hooks/use-i18n'; import { loadLearnerCourseware } from '@/lib/bundle/learner-load'; type LoadPhase = 'resolving' | 'downloading' | 'opening' | 'error'; export default function LearnPage() { const params = useParams(); const router = useRouter(); const searchParams = useSearchParams(); const { t } = useI18n(); const coursewareId = String(params?.coursewareId ?? ''); // Large-course mode pass-through: when the learner entered through a course // framework page, carry the course context into the classroom so it can show // a "back to course" affordance. const courseId = searchParams.get('course') ?? ''; const moduleIndex = searchParams.get('module') ?? ''; const courseVersion = searchParams.get('courseVersion') ?? ''; const versionParam = searchParams.get('version'); const expectedContentHash = searchParams.get('hash'); const [phase, setPhase] = useState('resolving'); const [error, setError] = useState(null); const startedRef = useRef(false); const start = useCallback(async () => { if (!coursewareId) return; startedRef.current = true; setPhase('resolving'); setError(null); try { const version = versionParam === null ? undefined : Number(versionParam); const result = await loadLearnerCourseware(coursewareId, { ...(version !== undefined ? { version } : {}), ...(expectedContentHash !== null ? { expectedContentHash } : {}), }); setPhase('opening'); const query = new URLSearchParams({ learner: '1', courseware: coursewareId }); if (courseId) query.set('course', courseId); if (moduleIndex) query.set('module', moduleIndex); if (courseVersion) query.set('courseVersion', courseVersion); router.replace(`/classroom/${result.stageId}?${query.toString()}`); } catch (err) { setError(err instanceof Error ? err.message : String(err)); setPhase('error'); } }, [ coursewareId, courseId, moduleIndex, courseVersion, versionParam, expectedContentHash, router, ]); useEffect(() => { if (startedRef.current || !coursewareId) return; // Entry redirect must show its own loading state before the async load // settles; the effect's synchronous call into `start` is intentional. /* eslint-disable react-hooks/set-state-in-effect */ void start(); /* eslint-enable react-hooks/set-state-in-effect */ }, [coursewareId, start]); return (
{phase === 'error' ? ( <>

{t('learner.loadFailed')}

{error}

) : ( <>
{phase === 'downloading' ? t('learner.downloading') : phase === 'opening' ? t('learner.opening') : t('learner.resolving')}

{t('learner.onceHint')}

)}
); }