@pie-players/pie-section-player 0.3.67 → 0.3.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -0
- package/dist/chunks/module-a516757f.js +388 -394
- package/dist/chunks/nl-NL-9312c316.js +503 -0
- package/dist/chunks/nl-NL-ef6dd604.js +503 -0
- package/dist/chunks/pie-api-client-22de9d9c.js +161 -0
- package/dist/chunks/rolldown-runtime-1287ba31.js +20 -0
- package/dist/chunks/sre-0162a7a9.js +3431 -3428
- package/dist/components/shared/section-player-host-runtime.d.ts +11 -7
- package/dist/components/shared/section-player-shell-layout.svelte.d.ts +55 -0
- package/dist/components/shared/section-player-view-state.d.ts +35 -0
- package/dist/components/shared/use-interface-i18n.svelte.d.ts +20 -0
- package/dist/controllers/SectionController.d.ts +174 -0
- package/dist/controllers/types.d.ts +128 -2
- package/dist/pie-section-player.js +19858 -15589
- package/package.json +10 -10
- package/dist/components/shared/section-player-props.d.ts +0 -1
- package/dist/components/shared/tool-surface-host.d.ts +0 -28
|
@@ -75,11 +75,15 @@ export declare function resolveSectionPlayerRuntimeState(args: RuntimeInputs): {
|
|
|
75
75
|
};
|
|
76
76
|
};
|
|
77
77
|
/**
|
|
78
|
-
* Fold a layout element's `nds-icons`
|
|
79
|
-
* `runtime` config handed to the kernel.
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* the
|
|
78
|
+
* Fold a layout element's presentation convenience attributes — `nds-icons` and
|
|
79
|
+
* `locale` — into the `runtime` config handed to the kernel.
|
|
80
|
+
*
|
|
81
|
+
* Only injects an attribute that is explicitly set, so an unset one never
|
|
82
|
+
* clobbers a value the host put on `runtime`; when both are present, `runtime`
|
|
83
|
+
* wins (spread last). Returns the input unchanged when neither is set, which
|
|
84
|
+
* keeps `kernelRuntime` referentially stable for a host that supplies neither.
|
|
84
85
|
*/
|
|
85
|
-
export declare function
|
|
86
|
+
export declare function mergeLayoutAttrsIntoRuntime(runtime: RuntimeConfig | null, attrs: {
|
|
87
|
+
ndsIcons?: boolean;
|
|
88
|
+
locale?: string;
|
|
89
|
+
}): RuntimeConfig | null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The layout plumbing every section-player custom-element shell needs.
|
|
3
|
+
*
|
|
4
|
+
* The three shells — vertical, tabbed, split-pane — differ in how they arrange
|
|
5
|
+
* panes and in nothing else at this level: each clamps the same host-configurable
|
|
6
|
+
* pixel values, finds its own host element through the same root-node walk, and
|
|
7
|
+
* watches the same narrow-layout media query. That was copied three times, so a
|
|
8
|
+
* changed clamp bound reached whichever shells someone remembered.
|
|
9
|
+
*
|
|
10
|
+
* What cannot be shared, and is not this module's failure: the `props` map inside
|
|
11
|
+
* `<svelte:options customElement={{…}}>`. The Svelte compiler rejects anything but
|
|
12
|
+
* a statically analyzable object literal there — `svelte_options_invalid_customelement_props`
|
|
13
|
+
* — so that block stays duplicated per shell by construction. The command
|
|
14
|
+
* forwarders are the same story: the compiler exposes a component's
|
|
15
|
+
* `export function` declarations as custom-element methods, so they must be
|
|
16
|
+
* declared in each `.svelte` file, though their bodies are one line each.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DEFAULT_NARROW_BREAKPOINT_PX = 1100;
|
|
19
|
+
/**
|
|
20
|
+
* A host-supplied pixel value, clamped into range. `undefined` for anything the
|
|
21
|
+
* host left unset or wrote as a non-number, so a caller can fall back rather than
|
|
22
|
+
* render at zero.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveConfiguredPx(value: unknown, min: number, max: number): number | undefined;
|
|
25
|
+
/** The narrow-layout breakpoint, clamped, defaulting when the host set nothing. */
|
|
26
|
+
export declare function clampNarrowBreakpoint(value: unknown): number;
|
|
27
|
+
/**
|
|
28
|
+
* The two content-width caps, clamped.
|
|
29
|
+
*
|
|
30
|
+
* `withPassage` is floored at `noPassage`: a passage adds a column, so the
|
|
31
|
+
* two-column cap narrower than the one-column cap would shrink the content on
|
|
32
|
+
* the wider layout.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveContentMaxWidths(contentMaxWidthNoPassage: unknown, contentMaxWidthWithPassage: unknown): {
|
|
35
|
+
noPassagePx: number | undefined;
|
|
36
|
+
withPassagePx: number | undefined;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The custom element hosting this shell, reached from the shell's anchor node.
|
|
40
|
+
*
|
|
41
|
+
* Through `getRootNode` so it works whether the shell renders into a shadow root
|
|
42
|
+
* or into light DOM, which these shells do (`shadow: "none"`, so item-player and
|
|
43
|
+
* runtime styles can cascade into rendered item content).
|
|
44
|
+
*/
|
|
45
|
+
export declare function getShellHostElement(anchor: HTMLElement | null): HTMLElement | null;
|
|
46
|
+
/**
|
|
47
|
+
* Track whether the viewport is below the shell's narrow-layout breakpoint.
|
|
48
|
+
*
|
|
49
|
+
* Call from a component's init; the effect it registers is torn down with that
|
|
50
|
+
* component. `getBreakpoint` is read reactively, so a host changing the
|
|
51
|
+
* breakpoint attribute re-subscribes to the new query.
|
|
52
|
+
*/
|
|
53
|
+
export declare function createNarrowLayoutWatch(getBreakpoint: () => number): {
|
|
54
|
+
readonly isNarrow: boolean;
|
|
55
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ItemEntity } from '@pie-players/pie-players-shared/types';
|
|
2
|
+
import { FormativeItemView } from '@pie-players/pie-players-shared/formative';
|
|
2
3
|
import { SectionCompositionModel } from '../../controllers/types.js';
|
|
3
4
|
import { PlayerElementParams } from './player-action.js';
|
|
4
5
|
/**
|
|
@@ -52,6 +53,39 @@ export declare function getPassagePlayerParams(args: {
|
|
|
52
53
|
playerStrategy: string;
|
|
53
54
|
baseHeadingLevel?: HeadingLevel;
|
|
54
55
|
}): PlayerElementParams;
|
|
56
|
+
/**
|
|
57
|
+
* The formative view for one item, or `null` when the section does not deliver
|
|
58
|
+
* formatively.
|
|
59
|
+
*
|
|
60
|
+
* Derived here rather than in a component so the card, the env projection, and
|
|
61
|
+
* any future consumer read one predicate. The projection arrives on the
|
|
62
|
+
* composition model, which the runtime republishes on every controller event, so
|
|
63
|
+
* this is recomputed from fresh state on each render pass.
|
|
64
|
+
*/
|
|
65
|
+
export declare function getFormativeItemView(args: {
|
|
66
|
+
compositionModel: SectionCompositionModel;
|
|
67
|
+
canonicalItemId: string;
|
|
68
|
+
}): FormativeItemView | null;
|
|
69
|
+
/**
|
|
70
|
+
* What a card needs to know about the cue timeline, or `null` when the section is
|
|
71
|
+
* not timed media.
|
|
72
|
+
*
|
|
73
|
+
* Derived here rather than in a component for the same reason the formative view
|
|
74
|
+
* is: two derivations of one predicate can disagree about whether an item is on
|
|
75
|
+
* screen.
|
|
76
|
+
*/
|
|
77
|
+
export interface TimedMediaItemView {
|
|
78
|
+
/** Awaiting its cue. Kept mounted; see `SectionItemsPane`. */
|
|
79
|
+
pending: boolean;
|
|
80
|
+
/** This item is what a currently held gate is waiting for. */
|
|
81
|
+
isGateTarget: boolean;
|
|
82
|
+
/** The gate is enforced rather than advisory — playback is really stopped. */
|
|
83
|
+
gateEnforced: boolean;
|
|
84
|
+
}
|
|
85
|
+
export declare function getTimedMediaItemView(args: {
|
|
86
|
+
compositionModel: SectionCompositionModel;
|
|
87
|
+
canonicalItemId: string;
|
|
88
|
+
}): TimedMediaItemView | null;
|
|
55
89
|
export declare function getItemPlayerParams(args: {
|
|
56
90
|
item: ItemEntity;
|
|
57
91
|
compositionModel: SectionCompositionModel;
|
|
@@ -61,6 +95,7 @@ export declare function getItemPlayerParams(args: {
|
|
|
61
95
|
playerStrategy: string;
|
|
62
96
|
itemIndex?: number;
|
|
63
97
|
baseHeadingLevel?: HeadingLevel;
|
|
98
|
+
formativeView?: FormativeItemView | null;
|
|
64
99
|
}): PlayerElementParams;
|
|
65
100
|
export declare function getCanonicalItemId(args: {
|
|
66
101
|
compositionModel: SectionCompositionModel;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { I18nProvider, InterpolationValues, MessageKeyInput, PluralOptions, TextDirection } from '@pie-players/pie-players-shared/i18n/types';
|
|
2
|
+
export interface InterfaceI18n {
|
|
3
|
+
/** The resolved provider: the published one, or the English-only default. */
|
|
4
|
+
readonly provider: I18nProvider;
|
|
5
|
+
/** Active locale tag, for stamping `lang`. */
|
|
6
|
+
readonly locale: string;
|
|
7
|
+
/** Writing direction, for stamping `dir`. */
|
|
8
|
+
readonly direction: TextDirection;
|
|
9
|
+
t(key: MessageKeyInput, values?: InterpolationValues): string;
|
|
10
|
+
plural(key: MessageKeyInput, options: PluralOptions): string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the interface-locale provider for a component.
|
|
14
|
+
*
|
|
15
|
+
* @param getHost Returns the element to make the context request from. Any
|
|
16
|
+
* element inside the player works: requests bubble to the toolkit's provider.
|
|
17
|
+
* Returning `undefined` before the element is bound is expected, and the
|
|
18
|
+
* connection is made when it appears.
|
|
19
|
+
*/
|
|
20
|
+
export declare function useInterfaceI18n(getHost: () => HTMLElement | null | undefined): InterfaceI18n;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { TestAttemptSession } from '@pie-players/pie-assessment-toolkit';
|
|
2
|
+
import { FormativeSectionProjection } from '@pie-players/pie-players-shared/formative';
|
|
3
|
+
import { MediaTimeSource, TimedMediaSectionProjection } from '@pie-players/pie-players-shared/timed-media';
|
|
2
4
|
import { SectionControllerHandle, SectionSessionPersistenceConfig, SectionControllerRuntimeState, SectionControllerSessionState } from './toolkit-section-contracts.js';
|
|
3
5
|
import { ItemEntity } from '@pie-players/pie-players-shared';
|
|
4
6
|
import { NavigationResult, SectionControllerChangeListener, SectionCompositionModel, SectionNavigationState, SectionViewModel, SessionChangedResult } from './types.js';
|
|
@@ -21,6 +23,19 @@ export declare class SectionController implements SectionControllerHandle {
|
|
|
21
23
|
private nextApplyRevision;
|
|
22
24
|
private lastReplayedApplyRevision;
|
|
23
25
|
private pendingApplyReplay;
|
|
26
|
+
private formativePolicies;
|
|
27
|
+
private formativeStates;
|
|
28
|
+
private formativeEnabled;
|
|
29
|
+
private lastMasterySignature;
|
|
30
|
+
private timedMediaData;
|
|
31
|
+
private timedMediaState;
|
|
32
|
+
private mediaTimeSource;
|
|
33
|
+
private mediaTimeSourceOrigin;
|
|
34
|
+
private unsubscribeMediaTimeSource;
|
|
35
|
+
private lastTimedMediaSignature;
|
|
36
|
+
private lastTimedMediaInvalidSignature;
|
|
37
|
+
private reportedDegradationKeys;
|
|
38
|
+
private mediaAttachWatch;
|
|
24
39
|
private emitChange;
|
|
25
40
|
subscribe(listener: SectionControllerChangeListener): () => void;
|
|
26
41
|
initialize(input?: unknown): Promise<void>;
|
|
@@ -49,7 +64,166 @@ export declare class SectionController implements SectionControllerHandle {
|
|
|
49
64
|
* This is intentionally compact and not a full runtime diagnostics view.
|
|
50
65
|
*/
|
|
51
66
|
getSession(): SectionControllerSessionState | null;
|
|
67
|
+
private rebuildFormativePolicies;
|
|
68
|
+
private computeMastery;
|
|
69
|
+
getFormativeProjection(): FormativeSectionProjection | null;
|
|
70
|
+
/**
|
|
71
|
+
* Record one Try from element outcomes.
|
|
72
|
+
*
|
|
73
|
+
* Correctness derivation lives here rather than at the call site so one
|
|
74
|
+
* aggregation policy applies to every caller. The reducer is a no-op when the
|
|
75
|
+
* item cannot currently be checked, which is what makes a double submit safe
|
|
76
|
+
* — a second click landing before the composition republishes is dropped
|
|
77
|
+
* rather than spending a Try.
|
|
78
|
+
*/
|
|
79
|
+
recordFormativeTry(args: {
|
|
80
|
+
itemId: string;
|
|
81
|
+
outcomes?: unknown[];
|
|
82
|
+
}): void;
|
|
83
|
+
/** Dismiss a reveal and reopen the item, withdrawing its env projection. */
|
|
84
|
+
retryFormativeItem(args: {
|
|
85
|
+
itemId: string;
|
|
86
|
+
}): void;
|
|
87
|
+
/**
|
|
88
|
+
* Reveal on host authority — a teacher-driven "show the answer". Spends no
|
|
89
|
+
* Try and ignores the Try budget.
|
|
90
|
+
*/
|
|
91
|
+
revealFormativeItem(args: {
|
|
92
|
+
itemId: string;
|
|
93
|
+
feedback: "correctness" | "solution";
|
|
94
|
+
}): void;
|
|
95
|
+
/** Withdraw a reveal on host authority, Try budget notwithstanding. */
|
|
96
|
+
hideFormativeItem(args: {
|
|
97
|
+
itemId: string;
|
|
98
|
+
}): void;
|
|
99
|
+
/**
|
|
100
|
+
* Store a reveal transition and announce it. Shared by the three callers so
|
|
101
|
+
* one event describes every reveal change a Try did not cause.
|
|
102
|
+
*/
|
|
103
|
+
private commitFormativeReveal;
|
|
104
|
+
/**
|
|
105
|
+
* Emit only on a rollup change, matching how
|
|
106
|
+
* `section-items-complete-changed` gates on the aggregate rather than on
|
|
107
|
+
* every intermediate count.
|
|
108
|
+
*/
|
|
109
|
+
private emitMasteryIfChanged;
|
|
52
110
|
private getSectionItemIdentifiers;
|
|
111
|
+
private rebuildTimedMedia;
|
|
112
|
+
/**
|
|
113
|
+
* Report malformed authored data once per distinct failure.
|
|
114
|
+
*
|
|
115
|
+
* `initialize` runs again on every same-cohort `updateInput`, and a PnP toggle
|
|
116
|
+
* must not re-report a section's authoring defect each time.
|
|
117
|
+
*/
|
|
118
|
+
private reportTimedMediaInvalid;
|
|
119
|
+
/**
|
|
120
|
+
* The live projection, uncloned and for internal readers only.
|
|
121
|
+
*
|
|
122
|
+
* Resolving is cheap and runs on every `timeupdate`; deep-cloning it there was
|
|
123
|
+
* not. `delivery` is threaded in where the caller already has it, because
|
|
124
|
+
* assembling it walks every item in the section.
|
|
125
|
+
*/
|
|
126
|
+
private resolveTimedMediaView;
|
|
127
|
+
/** The public boundary, so this is where the copy is made. */
|
|
128
|
+
getTimedMediaProjection(): TimedMediaSectionProjection | null;
|
|
129
|
+
/**
|
|
130
|
+
* The delivery state cue gates read, assembled from state this controller
|
|
131
|
+
* already keeps: completion answers `"responded"`, and the last Try's
|
|
132
|
+
* correctness answers the correctness conditions. A gate on `"responded"`
|
|
133
|
+
* therefore works in a section that does not deliver formatively at all.
|
|
134
|
+
*/
|
|
135
|
+
private getTimedMediaDeliveryState;
|
|
136
|
+
/**
|
|
137
|
+
* Bind the Media Time Source. Called by the stimulus card with a native adapter,
|
|
138
|
+
* and directly by a host supplying its own port — which is what lets a host
|
|
139
|
+
* deliver timed media without shipping a PIE element.
|
|
140
|
+
*/
|
|
141
|
+
attachMediaTimeSource(source: MediaTimeSource, options?: {
|
|
142
|
+
origin?: "native-adapter" | "host";
|
|
143
|
+
renderableId?: string;
|
|
144
|
+
}): void;
|
|
145
|
+
detachMediaTimeSource(options?: {
|
|
146
|
+
origin?: "native-adapter" | "host";
|
|
147
|
+
}): void;
|
|
148
|
+
/**
|
|
149
|
+
* Watch for a stimulus that resolves but never exposes a time source.
|
|
150
|
+
*
|
|
151
|
+
* Armed once the section's content has loaded, and only while no source is
|
|
152
|
+
* attached — which is already the common case, because the stimulus card
|
|
153
|
+
* registers its adapter as soon as the media element appears in its subtree.
|
|
154
|
+
*/
|
|
155
|
+
private armMediaAttachWatch;
|
|
156
|
+
private clearMediaAttachWatch;
|
|
157
|
+
/**
|
|
158
|
+
* The stimulus resolved to a renderable that exposes no media, so no cue can
|
|
159
|
+
* ever fire and every cued item would stay hidden for the whole session.
|
|
160
|
+
*
|
|
161
|
+
* Handled exactly as malformed `timedMedia` is: the timeline is dropped, the
|
|
162
|
+
* section delivers as an ordinary section with every item visible, and the
|
|
163
|
+
* defect is reported non-recoverably. Same outcome, same posture — refusing to
|
|
164
|
+
* deliver would turn one authoring slip into a blank section, and leaving the
|
|
165
|
+
* timeline in place would keep the items hidden behind cues nothing can fire.
|
|
166
|
+
*/
|
|
167
|
+
private reportStimulusExposesNoTimeSource;
|
|
168
|
+
/**
|
|
169
|
+
* Move a freshly attached source up to the restored position, forward only so it
|
|
170
|
+
* never fights a learner who has already scrubbed — the same rule the signing
|
|
171
|
+
* region applies when it seeks a fragment on `loadedmetadata`.
|
|
172
|
+
*/
|
|
173
|
+
private syncMediaPositionFromState;
|
|
174
|
+
/**
|
|
175
|
+
* One step of the cue reduction: fold the input into cue state, drive the port
|
|
176
|
+
* with whatever the reduction asked for, and emit only when cue state moved.
|
|
177
|
+
*/
|
|
178
|
+
private advanceTimedMedia;
|
|
179
|
+
/**
|
|
180
|
+
* Silence media audio so another audio owner can speak.
|
|
181
|
+
*
|
|
182
|
+
* The handoff rule the contract's accessibility requirements name, reduced to
|
|
183
|
+
* the half the section can carry out: the section owns the port, so it can stop
|
|
184
|
+
* media audio, and it owns no read-aloud, so it decides nothing about speech.
|
|
185
|
+
* Whoever holds both — the toolkit — calls this and yields the other way on
|
|
186
|
+
* `timed-media-audio-started`.
|
|
187
|
+
*
|
|
188
|
+
* Returns whether media audio is now silent, which is `false` only for a source
|
|
189
|
+
* that is playing and reports no `canPause`. The overlap then stands: refusing
|
|
190
|
+
* to speak would withhold an accommodation to protect a policy the port already
|
|
191
|
+
* said it cannot keep, so enforcement degrades here exactly as it does for a
|
|
192
|
+
* gate — advisory, and reported at attach.
|
|
193
|
+
*
|
|
194
|
+
* No pause is issued for a source already paused, so calling this repeatedly
|
|
195
|
+
* costs nothing and never fights a learner.
|
|
196
|
+
*/
|
|
197
|
+
pauseMediaForCompetingAudio(): boolean;
|
|
198
|
+
/** Announce media audio only where playback actually stood. */
|
|
199
|
+
private announceMediaAudioStarted;
|
|
200
|
+
/**
|
|
201
|
+
* Carry out an effect only where the port reports the capability for it.
|
|
202
|
+
*
|
|
203
|
+
* Skipping silently is the failure this contract names: the skip is already
|
|
204
|
+
* reported once per attach by `reportTimedMediaDegradations`, and the projection
|
|
205
|
+
* carries `enforcement: "advisory"` for as long as the gap lasts.
|
|
206
|
+
*/
|
|
207
|
+
private applyTimedMediaEffects;
|
|
208
|
+
/**
|
|
209
|
+
* Report a capability gap once per attached source.
|
|
210
|
+
*
|
|
211
|
+
* Enforcement rather than the whole projection: the degradation list is all this
|
|
212
|
+
* needs, and resolving a projection to read one field of it also paid for a gate
|
|
213
|
+
* view and two id sets.
|
|
214
|
+
*/
|
|
215
|
+
private reportTimedMediaDegradations;
|
|
216
|
+
/**
|
|
217
|
+
* Emit on cue state, never on the clock.
|
|
218
|
+
*
|
|
219
|
+
* `timedMediaProjectionSignature` is shared with the toolkit's composition
|
|
220
|
+
* revision key rather than restated here: two encodings of "what changes what a
|
|
221
|
+
* layout renders" drift silently, and the failure is an emit the toolkit
|
|
222
|
+
* coalesces away or a republish this never announced.
|
|
223
|
+
*/
|
|
224
|
+
private emitTimedMediaIfChanged;
|
|
225
|
+
/** Re-evaluate gates after the state a gate condition reads has changed. */
|
|
226
|
+
private notifyTimedMediaDeliveryChanged;
|
|
53
227
|
/**
|
|
54
228
|
* Runtime/debugger shape scoped to the current section.
|
|
55
229
|
* Use this for widgets that need a section-scoped live snapshot (debug panels, diagnostics).
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { TestAttemptSession } from '@pie-players/pie-assessment-toolkit';
|
|
2
2
|
import { ItemSessionUpdateIntent, AssessmentItemRef, AssessmentSection, ItemEntity, PassageEntity, RubricBlock } from '@pie-players/pie-players-shared';
|
|
3
3
|
import { ConfigContainerEntity } from '@pie-players/pie-players-shared/types';
|
|
4
|
+
import { FormativeFeedbackReveal, FormativeItemPolicy, FormativeMasteryRollup, FormativeSectionProjection, FormativeSectionSlice, FormativeTryOutcome } from '@pie-players/pie-players-shared/formative';
|
|
5
|
+
import { TimedMediaDegradation, TimedMediaSectionProjection, TimedMediaSectionSessionSlice, TimedMediaValidationError } from '@pie-players/pie-players-shared/timed-media';
|
|
4
6
|
export type SectionView = "candidate" | "scorer" | "author" | "proctor" | "testConstructor" | "tutor";
|
|
5
7
|
export interface SectionContentModel {
|
|
6
8
|
passages: PassageEntity[];
|
|
@@ -14,7 +16,24 @@ export interface SectionContentModel {
|
|
|
14
16
|
id?: string;
|
|
15
17
|
identifier?: string;
|
|
16
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* The item ref's formative override, carried through because this
|
|
21
|
+
* projection is where the canonical identifier is decided. Resolving
|
|
22
|
+
* policy from the raw `assessmentItemRefs` instead would mean recomputing
|
|
23
|
+
* that identifier, and two implementations of one mapping.
|
|
24
|
+
*/
|
|
25
|
+
formative?: FormativeItemPolicy;
|
|
17
26
|
}>;
|
|
27
|
+
/**
|
|
28
|
+
* How an authored `timedMedia.stimulusRef` resolves to a renderable.
|
|
29
|
+
*
|
|
30
|
+
* Keyed by every spelling a section may reference the stimulus under — the
|
|
31
|
+
* rubric block identifier, the authored passage id, and the normalized id —
|
|
32
|
+
* mapping to the normalized passage id the renderables carry. Empty for a
|
|
33
|
+
* section with no stimulus rubric block, which is what makes an unresolvable
|
|
34
|
+
* `stimulusRef` detectable rather than silently cue-less.
|
|
35
|
+
*/
|
|
36
|
+
stimulusRenderableIdsByRef: Record<string, string>;
|
|
18
37
|
}
|
|
19
38
|
export type SectionRenderableFlavor = "item" | "passage" | "rubric";
|
|
20
39
|
export interface SectionRenderable {
|
|
@@ -35,6 +54,8 @@ export interface SectionSessionState {
|
|
|
35
54
|
currentItemIndex?: number;
|
|
36
55
|
visitedItemIdentifiers?: string[];
|
|
37
56
|
itemSessions: Record<string, unknown>;
|
|
57
|
+
formative?: FormativeSectionSlice;
|
|
58
|
+
timedMedia?: TimedMediaSectionSessionSlice;
|
|
38
59
|
}
|
|
39
60
|
export interface SectionViewModel extends SectionContentModel {
|
|
40
61
|
currentItemIndex: number;
|
|
@@ -70,6 +91,26 @@ export interface SectionCompositionModel {
|
|
|
70
91
|
itemSessionsByItemId: Record<string, unknown>;
|
|
71
92
|
testAttemptSession: TestAttemptSession | null;
|
|
72
93
|
itemViewModels: SectionCanonicalItemViewModel[];
|
|
94
|
+
/**
|
|
95
|
+
* Formative delivery projection, keyed by canonical item id. Absent reads
|
|
96
|
+
* exactly as `null`: no item in the section delivers formatively, which is
|
|
97
|
+
* every section authored without a `formative` policy. Optional so anything
|
|
98
|
+
* else assembling this model — a host layout, a test double — omits the
|
|
99
|
+
* field instead of declaring a feature it does not use.
|
|
100
|
+
*
|
|
101
|
+
* Layouts read it from here rather than calling the controller: the runtime
|
|
102
|
+
* republishes the composition model on every controller event, so a recorded
|
|
103
|
+
* Try reaches the cards through the channel that already exists.
|
|
104
|
+
*/
|
|
105
|
+
formative?: FormativeSectionProjection | null;
|
|
106
|
+
/**
|
|
107
|
+
* Timed-media projection: cues, enforcement, which items a cue has revealed,
|
|
108
|
+
* and the gate holding playback. `null` for every section that is not timed
|
|
109
|
+
* media, and for one whose `timedMedia` failed validation — a layout that reads
|
|
110
|
+
* `null` renders exactly as it does today, which is what keeps the addition
|
|
111
|
+
* invisible to existing content.
|
|
112
|
+
*/
|
|
113
|
+
timedMedia?: TimedMediaSectionProjection | null;
|
|
73
114
|
}
|
|
74
115
|
export interface SectionCanonicalItemViewModel {
|
|
75
116
|
item: ItemEntity;
|
|
@@ -144,7 +185,7 @@ export interface SectionNavigationState {
|
|
|
144
185
|
}
|
|
145
186
|
export type SectionContentKind = "item" | "passage" | "rubric" | "unknown";
|
|
146
187
|
interface SectionControllerEventBase {
|
|
147
|
-
type: "item-session-data-changed" | "item-session-meta-changed" | "item-selected" | "section-navigation-change" | "section-session-applied" | "content-loaded" | "item-player-error" | "item-complete-changed" | "section-loading-complete" | "section-items-complete-changed" | "section-error";
|
|
188
|
+
type: "item-session-data-changed" | "item-session-meta-changed" | "item-selected" | "section-navigation-change" | "section-session-applied" | "content-loaded" | "item-player-error" | "item-complete-changed" | "section-loading-complete" | "section-items-complete-changed" | "section-error" | "formative-try-recorded" | "formative-reveal-changed" | "section-mastery-changed" | "timed-media-cue-changed" | "timed-media-audio-started" | "timed-media-policy-degraded" | "timed-media-invalid";
|
|
148
189
|
timestamp: number;
|
|
149
190
|
}
|
|
150
191
|
interface ItemScopedControllerEventBase extends SectionControllerEventBase {
|
|
@@ -229,6 +270,91 @@ export interface SectionErrorEvent extends ItemScopedControllerEventBase {
|
|
|
229
270
|
canonicalItemId?: string;
|
|
230
271
|
contentKind?: SectionContentKind;
|
|
231
272
|
}
|
|
232
|
-
export
|
|
273
|
+
export interface FormativeTryRecordedEvent extends ItemScopedControllerEventBase {
|
|
274
|
+
type: "formative-try-recorded";
|
|
275
|
+
itemId: string;
|
|
276
|
+
canonicalItemId: string;
|
|
277
|
+
tryCount: number;
|
|
278
|
+
outcome: FormativeTryOutcome;
|
|
279
|
+
revealed: boolean;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The reveal state changed without a Try being recorded: a learner dismissed
|
|
283
|
+
* feedback to edit again, or a host forced a reveal or withdrew one. A Try that
|
|
284
|
+
* reveals reports through `formative-try-recorded`.
|
|
285
|
+
*/
|
|
286
|
+
export interface FormativeRevealChangedEvent extends ItemScopedControllerEventBase {
|
|
287
|
+
type: "formative-reveal-changed";
|
|
288
|
+
itemId: string;
|
|
289
|
+
canonicalItemId: string;
|
|
290
|
+
revealed: boolean;
|
|
291
|
+
/** Reveal level in force, when a host overrode the policy's. */
|
|
292
|
+
feedback?: FormativeFeedbackReveal;
|
|
293
|
+
/** Tries already spent; none of these transitions consume one. */
|
|
294
|
+
tryCount: number;
|
|
295
|
+
source: "learner" | "host";
|
|
296
|
+
}
|
|
297
|
+
export interface SectionMasteryChangedEvent extends ItemScopedControllerEventBase {
|
|
298
|
+
type: "section-mastery-changed";
|
|
299
|
+
mastery: FormativeMasteryRollup;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Cue state changed: a cue activated, a gate released, or aggregate completion
|
|
303
|
+
* flipped.
|
|
304
|
+
*
|
|
305
|
+
* Media position is deliberately not an event. `timeupdate` fires about four
|
|
306
|
+
* times a second and changes nothing a layout renders — the media element draws
|
|
307
|
+
* its own clock — so position lives in the session slice and surfaces here only
|
|
308
|
+
* through the cue transitions it caused. Emitting per tick would republish the
|
|
309
|
+
* composition four times a second for no visible change.
|
|
310
|
+
*/
|
|
311
|
+
export interface TimedMediaCueChangedEvent extends ItemScopedControllerEventBase {
|
|
312
|
+
type: "timed-media-cue-changed";
|
|
313
|
+
/** The cue that activated in this transition, where one did. */
|
|
314
|
+
activatedCueIdentifier?: string;
|
|
315
|
+
/** The gate that released in this transition, where one did. */
|
|
316
|
+
releasedCueIdentifier?: string;
|
|
317
|
+
activeCueIdentifier?: string;
|
|
318
|
+
visitedCueIdentifiers: string[];
|
|
319
|
+
completedCueIdentifiers: string[];
|
|
320
|
+
revealedItemIds: string[];
|
|
321
|
+
gateCueIdentifier: string | null;
|
|
322
|
+
mediaCompleted: boolean;
|
|
323
|
+
aggregateComplete: boolean;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Media audio is now running, so any other audio owner must yield.
|
|
327
|
+
*
|
|
328
|
+
* The section owns no audio but the port, and no policy over read-aloud, so this
|
|
329
|
+
* announces rather than decides: the toolkit holds both capabilities and arbitrates
|
|
330
|
+
* between them. Emitted only where playback actually stood — a gate that re-paused
|
|
331
|
+
* on the same `play` produced no audio, and silencing read-aloud for it would take
|
|
332
|
+
* an accommodation away for nothing.
|
|
333
|
+
*
|
|
334
|
+
* Carries no payload. "Media audio started" is the whole fact; position and cue
|
|
335
|
+
* state travel on `timed-media-cue-changed`.
|
|
336
|
+
*/
|
|
337
|
+
export interface TimedMediaAudioStartedEvent extends ItemScopedControllerEventBase {
|
|
338
|
+
type: "timed-media-audio-started";
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* A playback policy the attached media time source cannot carry out. Cues still
|
|
342
|
+
* fire and state is still recorded; enforcement is what degrades, and it says so
|
|
343
|
+
* rather than appearing to hold.
|
|
344
|
+
*/
|
|
345
|
+
export interface TimedMediaPolicyDegradedEvent extends ItemScopedControllerEventBase {
|
|
346
|
+
type: "timed-media-policy-degraded";
|
|
347
|
+
degradations: TimedMediaDegradation[];
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Authored `timedMedia` this section cannot deliver. The section still renders as
|
|
351
|
+
* an ordinary section with every item visible; cues that silently never fire is
|
|
352
|
+
* the outcome this reports instead of producing.
|
|
353
|
+
*/
|
|
354
|
+
export interface TimedMediaInvalidEvent extends ItemScopedControllerEventBase {
|
|
355
|
+
type: "timed-media-invalid";
|
|
356
|
+
errors: TimedMediaValidationError[];
|
|
357
|
+
}
|
|
358
|
+
export type SectionControllerChangeEvent = ItemSessionDataChangedEvent | ItemSessionMetaChangedEvent | ItemSelectedEvent | SectionNavigationChangeEvent | SectionSessionAppliedEvent | ContentLoadedEvent | ItemPlayerErrorEvent | ItemCompleteChangedEvent | SectionLoadingCompleteEvent | SectionItemsCompleteChangedEvent | SectionErrorEvent | FormativeTryRecordedEvent | FormativeRevealChangedEvent | SectionMasteryChangedEvent | TimedMediaCueChangedEvent | TimedMediaAudioStartedEvent | TimedMediaPolicyDegradedEvent | TimedMediaInvalidEvent;
|
|
233
359
|
export type SectionControllerChangeListener = (event: SectionControllerChangeEvent) => void;
|
|
234
360
|
export {};
|