139 lines
4.6 KiB
TypeScript
139 lines
4.6 KiB
TypeScript
// Portable classroom serialization for the frozen bundle.
|
|
//
|
|
// Mirrors the interactive-ZIP export flow (`lib/export/use-export-classroom.ts`
|
|
// + `classroom-zip-utils.ts`) but is pure and server-safe: the browser export
|
|
// reads bytes from IndexedDB at call time, while the packager receives bytes
|
|
// through injected resolvers. The manifest shape is identical, so the L3
|
|
// player can reuse the existing import machinery
|
|
// (`rewriteAudioRefsToIds` / manifest → document) unchanged.
|
|
|
|
import type { Action, DiscussionAction, SpeechAction } from '@/lib/types/action';
|
|
import type { GeneratedAgentConfig, Scene, Stage } from '@/lib/types/stage';
|
|
import type {
|
|
ClassroomManifest,
|
|
ManifestAction,
|
|
ManifestAgent,
|
|
ManifestScene,
|
|
ManifestStage,
|
|
MediaIndexEntry,
|
|
} from '@/lib/export/classroom-zip-types';
|
|
import {
|
|
manifestAgentFromConfig,
|
|
CLASSROOM_ZIP_FORMAT_VERSION,
|
|
} from '@/lib/export/classroom-zip-types';
|
|
|
|
export interface ManifestBuildOptions {
|
|
appVersion: string;
|
|
/** audioId → ZIP path (`audio/<id>.<ext>`); speech actions are rewritten to `audioRef`. */
|
|
audioIdToPath: Map<string, string>;
|
|
agentConfigs: GeneratedAgentConfig[];
|
|
exportedAt?: string;
|
|
}
|
|
|
|
export function serializeActions(
|
|
actions: Action[],
|
|
audioIdToPath: Map<string, string>,
|
|
agentIdToIndex: Map<string, number>,
|
|
): ManifestAction[] {
|
|
return actions.map((action) => {
|
|
if (action.type === 'speech') {
|
|
const speech = action as SpeechAction;
|
|
const { audioId, audioUrl, ...rest } = speech;
|
|
const audioRef = audioId ? audioIdToPath.get(audioId) : undefined;
|
|
return {
|
|
...rest,
|
|
...(audioRef ? { audioRef } : {}),
|
|
// A frozen bundle must play its embedded bytes. Keep a remote URL only
|
|
// for legacy URL-only speech that could not be mapped to bundle audio.
|
|
...(!audioRef && audioUrl ? { audioUrl } : {}),
|
|
} as ManifestAction;
|
|
}
|
|
if (action.type === 'discussion') {
|
|
const discussion = action as DiscussionAction;
|
|
const { agentId, ...rest } = discussion;
|
|
const agentIndex = agentId ? agentIdToIndex.get(agentId) : undefined;
|
|
return {
|
|
...rest,
|
|
...(agentIndex !== undefined ? { agentIndex } : agentId ? { agentId } : {}),
|
|
} as ManifestAction;
|
|
}
|
|
return action as ManifestAction;
|
|
});
|
|
}
|
|
|
|
export interface SerializedScene {
|
|
scene: ManifestScene;
|
|
/** MediaIndexEntry additions for this scene's audio/media, keyed by ZIP path. */
|
|
}
|
|
|
|
/** Serialize one scene into its portable manifest shape (interactive HTML assumed pre-inlined). */
|
|
export function serializeScene(
|
|
scene: Scene,
|
|
audioIdToPath: Map<string, string>,
|
|
agentIdToIndex: Map<string, number>,
|
|
): ManifestScene {
|
|
return {
|
|
type: scene.type,
|
|
title: scene.title,
|
|
order: scene.order,
|
|
content: scene.content,
|
|
actions: scene.actions
|
|
? serializeActions(scene.actions, audioIdToPath, agentIdToIndex)
|
|
: undefined,
|
|
whiteboards: scene.whiteboards,
|
|
...(scene.multiAgent?.enabled
|
|
? {
|
|
multiAgent: {
|
|
enabled: true,
|
|
agentIndices: (scene.multiAgent.agentIds ?? [])
|
|
.map((id) => agentIdToIndex.get(id))
|
|
.filter((index): index is number => index !== undefined),
|
|
directorPrompt: scene.multiAgent.directorPrompt,
|
|
},
|
|
}
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
export function serializeStage(stage: Stage): ManifestStage {
|
|
return {
|
|
name: stage.name,
|
|
description: stage.description,
|
|
language: stage.languageDirective,
|
|
style: stage.style,
|
|
whiteboard: stage.whiteboard,
|
|
videoManifest: stage.videoManifest,
|
|
interactiveMode: stage.interactiveMode,
|
|
taskEngineMode: stage.taskEngineMode,
|
|
createdAt: stage.createdAt,
|
|
updatedAt: stage.updatedAt,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Build the portable classroom manifest.
|
|
*
|
|
* `scenes` must already carry inlined interactive HTML and prepared PBL
|
|
* scenes (the packager's responsibility); `mediaIndex` is supplied by the
|
|
* caller, which owns byte collection.
|
|
*/
|
|
export function buildManifest(
|
|
stage: Stage,
|
|
scenes: Scene[],
|
|
options: ManifestBuildOptions,
|
|
mediaIndex: Record<string, MediaIndexEntry>,
|
|
): ClassroomManifest {
|
|
const agentIdToIndex = new Map<string, number>();
|
|
options.agentConfigs.forEach((agent, index) => agentIdToIndex.set(agent.id, index));
|
|
|
|
return {
|
|
formatVersion: CLASSROOM_ZIP_FORMAT_VERSION,
|
|
exportedAt: options.exportedAt ?? new Date().toISOString(),
|
|
appVersion: options.appVersion,
|
|
stage: serializeStage(stage),
|
|
agents: options.agentConfigs.map(manifestAgentFromConfig) satisfies ManifestAgent[],
|
|
scenes: scenes.map((scene) => serializeScene(scene, options.audioIdToPath, agentIdToIndex)),
|
|
mediaIndex,
|
|
};
|
|
}
|