feat: productionize learning engine and classroom
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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')}>
|
||||
|
||||
Reference in New Issue
Block a user