@intellectif/lk-react 12.0.1 → 14.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.
Files changed (165) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +17 -6
  3. package/dist/{WrittenResponse-BgcpEM-e.d.ts → WrittenResponse-C0P34En1.d.ts} +1 -1
  4. package/dist/{WrittenResponse-ByzXYEoW.d.cts → WrittenResponse-pB1ik8hg.d.cts} +1 -1
  5. package/dist/chunk-36FTYJWJ.cjs +869 -0
  6. package/dist/chunk-36FTYJWJ.cjs.map +1 -0
  7. package/dist/chunk-47HHC4GD.js +763 -0
  8. package/dist/chunk-47HHC4GD.js.map +1 -0
  9. package/dist/chunk-62AKSD5Y.js +509 -0
  10. package/dist/chunk-62AKSD5Y.js.map +1 -0
  11. package/dist/{chunk-XKGBD3UK.js → chunk-72UGRIMM.js} +6 -5
  12. package/dist/chunk-72UGRIMM.js.map +1 -0
  13. package/dist/{chunk-FX7VMZHQ.cjs → chunk-B2XQLU2E.cjs} +58 -8
  14. package/dist/chunk-B2XQLU2E.cjs.map +1 -0
  15. package/dist/{chunk-ZFWC4KZC.js → chunk-BO6P723E.js} +26 -31
  16. package/dist/chunk-BO6P723E.js.map +1 -0
  17. package/dist/chunk-BRPEB43C.cjs +13 -0
  18. package/dist/chunk-BRPEB43C.cjs.map +1 -0
  19. package/dist/{chunk-6KMEB7TF.cjs → chunk-BVRUUYYR.cjs} +42 -15
  20. package/dist/chunk-BVRUUYYR.cjs.map +1 -0
  21. package/dist/{chunk-AMGK7DDM.cjs → chunk-D4DQFTA7.cjs} +2 -2
  22. package/dist/chunk-D4DQFTA7.cjs.map +1 -0
  23. package/dist/chunk-D57QZILZ.js +863 -0
  24. package/dist/chunk-D57QZILZ.js.map +1 -0
  25. package/dist/chunk-F4TBCRGI.cjs +518 -0
  26. package/dist/chunk-F4TBCRGI.cjs.map +1 -0
  27. package/dist/{chunk-V6M57YMK.js → chunk-I33FOKZ7.js} +227 -30
  28. package/dist/chunk-I33FOKZ7.js.map +1 -0
  29. package/dist/{chunk-GVBEI5R3.cjs → chunk-J2CDPIRC.cjs} +233 -36
  30. package/dist/chunk-J2CDPIRC.cjs.map +1 -0
  31. package/dist/{chunk-KYOCDRUO.cjs → chunk-LB3FEV7Z.cjs} +97 -2
  32. package/dist/chunk-LB3FEV7Z.cjs.map +1 -0
  33. package/dist/{chunk-QXYAOILF.js → chunk-LGCUOLP6.js} +6 -5
  34. package/dist/chunk-LGCUOLP6.js.map +1 -0
  35. package/dist/{chunk-GWD75Q25.js → chunk-LLXCX5KK.js} +6 -5
  36. package/dist/chunk-LLXCX5KK.js.map +1 -0
  37. package/dist/chunk-MCO52PV5.js +520 -0
  38. package/dist/chunk-MCO52PV5.js.map +1 -0
  39. package/dist/{chunk-G6PUWKQC.cjs → chunk-NDQD3ZRY.cjs} +15 -14
  40. package/dist/chunk-NDQD3ZRY.cjs.map +1 -0
  41. package/dist/chunk-OD65OZIW.cjs +159 -0
  42. package/dist/chunk-OD65OZIW.cjs.map +1 -0
  43. package/dist/chunk-RCMYEWBE.js +11 -0
  44. package/dist/chunk-RCMYEWBE.js.map +1 -0
  45. package/dist/{chunk-KSXEBV2H.cjs → chunk-RRH7ZPXC.cjs} +6 -6
  46. package/dist/{chunk-KSXEBV2H.cjs.map → chunk-RRH7ZPXC.cjs.map} +1 -1
  47. package/dist/{chunk-DQNVAXG6.js → chunk-RXS455MR.js} +2 -2
  48. package/dist/chunk-RXS455MR.js.map +1 -0
  49. package/dist/{chunk-LIHKQEIP.cjs → chunk-SE6WWDVW.cjs} +15 -14
  50. package/dist/chunk-SE6WWDVW.cjs.map +1 -0
  51. package/dist/{chunk-5IH3NRXE.js → chunk-SWY6PWXK.js} +4 -4
  52. package/dist/{chunk-5IH3NRXE.js.map → chunk-SWY6PWXK.js.map} +1 -1
  53. package/dist/chunk-TDJEKXEP.js +212 -0
  54. package/dist/chunk-TDJEKXEP.js.map +1 -0
  55. package/dist/{chunk-NK5EVL5C.cjs → chunk-U3APKQ7P.cjs} +15 -14
  56. package/dist/chunk-U3APKQ7P.cjs.map +1 -0
  57. package/dist/chunk-UDCQTOVU.cjs +523 -0
  58. package/dist/chunk-UDCQTOVU.cjs.map +1 -0
  59. package/dist/{chunk-EWTAMY6L.cjs → chunk-USBANZ7K.cjs} +27 -33
  60. package/dist/chunk-USBANZ7K.cjs.map +1 -0
  61. package/dist/chunk-WTS34S5E.cjs +765 -0
  62. package/dist/chunk-WTS34S5E.cjs.map +1 -0
  63. package/dist/{chunk-2Y6V5E7H.js → chunk-XIBOFTOL.js} +57 -7
  64. package/dist/chunk-XIBOFTOL.js.map +1 -0
  65. package/dist/{chunk-ZFSRMGWO.js → chunk-XUEIUEPI.js} +6 -5
  66. package/dist/chunk-XUEIUEPI.js.map +1 -0
  67. package/dist/{chunk-64AJVB6O.cjs → chunk-YEYKJFEC.cjs} +14 -13
  68. package/dist/chunk-YEYKJFEC.cjs.map +1 -0
  69. package/dist/{chunk-5S2YDMEJ.js → chunk-YMBBM5K7.js} +36 -9
  70. package/dist/chunk-YMBBM5K7.js.map +1 -0
  71. package/dist/chunk-YORQQXGO.js +155 -0
  72. package/dist/chunk-YORQQXGO.js.map +1 -0
  73. package/dist/components/ActivityPreview.cjs +15 -9
  74. package/dist/components/ActivityPreview.d.cts +9 -3
  75. package/dist/components/ActivityPreview.d.ts +9 -3
  76. package/dist/components/ActivityPreview.js +14 -8
  77. package/dist/components/ActivitySequence.cjs +16 -10
  78. package/dist/components/ActivitySequence.d.cts +112 -6
  79. package/dist/components/ActivitySequence.d.ts +112 -6
  80. package/dist/components/ActivitySequence.js +15 -9
  81. package/dist/components/Dictation.cjs +19 -0
  82. package/dist/components/Dictation.cjs.map +1 -0
  83. package/dist/components/Dictation.d.cts +17 -0
  84. package/dist/components/Dictation.d.ts +17 -0
  85. package/dist/components/Dictation.js +10 -0
  86. package/dist/components/Dictation.js.map +1 -0
  87. package/dist/components/FillInTheBlanks.cjs +6 -5
  88. package/dist/components/FillInTheBlanks.d.cts +2 -1
  89. package/dist/components/FillInTheBlanks.d.ts +2 -1
  90. package/dist/components/FillInTheBlanks.js +5 -4
  91. package/dist/components/GapSelect.cjs +6 -5
  92. package/dist/components/GapSelect.d.cts +2 -1
  93. package/dist/components/GapSelect.d.ts +2 -1
  94. package/dist/components/GapSelect.js +5 -4
  95. package/dist/components/MultipleChoice.cjs +6 -5
  96. package/dist/components/MultipleChoice.d.cts +2 -1
  97. package/dist/components/MultipleChoice.d.ts +2 -1
  98. package/dist/components/MultipleChoice.js +5 -4
  99. package/dist/components/PronunciationFeedback.cjs +16 -0
  100. package/dist/components/PronunciationFeedback.cjs.map +1 -0
  101. package/dist/components/PronunciationFeedback.d.cts +64 -0
  102. package/dist/components/PronunciationFeedback.d.ts +64 -0
  103. package/dist/components/PronunciationFeedback.js +7 -0
  104. package/dist/components/PronunciationFeedback.js.map +1 -0
  105. package/dist/components/ReadAloud.cjs +21 -0
  106. package/dist/components/ReadAloud.cjs.map +1 -0
  107. package/dist/components/ReadAloud.d.cts +15 -0
  108. package/dist/components/ReadAloud.d.ts +15 -0
  109. package/dist/components/ReadAloud.js +12 -0
  110. package/dist/components/ReadAloud.js.map +1 -0
  111. package/dist/components/StimulusPanel.cjs +4 -4
  112. package/dist/components/StimulusPanel.d.cts +2 -1
  113. package/dist/components/StimulusPanel.d.ts +2 -1
  114. package/dist/components/StimulusPanel.js +3 -3
  115. package/dist/components/WrittenResponse.cjs +6 -5
  116. package/dist/components/WrittenResponse.d.cts +4 -3
  117. package/dist/components/WrittenResponse.d.ts +4 -3
  118. package/dist/components/WrittenResponse.js +5 -4
  119. package/dist/hooks/useSpeechRecorder.cjs +17 -0
  120. package/dist/hooks/useSpeechRecorder.cjs.map +1 -0
  121. package/dist/hooks/useSpeechRecorder.d.cts +127 -0
  122. package/dist/hooks/useSpeechRecorder.d.ts +127 -0
  123. package/dist/hooks/useSpeechRecorder.js +4 -0
  124. package/dist/hooks/useSpeechRecorder.js.map +1 -0
  125. package/dist/i18n/LkIntlProvider.cjs +7 -7
  126. package/dist/i18n/LkIntlProvider.d.cts +3 -2
  127. package/dist/i18n/LkIntlProvider.d.ts +3 -2
  128. package/dist/i18n/LkIntlProvider.js +1 -1
  129. package/dist/index.cjs +59 -33
  130. package/dist/index.cjs.map +1 -1
  131. package/dist/index.d.cts +7 -2
  132. package/dist/index.d.ts +7 -2
  133. package/dist/index.js +17 -11
  134. package/dist/index.js.map +1 -1
  135. package/dist/machine-CuwKqWXC.d.cts +26 -0
  136. package/dist/machine-CuwKqWXC.d.ts +26 -0
  137. package/dist/{strings-CN7n-BlE.d.ts → strings--ADnHcaA.d.cts} +261 -8
  138. package/dist/{strings-CN7n-BlE.d.cts → strings-CtdSf2St.d.ts} +261 -8
  139. package/dist/theme/ThemeProvider.cjs +6 -6
  140. package/dist/theme/ThemeProvider.d.cts +4 -4
  141. package/dist/theme/ThemeProvider.d.ts +4 -4
  142. package/dist/theme/ThemeProvider.js +1 -1
  143. package/dist/theme/skin.css +1050 -3
  144. package/package.json +43 -3
  145. package/dist/chunk-2Y6V5E7H.js.map +0 -1
  146. package/dist/chunk-5S2YDMEJ.js.map +0 -1
  147. package/dist/chunk-64AJVB6O.cjs.map +0 -1
  148. package/dist/chunk-6KMEB7TF.cjs.map +0 -1
  149. package/dist/chunk-AMGK7DDM.cjs.map +0 -1
  150. package/dist/chunk-CK7Z275S.js +0 -117
  151. package/dist/chunk-CK7Z275S.js.map +0 -1
  152. package/dist/chunk-DQNVAXG6.js.map +0 -1
  153. package/dist/chunk-EWTAMY6L.cjs.map +0 -1
  154. package/dist/chunk-FX7VMZHQ.cjs.map +0 -1
  155. package/dist/chunk-G6PUWKQC.cjs.map +0 -1
  156. package/dist/chunk-GVBEI5R3.cjs.map +0 -1
  157. package/dist/chunk-GWD75Q25.js.map +0 -1
  158. package/dist/chunk-KYOCDRUO.cjs.map +0 -1
  159. package/dist/chunk-LIHKQEIP.cjs.map +0 -1
  160. package/dist/chunk-NK5EVL5C.cjs.map +0 -1
  161. package/dist/chunk-QXYAOILF.js.map +0 -1
  162. package/dist/chunk-V6M57YMK.js.map +0 -1
  163. package/dist/chunk-XKGBD3UK.js.map +0 -1
  164. package/dist/chunk-ZFSRMGWO.js.map +0 -1
  165. package/dist/chunk-ZFWC4KZC.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # @intellectif/lk-react
2
2
 
3
+ ## 14.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 7d25492: The browser half of `read-aloud`: the learner records a take, the SDK hands it to your storage and brings your server's judgement back, and the result is marked word by word. The type, the evidence shape and the grade are `@intellectif/lk-core` 0.14.0's. Nothing here judges a recording, calls a speech service, holds a key or keeps audio.
8
+
9
+ - `<ReadAloud>`, also at `@intellectif/lk-react/components/ReadAloud`: the text to read, the model recording and an optional slower one (both kept silent for as long as a take is being recorded, so the microphone never picks up the model), a recorder bounded by the item's take budget and its time limit, playback of the take before it is sent, and the result. A take leaves only through `recordingBinding`, a `RecordingBinding`:
10
+
11
+ - `upload(take)` puts the take in your storage and returns its `RecordingRef`. It is required outside `review`, and a missing one throws at render, in production too: a learner must never speak into a control that stores nothing.
12
+ - `assess(ref)` is optional and `practice` only. It returns a `ReadAloudAssessResult` — `graded` with the assessment and the grade, `unscorable` with a code, or `failed` with whether a retry makes sense. A grade calls `onComplete`. Without `assess` the take is still stored and submitted, and a notice takes the feedback's place.
13
+ - `playbackUrl(ref)` is `review` only.
14
+
15
+ In `exam` it uploads, submits and locks, and never assesses, scores, reveals or builds a statement. A take whose upload failed offers a retry and blocks submit rather than becoming a blank; a blank is handed in only through an explicit "submit without recording". In `review` it renders the stored `outcome`, marked from the `assessment` you pass or rebuilt from the grade's stored `details`. `onChange` is never called: the answer is a storage key, which exists only once the take is uploaded, and `onSubmit` carries it once. `onInteraction` reports `recording-started`, `recording-stopped`, `recording-discarded`, `recording-uploaded`, `recording-upload-failed`, `assessment-requested` and `assessment-failed`, each with `takes` and `durationMs`, and `retryable` on the two failures. The subpath exports `RecordingBinding`, `RecordedTake` and `ReadAloudAssessResult` beside the component.
16
+
17
+ - `<PronunciationFeedback>`, also at `@intellectif/lk-react/components/PronunciationFeedback`: the marks on their own, for a screen that has evidence but no activity. The four dimensions each read as a percentage or "not assessed", never 0%, and every word is `correct`, `mispronounced`, `omitted` or `inserted`, opening onto its syllables, its sounds, what was heard instead and a button that plays just that word. It checks the evidence with `validateSpeechAssessment` before aligning it: in development a refusal throws `ActivitySchemaError`, and in production the word list is left out while the grade beside it is still shown.
18
+ - `useSpeechRecorder(options)`, also at `@intellectif/lk-react/hooks/useSpeechRecorder`: microphone capture to 16 kHz mono 16-bit PCM WAV, the format `inspectWav` measures, with an input level, a minimum take length, a take budget and an auto-stop counted from the samples rather than from a timer. Support is discovered inside `start()`, which never rejects, so a server render and its hydration agree; the microphone and the audio context are released on stop, discard, error and unmount. `discard()` gives no take back, and `reset()` starts over with the budget refunded, for a recorder handed a different reading. The hook is the first entry in the bundle budgets that is not a component, with a limit of its own: 10 kB.
19
+ - `CAPTURE_PROCESSOR_SOURCE` is exported beside the hook, for a Content-Security-Policy whose `script-src` does not allow `blob:`. Serve it from a URL your policy allows and pass that URL as `workletUrl`; the contract a module there must meet is written on the constant. A `workletUrl` that fails to load is not reported: the hook records through the main-thread `ScriptProcessorNode` instead, so check that the module is fetched.
20
+ - `<ActivitySequence>` renders a `read-aloud` slot through `<ReadAloud>`, and takes two new props:
21
+
22
+ - `recordingBinding`, a `SequenceRecordingBinding`, whose `upload`, `assess` and `playbackUrl` each receive a second argument, a `SequenceRecordingSlot` — `{ slotId, index, activityId }`, the shape `onSubmit` already passes;
23
+ - `assessments`, a `Readonly<Record<string, SpeechAssessment>>` keyed by `slotId` and read live rather than at mount, so a `review` shows the marks behind each stored grade once they arrive.
24
+
25
+ A `renderers` override receives neither. `<ActivityPreview>` renders a read-aloud draft with an in-memory binding that has no `assess`, so an author can record, play back and submit, and sees the notice where feedback would be.
26
+
27
+ - A graded take's xAPI statement carries the grade as a fraction of 1 — `score.scaled`, with `score.max` 1 — whatever units the grade was recorded in, so the statement always validates. `onComplete` still receives the grade's own `score` and `maxScore`.
28
+ - `LkStrings` gains 40 strings, 20 for read-aloud and 20 for pronunciation feedback, which makes 114. A read-aloud review reuses `awaitingGrade` and `couldNotBeGraded`.
29
+ - `skin.css` styles both components. The marks reuse the dictation's vocabulary — the same glyphs, decorations and tokens, with `mispronounced` standing where `incorrect` stands there — under their own `.lk-ra-*` and `.lk-pf-*` classes, and are restated in forced colours.
30
+
31
+ **Upgrading.**
32
+
33
+ - **A `read-aloud` item now renders.** `<ActivitySequence>` and `<ActivityPreview>` used to show their unsupported-activity note for one. The sequence now renders `<ReadAloud>`, which throws without `recordingBinding.upload` outside `review` — so a `practice` or `exam` sequence carrying read-aloud items needs a `recordingBinding`, or those slots render the error boundary's fallback where the note used to be.
34
+ - **In `practice`, a read-aloud slot completes when it is submitted.** The sequence records a `responded` outcome on submit, as it already did for every slot in `exam`, and a `scored` outcome from the slot's grade replaces it. An unscorable take, a failed assessment or a binding with no `assess` therefore no longer keeps `onFinished` from firing. A learner who records again after a grade has the new grade recorded as well, so `onActivityComplete` can fire more than once for the same `slotId` — once per graded take. A `responded` never replaces a grade: a later take that comes back ungraded leaves the earlier grade in the slot, although `onSubmit` has already reported the later take.
35
+ - **`onFinished` waits for an assessment in flight.** When the last slot is filled while a read-aloud is still being assessed, the set is reported once the assessment ends — with the grade in place, or with the slot's `responded` outcome when no grade came back. An assessment that never settles leaves the set unreported. `onFinished` and `onComplete` still fire once per set: a take after that reaches you through `onSubmit`, and its grade through `onActivityComplete`.
36
+ - **A practice take that could not be stored still completes its slot**, as `{ kind: 'unsubmitted' }` with no response. It is reported through `onFinished` only, never through `onSubmit`. That is not `responded` with `recording: null`: a null recording is a blank the learner chose, and a grader marks it 0. A storage outage must not become a zero. A retry that stores the take replaces the slot.
37
+ - **`SequenceItemOutcome` gained `unsubmitted`, and a `scored` outcome may carry the `response` the grade belongs to.** An exhaustive `switch` on `kind` needs a new arm. `onFinished` is the only callback that sees `unsubmitted`.
38
+ - **`<ReadAloud>` no longer accepts `captureGroup`.** The pager still stops a hidden slot's microphone, through `SequenceSlotContext`, not through a public prop. A spread that passed `captureGroup` used to type-check and could stop another pager's take; it is now a type error.
39
+ - **For every activity type, `<ActivitySequence>` refuses an outcome that arrives after `activities` changed** and does not match the `slotId` and `activityId` of the slot now at its index. A grade still being computed when the paper was swapped used to land in the new paper's slot, and could report as finished a set whose question was never answered. A paper that is only re-created, with the same slots, still accepts it.
40
+ - A dictionary declared as a complete `LkStrings` does not type-check until it supplies the 40 new strings; partial overrides are unaffected. A `switch` that must be exhaustive over `RenderableActivity` gains a member.
41
+
42
+ ### Patch Changes
43
+
44
+ - Updated dependencies [7d25492]
45
+ - @intellectif/lk-core@0.14.0
46
+
47
+ ## 13.0.0
48
+
49
+ ### Major Changes
50
+
51
+ - f26bab6: A built-in `dictation` activity type: the learner listens and types what they hear, and the grade is how close their text is to the transcript.
52
+
53
+ **lk-core**
54
+
55
+ - `DictationData` carries a `transcript` (the answer key), optional `acceptedTranscripts` scored best-of, an audio recording with the usual playback policy, an optional second slower recording (`slowMedia`, a separate file that follows the recording's policy and never has a play budget of its own), progressive word hints for practice (`hints`), and whole-word rewrite rules in `tolerance.equivalences` — the place a contraction table or a regional spelling lives, as content rather than as code inside the scorer.
56
+ - The score is `(length − edit distance) / length` over the whole sentence, in NFC code points, after case, punctuation, spacing and invisible characters (control, format and default-ignorable characters, variation selectors included) are ignored; typographic apostrophes and the hyphens of other scripts are folded, compatibility characters such as fullwidth and halfwidth forms, Latin digraphs, ligatures and Kangxi radicals are read as the characters they stand for (a halfwidth voiced sound mark as the mark on the kana before it), and a few sequences written two ways — the Thai SARA AM typed as its two parts, a Malayalam chillu in its older spelling — compare as one. What is spelling counts: an apostrophe, a hyphen, a Catalan middle dot or a Tibetan tsheg inside a word, a zero-width joiner or non-joiner where it changes what is written (beside a virama, as Unicode 16 lists them, or the Persian half-space between two letters that would otherwise join), the Mongolian vowel separator and free variation selectors, the tag characters of a subdivision flag, the base of a keycap and the format characters that are drawn, such as the Arabic number sign; a combining mark belongs to its word. One `ScoringDetail` per transcript word carries its own `score` — a new optional field on `ScoringDetail` — so a stored result records how each word was marked; the item's `score` is not the mean of them.
57
+ - `alignDictation(data, text)` returns the same comparison as data: the normalised strings, which transcript was scored against, and the word pairings with their similarities. `diffDictationChars(reference, attempt)` returns the character-level edit operations for a marked display, and `dictationReferenceWords(data)` the ids and words a stored result can be checked against. A review screen or an analytics job never re-implements the arithmetic.
58
+ - Learner text is compared up to 8000 code points and a transcript is at most 2000; the alignment says when text was cut. An item carries at most ten accepted transcripts and 100 equivalence rules, each `from` and `to` at most 200 code points; a rule rewrites whole words — in a script written without spaces, whose words show no edges, wherever its letters stand, though a number is a word in every script — and inserts its rewrite as words, set apart by a space where a letter of a script written with spaces, or a digit, would touch one, so `50%` with a rule for `%` reads as `50 percent`; no rule deletes a word. While rules run the text is cut at twice the attempt cap — a transcript that needs that cut is refused, an attempt that needs it is reported as truncated — so no combination of rules, authored or stale, can exhaust the process that grades an exam. The caps are exported as constants.
59
+ - The schema refuses captions on a dictation recording (they are the answer), and `validateItemGroup` refuses them on a group's stimulus recording that a dictation plays. It refuses a title or recording description that contains the whole transcript, or a whole accepted transcript, as whole words — as written and with the item's rules applied, and also where punctuation joins them to other words — or, for a transcript with four or more letters of a script written without spaces, wherever those letters stand, the transcript always read as it is written; and one too long to be searched in full. `validateActivity` reports these beside a refusal in any other field of the item. `assertRedacted` refuses a dictation projection that carries captions, a slow recording beside a play budget or a slow recording with no recording, and `assertRedactedItemGroup` a captioned stimulus a dictation plays, so a payload built by hand cannot pass as learner-safe.
60
+ - `score()` and `evaluate()` accept an optional `{ rounding }` so a pass line can be compared the way a score is displayed; without it nothing changes, and a malformed policy throws a `RangeError` instead of failing every comparison.
61
+ - `dictationType` and the previously unexported `gapSelectType` are on the barrel, and so are `MediaPlaybackPolicy`, the type of `ActivityMedia.playback`, and the `NativeControlHint` it lists, by name. Twenty-one draft issue codes (`dc_*`) with their severities and paths are in the authoring guide.
62
+ - `validateDraft` matches a schema's failures against the issues already reported in the depth of each path, where it made a pass over every reported issue for each failure: a draft with thousands of malformed entries took seconds, for every type. The issues it reports are unchanged. `validateDraft` and `validateItemGroupDraft` also normalise each dictation transcript, title and recording description once, however many of their checks read it.
63
+ - Every existing grade replays unchanged: vectors were added, none changed.
64
+
65
+ **lk-react**
66
+
67
+ - `<Dictation>`, also at `@intellectif/lk-react/components/Dictation`: the recording(s), the text box, and after submission an accessible word-by-word marked result with a character-level diff and a solution toggle. A recording's play limit binds through `mediaBudget` as for every other activity. Starting one of the two recordings pauses the other, and pausing spends no play.
68
+ - The marks are built for assistive technology and for every script: the per-word sentences a screen reader hears are in the interface language and stay hidden without the optional skin; the dictation's own words carry its `data.locale`, and its word list and diff take their direction from that tag when it names one — by a script subtag, or a language written right to left — or else from the transcript's first letter or directional mark, rather than from whatever was typed; the form is named by its title; authored feedback in the live announcement is isolated from the interface text around it. A wrong character inside a wrong word is marked by its own shape, in forced colours too — a combining mark or a stacked letter together with the character it is drawn into — and a missing character's bullet keeps its place inside a number or a word of the other direction. Keyboard focus stays on the hint controls, and a response reports only the hints actually shown.
69
+ - `LkStrings` gains the dictation strings. A dictionary declared as a complete `LkStrings` must add them; partial overrides are unaffected.
70
+
71
+ ### Patch Changes
72
+
73
+ - Updated dependencies [f26bab6]
74
+ - @intellectif/lk-core@0.13.0
75
+
3
76
  ## 12.0.1
4
77
 
5
78
  ### Patch Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @intellectif/lk-react
2
2
 
3
- React 19 components, hooks, and theming for [learning-kit](https://github.com/intellectif/learning-kit): accessible activity components (Multiple Choice, Fill-in-the-Blanks, Written Response), a resumable in-place question-set pager with **exam** and **review** modes, a CSS-variable theming system with an optional skin, and an xAPI delivery hook.
3
+ React 19 components, hooks, and theming for [learning-kit](https://github.com/intellectif/learning-kit): accessible activity components (Multiple Choice, Fill-in-the-Blanks, Gap Select, Dictation, Read Aloud, Written Response), a resumable in-place question-set pager with **exam** and **review** modes, a CSS-variable theming system with an optional skin, and an xAPI delivery hook.
4
4
 
5
5
  A lightweight, composable, bring-your-own-backend alternative to H5P — and unlike a
6
6
  practice-quiz widget, it is built to render a **summative** paper: in `exam` mode the
@@ -67,8 +67,12 @@ export function Demo() {
67
67
 
68
68
  - **`<MultipleChoice>`** — single / multi select, all-or-nothing or partial scoring, per-option `feedback`, and a deterministic option shuffle seeded by `shuffleSeed` (without one the order is stable for the life of the mount only, and is not reproducible afterwards).
69
69
  - **`<FillInTheBlanks>`** — `{{id}}` placeholders, case/whitespace options, per-blank `hint` (Show/Hide toggle as an icon), per-blank `feedback` shown inline on submit with a learner-controlled **Hide/Show feedback** toggle, optional `showCorrectAnswers`.
70
+ - **`<GapSelect>`** — a dropdown cloze: `{{id}}` gaps answered from per-gap choices or a shared word bank, an empty first entry so "not answered" stays distinguishable from "answered wrongly", per-gap `feedback`, and a seeded per-gap choice shuffle.
71
+ - **`<Dictation>`** — the learner listens and types; one or two recordings (`media`, and a slower `slowMedia` that follows the same playback policy — starting one pauses the other), progressive word hints in `practice` (recorded as `hintsRevealed`, never charged), and an accessible word-by-word marked result after submit: a hidden sentence per word for screen readers, a glyph and a text decoration per state for everyone else, a character-level diff inside each wrong word, a legend, and a **Show/Hide solution** toggle. In `review`, pass a scored `outcome` to show marks: they are recomputed from the transcript and the learner's text when the component has both (full data or `redact(data, { reveal: 'after-submit' })`, plus `value` or `defaultValue`), and rebuilt from the outcome's stored `details` otherwise. Without a scored `outcome`, nothing is marked. Its form is a landmark named by the title, so two dictations with the same title on one page — two attempts at one item, reviewed together — are two landmarks with one name, which axe reports as `landmark-unique`: give each its own title.
72
+ - **`<ReadAloud>`** — the learner reads a text aloud. A model recording and an optional slower one (same playback policy, and both kept silent for as long as a take is being recorded, so the microphone never picks up the model), a microphone control bounded by the item's take budget and its time limit, playback of the take before it is sent, and a word-by-word pronunciation result. The SDK captures and renders; it stores nothing and judges nothing: `recordingBinding.upload` puts the take in **your** storage and `recordingBinding.assess` (`practice` only) returns **your** server's judgement, so no audio, no key and no assessor ever sit in the component. In `exam` it uploads, submits and locks without scoring, revealing or assessing — and a take whose upload failed offers a retry and blocks submit rather than silently becoming a blank. In `review` it renders the stored `outcome`, marked from an `assessment` you pass or, failing that, rebuilt from the outcome's stored `details`.
73
+ - **`<PronunciationFeedback>`** — the marks on their own, for a review screen that has evidence but no activity: the graded dimensions (accuracy, fluency, completeness, prosody — one the engine did not measure reads "not assessed", **never 0%**), and every word marked `correct` / `mispronounced` / `omitted` / `inserted` with a hidden sentence per word for screen readers and a glyph plus a text decoration for everyone else. A word opens onto its syllables, its sounds, what was heard instead, and a button that plays just that word. Evidence it cannot trust is checked before it is aligned, so in production a malformed assessment costs the word list — never the grade beside it.
70
74
  - **`<WrittenResponse>`** — free-text writing with a live word counter and bounds messaging, graded **asynchronously**: it emits an ungraded submission (never a fake zero) and a SUBMITTED-verb xAPI statement, and renders a returned `GradeRecord` in `review` mode with per-criterion scores and inline corrections.
71
- - **`<ActivitySequence>`** — in-place "question set" pager (Previous/Next, "Question X of N", no scrolling, focus-managed). Accepts item groups and keeps their stimulus beside every question; `onSubmit` reports every raw answer with its `slotId`, which is the only response channel an `exam` sequence has. `onFinished` is the completion signal for a set that mixes scored and deferred-graded items, where `onComplete` can never fire.
75
+ - **`<ActivitySequence>`** — in-place "question set" pager (Previous/Next, "Question X of N", no scrolling, focus-managed). Accepts item groups and keeps their stimulus beside every question; `onSubmit` reports every raw answer with its `slotId`, which is the only response channel an `exam` sequence has. `onFinished` is the completion signal for a set that mixes scored and deferred-graded items, where `onComplete` can never fire. A read-aloud slot takes its storage from `recordingBinding`, whose methods are told which slot a take belongs to, and a `review` its per-word marks from `assessments`, keyed by `slotId` — there is a [worked example](https://github.com/intellectif/learning-kit/blob/main/docs/speech-assessment.md#in-a-question-set).
72
76
  - **Resume and review a whole attempt** — `<ActivitySequence>` takes `defaultIndex`, `responses` and `submittedSlotIds` (read at mount; remount with a `key` to show a different attempt), so an interrupted paper reopens on the right question with the right answers and already-committed questions still committed. `onIndexChange` reports every position the pager lands on, including a clamp it had to apply. Pass `renderMode="review"` plus server-computed `outcomes` to render a finished attempt read-only. Pairs with `serializeAttemptState` / `restoreAttemptState` in `lk-core`.
73
77
  - **`renderMode`** — `practice` (default: the component scores locally and reveals correctness), `exam` (never scores, never reveals; submit emits the raw response for the server to grade), `review` (read-only, marks correctness only from an `outcome` you supply). This is the single switch that takes grading off the client.
74
78
  - **Controlled or uncontrolled** — every activity component follows the React convention: `defaultValue` to seed, `value` + `onChange` to own the answer outright, `defaultSubmitted` to mount an already-committed question as committed.
@@ -83,9 +87,10 @@ export function Demo() {
83
87
  - **Activity-level overall feedback** — `{ correct, incorrect }` shown after submit (h5p "Overall Feedback" parity).
84
88
  - **`useXAPI(config)`** — fire-and-forget LRS delivery with retry/backoff for 5xx/network (1 s / 2 s / 4 s), immediate fail on 4xx, never throws.
85
89
  - **`useActivityState()`** — `idle → in-progress → completed → reviewing` machine with `getTimeSpent()`.
90
+ - **`useSpeechRecorder(options)`** — the capture half of a read-aloud, usable on its own: microphone permission, an RMS level for a meter, a minimum take length, and an auto-stop measured from the **sample count** rather than a timer, so a take can never outrun its own limit. It yields **16 kHz mono 16-bit PCM WAV** — the one format `inspectWav` in `lk-core` measures without a decoder. SSR-safe by construction (support is discovered inside `start()`, never probed during render, so a server render and its hydration cannot disagree), and the microphone and the audio context are released on stop, discard, error and unmount alike. Where a Content-Security-Policy's `script-src` does not allow `blob:`, serve `CAPTURE_PROCESSOR_SOURCE` — exported beside the hook, with the contract it meets written on it — and pass its URL as `workletUrl`. A `workletUrl` that fails to load is not reported: the hook falls back to the main-thread `ScriptProcessorNode`, so check that the module is fetched.
86
91
  - **`<ThemeProvider>` + `defaults.css`** — `--lk-*` design-token system; automatic dark mode via `prefers-color-scheme` (SSR-safe with `useSyncExternalStore`).
87
92
  - **`createTailwindTheme(theme)`** — optional Tailwind interop; consume the SDK palette from your own utilities.
88
- - **`<LkIntlProvider>`** — all 54 strings the SDK's own chrome renders, replaceable in one place, with a per-component `strings` prop for the exceptions. Interpolation and plurals are **functions**, not format strings, so your `Intl.PluralRules` does the work and TypeScript checks the arity. `locale` sets `lang` and derives `dir`; the skin uses logical properties, so RTL follows. The SDK ships the mechanism and **English only** — see [docs/i18n.md](https://github.com/intellectif/learning-kit/blob/main/docs/i18n.md).
93
+ - **`<LkIntlProvider>`** — all 114 strings the SDK's own chrome renders, replaceable in one place, with a per-component `strings` prop for the exceptions. Interpolation and plurals are **functions**, not format strings, so your `Intl.PluralRules` does the work and TypeScript checks the arity. `locale` sets `lang` and derives `dir`; the skin uses logical properties, so RTL follows. The SDK ships the mechanism and **English only** — see [docs/i18n.md](https://github.com/intellectif/learning-kit/blob/main/docs/i18n.md).
89
94
  - **WCAG 2.2 AA** — axe-clean unit + Playwright e2e tests; full keyboard operability; numerically-verified contrast.
90
95
  - **RSC-compatible** — every component carries `'use client'` and hydrates inside a React Server Component tree.
91
96
 
@@ -97,11 +102,15 @@ export function Demo() {
97
102
  | `@intellectif/lk-react/components/MultipleChoice` | `<MultipleChoice>` (boundary-wrapped) |
98
103
  | `@intellectif/lk-react/components/FillInTheBlanks` | `<FillInTheBlanks>` (boundary-wrapped) |
99
104
  | `@intellectif/lk-react/components/GapSelect` | `<GapSelect>` dropdown cloze (boundary-wrapped) |
105
+ | `@intellectif/lk-react/components/Dictation` | `<Dictation>` listen-and-type (boundary-wrapped) |
106
+ | `@intellectif/lk-react/components/ReadAloud` | `<ReadAloud>` read-aloud speaking (boundary-wrapped), with the `RecordingBinding`, `RecordedTake` and `ReadAloudAssessResult` types a binding is written against |
107
+ | `@intellectif/lk-react/components/PronunciationFeedback` | `<PronunciationFeedback>` per-word pronunciation marks (boundary-wrapped) |
100
108
  | `@intellectif/lk-react/components/WrittenResponse` | `<WrittenResponse>` (boundary-wrapped) |
101
109
  | `@intellectif/lk-react/components/ActivityPreview` | `<ActivityPreview>` draft preview for editors |
102
110
  | `@intellectif/lk-react/components/ActivitySequence` | `<ActivitySequence>` question-set pager |
103
111
  | `@intellectif/lk-react/components/StimulusPanel` | `<StimulusPanel>` shared-stimulus region |
104
112
  | `@intellectif/lk-react/hooks/useActivityState` | Lifecycle + timing |
113
+ | `@intellectif/lk-react/hooks/useSpeechRecorder` | Microphone capture to 16 kHz mono WAV, and `CAPTURE_PROCESSOR_SOURCE` for a self-hosted worklet |
105
114
  | `@intellectif/lk-react/hooks/useXAPI` | LRS delivery (retry, never-throws) |
106
115
  | `@intellectif/lk-react/i18n/LkIntlProvider` | `<LkIntlProvider>`, `useLkStrings`, `useLkDirection`, `DEFAULT_STRINGS`, `mergeStrings`, `directionForLocale` |
107
116
  | `@intellectif/lk-react/theme/ThemeProvider` | `<ThemeProvider>`, `darkTheme`, `useTheme`, `createTailwindTheme` |
@@ -121,12 +130,13 @@ subpath of their own.
121
130
  |---|---|
122
131
  | **Scoring** | Pure & deterministic, in `lk-core`; per-activity `all-or-nothing` and `partial`. Weighted totals across items and sections are `composeAssessmentScore`; per-option weighting inside one activity is not implemented. |
123
132
  | **Grading on the client** | `practice` only. `exam` and `review` never score and never reveal correctness — pass a `redact()` projection and grade server-side. |
124
- | **Feedback** | Per-item (MC per-option, FIB per-blank) shown inline on submit with Hide/Show toggle, + activity-level overall. Score-band feedback is not implemented. |
133
+ | **Speech** | A read-aloud is graded **asynchronously**, as a written response is. The SDK captures the take, renders the marks, and does the arithmetic (`gradeReadAloud` in `lk-core`); **your** application stores the recording, calls a pronunciation assessor, holds its keys, and decides every threshold. `<ReadAloud>` names no provider and assesses nothing itself, and `recordingBinding.assess` is wired in `practice` only — for anything that counts, assess on your server, where evidence a browser posted cannot be forged. See [docs/speech-assessment.md](https://github.com/intellectif/learning-kit/blob/main/docs/speech-assessment.md). |
134
+ | **Feedback** | Per-item (MC per-option, FIB per-blank, GS per-gap) shown inline on submit with Hide/Show toggle, + activity-level overall. A dictation marks word by word instead, with a solution toggle. Score-band feedback is not implemented. |
125
135
  | **Retry** | Pass a new `data` reference or change the React `key` → the activity resets to **its seeded state**, not to blank. With `defaultValue` / `defaultSubmitted` set, a new `key` and a new `data` reference are indistinguishable: both return the restored answer, still locked as submitted. For a genuinely fresh attempt, change the `key` **and stop passing the seeds**. No built-in button — retry *policy* is yours. |
126
136
  | **Persistence / resume** | Storage is yours — capture `onSubmit` / `onChange` / `onInteraction` / `onIndexChange` and persist as you wish. Re-hydration is supported at **both** levels: `defaultValue` / `value` + `onChange` on a component (since 2.1.0), and `defaultIndex` / `responses` / `submittedSlotIds` on `<ActivitySequence>` (since 6.0.0). |
127
137
  | **Rich text** | Rendered only when you pass `sanitizeHtml`; the SDK bundles no sanitiser and injects no HTML without one. `FillInTheBlanks` deliberately **ignores** `passageHtml` — the passage hosts the answer inputs, so it is built from `passage` plus the blanks (dev-mode warning if you pass it). |
128
- | **i18n** | Every SDK-rendered string is replaceable through `<LkIntlProvider>` or a per-component `strings` prop (lk-react 7.1.0); `locale` sets `lang` and names xAPI statements. **No locale but English is bundled** — the SDK ships the mechanism and the English defaults, because it cannot review a translation it does not speak. RTL is supported: the provider derives `dir` from the locale — and declares neither `lang` nor `dir` when you supplied neither, so it cannot flip an RTL host back — and the skin uses logical properties throughout. Thrown errors stay English on purpose — they address the developer, not the learner. |
129
- | **Media** | `<audio>` / `<video>` are paused when the pager navigates away, preserving `currentTime` so a group resumes where the learner left it; nothing ever auto-plays. A provider `embed` (iframe) **cannot** be paused this way — controlling a third-party player needs its own JS API. Use `audio` / `video` for anything that must stop when the learner navigates. |
138
+ | **i18n** | Every SDK-rendered string is replaceable through `<LkIntlProvider>` or a per-component `strings` prop (lk-react 7.1.0); `locale` sets `lang` and names xAPI statements. **No locale but English is bundled** — the SDK ships the mechanism and the English defaults, because it cannot review a translation it does not speak. RTL is supported: the provider derives `dir` from the locale — and declares neither `lang` nor `dir` when you supplied neither, so it cannot flip an RTL host back — and the skin uses logical properties throughout. A dictation's own `data.locale` puts `lang` and `dir` on its title, hints, marks and solution. Thrown errors stay English on purpose — they address the developer, not the learner. |
139
+ | **Media** | `<audio>` / `<video>` are paused when the pager navigates away, preserving `currentTime` so a group resumes where the learner left it; nothing ever auto-plays. A dictation's two recordings never play at once: starting one pauses the other, which charges no play. A provider `embed` (iframe) **cannot** be paused this way — controlling a third-party player needs its own JS API. Use `audio` / `video` for anything that must stop when the learner navigates. |
130
140
  | **Playback policy** | Audio only. `maxPlays` / `seek` / `rate` are enforced by the SDK's own transport — a refused play is stopped inside the browser's `play` event, before a sample is audible. `nativeControlHints` is **advisory**: it emits `controlsList`, which some engines ignore, and never prevents a download. A budget is durable only if you persist it through `mediaBudget.onPlayConsumed`; the SDK stores nothing. Nothing here survives devtools. Not implemented for `video` or `embed`. |
131
141
  | **Authoring / content storage / CDN / auth** | Consumer responsibility. The contracts to build an editor on are provided: typed schemas, `validateActivity` and JSON Schema export, `validateDraft` / `createDraft` in `lk-core`, and `<ActivityPreview>` here. |
132
142
  | **SSR / RSC** | Fully supported; every component carries `'use client'`. |
@@ -149,6 +159,7 @@ before assuming a migration is needed.
149
159
  - [Authoring & content storage](https://github.com/intellectif/learning-kit/blob/main/docs/authoring.md)
150
160
  - [Styling](https://github.com/intellectif/learning-kit/blob/main/docs/styling.md) — tokens, the skin, overrides, dark mode, Tailwind.
151
161
  - [Internationalisation](https://github.com/intellectif/learning-kit/blob/main/docs/i18n.md) — the full string surface, precedence, plurals, RTL.
162
+ - [Speech assessment](https://github.com/intellectif/learning-kit/blob/main/docs/speech-assessment.md) — the read-aloud item, the evidence an assessor must produce, and how a grade is computed.
152
163
  - [Project README](https://github.com/intellectif/learning-kit#readme) — full picture & monorepo layout.
153
164
 
154
165
  ## License
@@ -1,5 +1,5 @@
1
1
  import { XAPIStatement, WrittenResponseData, LearnerResponse, ItemOutcome, InteractionEvent, ThemeTokens } from '@intellectif/lk-core';
2
- import { c as Renderable, R as RenderMode, H as HtmlSanitizer, M as MediaBudgetBinding, b as MediaTransportStrings, a as LkStringsOverride } from './strings-CN7n-BlE.js';
2
+ import { f as Renderable, e as RenderMode, H as HtmlSanitizer, M as MediaBudgetBinding, b as MediaTransportStrings, a as LkStringsOverride } from './strings-CtdSf2St.js';
3
3
 
4
4
  /**
5
5
  * Payload delivered when the learner submits a written response. Grading is
@@ -1,5 +1,5 @@
1
1
  import { XAPIStatement, WrittenResponseData, LearnerResponse, ItemOutcome, InteractionEvent, ThemeTokens } from '@intellectif/lk-core';
2
- import { c as Renderable, R as RenderMode, H as HtmlSanitizer, M as MediaBudgetBinding, b as MediaTransportStrings, a as LkStringsOverride } from './strings-CN7n-BlE.cjs';
2
+ import { f as Renderable, e as RenderMode, H as HtmlSanitizer, M as MediaBudgetBinding, b as MediaTransportStrings, a as LkStringsOverride } from './strings--ADnHcaA.cjs';
3
3
 
4
4
  /**
5
5
  * Payload delivered when the learner submits a written response. Grading is