@intellectif/lk-core 0.14.1 → 0.16.0
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/CHANGELOG.md +54 -0
- package/README.md +1 -0
- package/dist/{activity-BVkg9VDo.d.cts → activity-DcxtpX7Z.d.cts} +39 -2
- package/dist/{activity-BVkg9VDo.d.ts → activity-DcxtpX7Z.d.ts} +39 -2
- package/dist/{chunk-J5OIRIOW.cjs → chunk-6P6XK2LI.cjs} +31 -31
- package/dist/{chunk-J5OIRIOW.cjs.map → chunk-6P6XK2LI.cjs.map} +1 -1
- package/dist/{chunk-VCXIPLEW.cjs → chunk-F7UOLKWV.cjs} +95 -25
- package/dist/chunk-F7UOLKWV.cjs.map +1 -0
- package/dist/chunk-FJJ2GVBA.cjs +420 -0
- package/dist/chunk-FJJ2GVBA.cjs.map +1 -0
- package/dist/{chunk-NBADQ3JU.js → chunk-GOZGBVJP.js} +203 -10
- package/dist/chunk-GOZGBVJP.js.map +1 -0
- package/dist/{chunk-6RND47QE.js → chunk-IRUD73IQ.js} +95 -25
- package/dist/chunk-IRUD73IQ.js.map +1 -0
- package/dist/{chunk-KTDOZR7Y.js → chunk-OXF7VT7S.js} +2 -2
- package/dist/{chunk-RTIR6R2U.js → chunk-S63IOKFP.js} +2 -2
- package/dist/{chunk-E7LPIZXI.cjs → chunk-VFTVZJO5.cjs} +4 -4
- package/dist/{chunk-E7LPIZXI.cjs.map → chunk-VFTVZJO5.cjs.map} +1 -1
- package/dist/{index-v2_-Fyn3.d.ts → index-BWpwZRQw.d.cts} +367 -2
- package/dist/{index-BE1bM2Wi.d.cts → index-DRJqwO2y.d.ts} +367 -2
- package/dist/index.cjs +399 -97
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +116 -7
- package/dist/index.d.ts +116 -7
- package/dist/index.js +314 -12
- package/dist/index.js.map +1 -1
- package/dist/schemas.cjs +11 -3
- package/dist/schemas.cjs.map +1 -1
- package/dist/schemas.d.cts +2 -2
- package/dist/schemas.d.ts +2 -2
- package/dist/schemas.js +10 -2
- package/dist/scoring.cjs +3 -3
- package/dist/scoring.d.cts +2 -2
- package/dist/scoring.d.ts +2 -2
- package/dist/scoring.js +2 -2
- package/dist/xapi.cjs +3 -3
- package/dist/xapi.d.cts +1 -1
- package/dist/xapi.d.ts +1 -1
- package/dist/xapi.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-2NIALQEH.cjs +0 -227
- package/dist/chunk-2NIALQEH.cjs.map +0 -1
- package/dist/chunk-6RND47QE.js.map +0 -1
- package/dist/chunk-NBADQ3JU.js.map +0 -1
- package/dist/chunk-VCXIPLEW.cjs.map +0 -1
- /package/dist/{chunk-KTDOZR7Y.js.map → chunk-OXF7VT7S.js.map} +0 -0
- /package/dist/{chunk-RTIR6R2U.js.map → chunk-S63IOKFP.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -16,22 +16,31 @@ import {
|
|
|
16
16
|
roundGrade,
|
|
17
17
|
score,
|
|
18
18
|
validateSpeechAssessment
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-S63IOKFP.js";
|
|
20
20
|
import {
|
|
21
21
|
XAPIVerb,
|
|
22
22
|
XAPI_VERB_DISPLAY,
|
|
23
23
|
validateXAPIStatement,
|
|
24
24
|
xAPIBuilder,
|
|
25
25
|
xapiDefinitionFor
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-OXF7VT7S.js";
|
|
27
27
|
import {
|
|
28
|
+
INTERACTIVE_VIDEO_ITEM_TYPES,
|
|
28
29
|
ItemGroupSchema,
|
|
30
|
+
MediaTimelineSchema,
|
|
29
31
|
RedactedItemGroupSchema,
|
|
30
32
|
RedactedStimulusSchema,
|
|
31
33
|
StimulusSchema,
|
|
34
|
+
TIMELINE_MAX_CHAPTERS,
|
|
35
|
+
TIMELINE_MAX_ITEMS,
|
|
36
|
+
TIMELINE_MAX_QUIZZES,
|
|
37
|
+
TIMELINE_MAX_TITLE_LENGTH,
|
|
38
|
+
TimelineChapterSchema,
|
|
39
|
+
TimelineCueSchema,
|
|
32
40
|
dictationJsonSchema,
|
|
33
41
|
fillInTheBlanksJsonSchema,
|
|
34
42
|
gapSelectJsonSchema,
|
|
43
|
+
isInteractiveVideoItemType,
|
|
35
44
|
itemGroupJsonSchema,
|
|
36
45
|
jsonSchemaFor,
|
|
37
46
|
multipleChoiceJsonSchema,
|
|
@@ -40,7 +49,7 @@ import {
|
|
|
40
49
|
validateActivity,
|
|
41
50
|
validateItemGroup,
|
|
42
51
|
writtenResponseJsonSchema
|
|
43
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-GOZGBVJP.js";
|
|
44
53
|
import {
|
|
45
54
|
ActivitySchemaError,
|
|
46
55
|
BlankConfigSchema,
|
|
@@ -65,6 +74,7 @@ import {
|
|
|
65
74
|
MEDIA_FIELD_POLICY,
|
|
66
75
|
MediaPlaybackSchema,
|
|
67
76
|
MediaSchema,
|
|
77
|
+
MediaTrackSchema,
|
|
68
78
|
MediaUrlSchema,
|
|
69
79
|
MultipleChoiceDataSchema,
|
|
70
80
|
MultipleChoiceOptionMediaSchema,
|
|
@@ -108,7 +118,7 @@ import {
|
|
|
108
118
|
fillInTheBlanksType,
|
|
109
119
|
gapSelectType,
|
|
110
120
|
getActivityTypeDescriptor,
|
|
111
|
-
|
|
121
|
+
groupCaptionsField,
|
|
112
122
|
isMissingId,
|
|
113
123
|
isRecord,
|
|
114
124
|
isUnset,
|
|
@@ -124,7 +134,7 @@ import {
|
|
|
124
134
|
registeredActivityTypes,
|
|
125
135
|
withValidationScope,
|
|
126
136
|
writtenResponseType
|
|
127
|
-
} from "./chunk-
|
|
137
|
+
} from "./chunk-IRUD73IQ.js";
|
|
128
138
|
|
|
129
139
|
// src/content-hash.ts
|
|
130
140
|
function canonicalJson(value, seen = /* @__PURE__ */ new Set()) {
|
|
@@ -226,6 +236,20 @@ function keyOf(entry) {
|
|
|
226
236
|
}
|
|
227
237
|
return key;
|
|
228
238
|
}
|
|
239
|
+
function inQuizOrder(items, timeline) {
|
|
240
|
+
const placement = /* @__PURE__ */ new Map();
|
|
241
|
+
timeline.cues.forEach((cue, cueIndex) => {
|
|
242
|
+
cue.itemIds.forEach((itemId, position) => {
|
|
243
|
+
if (!placement.has(itemId)) {
|
|
244
|
+
placement.set(itemId, { at: cue.at, cue: cueIndex, position });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
const unplaced = { at: Number.POSITIVE_INFINITY, cue: Number.POSITIVE_INFINITY, position: 0 };
|
|
249
|
+
return items.map((entry) => ({ entry, place: placement.get(entry.item.id) ?? unplaced })).sort(
|
|
250
|
+
(a, b) => a.place.at - b.place.at || a.place.cue - b.place.cue || a.place.position - b.place.position || a.entry.itemIndex - b.entry.itemIndex
|
|
251
|
+
).map(({ entry }) => entry);
|
|
252
|
+
}
|
|
229
253
|
function flattenSequence(entries, options = {}) {
|
|
230
254
|
const shuffleEntries = options.shuffleEntries === true;
|
|
231
255
|
const needsSeed = shuffleEntries || entries.some((entry) => isItemGroup(entry) && entry.shuffle === "within-group");
|
|
@@ -249,11 +273,20 @@ function flattenSequence(entries, options = {}) {
|
|
|
249
273
|
`flattenSequence: item group "${entry.id}" has no items. An empty group would silently remove its stimulus and its questions from the presented sequence.`
|
|
250
274
|
);
|
|
251
275
|
}
|
|
276
|
+
const timeline = entry.timeline;
|
|
277
|
+
if (timeline !== void 0 && entry.shuffle === "within-group") {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`flattenSequence: item group "${entry.id}" has a timeline and shuffle: "within-group". An interactive video presents its questions in the order its quizzes open.`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
252
282
|
const items = entry.items.map((item, itemIndex) => ({ item, itemIndex }));
|
|
253
|
-
const presented = entry.shuffle === "within-group" ? seededShuffle(items, `${seed}:group:${entry.id}`) : items;
|
|
283
|
+
const presented = timeline !== void 0 ? inQuizOrder(items, timeline) : entry.shuffle === "within-group" ? seededShuffle(items, `${seed}:group:${entry.id}`) : items;
|
|
254
284
|
const size = presented.length;
|
|
255
285
|
presented.forEach(({ item, itemIndex }, position) => {
|
|
286
|
+
const cue = timeline?.cues.find((candidate) => candidate.itemIds.includes(item.id));
|
|
256
287
|
slots.push({
|
|
288
|
+
// Slot ids stay AUTHORED — the item's index or its key — whatever the
|
|
289
|
+
// presented order: moving a quiz must never re-map a stored answer.
|
|
257
290
|
slotId: `${entryKey}.${keyOf(item) ?? String(itemIndex)}`,
|
|
258
291
|
index: slots.length,
|
|
259
292
|
activity: item,
|
|
@@ -262,7 +295,9 @@ function flattenSequence(entries, options = {}) {
|
|
|
262
295
|
...entry.title !== void 0 ? { title: entry.title } : {},
|
|
263
296
|
stimulus: entry.stimulus,
|
|
264
297
|
position,
|
|
265
|
-
size
|
|
298
|
+
size,
|
|
299
|
+
...timeline !== void 0 ? { timeline } : {},
|
|
300
|
+
...cue !== void 0 ? { cue } : {}
|
|
266
301
|
}
|
|
267
302
|
});
|
|
268
303
|
});
|
|
@@ -445,7 +480,14 @@ function planSlot(slot, points) {
|
|
|
445
480
|
group: {
|
|
446
481
|
id: slot.group.id,
|
|
447
482
|
...slot.group.title !== void 0 ? { title: slot.group.title } : {},
|
|
448
|
-
stimulusHash: contentHash(slot.group.stimulus)
|
|
483
|
+
stimulusHash: contentHash(slot.group.stimulus),
|
|
484
|
+
...slot.group.cue !== void 0 ? {
|
|
485
|
+
cue: {
|
|
486
|
+
id: slot.group.cue.id,
|
|
487
|
+
at: slot.group.cue.at,
|
|
488
|
+
...slot.group.cue.required !== void 0 ? { required: slot.group.cue.required } : {}
|
|
489
|
+
}
|
|
490
|
+
} : {}
|
|
449
491
|
}
|
|
450
492
|
} : {},
|
|
451
493
|
...mediaBudgets !== void 0 ? { mediaBudgets } : {}
|
|
@@ -479,6 +521,7 @@ function verifyAttemptPlan(plan, current) {
|
|
|
479
521
|
const addedSlotIds = current.slots.filter((s) => !before.has(s.slotId)).map((s) => s.slotId);
|
|
480
522
|
const changedSlotIds = [];
|
|
481
523
|
const changedStimulusSlotIds = [];
|
|
524
|
+
const changedCueSlotIds = [];
|
|
482
525
|
const changedPointsSlotIds = [];
|
|
483
526
|
const reorderedSlotIds = [];
|
|
484
527
|
for (const slot of plan.slots) {
|
|
@@ -492,6 +535,9 @@ function verifyAttemptPlan(plan, current) {
|
|
|
492
535
|
if (now.group?.stimulusHash !== slot.group?.stimulusHash) {
|
|
493
536
|
changedStimulusSlotIds.push(slot.slotId);
|
|
494
537
|
}
|
|
538
|
+
if (contentHash(now.group?.cue ?? null) !== contentHash(slot.group?.cue ?? null)) {
|
|
539
|
+
changedCueSlotIds.push(slot.slotId);
|
|
540
|
+
}
|
|
495
541
|
if (now.points !== slot.points) {
|
|
496
542
|
changedPointsSlotIds.push(slot.slotId);
|
|
497
543
|
}
|
|
@@ -500,11 +546,12 @@ function verifyAttemptPlan(plan, current) {
|
|
|
500
546
|
}
|
|
501
547
|
}
|
|
502
548
|
return {
|
|
503
|
-
matches: missingSlotIds.length === 0 && addedSlotIds.length === 0 && changedSlotIds.length === 0 && changedStimulusSlotIds.length === 0 && changedPointsSlotIds.length === 0 && reorderedSlotIds.length === 0,
|
|
549
|
+
matches: missingSlotIds.length === 0 && addedSlotIds.length === 0 && changedSlotIds.length === 0 && changedStimulusSlotIds.length === 0 && changedCueSlotIds.length === 0 && changedPointsSlotIds.length === 0 && reorderedSlotIds.length === 0,
|
|
504
550
|
missingSlotIds,
|
|
505
551
|
addedSlotIds,
|
|
506
552
|
changedSlotIds,
|
|
507
553
|
changedStimulusSlotIds,
|
|
554
|
+
changedCueSlotIds,
|
|
508
555
|
changedPointsSlotIds,
|
|
509
556
|
reorderedSlotIds
|
|
510
557
|
};
|
|
@@ -652,6 +699,14 @@ function createItemGroupDraft(context) {
|
|
|
652
699
|
items: []
|
|
653
700
|
};
|
|
654
701
|
}
|
|
702
|
+
function createInteractiveVideoDraft(context) {
|
|
703
|
+
const draft = createItemGroupDraft(context);
|
|
704
|
+
return {
|
|
705
|
+
...draft,
|
|
706
|
+
stimulus: { id: draft.stimulus.id, kind: "video", media: { type: "video", url: "" } },
|
|
707
|
+
timeline: { cues: [] }
|
|
708
|
+
};
|
|
709
|
+
}
|
|
655
710
|
function validateItemGroupDraft(draft) {
|
|
656
711
|
return withValidationScope(() => validateItemGroupDraftInScope(draft));
|
|
657
712
|
}
|
|
@@ -676,11 +731,15 @@ function validateItemGroupDraftInScope(draft) {
|
|
|
676
731
|
);
|
|
677
732
|
}
|
|
678
733
|
issues.push(...checkItems(draft));
|
|
679
|
-
if (
|
|
734
|
+
if (draft.timeline !== void 0 && draft.timeline !== null) {
|
|
735
|
+
issues.push(...checkTimeline(draft));
|
|
736
|
+
}
|
|
737
|
+
const captions = groupCaptionsField(draft, (captionsUrl) => !isUnwritten(captionsUrl));
|
|
738
|
+
if (captions !== void 0) {
|
|
680
739
|
issues.push(
|
|
681
740
|
issue(
|
|
682
741
|
"dc_captions_not_allowed",
|
|
683
|
-
["stimulus", "media",
|
|
742
|
+
["stimulus", "media", captions],
|
|
684
743
|
GROUP_CAPTIONS_REVEAL_DICTATION
|
|
685
744
|
)
|
|
686
745
|
);
|
|
@@ -722,6 +781,183 @@ function validateItemGroupDraftInScope(draft) {
|
|
|
722
781
|
const invalid = issues.some((found) => found.severity !== "incomplete");
|
|
723
782
|
return { status: invalid ? "invalid" : "incomplete", issues };
|
|
724
783
|
}
|
|
784
|
+
function checkTimeline(draft) {
|
|
785
|
+
const issues = [];
|
|
786
|
+
const timeline = draft.timeline;
|
|
787
|
+
if (!isRecord(timeline)) {
|
|
788
|
+
return issues;
|
|
789
|
+
}
|
|
790
|
+
const stimulus = isRecord(draft.stimulus) ? draft.stimulus : void 0;
|
|
791
|
+
if (stimulus !== void 0 && !isUnwritten(stimulus.kind) && stimulus.kind !== "video") {
|
|
792
|
+
issues.push(
|
|
793
|
+
issue(
|
|
794
|
+
"ig_timeline_stimulus_kind",
|
|
795
|
+
["stimulus", "kind"],
|
|
796
|
+
"An interactive video needs a video: quizzes open at moments of it."
|
|
797
|
+
)
|
|
798
|
+
);
|
|
799
|
+
} else if (stimulus !== void 0 && isRecord(stimulus.media) && !isUnwritten(stimulus.media.type) && stimulus.media.type !== "video") {
|
|
800
|
+
issues.push(
|
|
801
|
+
issue(
|
|
802
|
+
"ig_timeline_stimulus_kind",
|
|
803
|
+
["stimulus", "media", "type"],
|
|
804
|
+
"Upload a video file. An embedded player from another site cannot be paused when a quiz opens."
|
|
805
|
+
)
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
if (draft.shuffle === "within-group") {
|
|
809
|
+
issues.push(
|
|
810
|
+
issue(
|
|
811
|
+
"ig_timeline_shuffle",
|
|
812
|
+
["shuffle"],
|
|
813
|
+
"The questions of an interactive video appear in the order their quizzes open, so they cannot be shuffled."
|
|
814
|
+
)
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
const items = Array.isArray(draft.items) ? draft.items : [];
|
|
818
|
+
const itemIndexById = /* @__PURE__ */ new Map();
|
|
819
|
+
items.forEach((item, index) => {
|
|
820
|
+
if (!isRecord(item)) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (typeof item.id === "string" && item.id !== "") {
|
|
824
|
+
itemIndexById.set(item.id, index);
|
|
825
|
+
}
|
|
826
|
+
const type = item.type;
|
|
827
|
+
if (typeof type === "string" && type !== "" && type !== "item-group" && !isInteractiveVideoItemType(type) && getActivityTypeDescriptor(type) !== void 0) {
|
|
828
|
+
issues.push(
|
|
829
|
+
issue(
|
|
830
|
+
"ig_timeline_item_type",
|
|
831
|
+
["items", index, "type"],
|
|
832
|
+
`An interactive video can hold ${INTERACTIVE_VIDEO_ITEM_TYPES.join(", ")} questions only.`
|
|
833
|
+
)
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
if (type === "dictation" && isUnset(item.media)) {
|
|
837
|
+
issues.push(
|
|
838
|
+
issue(
|
|
839
|
+
"ig_timeline_dictation_media",
|
|
840
|
+
["items", index, "media"],
|
|
841
|
+
"Add the recording this dictation plays: inside a video it cannot play the video."
|
|
842
|
+
)
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
const cues = Array.isArray(timeline.cues) ? timeline.cues : [];
|
|
847
|
+
const quizIds = /* @__PURE__ */ new Set();
|
|
848
|
+
const placed = /* @__PURE__ */ new Set();
|
|
849
|
+
cues.forEach((cue, cueIndex) => {
|
|
850
|
+
if (!isRecord(cue)) {
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const at = (field, ...rest) => [
|
|
854
|
+
"timeline",
|
|
855
|
+
"cues",
|
|
856
|
+
cueIndex,
|
|
857
|
+
field,
|
|
858
|
+
...rest
|
|
859
|
+
];
|
|
860
|
+
if (isMissingId(cue.id)) {
|
|
861
|
+
issues.push(issue("ig_timeline_quiz_id_required", at("id"), "This quiz has no id."));
|
|
862
|
+
} else if (typeof cue.id === "string") {
|
|
863
|
+
if (quizIds.has(cue.id)) {
|
|
864
|
+
issues.push(
|
|
865
|
+
issue("ig_timeline_quiz_id_duplicate", at("id"), `Two quizzes share the id "${cue.id}".`)
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
quizIds.add(cue.id);
|
|
869
|
+
}
|
|
870
|
+
if (isUnset(cue.at)) {
|
|
871
|
+
issues.push(
|
|
872
|
+
issue(
|
|
873
|
+
"ig_timeline_quiz_time_required",
|
|
874
|
+
at("at"),
|
|
875
|
+
"Choose when in the video this quiz opens."
|
|
876
|
+
)
|
|
877
|
+
);
|
|
878
|
+
} else if (typeof cue.at !== "number" || !Number.isFinite(cue.at) || cue.at < 0) {
|
|
879
|
+
issues.push(
|
|
880
|
+
issue(
|
|
881
|
+
"ig_timeline_quiz_time_invalid",
|
|
882
|
+
at("at"),
|
|
883
|
+
"A quiz opens at a number of seconds, 0 or more."
|
|
884
|
+
)
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
if (isUnset(cue.itemIds) || Array.isArray(cue.itemIds) && cue.itemIds.length === 0) {
|
|
888
|
+
issues.push(
|
|
889
|
+
issue("ig_timeline_quiz_empty", at("itemIds"), "Add at least one question to this quiz.")
|
|
890
|
+
);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (!Array.isArray(cue.itemIds)) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
cue.itemIds.forEach((itemId, position) => {
|
|
897
|
+
if (typeof itemId !== "string") {
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
if (!itemIndexById.has(itemId)) {
|
|
901
|
+
issues.push(
|
|
902
|
+
issue(
|
|
903
|
+
"ig_timeline_quiz_unknown_item",
|
|
904
|
+
at("itemIds", position),
|
|
905
|
+
`This quiz names a question that is not in the video ("${itemId}").`
|
|
906
|
+
)
|
|
907
|
+
);
|
|
908
|
+
} else if (placed.has(itemId)) {
|
|
909
|
+
issues.push(
|
|
910
|
+
issue(
|
|
911
|
+
"ig_timeline_item_duplicate",
|
|
912
|
+
at("itemIds", position),
|
|
913
|
+
"This question is already in a quiz: each question belongs to one quiz."
|
|
914
|
+
)
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
placed.add(itemId);
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
for (const [itemId, index] of itemIndexById) {
|
|
921
|
+
if (!placed.has(itemId)) {
|
|
922
|
+
issues.push(
|
|
923
|
+
issue(
|
|
924
|
+
"ig_timeline_item_unplaced",
|
|
925
|
+
["items", index],
|
|
926
|
+
"Add this question to a quiz, or the video never shows it."
|
|
927
|
+
)
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
const chapters = Array.isArray(timeline.chapters) ? timeline.chapters : [];
|
|
932
|
+
let previousAt;
|
|
933
|
+
chapters.forEach((chapter, index) => {
|
|
934
|
+
if (!isRecord(chapter)) {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (isUnwritten(chapter.title)) {
|
|
938
|
+
issues.push(
|
|
939
|
+
issue(
|
|
940
|
+
"ig_timeline_chapter_title",
|
|
941
|
+
["timeline", "chapters", index, "title"],
|
|
942
|
+
"Give this chapter a title."
|
|
943
|
+
)
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
if (typeof chapter.at === "number" && Number.isFinite(chapter.at)) {
|
|
947
|
+
if (previousAt !== void 0 && chapter.at <= previousAt) {
|
|
948
|
+
issues.push(
|
|
949
|
+
issue(
|
|
950
|
+
"ig_timeline_chapter_order",
|
|
951
|
+
["timeline", "chapters", index, "at"],
|
|
952
|
+
"Chapters must start in order, each after the one before it."
|
|
953
|
+
)
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
previousAt = chapter.at;
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
return issues;
|
|
960
|
+
}
|
|
725
961
|
function checkGroupIdentity(draft) {
|
|
726
962
|
const issues = [];
|
|
727
963
|
if (draft.schemaVersion !== "1.0") {
|
|
@@ -1127,7 +1363,25 @@ var ITEM_GROUP_FIELD_POLICY = {
|
|
|
1127
1363
|
// and identity has to cross the redaction boundary intact.
|
|
1128
1364
|
slotKey: "public",
|
|
1129
1365
|
shuffle: "public",
|
|
1130
|
-
stimulus: STIMULUS_FIELD_POLICY
|
|
1366
|
+
stimulus: STIMULUS_FIELD_POLICY,
|
|
1367
|
+
// Public, and classified key by key rather than as one leaf: the learner's
|
|
1368
|
+
// player needs every part of it, none of it is a key — and a leaf would hand
|
|
1369
|
+
// over the author's object by reference, with whatever else was parked in it.
|
|
1370
|
+
timeline: {
|
|
1371
|
+
// Applies to every element of the array.
|
|
1372
|
+
cues: {
|
|
1373
|
+
id: "public",
|
|
1374
|
+
at: "public",
|
|
1375
|
+
itemIds: "public",
|
|
1376
|
+
title: "public",
|
|
1377
|
+
required: "public"
|
|
1378
|
+
},
|
|
1379
|
+
chapters: {
|
|
1380
|
+
at: "public",
|
|
1381
|
+
title: "public"
|
|
1382
|
+
},
|
|
1383
|
+
navigation: "public"
|
|
1384
|
+
}
|
|
1131
1385
|
};
|
|
1132
1386
|
function redactItemGroup(group, options = {}) {
|
|
1133
1387
|
const reveal = options.reveal ?? "none";
|
|
@@ -1197,6 +1451,41 @@ function assertRedactedItemGroup(data) {
|
|
|
1197
1451
|
}
|
|
1198
1452
|
});
|
|
1199
1453
|
}
|
|
1454
|
+
|
|
1455
|
+
// src/timeline.ts
|
|
1456
|
+
function readMediaProgress(value, duration) {
|
|
1457
|
+
let version;
|
|
1458
|
+
let at;
|
|
1459
|
+
let furthest;
|
|
1460
|
+
try {
|
|
1461
|
+
if (typeof value !== "object" || value === null) {
|
|
1462
|
+
return null;
|
|
1463
|
+
}
|
|
1464
|
+
({ progressVersion: version, at, furthest } = value);
|
|
1465
|
+
} catch {
|
|
1466
|
+
return null;
|
|
1467
|
+
}
|
|
1468
|
+
if (version !== "1.0" || typeof at !== "number" || !Number.isFinite(at)) {
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
1471
|
+
const end = typeof duration === "number" && Number.isFinite(duration) && duration >= 0 ? duration : Number.POSITIVE_INFINITY;
|
|
1472
|
+
const clampedAt = Math.min(Math.max(0, at), end);
|
|
1473
|
+
const reached = typeof furthest === "number" && Number.isFinite(furthest) ? furthest : clampedAt;
|
|
1474
|
+
return {
|
|
1475
|
+
progressVersion: "1.0",
|
|
1476
|
+
at: clampedAt,
|
|
1477
|
+
furthest: Math.min(Math.max(clampedAt, reached), end)
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
function composeTimelineScore(entryKey, items, policy) {
|
|
1481
|
+
const own = items.filter((item) => entryKeyOf(item.slotId) === entryKey);
|
|
1482
|
+
if (own.length === 0) {
|
|
1483
|
+
throw new Error(
|
|
1484
|
+
`composeTimelineScore: no scored item belongs to entry "${entryKey}". Pass the video's slotKey (or its position in the sequence), and the items of the attempt that holds it.`
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
return composeAssessmentScore([{ id: entryKey, weight: 1, items: own }], policy);
|
|
1488
|
+
}
|
|
1200
1489
|
export {
|
|
1201
1490
|
ActivitySchemaError,
|
|
1202
1491
|
BlankConfigSchema,
|
|
@@ -1217,9 +1506,12 @@ export {
|
|
|
1217
1506
|
GapSelectChoiceSchema,
|
|
1218
1507
|
GapSelectDataSchema,
|
|
1219
1508
|
GapSelectGapSchema,
|
|
1509
|
+
INTERACTIVE_VIDEO_ITEM_TYPES,
|
|
1220
1510
|
ItemGroupSchema,
|
|
1221
1511
|
MediaPlaybackSchema,
|
|
1222
1512
|
MediaSchema,
|
|
1513
|
+
MediaTimelineSchema,
|
|
1514
|
+
MediaTrackSchema,
|
|
1223
1515
|
MediaUrlSchema,
|
|
1224
1516
|
MultipleChoiceDataSchema,
|
|
1225
1517
|
MultipleChoiceOptionMediaSchema,
|
|
@@ -1250,7 +1542,13 @@ export {
|
|
|
1250
1542
|
RedactedWrittenResponseDataSchema,
|
|
1251
1543
|
SPEECH_ASSESSMENT_MAX_WORDS,
|
|
1252
1544
|
StimulusSchema,
|
|
1545
|
+
TIMELINE_MAX_CHAPTERS,
|
|
1546
|
+
TIMELINE_MAX_ITEMS,
|
|
1547
|
+
TIMELINE_MAX_QUIZZES,
|
|
1548
|
+
TIMELINE_MAX_TITLE_LENGTH,
|
|
1253
1549
|
TextMatchPolicySchema,
|
|
1550
|
+
TimelineChapterSchema,
|
|
1551
|
+
TimelineCueSchema,
|
|
1254
1552
|
UnknownActivityTypeError,
|
|
1255
1553
|
WrittenResponseDataSchema,
|
|
1256
1554
|
WrittenResponseRubricCriterionSchema,
|
|
@@ -1264,10 +1562,12 @@ export {
|
|
|
1264
1562
|
canonicalJson,
|
|
1265
1563
|
classifyBand,
|
|
1266
1564
|
composeAssessmentScore,
|
|
1565
|
+
composeTimelineScore,
|
|
1267
1566
|
computePassThreshold,
|
|
1268
1567
|
contentHash,
|
|
1269
1568
|
countWords,
|
|
1270
1569
|
createDraft,
|
|
1570
|
+
createInteractiveVideoDraft,
|
|
1271
1571
|
createItemGroupDraft,
|
|
1272
1572
|
defineActivityType,
|
|
1273
1573
|
dictationJsonSchema,
|
|
@@ -1290,6 +1590,7 @@ export {
|
|
|
1290
1590
|
hasGrade,
|
|
1291
1591
|
hashSeed,
|
|
1292
1592
|
inspectWav,
|
|
1593
|
+
isInteractiveVideoItemType,
|
|
1293
1594
|
isItemGroup,
|
|
1294
1595
|
itemGroupJsonSchema,
|
|
1295
1596
|
jsonSchemaFor,
|
|
@@ -1303,6 +1604,7 @@ export {
|
|
|
1303
1604
|
planMediaBudgets,
|
|
1304
1605
|
readAloudJsonSchema,
|
|
1305
1606
|
readAloudType,
|
|
1607
|
+
readMediaProgress,
|
|
1306
1608
|
redact,
|
|
1307
1609
|
redactItemGroup,
|
|
1308
1610
|
registerActivityType,
|