@intellectif/lk-react 18.0.0 → 19.0.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 +74 -0
- package/README.md +4 -2
- package/dist/{WrittenResponse-B4kY-U0v.d.ts → WrittenResponse-CeHY5u3W.d.ts} +1 -1
- package/dist/{WrittenResponse-Dl3zah22.d.cts → WrittenResponse-DT_Yqbq1.d.cts} +1 -1
- package/dist/ai/useAiHelp.cjs +20 -0
- package/dist/ai/useAiHelp.cjs.map +1 -0
- package/dist/ai/useAiHelp.d.cts +146 -0
- package/dist/ai/useAiHelp.d.ts +146 -0
- package/dist/ai/useAiHelp.js +7 -0
- package/dist/ai/useAiHelp.js.map +1 -0
- package/dist/{chunk-XEGBGKHV.cjs → chunk-2X2NZF46.cjs} +206 -52
- package/dist/chunk-2X2NZF46.cjs.map +1 -0
- package/dist/{chunk-V5BTVGPA.js → chunk-4NERCBOW.js} +4 -3
- package/dist/chunk-4NERCBOW.js.map +1 -0
- package/dist/{chunk-Z5XY66PO.cjs → chunk-55W2472T.cjs} +16 -16
- package/dist/chunk-55W2472T.cjs.map +1 -0
- package/dist/{chunk-HZMILQYO.js → chunk-5QCZZBOX.js} +5 -4
- package/dist/chunk-5QCZZBOX.js.map +1 -0
- package/dist/{chunk-H3IRVX7J.js → chunk-7NNMDZUU.js} +9 -9
- package/dist/chunk-7NNMDZUU.js.map +1 -0
- package/dist/{chunk-BAKLJKFZ.cjs → chunk-APZ6JFN6.cjs} +4 -29
- package/dist/chunk-APZ6JFN6.cjs.map +1 -0
- package/dist/{chunk-H63O5HPW.cjs → chunk-CQNKZ435.cjs} +8 -7
- package/dist/chunk-CQNKZ435.cjs.map +1 -0
- package/dist/chunk-EANGKAKG.cjs +32 -0
- package/dist/chunk-EANGKAKG.cjs.map +1 -0
- package/dist/{chunk-YTGRXD23.js → chunk-FEOFX4QW.js} +4 -25
- package/dist/chunk-FEOFX4QW.js.map +1 -0
- package/dist/{chunk-QJW3K6JN.js → chunk-G7AGM2Y7.js} +4 -3
- package/dist/chunk-G7AGM2Y7.js.map +1 -0
- package/dist/chunk-JVWLXVYG.cjs +27 -0
- package/dist/chunk-JVWLXVYG.cjs.map +1 -0
- package/dist/chunk-KGDGYRBL.cjs +117 -0
- package/dist/chunk-KGDGYRBL.cjs.map +1 -0
- package/dist/{chunk-UYQBEPNZ.js → chunk-KOFA54C7.js} +15 -8
- package/dist/chunk-KOFA54C7.js.map +1 -0
- package/dist/{chunk-UQIRDE52.cjs → chunk-LX2X4KC7.cjs} +6 -5
- package/dist/chunk-LX2X4KC7.cjs.map +1 -0
- package/dist/{chunk-RZ52SEYV.cjs → chunk-NGFTSRKO.cjs} +10 -9
- package/dist/chunk-NGFTSRKO.cjs.map +1 -0
- package/dist/{chunk-ALH6TUX2.js → chunk-NLTU5JFD.js} +5 -4
- package/dist/chunk-NLTU5JFD.js.map +1 -0
- package/dist/{chunk-6RRFH7C4.cjs → chunk-NZM3M5IR.cjs} +39 -54
- package/dist/chunk-NZM3M5IR.cjs.map +1 -0
- package/dist/{chunk-KWKB74KL.js → chunk-OWV7VRMQ.js} +198 -44
- package/dist/chunk-OWV7VRMQ.js.map +1 -0
- package/dist/chunk-PPFVTXGJ.js +27 -0
- package/dist/chunk-PPFVTXGJ.js.map +1 -0
- package/dist/chunk-QRT2W5N5.js +114 -0
- package/dist/chunk-QRT2W5N5.js.map +1 -0
- package/dist/{chunk-QHK6PTUR.js → chunk-SEQTGOOU.js} +5 -4
- package/dist/chunk-SEQTGOOU.js.map +1 -0
- package/dist/chunk-SXU26SFO.cjs +253 -0
- package/dist/chunk-SXU26SFO.cjs.map +1 -0
- package/dist/chunk-TEAVPUMX.js +22 -0
- package/dist/chunk-TEAVPUMX.js.map +1 -0
- package/dist/{chunk-KP57YCYQ.js → chunk-UXV3V5NH.js} +23 -35
- package/dist/chunk-UXV3V5NH.js.map +1 -0
- package/dist/{chunk-3TJSFPO4.cjs → chunk-WKA6FUR4.cjs} +26 -19
- package/dist/chunk-WKA6FUR4.cjs.map +1 -0
- package/dist/{chunk-QA42VDTA.cjs → chunk-XAIJNGTU.cjs} +12 -11
- package/dist/chunk-XAIJNGTU.cjs.map +1 -0
- package/dist/{chunk-6DTW5T3Z.js → chunk-XHF376ES.js} +5 -4
- package/dist/chunk-XHF376ES.js.map +1 -0
- package/dist/{chunk-TI23GIFM.cjs → chunk-XUQ4KRTT.cjs} +12 -11
- package/dist/chunk-XUQ4KRTT.cjs.map +1 -0
- package/dist/chunk-YIYMBCPV.js +250 -0
- package/dist/chunk-YIYMBCPV.js.map +1 -0
- package/dist/{chunk-JKMBFHER.cjs → chunk-ZME5FMED.cjs} +12 -11
- package/dist/chunk-ZME5FMED.cjs.map +1 -0
- package/dist/components/ActivityPreview.cjs +14 -11
- package/dist/components/ActivityPreview.d.cts +7 -3
- package/dist/components/ActivityPreview.d.ts +7 -3
- package/dist/components/ActivityPreview.js +13 -10
- package/dist/components/ActivitySequence.cjs +14 -11
- package/dist/components/ActivitySequence.d.cts +75 -11
- package/dist/components/ActivitySequence.d.ts +75 -11
- package/dist/components/ActivitySequence.js +13 -10
- package/dist/components/Dictation.cjs +7 -4
- package/dist/components/Dictation.d.cts +1 -1
- package/dist/components/Dictation.d.ts +1 -1
- package/dist/components/Dictation.js +6 -3
- package/dist/components/FillInTheBlanks.cjs +7 -4
- package/dist/components/FillInTheBlanks.d.cts +1 -1
- package/dist/components/FillInTheBlanks.d.ts +1 -1
- package/dist/components/FillInTheBlanks.js +6 -3
- package/dist/components/GapSelect.cjs +7 -4
- package/dist/components/GapSelect.d.cts +1 -1
- package/dist/components/GapSelect.d.ts +1 -1
- package/dist/components/GapSelect.js +6 -3
- package/dist/components/InteractiveVideo.cjs +14 -11
- package/dist/components/InteractiveVideo.d.cts +2 -2
- package/dist/components/InteractiveVideo.d.ts +2 -2
- package/dist/components/InteractiveVideo.js +12 -9
- package/dist/components/MultipleChoice.cjs +7 -4
- package/dist/components/MultipleChoice.d.cts +1 -1
- package/dist/components/MultipleChoice.d.ts +1 -1
- package/dist/components/MultipleChoice.js +6 -3
- package/dist/components/PronunciationFeedback.cjs +4 -3
- package/dist/components/PronunciationFeedback.d.cts +1 -1
- package/dist/components/PronunciationFeedback.d.ts +1 -1
- package/dist/components/PronunciationFeedback.js +3 -2
- package/dist/components/ReadAloud.cjs +6 -4
- package/dist/components/ReadAloud.d.cts +2 -2
- package/dist/components/ReadAloud.d.ts +2 -2
- package/dist/components/ReadAloud.js +5 -3
- package/dist/components/StimulusPanel.d.cts +1 -1
- package/dist/components/StimulusPanel.d.ts +1 -1
- package/dist/components/WrittenResponse.cjs +4 -3
- package/dist/components/WrittenResponse.d.cts +3 -3
- package/dist/components/WrittenResponse.d.ts +3 -3
- package/dist/components/WrittenResponse.js +3 -2
- package/dist/i18n/LkIntlProvider.d.cts +2 -2
- package/dist/i18n/LkIntlProvider.d.ts +2 -2
- package/dist/index.cjs +34 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +15 -12
- package/dist/index.js.map +1 -1
- package/dist/{strings-PCYKWCwd.d.ts → types-BY-4S5Rd.d.ts} +761 -761
- package/dist/{strings-BOPKhnS-.d.cts → types-DtPYG1hh.d.cts} +761 -761
- package/package.json +13 -3
- package/dist/chunk-3TJSFPO4.cjs.map +0 -1
- package/dist/chunk-5F4Q6JT7.cjs +0 -289
- package/dist/chunk-5F4Q6JT7.cjs.map +0 -1
- package/dist/chunk-6DTW5T3Z.js.map +0 -1
- package/dist/chunk-6RRFH7C4.cjs.map +0 -1
- package/dist/chunk-ALH6TUX2.js.map +0 -1
- package/dist/chunk-BAKLJKFZ.cjs.map +0 -1
- package/dist/chunk-GR33G5QM.js +0 -286
- package/dist/chunk-GR33G5QM.js.map +0 -1
- package/dist/chunk-H3IRVX7J.js.map +0 -1
- package/dist/chunk-H63O5HPW.cjs.map +0 -1
- package/dist/chunk-HZMILQYO.js.map +0 -1
- package/dist/chunk-JKMBFHER.cjs.map +0 -1
- package/dist/chunk-KP57YCYQ.js.map +0 -1
- package/dist/chunk-KWKB74KL.js.map +0 -1
- package/dist/chunk-QA42VDTA.cjs.map +0 -1
- package/dist/chunk-QHK6PTUR.js.map +0 -1
- package/dist/chunk-QJW3K6JN.js.map +0 -1
- package/dist/chunk-RZ52SEYV.cjs.map +0 -1
- package/dist/chunk-TI23GIFM.cjs.map +0 -1
- package/dist/chunk-UQIRDE52.cjs.map +0 -1
- package/dist/chunk-UYQBEPNZ.js.map +0 -1
- package/dist/chunk-V5BTVGPA.js.map +0 -1
- package/dist/chunk-XEGBGKHV.cjs.map +0 -1
- package/dist/chunk-YTGRXD23.js.map +0 -1
- package/dist/chunk-Z5XY66PO.cjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { SpeechAssessment, GradeRecord, ReadAloudData, RecordingRef, LearnerResponse, ActivityData, ActivityResult, ItemOutcome, MediaPlayLedgerEntry, MediaPlayClaim, MediaPlayGrant, InteractionEvent, ThemeTokens, RedactedActivityData, ItemGroup, SequenceEntry
|
|
1
|
+
import { ReadAloudDimension, ReadAloudWordState, Stimulus, SpeechAssessment, GradeRecord, ReadAloudData, RecordingRef, LearnerResponse, ActivityData, ActivityResult, ItemOutcome, MediaPlayLedgerEntry, MediaPlayClaim, MediaPlayGrant, InteractionEvent, ThemeTokens, RedactedActivityData, ItemGroup, SequenceEntry } from '@intellectif/lk-core';
|
|
2
2
|
import { LearnerAi } from './ai/LkAiProvider.js';
|
|
3
|
-
import {
|
|
3
|
+
import { S as SpeechRecorderError, R as RecordedTake } from './machine-CuwKqWXC.js';
|
|
4
4
|
|
|
5
5
|
/** The size captions are drawn at. */
|
|
6
6
|
type CaptionSize = 'small' | 'medium' | 'large';
|
|
@@ -39,861 +39,861 @@ interface VideoPreferences {
|
|
|
39
39
|
type VideoShortcutAction = 'play-pause' | 'jump-10' | 'jump-5' | 'jump-1' | 'frame' | 'speed' | 'percent' | 'start-end' | 'chapter' | 'mute' | 'captions' | 'secondary-captions' | 'transcript' | 'picture-in-picture' | 'fullscreen' | 'shortcut-list';
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
42
|
+
* Every string the SDK's own chrome renders.
|
|
43
43
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*/
|
|
49
|
-
type ReadAloudAssessResult = {
|
|
50
|
-
status: 'graded';
|
|
51
|
-
assessment: SpeechAssessment;
|
|
52
|
-
grade: GradeRecord;
|
|
53
|
-
} | {
|
|
54
|
-
status: 'unscorable';
|
|
55
|
-
code: string;
|
|
56
|
-
assessment?: SpeechAssessment;
|
|
57
|
-
} | {
|
|
58
|
-
status: 'failed';
|
|
59
|
-
retryable: boolean;
|
|
60
|
-
};
|
|
61
|
-
/**
|
|
62
|
-
* How a take reaches the application, and how a judgement comes back.
|
|
44
|
+
* Until 0.8.1 these were literals, and `locale` set only the `lang` attribute —
|
|
45
|
+
* so a learner sitting a Spanish paper read a Spanish passage inside an English
|
|
46
|
+
* scaffold, and "No plays remaining" on a listening exam was untranslatable.
|
|
47
|
+
* This is the whole surface, in one place, so a consumer can replace it.
|
|
63
48
|
*
|
|
64
|
-
*
|
|
65
|
-
* `
|
|
66
|
-
*
|
|
49
|
+
* **Interpolation and plurals are functions, not format strings.** A message
|
|
50
|
+
* like `"Question {n} of {total}"` needs a parser, and a parser needs plural
|
|
51
|
+
* rules for every locale the SDK does not know. A function hands both problems
|
|
52
|
+
* to the consumer, who has `Intl.PluralRules` and knows their own language:
|
|
53
|
+
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* questionProgress: (index, total) => `Pregunta ${index} de ${total}`,
|
|
56
|
+
* wordCount: (n) => `${n} ${n === 1 ? 'palabra' : 'palabras'}`,
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* It also lets a translation reorder its arguments, which a positional token
|
|
60
|
+
* cannot, and TypeScript checks the arity. No parser, no message catalogue
|
|
61
|
+
* format, no runtime dependency.
|
|
62
|
+
*
|
|
63
|
+
* **What is deliberately NOT here: thrown errors.** Every `throw` in this
|
|
64
|
+
* package is addressed to the developer who wired the component up, not to the
|
|
65
|
+
* learner. Translating them would make them unsearchable and would put the one
|
|
66
|
+
* audience who can act on them behind a translation. They stay English.
|
|
67
67
|
*/
|
|
68
|
-
interface
|
|
68
|
+
interface LkStrings {
|
|
69
|
+
/** Multiple Choice submit button, and Written Response. */
|
|
70
|
+
submit: string;
|
|
71
|
+
/** Fill-in-the-Blanks in `practice`: the learner is checking their own work. */
|
|
72
|
+
checkAnswers: string;
|
|
73
|
+
/** Fill-in-the-Blanks in `exam`: nothing is checked on the client. */
|
|
74
|
+
submitAnswers: string;
|
|
69
75
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
76
|
+
* The scored result, read by a screen reader on submit.
|
|
77
|
+
*
|
|
78
|
+
* `percent` is already rounded to a whole number. The authored overall
|
|
79
|
+
* feedback is appended SEPARATELY by the component, so a translation of this
|
|
80
|
+
* sentence never has to carry the author's words — the two were previously
|
|
81
|
+
* concatenated into one literal, which made the score sentence untranslatable
|
|
82
|
+
* without also touching authored content.
|
|
74
83
|
*/
|
|
75
|
-
|
|
84
|
+
scoreAnnouncement: (percent: number, passed: boolean) => string;
|
|
85
|
+
/** Prefix announced on submit, before the score sentence. */
|
|
86
|
+
answerSubmitted: string;
|
|
87
|
+
/** An answer submitted for grading that has none yet. Never rendered as 0%. */
|
|
88
|
+
notGradedYet: string;
|
|
89
|
+
/** An outcome that carries no grade and never will. */
|
|
90
|
+
noGradeAvailable: string;
|
|
91
|
+
showFeedback: string;
|
|
92
|
+
hideFeedback: string;
|
|
93
|
+
/** Accessible name of a blank's input. `ordinal` is 1-based. */
|
|
94
|
+
blankLabel: (ordinal: number) => string;
|
|
95
|
+
/** Accessible name of the selector for one gap in a Gap Select passage. */
|
|
96
|
+
gapLabel: (ordinal: number) => string;
|
|
76
97
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* the feedback is replaced by a notice.
|
|
98
|
+
* The empty first entry of a gap's selector — how a learner leaves a gap
|
|
99
|
+
* alone, and how they take an answer back. Never a real choice.
|
|
80
100
|
*/
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
interface ReadAloudProps extends ActivityProps<ReadAloudData> {
|
|
86
|
-
/** How a take is stored and judged. See {@link RecordingBinding}. */
|
|
87
|
-
recordingBinding?: RecordingBinding;
|
|
101
|
+
gapPlaceholder: string;
|
|
102
|
+
showHint: string;
|
|
103
|
+
hideHint: string;
|
|
88
104
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
105
|
+
* Announced when an essay is handed in. Distinct from {@link LkStrings.awaitingGrade},
|
|
106
|
+
* which is the `review`-mode state of a submission made earlier: this one
|
|
107
|
+
* confirms the act, and is the only thing a learner hears at the moment they
|
|
108
|
+
* commit work that nothing on the client will grade.
|
|
92
109
|
*/
|
|
93
|
-
|
|
110
|
+
responseSubmitted: string;
|
|
111
|
+
/** Live word counter. Needs the count for languages whose plural rules differ. */
|
|
112
|
+
wordCount: (count: number) => string;
|
|
113
|
+
/** The authored bounds, e.g. "50–200 words". `min` is 0 when unbounded below. */
|
|
114
|
+
wordBounds: (min: number, max: number) => string;
|
|
115
|
+
/** A rubric criterion the grader marked not applicable. */
|
|
116
|
+
notApplicable: string;
|
|
117
|
+
/** A returned grade flagged for a human to look at — learner-visible by design. */
|
|
118
|
+
awaitingHumanReview: string;
|
|
119
|
+
/** Submitted, no grade back yet. */
|
|
120
|
+
awaitingGrade: string;
|
|
121
|
+
/** The grader could not produce a grade. */
|
|
122
|
+
couldNotBeGraded: string;
|
|
123
|
+
previous: string;
|
|
124
|
+
next: string;
|
|
125
|
+
/** Progress label, also the question region's accessible name. `index` is 1-based. */
|
|
126
|
+
questionProgress: (index: number, total: number) => string;
|
|
127
|
+
/** Shown when no renderer is registered for an activity type. */
|
|
128
|
+
unsupportedActivity: string;
|
|
129
|
+
/** Accessible name of the box the learner types the recording into. */
|
|
130
|
+
dictationInputLabel: string;
|
|
131
|
+
/** Accessible name of the group holding the recording. */
|
|
132
|
+
dictationRecording: string;
|
|
133
|
+
/** Accessible name of the group holding the slower recording, and its default `alt`. */
|
|
134
|
+
dictationSlowRecording: string;
|
|
135
|
+
/** The progressive-hint button, with how many transcript words are shown so far. */
|
|
136
|
+
dictationRevealNextWord: (revealed: number, total: number) => string;
|
|
137
|
+
/** Hides every revealed hint word again. */
|
|
138
|
+
dictationResetHints: string;
|
|
139
|
+
/** The solution toggle: shows the transcript after the attempt is marked. */
|
|
140
|
+
showSolution: string;
|
|
141
|
+
hideSolution: string;
|
|
142
|
+
/** Accessible name of the revealed transcript. */
|
|
143
|
+
dictationSolutionLabel: string;
|
|
144
|
+
/** Accessible name of the word-by-word marked result. */
|
|
145
|
+
dictationMarksLabel: string;
|
|
146
|
+
/** Tells the learner the marks compare the normalised text, not what they typed. */
|
|
147
|
+
dictationDiffNote: string;
|
|
94
148
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
149
|
+
* The four sentences a screen reader hears for a marked word — the only
|
|
150
|
+
* channel the marks have for assistive technology; the glyphs and the
|
|
151
|
+
* character diff are decoration.
|
|
97
152
|
*/
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
153
|
+
dictationWordCorrect: (word: string) => string;
|
|
154
|
+
dictationWordWrong: (word: string, expected: string) => string;
|
|
155
|
+
dictationWordMissing: (expected: string) => string;
|
|
156
|
+
dictationWordExtra: (word: string) => string;
|
|
157
|
+
/** The legend rows; the character-level marks reuse the same four. */
|
|
158
|
+
dictationLegendCorrect: string;
|
|
159
|
+
dictationLegendWrong: string;
|
|
160
|
+
dictationLegendMissing: string;
|
|
161
|
+
dictationLegendExtra: string;
|
|
162
|
+
/** Marked result of an attempt with no words in it once punctuation and spacing are ignored, an empty one included. */
|
|
163
|
+
dictationNothingTyped: string;
|
|
101
164
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* `blob:`. Without it the capture module is loaded from a `blob:` URL, and a
|
|
105
|
-
* policy that refuses one sends every take through the deprecated
|
|
106
|
-
* main-thread `ScriptProcessorNode` — a take that still records, so nothing
|
|
107
|
-
* says so. See `useSpeechRecorder`'s option of the same name.
|
|
165
|
+
* Appended to the score announcement, e.g. "4 of 6 words correct." — a whole
|
|
166
|
+
* sentence, because authored feedback may follow it.
|
|
108
167
|
*/
|
|
109
|
-
|
|
168
|
+
dictationWordsSummary: (correct: number, total: number) => string;
|
|
169
|
+
/** Accessible name of the group holding the model recording. */
|
|
170
|
+
readAloudModelRecording: string;
|
|
171
|
+
/** Accessible name of the group holding the slower model recording, and its default `alt`. */
|
|
172
|
+
readAloudSlowRecording: string;
|
|
173
|
+
/** Starts a take. */
|
|
174
|
+
readAloudRecord: string;
|
|
175
|
+
/** Ends the take in progress. */
|
|
176
|
+
readAloudStop: string;
|
|
177
|
+
/** Throws the take away so the learner can start over. */
|
|
178
|
+
readAloudRerecord: string;
|
|
179
|
+
/** Accessible name of the player for the take the learner just made. */
|
|
180
|
+
readAloudYourRecording: string;
|
|
110
181
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
182
|
+
* In place of the player for the learner's own take, and of the buttons that
|
|
183
|
+
* play one word of it, when the page refuses to play it — most often a
|
|
184
|
+
* Content-Security-Policy whose `media-src` does not allow `blob:`. Said
|
|
185
|
+
* rather than left as controls that do nothing when pressed.
|
|
115
186
|
*/
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* How an activity is being presented. This is the single switch that decides
|
|
121
|
-
* whether the component may grade, reveal, or submit — the three things a
|
|
122
|
-
* summative exam must take away from the client.
|
|
123
|
-
*
|
|
124
|
-
* - `practice` (default, and the only behaviour before `renderMode` existed):
|
|
125
|
-
* the component owns the attempt. It
|
|
126
|
-
* scores locally on submit, reveals correctness and feedback, and calls
|
|
127
|
-
* `onComplete` with a full {@link ActivityResult}.
|
|
128
|
-
* - `exam`: the component NEVER scores and NEVER reveals correctness. Submit
|
|
129
|
-
* emits the raw learner response through `onSubmit`; the server grades it.
|
|
130
|
-
* Safe to render redacted data, because nothing here needs the answer key.
|
|
131
|
-
* - `review`: read-only. Renders the learner's submitted answer, and marks
|
|
132
|
-
* correctness only from an `outcome` the caller supplies (which the server
|
|
133
|
-
* computed). Nothing is submittable.
|
|
134
|
-
*/
|
|
135
|
-
type RenderMode = 'practice' | 'exam' | 'review';
|
|
136
|
-
/**
|
|
137
|
-
* Makes the answer-key-bearing parts of an activity optional, so a component
|
|
138
|
-
* can render either full activity data OR a `redact()` projection with the
|
|
139
|
-
* same props. Fields the SDK classifies `answer-key` become optional here;
|
|
140
|
-
* a component in `exam` mode must not read them at all.
|
|
141
|
-
*/
|
|
142
|
-
type Renderable<TData> = Omit<TData, 'scoringStrategy'> & {
|
|
143
|
-
scoringStrategy?: unknown;
|
|
144
|
-
/** Present on a `redact()` projection. */
|
|
145
|
-
redacted?: true;
|
|
146
|
-
};
|
|
147
|
-
/**
|
|
148
|
-
* {@link Renderable} DISTRIBUTED over a union of activity types.
|
|
149
|
-
*
|
|
150
|
-
* This distinction is load-bearing, not cosmetic. `Renderable<T>` is built on
|
|
151
|
-
* `Omit`, and `Omit` does not distribute: `Omit<A | B, K>` collapses to the
|
|
152
|
-
* keys A and B have IN COMMON, so `Renderable<ActivityData>` is a single
|
|
153
|
-
* object type carrying only the fields every activity shares. Narrowing it
|
|
154
|
-
* dies with it — after `if (data.type === 'multiple-choice')` the compiler
|
|
155
|
-
* still refuses `data.options`, because the union it would narrow to no
|
|
156
|
-
* longer exists.
|
|
157
|
-
*
|
|
158
|
-
* The conditional below re-distributes, so `RenderableActivity` is a real
|
|
159
|
-
* union of per-type renderables and `.type` narrows again. Anything that
|
|
160
|
-
* accepts "some renderable activity, I don't know which" — a custom renderer,
|
|
161
|
-
* a sequence entry — must use THIS, not `Renderable<ActivityData>`.
|
|
162
|
-
*/
|
|
163
|
-
type RenderableActivity<TData extends ActivityData = ActivityData> = TData extends unknown ? Renderable<TData> : never;
|
|
164
|
-
/**
|
|
165
|
-
* Sanitiser for author-supplied rich text (`questionHtml`, `promptHtml`, and a
|
|
166
|
-
* stimulus's `bodyHtml`). The SDK deliberately ships NO sanitiser — that would
|
|
167
|
-
* add a dependency and, worse, a false promise. Rich text is rendered only when
|
|
168
|
-
* you supply this function; without it the component falls back to the
|
|
169
|
-
* plain-text field, which is always escaped. Fail-safe by construction: the SDK
|
|
170
|
-
* never injects HTML it was not explicitly given a sanitiser for.
|
|
171
|
-
*
|
|
172
|
-
* `FillInTheBlanks.passageHtml` is the one exception, and is **never**
|
|
173
|
-
* rendered: the passage hosts the answer inputs, so it cannot be split at the
|
|
174
|
-
* `{{blank}}` placeholders without voiding the sanitiser. The plain `passage`
|
|
175
|
-
* is always used, and passing `passageHtml` warns in development.
|
|
176
|
-
*/
|
|
177
|
-
type HtmlSanitizer = (html: string) => string;
|
|
178
|
-
/**
|
|
179
|
-
* The prop contract shared by every activity component (Req 3.1). Defined
|
|
180
|
-
* here (React-specific) rather than in lk-core, which is React-free.
|
|
181
|
-
*
|
|
182
|
-
* Controlled / uncontrolled follows the React convention: pass `value` +
|
|
183
|
-
* `onChange` to own the learner's answer (restore an in-progress attempt,
|
|
184
|
-
* autosave a delta, drive a review); pass `defaultValue` to seed an
|
|
185
|
-
* uncontrolled component; pass neither to keep the pre-2.1.0 behaviour, where
|
|
186
|
-
* the component owns the answer outright.
|
|
187
|
-
*/
|
|
188
|
-
interface ActivityProps<TData extends ActivityData = ActivityData> {
|
|
189
|
-
/** Activity content. Accepts a `redact()` projection in `exam` mode. */
|
|
190
|
-
data: RenderableActivity<TData>;
|
|
187
|
+
readAloudPlaybackUnavailable: string;
|
|
191
188
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
189
|
+
* Live progress while recording. Whole seconds, not a `m:ss` clock: a
|
|
190
|
+
* recording is bounded in seconds by the item, and a translation should not
|
|
191
|
+
* have to reimplement a clock to change the word around it.
|
|
192
|
+
*
|
|
193
|
+
* Shown on screen and hidden from assistive technology: it changes every
|
|
194
|
+
* second, and a screen reader that spoke each change would speak into the
|
|
195
|
+
* take being assessed. The two sentences below are what is announced instead.
|
|
195
196
|
*/
|
|
196
|
-
|
|
197
|
+
readAloudRecordingProgress: (seconds: number, maxSeconds: number) => string;
|
|
198
|
+
/** Announced once when a capture begins. */
|
|
199
|
+
readAloudRecordingStarted: string;
|
|
197
200
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
+
* Announced once when a capture ends, by the learner or at its own bound.
|
|
202
|
+
* Carries the length because the progress counter is not announced: a take
|
|
203
|
+
* that reads back as "20 of 20 seconds" is one that stopped itself.
|
|
201
204
|
*/
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
|
|
205
|
+
readAloudRecordingStopped: (seconds: number, maxSeconds: number) => string;
|
|
206
|
+
/** How many takes are left. Called with 0 as well, so the wording is one translation's. */
|
|
207
|
+
readAloudTakesRemaining: (remaining: number, max: number) => string;
|
|
208
|
+
/** `exam` only: hands the item in with no recording at all, deliberately. */
|
|
209
|
+
readAloudSubmitWithoutRecording: string;
|
|
210
|
+
/** Pending, while the take travels to the application's storage. */
|
|
211
|
+
readAloudUploading: string;
|
|
212
|
+
/** The take never reached storage, so nothing was submitted. */
|
|
213
|
+
readAloudUploadFailed: string;
|
|
214
|
+
/** Retries whichever step just failed. */
|
|
215
|
+
readAloudTryAgain: string;
|
|
216
|
+
/** Pending, while the assessor judges the take. */
|
|
217
|
+
readAloudAssessing: string;
|
|
218
|
+
/** The binding stores recordings but judges none, so there is no feedback to show. */
|
|
219
|
+
readAloudAssessmentUnavailable: string;
|
|
220
|
+
/** The assessor found no speech, or too little of it, in the take. */
|
|
221
|
+
readAloudNotHeard: string;
|
|
222
|
+
/** The assessor produced no grade for any other reason. The code itself is never shown. */
|
|
223
|
+
readAloudNotAssessed: string;
|
|
224
|
+
/** The assessment could not be run at all — a failure of the call, not of the reading. */
|
|
225
|
+
readAloudAssessmentFailed: string;
|
|
207
226
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* Restoring an attempt without it reopens a question the learner had already
|
|
212
|
-
* submitted as answerable, so on a summative paper they can change and
|
|
213
|
-
* re-submit it. `AttemptState.submittedSlotIds` is what this consumes.
|
|
227
|
+
* What went wrong with the microphone or with the take. One function rather
|
|
228
|
+
* than five keys, so a translation keeps the five sentences together and the
|
|
229
|
+
* SDK keeps one name for the concept.
|
|
214
230
|
*/
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
|
|
231
|
+
readAloudRecorderError: (reason: SpeechRecorderError) => string;
|
|
232
|
+
/** Accessible name of the whole feedback region. */
|
|
233
|
+
pronunciationFeedbackLabel: string;
|
|
234
|
+
/** Name of one scored dimension, as a learner reads it. */
|
|
235
|
+
pronunciationDimension: (dimension: ReadAloudDimension) => string;
|
|
236
|
+
/** A dimension or a word the engine did not measure. NEVER rendered as 0%. */
|
|
237
|
+
pronunciationNotAssessed: string;
|
|
238
|
+
/** Accessible name of the word-by-word marks. */
|
|
239
|
+
pronunciationWordsLabel: string;
|
|
240
|
+
/** One legend row per marking state — the same four states the marks carry. */
|
|
241
|
+
pronunciationLegend: (state: ReadAloudWordState) => string;
|
|
220
242
|
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
243
|
+
* The four sentences a screen reader hears for a marked word: the only
|
|
244
|
+
* channel the marks have for assistive technology, since the glyph and the
|
|
245
|
+
* decoration are both hidden from it.
|
|
223
246
|
*/
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
247
|
+
pronunciationWordCorrect: (word: string) => string;
|
|
248
|
+
pronunciationWordMispronounced: (word: string) => string;
|
|
249
|
+
pronunciationWordOmitted: (word: string) => string;
|
|
250
|
+
pronunciationWordInserted: (word: string) => string;
|
|
251
|
+
/** Opens one word's syllables, sounds and timings. */
|
|
252
|
+
pronunciationWordDetails: (word: string) => string;
|
|
253
|
+
pronunciationSyllables: string;
|
|
254
|
+
pronunciationPhonemes: string;
|
|
255
|
+
/** A syllable the engine also spelled out, e.g. `('ˈhæ', 'ha')`. */
|
|
256
|
+
pronunciationSyllableSpelling: (syllable: string, grapheme: string) => string;
|
|
257
|
+
/** A sound the engine named no symbol for; `ordinal` is 1-based. */
|
|
258
|
+
pronunciationPhonemePosition: (ordinal: number) => string;
|
|
259
|
+
/** Introduces the sounds the engine thought it heard in a sound's place. */
|
|
260
|
+
pronunciationHeardAs: string;
|
|
261
|
+
/** Plays one word out of the learner's own take. */
|
|
262
|
+
pronunciationPlayWord: (word: string) => string;
|
|
263
|
+
pronunciationBreakUnexpected: string;
|
|
264
|
+
pronunciationBreakMissing: string;
|
|
265
|
+
/** Shown only past the caller's own monotone threshold; the SDK sets none. */
|
|
266
|
+
pronunciationMonotone: string;
|
|
267
|
+
/** Names the alphabet the phoneme symbols are written in. */
|
|
268
|
+
pronunciationIpaNote: string;
|
|
269
|
+
/** Jump buttons, e.g. "Back 10 seconds". */
|
|
270
|
+
videoBack: (seconds: number) => string;
|
|
271
|
+
videoForward: (seconds: number) => string;
|
|
272
|
+
/** The centre button once the video has ended. */
|
|
273
|
+
videoReplay: string;
|
|
274
|
+
/** The time readout flips between elapsed and remaining; these name what a press shows next. */
|
|
275
|
+
videoShowRemaining: string;
|
|
276
|
+
videoShowElapsed: string;
|
|
277
|
+
/** The captions button, named by what a press does. */
|
|
278
|
+
videoCaptionsShow: string;
|
|
279
|
+
videoCaptionsHide: string;
|
|
280
|
+
/** In the captions menu when a caption file could not be read. */
|
|
281
|
+
videoCaptionsFailed: string;
|
|
282
|
+
/** The same, for the second language: the first line keeps working. */
|
|
283
|
+
videoSecondaryCaptionsFailed: string;
|
|
284
|
+
/** A speed in the speed menu, e.g. "1.25×". */
|
|
285
|
+
videoSpeedValue: (rate: number) => string;
|
|
286
|
+
videoSettings: string;
|
|
287
|
+
videoCaptionLanguage: string;
|
|
288
|
+
/** The captions page's item, and page, for a second caption line under the first. */
|
|
289
|
+
videoSecondCaptionLanguage: string;
|
|
227
290
|
/**
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
291
|
+
* The captions row of the settings menu when two languages show, from the
|
|
292
|
+
* tracks' own labels: "English + Español". A translation may order them as its
|
|
293
|
+
* language reads, but the first argument is always the upper line.
|
|
231
294
|
*/
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
|
|
295
|
+
videoCaptionPair: (primary: string, secondary: string) => string;
|
|
296
|
+
/** Announced when Shift + C finds no other language to show. */
|
|
297
|
+
videoNoSecondLanguage: string;
|
|
298
|
+
videoCaptionSize: string;
|
|
299
|
+
videoCaptionSizeValue: (size: CaptionSize) => string;
|
|
300
|
+
videoCaptionBackground: string;
|
|
301
|
+
videoKeyboardShortcuts: string;
|
|
302
|
+
videoShortcutList: string;
|
|
303
|
+
/** Closes the shortcut list. */
|
|
304
|
+
videoClose: string;
|
|
305
|
+
/** A setting's state in the settings menu. */
|
|
306
|
+
videoOn: string;
|
|
307
|
+
videoOff: string;
|
|
308
|
+
/** What each row of the shortcut list does. */
|
|
309
|
+
videoShortcut: (action: VideoShortcutAction) => string;
|
|
310
|
+
/** The button that opens the panel below the video, and its two tabs. */
|
|
311
|
+
videoPanel: string;
|
|
312
|
+
videoContents: string;
|
|
313
|
+
videoTranscript: string;
|
|
314
|
+
videoTranscriptSearch: string;
|
|
315
|
+
videoTranscriptNoMatch: string;
|
|
316
|
+
videoPictureInPicture: string;
|
|
317
|
+
videoFullscreen: string;
|
|
318
|
+
videoExitFullscreen: string;
|
|
235
319
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
* this, so an existing 0.8.x call site keeps behaving as it did.
|
|
320
|
+
* Why the video will not play, by `MediaError.code` (1 aborted, 2 network,
|
|
321
|
+
* 3 decode, 4 source), with 0 for anything else.
|
|
239
322
|
*/
|
|
240
|
-
|
|
241
|
-
|
|
323
|
+
videoError: (code: number) => string;
|
|
324
|
+
videoTryAgain: string;
|
|
325
|
+
/** A quiz with no title of its own. */
|
|
326
|
+
videoQuiz: string;
|
|
327
|
+
/** Where the learner is inside a quiz, e.g. "Question 2 of 5". */
|
|
328
|
+
videoQuestionProgress: (index: number, total: number) => string;
|
|
329
|
+
/** A step of the quiz's step indicator, for assistive technology. */
|
|
330
|
+
videoQuestionStep: (index: number, answered: boolean) => string;
|
|
331
|
+
videoNextQuestion: string;
|
|
332
|
+
videoContinue: string;
|
|
333
|
+
videoSkipQuiz: string;
|
|
334
|
+
videoRewatch: string;
|
|
335
|
+
/** Exam mode, once a question is submitted: saved, not graded. */
|
|
336
|
+
videoAnswerSaved: string;
|
|
337
|
+
/** The end card, while an answer is still being stored or graded: why Finish waits. */
|
|
338
|
+
videoAnswerPending: string;
|
|
339
|
+
videoRequired: string;
|
|
340
|
+
/** A quiz's progress in the contents panel and on the end screen. */
|
|
341
|
+
videoQuizProgress: (answered: number, total: number) => string;
|
|
342
|
+
/** Announced when a quiz opens. */
|
|
343
|
+
videoQuizOpened: (time: string, questions: number) => string;
|
|
344
|
+
/** Announced when a seek stopped at a required quiz. */
|
|
345
|
+
videoHeldAtQuiz: (time: string) => string;
|
|
346
|
+
/** Announced when a seek stopped at the furthest point reached. */
|
|
347
|
+
videoHeldAhead: string;
|
|
348
|
+
videoEnded: string;
|
|
349
|
+
/** Practice end screen: questions answered, and the score. */
|
|
350
|
+
videoEndScore: (answered: number, total: number, percent: number) => string;
|
|
351
|
+
/** Exam end screen: questions answered, no score. */
|
|
352
|
+
videoEndAnswered: (answered: number, total: number) => string;
|
|
353
|
+
videoWatchAgain: string;
|
|
354
|
+
videoFinish: string;
|
|
242
355
|
/**
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
* that help off. Nothing AI appears in `exam`, and an item whose author
|
|
246
|
-
* switched a feature off (`data.ai`) never shows it. See {@link LearnerAi}.
|
|
356
|
+
* `<ActivityPreview>`, in place of a draft that still has something missing.
|
|
357
|
+
* Author-facing, not learner-facing — but an editor is translated too.
|
|
247
358
|
*/
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
|
|
359
|
+
previewIncomplete: string;
|
|
360
|
+
/** `<ActivityPreview>`, in place of a draft with something wrong in it. */
|
|
361
|
+
previewInvalid: string;
|
|
251
362
|
/**
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* subtree without changing the words.
|
|
257
|
-
*
|
|
258
|
-
* It is NOT `data.locale`, which labels xAPI statements — and, on a
|
|
259
|
-
* dictation, places the dictation's own words: `<Dictation>` puts it, and
|
|
260
|
-
* the direction it names, on its title, hints, marks and solution. Other
|
|
261
|
-
* authored content in another language belongs on `stimulus.locale`, which
|
|
262
|
-
* `<StimulusPanel>` puts on the passage alone.
|
|
363
|
+
* Accessible name of a provider iframe whose author supplied no `alt`.
|
|
364
|
+
* `MediaSchema` requires `alt` on an embed, so validated content never
|
|
365
|
+
* reaches this — but `validateActivity` is opt-in, and a screen-reader
|
|
366
|
+
* learner meeting an unvalidated one should not meet it in English.
|
|
263
367
|
*/
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
*/
|
|
294
|
-
|
|
368
|
+
embeddedMedia: string;
|
|
369
|
+
/**
|
|
370
|
+
* Accessible name of the stimulus region when the author gave it no title.
|
|
371
|
+
* Keyed by `Stimulus['kind']`, because "Recording" and "Passage" do not share
|
|
372
|
+
* a gender or an article in every language.
|
|
373
|
+
*/
|
|
374
|
+
stimulusKind: Record<Stimulus['kind'], string>;
|
|
375
|
+
/**
|
|
376
|
+
* How far the material carries, e.g. "Questions 3–8". Called with equal
|
|
377
|
+
* `first` and `last` for a single question, so a translation can choose its
|
|
378
|
+
* own singular rather than receive a pre-pluralised English one.
|
|
379
|
+
*/
|
|
380
|
+
stimulusRange: (first: number, last: number) => string;
|
|
381
|
+
/** Production fallback when an activity throws during render. */
|
|
382
|
+
activityFailed: string;
|
|
383
|
+
/** The same, when the activity's title is known. */
|
|
384
|
+
activityFailedNamed: (title: string) => string;
|
|
385
|
+
/** Production fallback when the activity has no title to name. */
|
|
386
|
+
activityFailedUnnamed: string;
|
|
387
|
+
/** The button that asks for an explanation of a graded answer. */
|
|
388
|
+
aiExplain: string;
|
|
389
|
+
/** The same button while the explanation is being written. */
|
|
390
|
+
aiExplaining: string;
|
|
391
|
+
/** The heading over an explanation, and its accessible name. */
|
|
392
|
+
aiExplanation: string;
|
|
393
|
+
/** When no explanation can be shown: the port failed, or its answer was refused. */
|
|
394
|
+
aiExplanationUnavailable: string;
|
|
395
|
+
/** The button that asks for a hint. */
|
|
396
|
+
aiHint: string;
|
|
397
|
+
/** The same button while the hint is being written. */
|
|
398
|
+
aiHintLoading: string;
|
|
399
|
+
/** The label before each hint. */
|
|
400
|
+
aiHintNumber: (hintNumber: number) => string;
|
|
401
|
+
/** The accessible name of the list of hints. */
|
|
402
|
+
aiHints: string;
|
|
403
|
+
/** When no hint can be shown: the port failed, or its hint gave the answer away. */
|
|
404
|
+
aiHintUnavailable: string;
|
|
405
|
+
/** In place of the hint button once every hint has been used. */
|
|
406
|
+
aiNoMoreHints: string;
|
|
407
|
+
/** Beneath every AI text: who wrote it. */
|
|
408
|
+
aiNotice: string;
|
|
409
|
+
/**
|
|
410
|
+
* The listening-paper transport. Shipped in 0.8.0 as its own `mediaStrings`
|
|
411
|
+
* prop, which still works — this is the same shape, reachable from the one
|
|
412
|
+
* provider so a consumer does not wire two mechanisms.
|
|
413
|
+
*/
|
|
414
|
+
media: MediaTransportStrings;
|
|
415
|
+
}
|
|
416
|
+
/** A partial override, nested one level for {@link LkStrings.media}. */
|
|
417
|
+
type LkStringsOverride = Partial<Omit<LkStrings, 'media' | 'stimulusKind'>> & {
|
|
418
|
+
media?: Partial<MediaTransportStrings>;
|
|
419
|
+
stimulusKind?: Partial<Record<Stimulus['kind'], string>>;
|
|
420
|
+
};
|
|
295
421
|
/**
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
* Needed for the same reason and no other. `redactItemGroup` returns
|
|
300
|
-
* `ItemGroup<RedactedActivityData>`, and `RedactedActivityData` is an
|
|
301
|
-
* index-signature type whose fields are all `unknown` — so its `question` is
|
|
302
|
-
* not a `string` and it satisfies no per-type renderable, however the prop is
|
|
303
|
-
* widened. Widening alone cannot fix this; a crossing point is required, and
|
|
304
|
-
* having exactly one keeps `as unknown as` out of consumer code.
|
|
305
|
-
*
|
|
306
|
-
* ```tsx
|
|
307
|
-
* const entries = await fetchExam(); // redacted, server-side
|
|
308
|
-
* <ActivitySequence
|
|
309
|
-
* activities={asRenderableSequence(entries)}
|
|
310
|
-
* renderMode="exam" // REQUIRED: see below
|
|
311
|
-
* shuffleSeed={attemptId}
|
|
312
|
-
* onSubmit={persist}
|
|
313
|
-
* />
|
|
314
|
-
* ```
|
|
315
|
-
*
|
|
316
|
-
* Pass `renderMode="exam"` (or `"review"`). Redacted data has no answer key, and
|
|
317
|
-
* almost every built-in activity throws at render in the default `practice`
|
|
318
|
-
* mode rather than fail later: `<MultipleChoice>`, `<FillInTheBlanks>`,
|
|
319
|
-
* `<GapSelect>` and `<Dictation>` because they grade locally, and
|
|
320
|
-
* `<WrittenResponse>` because `practice` still runs its local submit path and
|
|
321
|
-
* emits a practice-mode xAPI statement.
|
|
322
|
-
*
|
|
323
|
-
* `<ReadAloud>` is the exception and renders a projection in every mode: a
|
|
324
|
-
* read-aloud item has no answer key to withhold, so `redact()` removes only the
|
|
325
|
-
* authored feedback and leaves the reading, its language and its bounds — which
|
|
326
|
-
* is everything the component puts on screen. It grades nothing locally either;
|
|
327
|
-
* the grade comes back from whatever the application asked to judge the take.
|
|
422
|
+
* English defaults, byte-identical to what every component rendered before the
|
|
423
|
+
* strings surface existed. Supplying no override changes nothing on screen.
|
|
328
424
|
*/
|
|
329
|
-
declare
|
|
330
|
-
/** Structural shape of a `redactItemGroup()` projection, as it arrives from a server. */
|
|
331
|
-
type RedactedItemGroupData = ItemGroup<RedactedActivityData> & {
|
|
332
|
-
redacted: true;
|
|
333
|
-
};
|
|
425
|
+
declare const DEFAULT_STRINGS: LkStrings;
|
|
334
426
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
* Words, not characters: the `m:ss / m:ss` clock is digits and punctuation and
|
|
338
|
-
* is formatted by the component, because it reads identically in every locale
|
|
339
|
-
* this SDK targets. The scrubber's SPOKEN value does have a word in it and does
|
|
340
|
-
* have a key ({@link MediaTransportStrings.timeValue}).
|
|
427
|
+
* Layers an override onto a base dictionary.
|
|
341
428
|
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
* Spanish panel was not acceptable. Supplying any of them also sets `lang` on
|
|
346
|
-
* the transport chrome, so a screen reader does not read the SDK's own words
|
|
347
|
-
* with the passage's phonetics.
|
|
429
|
+
* Two levels deep and no further, because the shape is two levels deep. A
|
|
430
|
+
* generic deep merge would silently accept a nested object where a function
|
|
431
|
+
* belongs and fail at render instead of at the call site.
|
|
348
432
|
*/
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
pause: string;
|
|
352
|
-
preparing: string;
|
|
353
|
-
mute: string;
|
|
354
|
-
unmute: string;
|
|
355
|
-
volume: string;
|
|
356
|
-
speed: string;
|
|
357
|
-
seek: string;
|
|
358
|
-
/**
|
|
359
|
-
* Spoken value of the scrubber, e.g. `('1:05', '4:30') => '1:05 of 4:30'`.
|
|
360
|
-
* Takes ALREADY-FORMATTED `m:ss` strings: a translation should not have to
|
|
361
|
-
* reimplement the clock to change the word between them.
|
|
362
|
-
*/
|
|
363
|
-
timeValue: (elapsed: string, duration: string) => string;
|
|
364
|
-
/** e.g. `(1, 2) => '1 of 2 plays remaining'`. */
|
|
365
|
-
playsRemaining: (remaining: number, max: number) => string;
|
|
366
|
-
noPlaysRemaining: string;
|
|
367
|
-
/** Shown before the LAST play is spent, so a stray press cannot cost it. */
|
|
368
|
-
lastPlayConfirm: string;
|
|
369
|
-
lastPlayStart: string;
|
|
370
|
-
lastPlayCancel: string;
|
|
371
|
-
seekBlocked: string;
|
|
372
|
-
rateBlocked: string;
|
|
373
|
-
playFailed: string;
|
|
374
|
-
}
|
|
433
|
+
declare function mergeStrings(base: LkStrings, override?: LkStringsOverride): LkStrings;
|
|
434
|
+
|
|
375
435
|
/**
|
|
376
|
-
*
|
|
436
|
+
* What an application's assessor came back with.
|
|
377
437
|
*
|
|
378
|
-
*
|
|
379
|
-
* the
|
|
438
|
+
* `code` is a plain `string` and not `SpeechUnscorableCode`: an application's
|
|
439
|
+
* own assessor may refuse a take for a reason the SDK has no name for, and a
|
|
440
|
+
* narrower type would make it unreportable. This component classifies the two
|
|
441
|
+
* codes that mean "nothing was heard" and treats every other one the same way.
|
|
380
442
|
*/
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
onPlayRefunded?: (claim: MediaPlayClaim) => void;
|
|
394
|
-
onPosition?: (key: string, seconds: number) => void;
|
|
395
|
-
/** Translations for the transport chrome. */
|
|
396
|
-
strings?: Partial<MediaTransportStrings>;
|
|
397
|
-
}
|
|
443
|
+
type ReadAloudAssessResult = {
|
|
444
|
+
status: 'graded';
|
|
445
|
+
assessment: SpeechAssessment;
|
|
446
|
+
grade: GradeRecord;
|
|
447
|
+
} | {
|
|
448
|
+
status: 'unscorable';
|
|
449
|
+
code: string;
|
|
450
|
+
assessment?: SpeechAssessment;
|
|
451
|
+
} | {
|
|
452
|
+
status: 'failed';
|
|
453
|
+
retryable: boolean;
|
|
454
|
+
};
|
|
398
455
|
/**
|
|
399
|
-
*
|
|
456
|
+
* How a take reaches the application, and how a judgement comes back.
|
|
457
|
+
*
|
|
458
|
+
* Named `recordingBinding` rather than `recording` because both
|
|
459
|
+
* `ReadAloudData.recording` (the bounds) and `ReadAloudLearnerResponse.recording`
|
|
460
|
+
* (the stored take) already exist: the plain name would read as one of those.
|
|
400
461
|
*/
|
|
401
|
-
interface
|
|
462
|
+
interface RecordingBinding {
|
|
402
463
|
/**
|
|
403
|
-
*
|
|
404
|
-
*
|
|
464
|
+
* Puts the take in your storage and returns its key. **Required** outside
|
|
465
|
+
* `review`: without it a learner can speak into a control that submits
|
|
466
|
+
* nothing. Settle it — a promise that never does leaves the submit pending
|
|
467
|
+
* for ever.
|
|
405
468
|
*/
|
|
406
|
-
|
|
469
|
+
upload(take: RecordedTake): Promise<RecordingRef>;
|
|
407
470
|
/**
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
* their focus, their scroll position and an unsaved answer.
|
|
471
|
+
* Judges the stored take. `practice` only — an exam never assesses on the
|
|
472
|
+
* client. Optional: a binding without it records, uploads and submits, and
|
|
473
|
+
* the feedback is replaced by a notice.
|
|
412
474
|
*/
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
|
|
475
|
+
assess?(ref: RecordingRef): Promise<ReadAloudAssessResult>;
|
|
476
|
+
/** A playable link to a stored take. `review` only. */
|
|
477
|
+
playbackUrl?(ref: RecordingRef): Promise<string>;
|
|
478
|
+
}
|
|
479
|
+
interface ReadAloudProps extends ActivityProps<ReadAloudData> {
|
|
480
|
+
/** How a take is stored and judged. See {@link RecordingBinding}. */
|
|
481
|
+
recordingBinding?: RecordingBinding;
|
|
416
482
|
/**
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
* - **Return nothing (optimistic).** Playback starts immediately and the
|
|
422
|
-
* count is only as durable as your write. **Do not debounce this, and do
|
|
423
|
-
* not batch it with the answer autosave** — an eight-second debounce is
|
|
424
|
-
* exactly long enough to start a third play and hard-reload. A `pagehide`
|
|
425
|
-
* beacon is a backstop, not the mechanism. A crash between this call and
|
|
426
|
-
* your write landing RETURNS the play to the learner; that is the honest
|
|
427
|
-
* description of what you are buying.
|
|
428
|
-
* - **Return a promise (confirmed).** Playback is held — the button reads
|
|
429
|
-
* "Preparing…" and is `aria-busy` — until it settles. Resolve with
|
|
430
|
-
* `{ playsUsed }` from an ATOMIC server write (`UPDATE … SET plays = plays
|
|
431
|
-
* + 1 … RETURNING plays`, or a compare-and-set on
|
|
432
|
-
* `claim.previousPlaysUsed`). A resolved count above `maxPlays` refuses the
|
|
433
|
-
* play, which is how a second tab is caught: two mounts both seeded at 0
|
|
434
|
-
* both claim 1, and only an atomic increment can tell them apart. A
|
|
435
|
-
* rejection charges nothing and lets the learner retry. This is the only
|
|
436
|
-
* tier in which "consumed before audible" is true of storage rather than
|
|
437
|
-
* only of memory; use it for summative papers.
|
|
438
|
-
*
|
|
439
|
-
* Never settle the promise and the learner cannot play at all: settle it.
|
|
483
|
+
* The evidence behind a stored grade, for `review`. Without it a graded
|
|
484
|
+
* outcome still shows its marks, rebuilt from `outcome.grade.details`; with
|
|
485
|
+
* neither, the score is shown and no marks are.
|
|
440
486
|
*/
|
|
441
|
-
|
|
487
|
+
assessment?: SpeechAssessment;
|
|
442
488
|
/**
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
* cause. Supply it to give the play back, decrementing with a compare-and-set
|
|
446
|
-
* on `claim.playsUsed`. Omit it and the play stays spent: the SDK will not
|
|
447
|
-
* decrement a ledger it has no channel to correct.
|
|
489
|
+
* Confidence above which a break note is shown, 0..1, forwarded to
|
|
490
|
+
* {@link PronunciationFeedback}. No default — see its own prop.
|
|
448
491
|
*/
|
|
449
|
-
|
|
492
|
+
breakThreshold?: number;
|
|
493
|
+
/** The same, for the monotone note. */
|
|
494
|
+
monotoneThreshold?: number;
|
|
450
495
|
/**
|
|
451
|
-
*
|
|
452
|
-
* for
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
* minutes is not.
|
|
496
|
+
* The URL of a self-hosted copy of `CAPTURE_PROCESSOR_SOURCE`, handed to the
|
|
497
|
+
* recorder, for a Content-Security-Policy whose `script-src` does not allow
|
|
498
|
+
* `blob:`. Without it the capture module is loaded from a `blob:` URL, and a
|
|
499
|
+
* policy that refuses one sends every take through the deprecated
|
|
500
|
+
* main-thread `ScriptProcessorNode` — a take that still records, so nothing
|
|
501
|
+
* says so. See `useSpeechRecorder`'s option of the same name.
|
|
458
502
|
*/
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
|
|
503
|
+
workletUrl?: string;
|
|
504
|
+
/**
|
|
505
|
+
* Never called. A read-aloud answer is a key in your storage, and there is
|
|
506
|
+
* none until the take has been uploaded — so there is no intermediate
|
|
507
|
+
* response to report, and reporting the take itself would offer a host a
|
|
508
|
+
* recording it has nowhere to put. `onSubmit` carries the response, once.
|
|
509
|
+
*/
|
|
510
|
+
onChange?: (response: LearnerResponse) => void;
|
|
462
511
|
}
|
|
512
|
+
|
|
463
513
|
/**
|
|
464
|
-
*
|
|
514
|
+
* How an activity is being presented. This is the single switch that decides
|
|
515
|
+
* whether the component may grade, reveal, or submit — the three things a
|
|
516
|
+
* summative exam must take away from the client.
|
|
465
517
|
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
518
|
+
* - `practice` (default, and the only behaviour before `renderMode` existed):
|
|
519
|
+
* the component owns the attempt. It
|
|
520
|
+
* scores locally on submit, reveals correctness and feedback, and calls
|
|
521
|
+
* `onComplete` with a full {@link ActivityResult}.
|
|
522
|
+
* - `exam`: the component NEVER scores and NEVER reveals correctness. Submit
|
|
523
|
+
* emits the raw learner response through `onSubmit`; the server grades it.
|
|
524
|
+
* Safe to render redacted data, because nothing here needs the answer key.
|
|
525
|
+
* - `review`: read-only. Renders the learner's submitted answer, and marks
|
|
526
|
+
* correctness only from an `outcome` the caller supplies (which the server
|
|
527
|
+
* computed). Nothing is submittable.
|
|
470
528
|
*/
|
|
471
|
-
|
|
472
|
-
/** Identity from `flattenSequence` — stable under shuffling. Store against THIS. */
|
|
473
|
-
slotId: string;
|
|
474
|
-
/** Presented position, which moves under shuffling. */
|
|
475
|
-
index: number;
|
|
476
|
-
activityId: string;
|
|
477
|
-
}
|
|
529
|
+
type RenderMode = 'practice' | 'exam' | 'review';
|
|
478
530
|
/**
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
* precedent ({@link SequenceMediaBudget} beside {@link MediaBudgetBinding}).
|
|
484
|
-
* The difference is the second argument: a per-slot binding knows which slot it
|
|
485
|
-
* belongs to, so a sequence-level one is told.
|
|
486
|
-
*
|
|
487
|
-
* The SDK records and hands over; it never judges. `assess` is `practice` only
|
|
488
|
-
* and optional — a sequence that only stores takes gets the "assessment is not
|
|
489
|
-
* available" notice rather than a throw — and `playbackUrl` is `review` only.
|
|
490
|
-
* A slot whose activity is not a read-aloud never calls any of them.
|
|
531
|
+
* Makes the answer-key-bearing parts of an activity optional, so a component
|
|
532
|
+
* can render either full activity data OR a `redact()` projection with the
|
|
533
|
+
* same props. Fields the SDK classifies `answer-key` become optional here;
|
|
534
|
+
* a component in `exam` mode must not read them at all.
|
|
491
535
|
*/
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
*/
|
|
498
|
-
upload(take: RecordedTake, slot: SequenceRecordingSlot): Promise<RecordingRef>;
|
|
499
|
-
/** Judges the stored take. `practice` only; an exam never assesses on the client. */
|
|
500
|
-
assess?(ref: RecordingRef, slot: SequenceRecordingSlot): Promise<ReadAloudAssessResult>;
|
|
501
|
-
/** A playable link to a stored take. `review` only. */
|
|
502
|
-
playbackUrl?(ref: RecordingRef, slot: SequenceRecordingSlot): Promise<string>;
|
|
503
|
-
}
|
|
504
|
-
|
|
536
|
+
type Renderable<TData> = Omit<TData, 'scoringStrategy'> & {
|
|
537
|
+
scoringStrategy?: unknown;
|
|
538
|
+
/** Present on a `redact()` projection. */
|
|
539
|
+
redacted?: true;
|
|
540
|
+
};
|
|
505
541
|
/**
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
* Until 0.8.1 these were literals, and `locale` set only the `lang` attribute —
|
|
509
|
-
* so a learner sitting a Spanish paper read a Spanish passage inside an English
|
|
510
|
-
* scaffold, and "No plays remaining" on a listening exam was untranslatable.
|
|
511
|
-
* This is the whole surface, in one place, so a consumer can replace it.
|
|
542
|
+
* {@link Renderable} DISTRIBUTED over a union of activity types.
|
|
512
543
|
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
544
|
+
* This distinction is load-bearing, not cosmetic. `Renderable<T>` is built on
|
|
545
|
+
* `Omit`, and `Omit` does not distribute: `Omit<A | B, K>` collapses to the
|
|
546
|
+
* keys A and B have IN COMMON, so `Renderable<ActivityData>` is a single
|
|
547
|
+
* object type carrying only the fields every activity shares. Narrowing it
|
|
548
|
+
* dies with it — after `if (data.type === 'multiple-choice')` the compiler
|
|
549
|
+
* still refuses `data.options`, because the union it would narrow to no
|
|
550
|
+
* longer exists.
|
|
517
551
|
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
552
|
+
* The conditional below re-distributes, so `RenderableActivity` is a real
|
|
553
|
+
* union of per-type renderables and `.type` narrows again. Anything that
|
|
554
|
+
* accepts "some renderable activity, I don't know which" — a custom renderer,
|
|
555
|
+
* a sequence entry — must use THIS, not `Renderable<ActivityData>`.
|
|
556
|
+
*/
|
|
557
|
+
type RenderableActivity<TData extends ActivityData = ActivityData> = TData extends unknown ? Renderable<TData> : never;
|
|
558
|
+
/**
|
|
559
|
+
* Sanitiser for author-supplied rich text (`questionHtml`, `promptHtml`, and a
|
|
560
|
+
* stimulus's `bodyHtml`). The SDK deliberately ships NO sanitiser — that would
|
|
561
|
+
* add a dependency and, worse, a false promise. Rich text is rendered only when
|
|
562
|
+
* you supply this function; without it the component falls back to the
|
|
563
|
+
* plain-text field, which is always escaped. Fail-safe by construction: the SDK
|
|
564
|
+
* never injects HTML it was not explicitly given a sanitiser for.
|
|
522
565
|
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
566
|
+
* `FillInTheBlanks.passageHtml` is the one exception, and is **never**
|
|
567
|
+
* rendered: the passage hosts the answer inputs, so it cannot be split at the
|
|
568
|
+
* `{{blank}}` placeholders without voiding the sanitiser. The plain `passage`
|
|
569
|
+
* is always used, and passing `passageHtml` warns in development.
|
|
570
|
+
*/
|
|
571
|
+
type HtmlSanitizer = (html: string) => string;
|
|
572
|
+
/**
|
|
573
|
+
* The prop contract shared by every activity component (Req 3.1). Defined
|
|
574
|
+
* here (React-specific) rather than in lk-core, which is React-free.
|
|
526
575
|
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
576
|
+
* Controlled / uncontrolled follows the React convention: pass `value` +
|
|
577
|
+
* `onChange` to own the learner's answer (restore an in-progress attempt,
|
|
578
|
+
* autosave a delta, drive a review); pass `defaultValue` to seed an
|
|
579
|
+
* uncontrolled component; pass neither to keep the pre-2.1.0 behaviour, where
|
|
580
|
+
* the component owns the answer outright.
|
|
531
581
|
*/
|
|
532
|
-
interface
|
|
533
|
-
/**
|
|
534
|
-
|
|
535
|
-
/** Fill-in-the-Blanks in `practice`: the learner is checking their own work. */
|
|
536
|
-
checkAnswers: string;
|
|
537
|
-
/** Fill-in-the-Blanks in `exam`: nothing is checked on the client. */
|
|
538
|
-
submitAnswers: string;
|
|
539
|
-
/**
|
|
540
|
-
* The scored result, read by a screen reader on submit.
|
|
541
|
-
*
|
|
542
|
-
* `percent` is already rounded to a whole number. The authored overall
|
|
543
|
-
* feedback is appended SEPARATELY by the component, so a translation of this
|
|
544
|
-
* sentence never has to carry the author's words — the two were previously
|
|
545
|
-
* concatenated into one literal, which made the score sentence untranslatable
|
|
546
|
-
* without also touching authored content.
|
|
547
|
-
*/
|
|
548
|
-
scoreAnnouncement: (percent: number, passed: boolean) => string;
|
|
549
|
-
/** Prefix announced on submit, before the score sentence. */
|
|
550
|
-
answerSubmitted: string;
|
|
551
|
-
/** An answer submitted for grading that has none yet. Never rendered as 0%. */
|
|
552
|
-
notGradedYet: string;
|
|
553
|
-
/** An outcome that carries no grade and never will. */
|
|
554
|
-
noGradeAvailable: string;
|
|
555
|
-
showFeedback: string;
|
|
556
|
-
hideFeedback: string;
|
|
557
|
-
/** Accessible name of a blank's input. `ordinal` is 1-based. */
|
|
558
|
-
blankLabel: (ordinal: number) => string;
|
|
559
|
-
/** Accessible name of the selector for one gap in a Gap Select passage. */
|
|
560
|
-
gapLabel: (ordinal: number) => string;
|
|
561
|
-
/**
|
|
562
|
-
* The empty first entry of a gap's selector — how a learner leaves a gap
|
|
563
|
-
* alone, and how they take an answer back. Never a real choice.
|
|
564
|
-
*/
|
|
565
|
-
gapPlaceholder: string;
|
|
566
|
-
showHint: string;
|
|
567
|
-
hideHint: string;
|
|
568
|
-
/**
|
|
569
|
-
* Announced when an essay is handed in. Distinct from {@link LkStrings.awaitingGrade},
|
|
570
|
-
* which is the `review`-mode state of a submission made earlier: this one
|
|
571
|
-
* confirms the act, and is the only thing a learner hears at the moment they
|
|
572
|
-
* commit work that nothing on the client will grade.
|
|
573
|
-
*/
|
|
574
|
-
responseSubmitted: string;
|
|
575
|
-
/** Live word counter. Needs the count for languages whose plural rules differ. */
|
|
576
|
-
wordCount: (count: number) => string;
|
|
577
|
-
/** The authored bounds, e.g. "50–200 words". `min` is 0 when unbounded below. */
|
|
578
|
-
wordBounds: (min: number, max: number) => string;
|
|
579
|
-
/** A rubric criterion the grader marked not applicable. */
|
|
580
|
-
notApplicable: string;
|
|
581
|
-
/** A returned grade flagged for a human to look at — learner-visible by design. */
|
|
582
|
-
awaitingHumanReview: string;
|
|
583
|
-
/** Submitted, no grade back yet. */
|
|
584
|
-
awaitingGrade: string;
|
|
585
|
-
/** The grader could not produce a grade. */
|
|
586
|
-
couldNotBeGraded: string;
|
|
587
|
-
previous: string;
|
|
588
|
-
next: string;
|
|
589
|
-
/** Progress label, also the question region's accessible name. `index` is 1-based. */
|
|
590
|
-
questionProgress: (index: number, total: number) => string;
|
|
591
|
-
/** Shown when no renderer is registered for an activity type. */
|
|
592
|
-
unsupportedActivity: string;
|
|
593
|
-
/** Accessible name of the box the learner types the recording into. */
|
|
594
|
-
dictationInputLabel: string;
|
|
595
|
-
/** Accessible name of the group holding the recording. */
|
|
596
|
-
dictationRecording: string;
|
|
597
|
-
/** Accessible name of the group holding the slower recording, and its default `alt`. */
|
|
598
|
-
dictationSlowRecording: string;
|
|
599
|
-
/** The progressive-hint button, with how many transcript words are shown so far. */
|
|
600
|
-
dictationRevealNextWord: (revealed: number, total: number) => string;
|
|
601
|
-
/** Hides every revealed hint word again. */
|
|
602
|
-
dictationResetHints: string;
|
|
603
|
-
/** The solution toggle: shows the transcript after the attempt is marked. */
|
|
604
|
-
showSolution: string;
|
|
605
|
-
hideSolution: string;
|
|
606
|
-
/** Accessible name of the revealed transcript. */
|
|
607
|
-
dictationSolutionLabel: string;
|
|
608
|
-
/** Accessible name of the word-by-word marked result. */
|
|
609
|
-
dictationMarksLabel: string;
|
|
610
|
-
/** Tells the learner the marks compare the normalised text, not what they typed. */
|
|
611
|
-
dictationDiffNote: string;
|
|
582
|
+
interface ActivityProps<TData extends ActivityData = ActivityData> {
|
|
583
|
+
/** Activity content. Accepts a `redact()` projection in `exam` mode. */
|
|
584
|
+
data: RenderableActivity<TData>;
|
|
612
585
|
/**
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
586
|
+
* Called when the component scored the attempt itself. Only ever fires in
|
|
587
|
+
* `practice` mode — in `exam` mode the client does not grade, so there is
|
|
588
|
+
* no `ActivityResult` to give you; use `onSubmit`.
|
|
616
589
|
*/
|
|
617
|
-
|
|
618
|
-
dictationWordWrong: (word: string, expected: string) => string;
|
|
619
|
-
dictationWordMissing: (expected: string) => string;
|
|
620
|
-
dictationWordExtra: (word: string) => string;
|
|
621
|
-
/** The legend rows; the character-level marks reuse the same four. */
|
|
622
|
-
dictationLegendCorrect: string;
|
|
623
|
-
dictationLegendWrong: string;
|
|
624
|
-
dictationLegendMissing: string;
|
|
625
|
-
dictationLegendExtra: string;
|
|
626
|
-
/** Marked result of an attempt with no words in it once punctuation and spacing are ignored, an empty one included. */
|
|
627
|
-
dictationNothingTyped: string;
|
|
590
|
+
onComplete?: (result: ActivityResult) => void;
|
|
628
591
|
/**
|
|
629
|
-
*
|
|
630
|
-
*
|
|
592
|
+
* Called on submit with the raw learner response and no grade. Fires in
|
|
593
|
+
* every mode, before `onComplete`, so an exam runner can persist the
|
|
594
|
+
* response and let the server score it.
|
|
631
595
|
*/
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
|
|
635
|
-
/**
|
|
636
|
-
|
|
637
|
-
/** Starts a take. */
|
|
638
|
-
readAloudRecord: string;
|
|
639
|
-
/** Ends the take in progress. */
|
|
640
|
-
readAloudStop: string;
|
|
641
|
-
/** Throws the take away so the learner can start over. */
|
|
642
|
-
readAloudRerecord: string;
|
|
643
|
-
/** Accessible name of the player for the take the learner just made. */
|
|
644
|
-
readAloudYourRecording: string;
|
|
596
|
+
onSubmit?: (response: LearnerResponse) => void;
|
|
597
|
+
/** Controlled value: the learner's current response. */
|
|
598
|
+
value?: LearnerResponse;
|
|
599
|
+
/** Initial response for an uncontrolled component (ignored when `value` is set). */
|
|
600
|
+
defaultValue?: LearnerResponse;
|
|
645
601
|
/**
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
602
|
+
* Mount the component as already submitted — read at mount only, like any
|
|
603
|
+
* `default*` prop.
|
|
604
|
+
*
|
|
605
|
+
* Restoring an attempt without it reopens a question the learner had already
|
|
606
|
+
* submitted as answerable, so on a summative paper they can change and
|
|
607
|
+
* re-submit it. `AttemptState.submittedSlotIds` is what this consumes.
|
|
650
608
|
*/
|
|
651
|
-
|
|
609
|
+
defaultSubmitted?: boolean;
|
|
610
|
+
/** Fires on every change to the learner's response. Required for a controlled component. */
|
|
611
|
+
onChange?: (response: LearnerResponse) => void;
|
|
612
|
+
/** Presentation mode. Defaults to `practice`. */
|
|
613
|
+
renderMode?: RenderMode;
|
|
652
614
|
/**
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
* have to reimplement a clock to change the word around it.
|
|
656
|
-
*
|
|
657
|
-
* Shown on screen and hidden from assistive technology: it changes every
|
|
658
|
-
* second, and a screen reader that spoke each change would speak into the
|
|
659
|
-
* take being assessed. The two sentences below are what is announced instead.
|
|
615
|
+
* Server-computed outcome, used by `review` mode to mark correctness
|
|
616
|
+
* without the client ever scoring. Ignored in other modes.
|
|
660
617
|
*/
|
|
661
|
-
|
|
662
|
-
/**
|
|
663
|
-
|
|
618
|
+
outcome?: ItemOutcome;
|
|
619
|
+
/** Renders author-supplied rich text when provided. See {@link HtmlSanitizer}. */
|
|
620
|
+
sanitizeHtml?: HtmlSanitizer;
|
|
664
621
|
/**
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
622
|
+
* Binds this activity's own `data.media` to a play budget the consumer
|
|
623
|
+
* persists. Usually supplied by `<ActivitySequence mediaBudget={…}>`; pass it
|
|
624
|
+
* yourself when rendering an activity standalone.
|
|
668
625
|
*/
|
|
669
|
-
|
|
670
|
-
/**
|
|
671
|
-
|
|
672
|
-
/** `exam` only: hands the item in with no recording at all, deliberately. */
|
|
673
|
-
readAloudSubmitWithoutRecording: string;
|
|
674
|
-
/** Pending, while the take travels to the application's storage. */
|
|
675
|
-
readAloudUploading: string;
|
|
676
|
-
/** The take never reached storage, so nothing was submitted. */
|
|
677
|
-
readAloudUploadFailed: string;
|
|
678
|
-
/** Retries whichever step just failed. */
|
|
679
|
-
readAloudTryAgain: string;
|
|
680
|
-
/** Pending, while the assessor judges the take. */
|
|
681
|
-
readAloudAssessing: string;
|
|
682
|
-
/** The binding stores recordings but judges none, so there is no feedback to show. */
|
|
683
|
-
readAloudAssessmentUnavailable: string;
|
|
684
|
-
/** The assessor found no speech, or too little of it, in the take. */
|
|
685
|
-
readAloudNotHeard: string;
|
|
686
|
-
/** The assessor produced no grade for any other reason. The code itself is never shown. */
|
|
687
|
-
readAloudNotAssessed: string;
|
|
688
|
-
/** The assessment could not be run at all — a failure of the call, not of the reading. */
|
|
689
|
-
readAloudAssessmentFailed: string;
|
|
626
|
+
mediaBudget?: MediaBudgetBinding;
|
|
627
|
+
/** Translations for the audio transport chrome. See {@link MediaTransportStrings}. */
|
|
628
|
+
mediaStrings?: Partial<MediaTransportStrings>;
|
|
690
629
|
/**
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
630
|
+
* Overrides the SDK's chrome text for this activity, layered on whatever
|
|
631
|
+
* `LkIntlProvider` supplies. `mediaStrings` still works and is merged after
|
|
632
|
+
* this, so an existing 0.8.x call site keeps behaving as it did.
|
|
694
633
|
*/
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
pronunciationFeedbackLabel: string;
|
|
698
|
-
/** Name of one scored dimension, as a learner reads it. */
|
|
699
|
-
pronunciationDimension: (dimension: ReadAloudDimension) => string;
|
|
700
|
-
/** A dimension or a word the engine did not measure. NEVER rendered as 0%. */
|
|
701
|
-
pronunciationNotAssessed: string;
|
|
702
|
-
/** Accessible name of the word-by-word marks. */
|
|
703
|
-
pronunciationWordsLabel: string;
|
|
704
|
-
/** One legend row per marking state — the same four states the marks carry. */
|
|
705
|
-
pronunciationLegend: (state: ReadAloudWordState) => string;
|
|
634
|
+
strings?: LkStringsOverride;
|
|
635
|
+
onInteraction?: (event: InteractionEvent) => void;
|
|
706
636
|
/**
|
|
707
|
-
* The
|
|
708
|
-
*
|
|
709
|
-
*
|
|
637
|
+
* The host's AI ports for this activity — an explanation after grading, hints
|
|
638
|
+
* before submit — overriding `LkAiProvider`'s. Leave a port out to switch
|
|
639
|
+
* that help off. Nothing AI appears in `exam`, and an item whose author
|
|
640
|
+
* switched a feature off (`data.ai`) never shows it. See {@link LearnerAi}.
|
|
710
641
|
*/
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
pronunciationWordInserted: (word: string) => string;
|
|
715
|
-
/** Opens one word's syllables, sounds and timings. */
|
|
716
|
-
pronunciationWordDetails: (word: string) => string;
|
|
717
|
-
pronunciationSyllables: string;
|
|
718
|
-
pronunciationPhonemes: string;
|
|
719
|
-
/** A syllable the engine also spelled out, e.g. `('ˈhæ', 'ha')`. */
|
|
720
|
-
pronunciationSyllableSpelling: (syllable: string, grapheme: string) => string;
|
|
721
|
-
/** A sound the engine named no symbol for; `ordinal` is 1-based. */
|
|
722
|
-
pronunciationPhonemePosition: (ordinal: number) => string;
|
|
723
|
-
/** Introduces the sounds the engine thought it heard in a sound's place. */
|
|
724
|
-
pronunciationHeardAs: string;
|
|
725
|
-
/** Plays one word out of the learner's own take. */
|
|
726
|
-
pronunciationPlayWord: (word: string) => string;
|
|
727
|
-
pronunciationBreakUnexpected: string;
|
|
728
|
-
pronunciationBreakMissing: string;
|
|
729
|
-
/** Shown only past the caller's own monotone threshold; the SDK sets none. */
|
|
730
|
-
pronunciationMonotone: string;
|
|
731
|
-
/** Names the alphabet the phoneme symbols are written in. */
|
|
732
|
-
pronunciationIpaNote: string;
|
|
733
|
-
/** Jump buttons, e.g. "Back 10 seconds". */
|
|
734
|
-
videoBack: (seconds: number) => string;
|
|
735
|
-
videoForward: (seconds: number) => string;
|
|
736
|
-
/** The centre button once the video has ended. */
|
|
737
|
-
videoReplay: string;
|
|
738
|
-
/** The time readout flips between elapsed and remaining; these name what a press shows next. */
|
|
739
|
-
videoShowRemaining: string;
|
|
740
|
-
videoShowElapsed: string;
|
|
741
|
-
/** The captions button, named by what a press does. */
|
|
742
|
-
videoCaptionsShow: string;
|
|
743
|
-
videoCaptionsHide: string;
|
|
744
|
-
/** In the captions menu when a caption file could not be read. */
|
|
745
|
-
videoCaptionsFailed: string;
|
|
746
|
-
/** The same, for the second language: the first line keeps working. */
|
|
747
|
-
videoSecondaryCaptionsFailed: string;
|
|
748
|
-
/** A speed in the speed menu, e.g. "1.25×". */
|
|
749
|
-
videoSpeedValue: (rate: number) => string;
|
|
750
|
-
videoSettings: string;
|
|
751
|
-
videoCaptionLanguage: string;
|
|
752
|
-
/** The captions page's item, and page, for a second caption line under the first. */
|
|
753
|
-
videoSecondCaptionLanguage: string;
|
|
642
|
+
ai?: LearnerAi;
|
|
643
|
+
/** Per-instance token overrides, applied as inline CSS vars on the root. */
|
|
644
|
+
theme?: Partial<ThemeTokens>;
|
|
754
645
|
/**
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
646
|
+
* BCP 47 tag stamped as `lang` on this component's root. This is the
|
|
647
|
+
* INTERFACE language — the SDK's own chrome renders inside that element — so
|
|
648
|
+
* it should carry the same value you give `<LkIntlProvider locale>`. Passing
|
|
649
|
+
* a different one re-declares the language of every SDK string in this
|
|
650
|
+
* subtree without changing the words.
|
|
651
|
+
*
|
|
652
|
+
* It is NOT `data.locale`, which labels xAPI statements — and, on a
|
|
653
|
+
* dictation, places the dictation's own words: `<Dictation>` puts it, and
|
|
654
|
+
* the direction it names, on its title, hints, marks and solution. Other
|
|
655
|
+
* authored content in another language belongs on `stimulus.locale`, which
|
|
656
|
+
* `<StimulusPanel>` puts on the passage alone.
|
|
758
657
|
*/
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
658
|
+
locale?: string;
|
|
659
|
+
disabled?: boolean;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Bridges a server-produced `redact()` projection into the `data` prop.
|
|
663
|
+
*
|
|
664
|
+
* `RedactedActivityData` is deliberately index-signature typed in lk-core — it
|
|
665
|
+
* proves a payload is learner-safe, not what shape it has — so TypeScript
|
|
666
|
+
* cannot know it still carries the public fields a renderer needs. This is the
|
|
667
|
+
* SDK's single, documented crossing of that gap, so an exam runner does not
|
|
668
|
+
* have to write `as unknown as` at every call site:
|
|
669
|
+
*
|
|
670
|
+
* ```tsx
|
|
671
|
+
* <MultipleChoice
|
|
672
|
+
* data={asRenderable<MultipleChoiceData>(redactedFromServer)}
|
|
673
|
+
* renderMode="exam"
|
|
674
|
+
* onSubmit={persist}
|
|
675
|
+
* />
|
|
676
|
+
* ```
|
|
677
|
+
*
|
|
678
|
+
* Safe because `exam` mode reads only public fields; the answer-key fields the
|
|
679
|
+
* type claims are exactly the ones the component is forbidden to touch there.
|
|
680
|
+
*
|
|
681
|
+
* Per-type redacted interfaces (`RedactedMultipleChoiceData`, …) ship from
|
|
682
|
+
* lk-core since 0.6.0, but they are not yet *assignable* to `data`:
|
|
683
|
+
* {@link Renderable} widens `scoringStrategy` and leaves nested answer-key
|
|
684
|
+
* fields (`options[].isCorrect`, `blanks[].acceptedAnswers`) required, so a
|
|
685
|
+
* real redacted payload still needs this bridge. Closing that gap is tracked
|
|
686
|
+
* in the roadmap.
|
|
687
|
+
*/
|
|
688
|
+
declare function asRenderable<TData extends ActivityData>(redacted: RedactedActivityData): Renderable<TData>;
|
|
689
|
+
/**
|
|
690
|
+
* The same bridge as {@link asRenderable}, for a whole sequence: activities
|
|
691
|
+
* and item groups as a server hands them over, ready for `<ActivitySequence>`.
|
|
692
|
+
*
|
|
693
|
+
* Needed for the same reason and no other. `redactItemGroup` returns
|
|
694
|
+
* `ItemGroup<RedactedActivityData>`, and `RedactedActivityData` is an
|
|
695
|
+
* index-signature type whose fields are all `unknown` — so its `question` is
|
|
696
|
+
* not a `string` and it satisfies no per-type renderable, however the prop is
|
|
697
|
+
* widened. Widening alone cannot fix this; a crossing point is required, and
|
|
698
|
+
* having exactly one keeps `as unknown as` out of consumer code.
|
|
699
|
+
*
|
|
700
|
+
* ```tsx
|
|
701
|
+
* const entries = await fetchExam(); // redacted, server-side
|
|
702
|
+
* <ActivitySequence
|
|
703
|
+
* activities={asRenderableSequence(entries)}
|
|
704
|
+
* renderMode="exam" // REQUIRED: see below
|
|
705
|
+
* shuffleSeed={attemptId}
|
|
706
|
+
* onSubmit={persist}
|
|
707
|
+
* />
|
|
708
|
+
* ```
|
|
709
|
+
*
|
|
710
|
+
* Pass `renderMode="exam"` (or `"review"`). Redacted data has no answer key, and
|
|
711
|
+
* almost every built-in activity throws at render in the default `practice`
|
|
712
|
+
* mode rather than fail later: `<MultipleChoice>`, `<FillInTheBlanks>`,
|
|
713
|
+
* `<GapSelect>` and `<Dictation>` because they grade locally, and
|
|
714
|
+
* `<WrittenResponse>` because `practice` still runs its local submit path and
|
|
715
|
+
* emits a practice-mode xAPI statement.
|
|
716
|
+
*
|
|
717
|
+
* `<ReadAloud>` is the exception and renders a projection in every mode: a
|
|
718
|
+
* read-aloud item has no answer key to withhold, so `redact()` removes only the
|
|
719
|
+
* authored feedback and leaves the reading, its language and its bounds — which
|
|
720
|
+
* is everything the component puts on screen. It grades nothing locally either;
|
|
721
|
+
* the grade comes back from whatever the application asked to judge the take.
|
|
722
|
+
*/
|
|
723
|
+
declare function asRenderableSequence(entries: readonly (RedactedActivityData | RedactedItemGroupData)[]): readonly SequenceEntry<RenderableActivity>[];
|
|
724
|
+
/** Structural shape of a `redactItemGroup()` projection, as it arrives from a server. */
|
|
725
|
+
type RedactedItemGroupData = ItemGroup<RedactedActivityData> & {
|
|
726
|
+
redacted: true;
|
|
727
|
+
};
|
|
728
|
+
/**
|
|
729
|
+
* Every word the SDK's audio transport renders.
|
|
730
|
+
*
|
|
731
|
+
* Words, not characters: the `m:ss / m:ss` clock is digits and punctuation and
|
|
732
|
+
* is formatted by the component, because it reads identically in every locale
|
|
733
|
+
* this SDK targets. The scrubber's SPOKEN value does have a word in it and does
|
|
734
|
+
* have a key ({@link MediaTransportStrings.timeValue}).
|
|
735
|
+
*
|
|
736
|
+
* Supply them to translate it. These are the highest-stakes strings on a
|
|
737
|
+
* listening paper — "No plays remaining" decides whether a learner believes
|
|
738
|
+
* they may try again — so shipping them as untranslatable English inside a
|
|
739
|
+
* Spanish panel was not acceptable. Supplying any of them also sets `lang` on
|
|
740
|
+
* the transport chrome, so a screen reader does not read the SDK's own words
|
|
741
|
+
* with the passage's phonetics.
|
|
742
|
+
*/
|
|
743
|
+
interface MediaTransportStrings {
|
|
744
|
+
play: string;
|
|
745
|
+
pause: string;
|
|
746
|
+
preparing: string;
|
|
747
|
+
mute: string;
|
|
748
|
+
unmute: string;
|
|
749
|
+
volume: string;
|
|
750
|
+
speed: string;
|
|
751
|
+
seek: string;
|
|
783
752
|
/**
|
|
784
|
-
*
|
|
785
|
-
*
|
|
753
|
+
* Spoken value of the scrubber, e.g. `('1:05', '4:30') => '1:05 of 4:30'`.
|
|
754
|
+
* Takes ALREADY-FORMATTED `m:ss` strings: a translation should not have to
|
|
755
|
+
* reimplement the clock to change the word between them.
|
|
786
756
|
*/
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
/**
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
/**
|
|
807
|
-
|
|
808
|
-
/**
|
|
809
|
-
|
|
810
|
-
/**
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
757
|
+
timeValue: (elapsed: string, duration: string) => string;
|
|
758
|
+
/** e.g. `(1, 2) => '1 of 2 plays remaining'`. */
|
|
759
|
+
playsRemaining: (remaining: number, max: number) => string;
|
|
760
|
+
noPlaysRemaining: string;
|
|
761
|
+
/** Shown before the LAST play is spent, so a stray press cannot cost it. */
|
|
762
|
+
lastPlayConfirm: string;
|
|
763
|
+
lastPlayStart: string;
|
|
764
|
+
lastPlayCancel: string;
|
|
765
|
+
seekBlocked: string;
|
|
766
|
+
rateBlocked: string;
|
|
767
|
+
playFailed: string;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Binds ONE media block to a play budget the consumer persists.
|
|
771
|
+
*
|
|
772
|
+
* The SDK refuses a play; it does not remember one. Everything durable here is
|
|
773
|
+
* the consuming application's — see {@link SequenceMediaBudget.onPlayConsumed}.
|
|
774
|
+
*/
|
|
775
|
+
interface MediaBudgetBinding {
|
|
776
|
+
/** From `slotMediaKey(slotId)` / `stimulusMediaKey(slotId)` in lk-core. */
|
|
777
|
+
key: string;
|
|
778
|
+
/** Plays already spent, and where playback stood. Read at mount only. */
|
|
779
|
+
entry?: MediaPlayLedgerEntry;
|
|
780
|
+
/** Defaults to `renderMode !== 'review'`. An explicit boolean wins either way. */
|
|
781
|
+
enforced?: boolean;
|
|
782
|
+
/** Slot context stamped onto the claim and the interaction event. */
|
|
783
|
+
slotId: string;
|
|
784
|
+
index: number;
|
|
785
|
+
activityId?: string;
|
|
786
|
+
onPlayConsumed?: (claim: MediaPlayClaim) => undefined | Promise<MediaPlayGrant | undefined>;
|
|
787
|
+
onPlayRefunded?: (claim: MediaPlayClaim) => void;
|
|
788
|
+
onPosition?: (key: string, seconds: number) => void;
|
|
789
|
+
/** Translations for the transport chrome. */
|
|
790
|
+
strings?: Partial<MediaTransportStrings>;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* The pager-level half of a play budget. One prop, because it is one concept.
|
|
794
|
+
*/
|
|
795
|
+
interface SequenceMediaBudget {
|
|
819
796
|
/**
|
|
820
|
-
*
|
|
821
|
-
*
|
|
797
|
+
* `MediaPlayLedger.entries` goes straight in. An absent key means nothing
|
|
798
|
+
* spent. Read at mount, like `responses`.
|
|
822
799
|
*/
|
|
823
|
-
|
|
824
|
-
/** `<ActivityPreview>`, in place of a draft with something wrong in it. */
|
|
825
|
-
previewInvalid: string;
|
|
800
|
+
plays?: Readonly<Record<string, MediaPlayLedgerEntry>>;
|
|
826
801
|
/**
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
*
|
|
830
|
-
*
|
|
802
|
+
* Re-seed token. Change this string and the budgets re-seed from `plays`
|
|
803
|
+
* WITHOUT remounting the pager — the invigilator path ("the audio never
|
|
804
|
+
* started, give her the play back") that would otherwise cost the learner
|
|
805
|
+
* their focus, their scroll position and an unsaved answer.
|
|
831
806
|
*/
|
|
832
|
-
|
|
807
|
+
resumeKey?: string;
|
|
808
|
+
/** Explicit override of the default (`renderMode !== 'review'`). */
|
|
809
|
+
enforced?: boolean;
|
|
833
810
|
/**
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
811
|
+
* Called the instant a play is claimed, BEFORE any audio is audible.
|
|
812
|
+
*
|
|
813
|
+
* Two tiers, chosen by what you return:
|
|
814
|
+
*
|
|
815
|
+
* - **Return nothing (optimistic).** Playback starts immediately and the
|
|
816
|
+
* count is only as durable as your write. **Do not debounce this, and do
|
|
817
|
+
* not batch it with the answer autosave** — an eight-second debounce is
|
|
818
|
+
* exactly long enough to start a third play and hard-reload. A `pagehide`
|
|
819
|
+
* beacon is a backstop, not the mechanism. A crash between this call and
|
|
820
|
+
* your write landing RETURNS the play to the learner; that is the honest
|
|
821
|
+
* description of what you are buying.
|
|
822
|
+
* - **Return a promise (confirmed).** Playback is held — the button reads
|
|
823
|
+
* "Preparing…" and is `aria-busy` — until it settles. Resolve with
|
|
824
|
+
* `{ playsUsed }` from an ATOMIC server write (`UPDATE … SET plays = plays
|
|
825
|
+
* + 1 … RETURNING plays`, or a compare-and-set on
|
|
826
|
+
* `claim.previousPlaysUsed`). A resolved count above `maxPlays` refuses the
|
|
827
|
+
* play, which is how a second tab is caught: two mounts both seeded at 0
|
|
828
|
+
* both claim 1, and only an atomic increment can tell them apart. A
|
|
829
|
+
* rejection charges nothing and lets the learner retry. This is the only
|
|
830
|
+
* tier in which "consumed before audible" is true of storage rather than
|
|
831
|
+
* only of memory; use it for summative papers.
|
|
832
|
+
*
|
|
833
|
+
* Never settle the promise and the learner cannot play at all: settle it.
|
|
837
834
|
*/
|
|
838
|
-
|
|
835
|
+
onPlayConsumed?: (claim: MediaPlayClaim) => undefined | Promise<MediaPlayGrant | undefined>;
|
|
839
836
|
/**
|
|
840
|
-
*
|
|
841
|
-
*
|
|
842
|
-
*
|
|
837
|
+
* Called when a charged play produced no audio — the element errored before
|
|
838
|
+
* playback advanced past 0.25 s, an expired signed URL being the realistic
|
|
839
|
+
* cause. Supply it to give the play back, decrementing with a compare-and-set
|
|
840
|
+
* on `claim.playsUsed`. Omit it and the play stays spent: the SDK will not
|
|
841
|
+
* decrement a ledger it has no channel to correct.
|
|
843
842
|
*/
|
|
844
|
-
|
|
845
|
-
/** Production fallback when an activity throws during render. */
|
|
846
|
-
activityFailed: string;
|
|
847
|
-
/** The same, when the activity's title is known. */
|
|
848
|
-
activityFailedNamed: (title: string) => string;
|
|
849
|
-
/** Production fallback when the activity has no title to name. */
|
|
850
|
-
activityFailedUnnamed: string;
|
|
851
|
-
/** The button that asks for an explanation of a graded answer. */
|
|
852
|
-
aiExplain: string;
|
|
853
|
-
/** The same button while the explanation is being written. */
|
|
854
|
-
aiExplaining: string;
|
|
855
|
-
/** The heading over an explanation, and its accessible name. */
|
|
856
|
-
aiExplanation: string;
|
|
857
|
-
/** When no explanation can be shown: the port failed, or its answer was refused. */
|
|
858
|
-
aiExplanationUnavailable: string;
|
|
859
|
-
/** The button that asks for a hint. */
|
|
860
|
-
aiHint: string;
|
|
861
|
-
/** The same button while the hint is being written. */
|
|
862
|
-
aiHintLoading: string;
|
|
863
|
-
/** The label before each hint. */
|
|
864
|
-
aiHintNumber: (hintNumber: number) => string;
|
|
865
|
-
/** The accessible name of the list of hints. */
|
|
866
|
-
aiHints: string;
|
|
867
|
-
/** When no hint can be shown: the port failed, or its hint gave the answer away. */
|
|
868
|
-
aiHintUnavailable: string;
|
|
869
|
-
/** In place of the hint button once every hint has been used. */
|
|
870
|
-
aiNoMoreHints: string;
|
|
871
|
-
/** Beneath every AI text: who wrote it. */
|
|
872
|
-
aiNotice: string;
|
|
843
|
+
onPlayRefunded?: (claim: MediaPlayClaim) => void;
|
|
873
844
|
/**
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
845
|
+
* Position reports, so a refresh resumes the play the learner already paid
|
|
846
|
+
* for instead of charging them again. Fires on pause, on end (with 0), and at
|
|
847
|
+
* most once per whole second of playback.
|
|
848
|
+
*
|
|
849
|
+
* **This one you MAY throttle** — the granularity you persist is the
|
|
850
|
+
* granularity of the replay a crash grants. Three seconds is sane; three
|
|
851
|
+
* minutes is not.
|
|
877
852
|
*/
|
|
878
|
-
|
|
853
|
+
onPosition?: (key: string, seconds: number) => void;
|
|
854
|
+
/** Translations for the transport chrome. Defaults are English. */
|
|
855
|
+
strings?: Partial<MediaTransportStrings>;
|
|
879
856
|
}
|
|
880
|
-
/** A partial override, nested one level for {@link LkStrings.media}. */
|
|
881
|
-
type LkStringsOverride = Partial<Omit<LkStrings, 'media' | 'stimulusKind'>> & {
|
|
882
|
-
media?: Partial<MediaTransportStrings>;
|
|
883
|
-
stimulusKind?: Partial<Record<Stimulus['kind'], string>>;
|
|
884
|
-
};
|
|
885
857
|
/**
|
|
886
|
-
*
|
|
887
|
-
*
|
|
858
|
+
* Which slot a recording belongs to, stamped on every call the pager makes.
|
|
859
|
+
*
|
|
860
|
+
* Exactly the shape {@link ActivitySequenceProps.onSubmit} already passes, so
|
|
861
|
+
* one identity travels with the answer and with the take that answer points at
|
|
862
|
+
* — and a consumer writes `slotId` in both places rather than reconciling two
|
|
863
|
+
* spellings of "which question was this".
|
|
888
864
|
*/
|
|
889
|
-
|
|
865
|
+
interface SequenceRecordingSlot {
|
|
866
|
+
/** Identity from `flattenSequence` — stable under shuffling. Store against THIS. */
|
|
867
|
+
slotId: string;
|
|
868
|
+
/** Presented position, which moves under shuffling. */
|
|
869
|
+
index: number;
|
|
870
|
+
activityId: string;
|
|
871
|
+
}
|
|
890
872
|
/**
|
|
891
|
-
*
|
|
873
|
+
* The pager-level half of a read-aloud recording binding: where every take in
|
|
874
|
+
* this sequence is stored, and how a judgement comes back.
|
|
892
875
|
*
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
*
|
|
876
|
+
* One prop at both levels, because it is one concept — the media-budget
|
|
877
|
+
* precedent ({@link SequenceMediaBudget} beside {@link MediaBudgetBinding}).
|
|
878
|
+
* The difference is the second argument: a per-slot binding knows which slot it
|
|
879
|
+
* belongs to, so a sequence-level one is told.
|
|
880
|
+
*
|
|
881
|
+
* The SDK records and hands over; it never judges. `assess` is `practice` only
|
|
882
|
+
* and optional — a sequence that only stores takes gets the "assessment is not
|
|
883
|
+
* available" notice rather than a throw — and `playbackUrl` is `review` only.
|
|
884
|
+
* A slot whose activity is not a read-aloud never calls any of them.
|
|
896
885
|
*/
|
|
897
|
-
|
|
886
|
+
interface SequenceRecordingBinding {
|
|
887
|
+
/**
|
|
888
|
+
* Puts the take in your storage and returns its key. **Required** outside
|
|
889
|
+
* `review`: without it a learner can speak into a control that submits
|
|
890
|
+
* nothing, which the component refuses to render. Settle it.
|
|
891
|
+
*/
|
|
892
|
+
upload(take: RecordedTake, slot: SequenceRecordingSlot): Promise<RecordingRef>;
|
|
893
|
+
/** Judges the stored take. `practice` only; an exam never assesses on the client. */
|
|
894
|
+
assess?(ref: RecordingRef, slot: SequenceRecordingSlot): Promise<ReadAloudAssessResult>;
|
|
895
|
+
/** A playable link to a stored take. `review` only. */
|
|
896
|
+
playbackUrl?(ref: RecordingRef, slot: SequenceRecordingSlot): Promise<string>;
|
|
897
|
+
}
|
|
898
898
|
|
|
899
899
|
export { type ActivityProps as A, type CaptionSize as C, DEFAULT_STRINGS as D, type HtmlSanitizer as H, type LkStrings as L, type MediaBudgetBinding as M, type ReadAloudAssessResult as R, type SequenceMediaBudget as S, type VideoPreferences as V, type LkStringsOverride as a, type MediaTransportStrings as b, type ReadAloudProps as c, type RecordingBinding as d, type RenderMode as e, type Renderable as f, type RenderableActivity as g, type SequenceRecordingBinding as h, type SequenceRecordingSlot as i, asRenderable as j, asRenderableSequence as k, mergeStrings as m };
|