feat: productionize learning engine and classroom

This commit is contained in:
inman
2026-08-16 21:44:52 +08:00
parent 2d04197f3f
commit ba35adfbfa
124 changed files with 9071 additions and 796 deletions

View File

@@ -54,6 +54,7 @@ import {
} from '@/components/ui/alert-dialog';
import { AlertTriangle } from 'lucide-react';
import { VisuallyHidden } from 'radix-ui';
import { emitMakelorePlaybackProgress } from '@/lib/makelore-runtime/progress-events';
/**
* Imperative handle exposed via `ref` so the parent (`Stage`) can tear
@@ -221,10 +222,21 @@ export const PlaybackChromeRoot = forwardRef<PlaybackChromeRootHandle, PlaybackC
// Guard to prevent double flash when manual stop triggers onDiscussionEnd
const manualStopRef = useRef(false);
const updateCurrentPlaybackActionIndex = useCallback((actionIndex: number | null) => {
const updateCurrentPlaybackActionIndex = useCallback((
actionIndex: number | null,
completed = false,
) => {
currentPlaybackActionIndexRef.current = actionIndex;
setCurrentPlaybackActionIndex(actionIndex);
}, []);
if (currentScene) {
emitMakelorePlaybackProgress({
sceneId: currentScene.id,
sceneOrder: currentScene.order,
actionIndex,
completed,
});
}
}, [currentScene]);
const persistCursorSafely = useCallback(
({ stageId, cursor }: { stageId: string; cursor: PlaybackCursor }) => {
@@ -598,8 +610,33 @@ export const PlaybackChromeRoot = forwardRef<PlaybackChromeRootHandle, PlaybackC
)
: { actionIndex: 0, position: null };
let savedResumeActionIndex = sessionResumeCursor.actionIndex;
const offlineProgress = typeof window !== 'undefined'
? window.__MAKELORE_INITIAL_PROGRESS__
: undefined;
let restoredOfflineProgress = false;
if (
currentScene
&& offlineProgress?.sceneOrder === currentScene.order
&& Number.isInteger(offlineProgress.actionIndex)
&& offlineProgress.actionIndex! >= 0
&& currentScene.actions?.[offlineProgress.actionIndex!]
&& canJumpWithinReconstructablePrefix(
currentScene.actions,
0,
offlineProgress.actionIndex!,
)
) {
savedResumeActionIndex = offlineProgress.actionIndex!;
restoredOfflineProgress = true;
delete window.__MAKELORE_INITIAL_PROGRESS__;
}
const playbackStageId = stage?.id ?? currentScene?.stageId;
if (currentScene && playbackStageId && !sessionResumeCursor.position) {
if (
currentScene
&& playbackStageId
&& !sessionResumeCursor.position
&& !restoredOfflineProgress
) {
try {
const cursor = await loadCursor(playbackStageId);
if (
@@ -766,6 +803,7 @@ export const PlaybackChromeRoot = forwardRef<PlaybackChromeRootHandle, PlaybackC
// If all actions are exhausted (discussion was the last action), mark
// playback as completed so the bubble shows reset instead of play.
if (engineRef.current?.isExhausted()) {
updateCurrentPlaybackActionIndex(currentScene.actions?.length ?? 0, true);
setPlaybackCompleted(true);
}
},
@@ -782,7 +820,7 @@ export const PlaybackChromeRoot = forwardRef<PlaybackChromeRootHandle, PlaybackC
// lectureSpeech intentionally NOT cleared — last sentence stays visible
// until scene transition (auto-play) or user restarts. Scene change
// effect handles the reset.
updateCurrentPlaybackActionIndex(currentScene.actions?.length ?? 0);
updateCurrentPlaybackActionIndex(currentScene.actions?.length ?? 0, true);
clearSceneResumePosition(currentScene.id);
setPlaybackCompleted(true);

View File

@@ -16,6 +16,7 @@ import { CHROME_DURATION_MS, CHROME_EASE, CHROME_EASE_CSS } from '@/lib/edit/tra
import type { Scene } from '@/lib/types/stage';
import { ThumbItem } from './ThumbItem';
import { InsertionZone } from './InsertionZone';
import { opsApiPath } from '@/lib/ops/api-fetch';
const RAIL_COLLAPSED_PX = 56;
const RAIL_MIN_PX = 180;
@@ -398,7 +399,7 @@ export function SlideNavRail() {
title={t('generation.backToHome')}
className="flex items-center gap-2 cursor-pointer rounded-lg px-1.5 -mx-1.5 py-1 -my-1 hover:bg-gray-100/80 dark:hover:bg-gray-800/60 active:scale-[0.97] transition-all duration-150"
>
<img src="/mailuo-logo.png" alt="麦洛学习" className="h-6" />
<img src={opsApiPath('/mailuo-logo.png')} alt="麦洛学习" className="h-6" />
</button>
)}
<div className={cn('flex items-center gap-1', collapsed && 'flex-col')}>