'use client'; import { useEffect, useState } from 'react'; import { Stage } from '@/components/stage'; import { MediaStageProvider } from '@/lib/contexts/media-stage-context'; import { ThemeProvider } from '@/lib/hooks/use-theme'; import { applyClassroomStageAndScenes, type ClassroomPayload, } from '@/lib/classroom/load-classroom'; import { applyGeneratedAgentsToRegistry } from '@/lib/orchestration/registry/store'; import { useSettingsStore } from '@/lib/store/settings'; import { useStageStore } from '@/lib/store'; import { installMakeloreRuntimeFetchBridge, type MakeloreCourseRuntimeContext, } from '@/lib/makelore-runtime/browser-bridge'; import { MAKELORE_PLAYBACK_PROGRESS_EVENT, type MakelorePlaybackProgress, } from '@/lib/makelore-runtime/progress-events'; import classroomFixture from '@/fixtures/makelore/python-basics-classroom.json'; const FIXTURE = classroomFixture as unknown as ClassroomPayload; type CourseLoadMessage = { type: 'makelore:course:load'; courseId: string; /** Aggregate hash (canonical). */ courseContentHash?: string; /** Aggregate hash compatibility alias. */ contentHash?: string; moduleId?: string | null; moduleContentHash?: string; classroom: ClassroomPayload; progress?: { sceneOrder?: number; actionIndex?: number; positionMs?: number; completed?: boolean }; }; function isClassroomPayload(value: unknown): value is ClassroomPayload { if (!value || typeof value !== 'object') return false; const record = value as Record; return Boolean(record.stage) && typeof record.stage === 'object' && Array.isArray(record.scenes); } /** * Playback-only integration fixture for the Makelore desktop player. * * This route deliberately renders the production Stage instead of a reduced * mock, so slides, interactive HTML, quizzes, the whiteboard, playback * actions, and the assistant chrome are exercised together. The final desktop * bridge supplies the same payload from a verified local course package. */ export default function MakelorePlayerPage() { const [classroom, setClassroom] = useState(null); const [error, setError] = useState(null); const [initialSceneOrder, setInitialSceneOrder] = useState(null); useEffect(() => { const embedded = new URLSearchParams(window.location.search).get('embedded') === '1'; if (!embedded) { setClassroom(FIXTURE); return; } window.__MAKELORE_OFFLINE_PLAYER__ = true; const uninstallRuntimeBridge = installMakeloreRuntimeFetchBridge({ getAnchor: () => { const state = useStageStore.getState(); const scene = state.scenes.find((candidate) => candidate.id === state.currentSceneId); if (!scene) return null; return { sceneId: scene.id, sceneOrder: scene.order, sceneTitle: scene.title }; }, }); const onPlaybackProgress = (event: Event) => { const detail = (event as CustomEvent).detail; if (!detail || typeof detail.sceneId !== 'string') return; window.parent.postMessage({ type: 'makelore:progress', sceneOrder: detail.sceneOrder, actionIndex: detail.actionIndex, positionMs: null, completed: detail.completed, }, '*'); }; window.addEventListener(MAKELORE_PLAYBACK_PROGRESS_EVENT, onPlaybackProgress); let timeout = 0; const onMessage = (event: MessageEvent) => { if (event.source !== window.parent || !event.data || typeof event.data !== 'object') return; const message = event.data as Partial; const courseContentHash = typeof message.courseContentHash === 'string' ? message.courseContentHash : message.contentHash; if (message.type !== 'makelore:course:load' || typeof message.courseId !== 'string' || typeof courseContentHash !== 'string' || !isClassroomPayload(message.classroom)) return; const moduleContentHash = typeof message.moduleContentHash === 'string' ? message.moduleContentHash : courseContentHash; window.__MAKELORE_COURSE_CONTEXT__ = { courseId: message.courseId, courseContentHash, contentHash: courseContentHash, moduleId: typeof message.moduleId === 'string' ? message.moduleId : null, moduleContentHash, }; window.clearTimeout(timeout); setError(null); setInitialSceneOrder(typeof message.progress?.sceneOrder === 'number' ? message.progress.sceneOrder : null); window.__MAKELORE_INITIAL_PROGRESS__ = { sceneOrder: typeof message.progress?.sceneOrder === 'number' ? message.progress.sceneOrder : undefined, actionIndex: typeof message.progress?.actionIndex === 'number' ? message.progress.actionIndex : null, completed: message.progress?.completed === true, }; setClassroom(message.classroom); }; window.addEventListener('message', onMessage); window.parent.postMessage({ type: 'makelore:player:ready' }, '*'); timeout = window.setTimeout(() => setError('没有收到本地课件数据'), 15_000); return () => { window.clearTimeout(timeout); window.removeEventListener('message', onMessage); window.removeEventListener(MAKELORE_PLAYBACK_PROGRESS_EVENT, onPlaybackProgress); uninstallRuntimeBridge(); delete window.__MAKELORE_COURSE_CONTEXT__; delete window.__MAKELORE_INITIAL_PROGRESS__; delete window.__MAKELORE_OFFLINE_PLAYER__; }; }, []); useEffect(() => { if (!classroom) return; const { stage, scenes } = classroom; applyClassroomStageAndScenes(stage, scenes, { persist: false }); const generatedAgentIds = applyGeneratedAgentsToRegistry( stage.id, stage.generatedAgentConfigs ?? [], ); const settings = useSettingsStore.getState(); settings.setAgentMode('auto'); settings.setSelectedAgentIds(generatedAgentIds); settings.setAgentSelectionIsUserSet(false); const resumedScene = initialSceneOrder === null ? undefined : scenes.find((scene) => scene.order === initialSceneOrder); useStageStore.setState({ generationComplete: true, mode: 'playback', ...(resumedScene ? { currentSceneId: resumedScene.id } : {}), }); window.parent.postMessage({ type: 'makelore:player:loaded', stageId: stage.id }, '*'); let lastSceneId = useStageStore.getState().currentSceneId; return useStageStore.subscribe((state) => { if (!state.currentSceneId || state.currentSceneId === lastSceneId) return; lastSceneId = state.currentSceneId; const scene = state.scenes.find((candidate) => candidate.id === state.currentSceneId); if (scene) window.parent.postMessage({ type: 'makelore:progress', sceneOrder: scene.order, actionIndex: 0, positionMs: null, completed: false, }, '*'); }); }, [classroom, initialSceneOrder]); return (
{classroom ? ( ) : (
{error ?? '正在打开课件…'}
)}
); } declare global { interface Window { __MAKELORE_OFFLINE_PLAYER__?: boolean; __MAKELORE_COURSE_CONTEXT__?: MakeloreCourseRuntimeContext; __MAKELORE_INITIAL_PROGRESS__?: { sceneOrder?: number; actionIndex?: number | null; completed?: boolean; }; } }