@intellectif/lk-react 13.0.0 → 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 (162) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +11 -3
  3. package/dist/{WrittenResponse-B5p_-RnJ.d.ts → WrittenResponse-C0P34En1.d.ts} +1 -1
  4. package/dist/{WrittenResponse-DWtS4tOL.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-PSHNTDKG.js → chunk-47HHC4GD.js} +13 -161
  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-XAGWBVK3.js → chunk-72UGRIMM.js} +6 -5
  12. package/dist/chunk-72UGRIMM.js.map +1 -0
  13. package/dist/{chunk-SBIJJ374.cjs → chunk-B2XQLU2E.cjs} +5 -5
  14. package/dist/{chunk-SBIJJ374.cjs.map → chunk-B2XQLU2E.cjs.map} +1 -1
  15. package/dist/{chunk-DQERNAWI.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-NVUFEMPZ.cjs → chunk-BVRUUYYR.cjs} +30 -16
  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-YILH4ETT.js → chunk-I33FOKZ7.js} +224 -31
  28. package/dist/chunk-I33FOKZ7.js.map +1 -0
  29. package/dist/{chunk-HT4FFZZQ.cjs → chunk-J2CDPIRC.cjs} +231 -38
  30. package/dist/chunk-J2CDPIRC.cjs.map +1 -0
  31. package/dist/{chunk-MA7GXUWC.cjs → chunk-LB3FEV7Z.cjs} +77 -2
  32. package/dist/chunk-LB3FEV7Z.cjs.map +1 -0
  33. package/dist/{chunk-5N7OLFHB.js → chunk-LGCUOLP6.js} +6 -5
  34. package/dist/chunk-LGCUOLP6.js.map +1 -0
  35. package/dist/{chunk-YZWXVZB3.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-K7NMNOPI.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-74QXBWCI.cjs → chunk-RRH7ZPXC.cjs} +6 -6
  46. package/dist/{chunk-74QXBWCI.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-Z63JRG4M.cjs → chunk-SE6WWDVW.cjs} +15 -14
  50. package/dist/chunk-SE6WWDVW.cjs.map +1 -0
  51. package/dist/{chunk-NVP6UEQQ.js → chunk-SWY6PWXK.js} +4 -4
  52. package/dist/{chunk-NVP6UEQQ.js.map → chunk-SWY6PWXK.js.map} +1 -1
  53. package/dist/{chunk-3Q4I4KT6.js → chunk-TDJEKXEP.js} +77 -2
  54. package/dist/chunk-TDJEKXEP.js.map +1 -0
  55. package/dist/{chunk-2GXSA322.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-K3EA547J.cjs → chunk-USBANZ7K.cjs} +27 -33
  60. package/dist/chunk-USBANZ7K.cjs.map +1 -0
  61. package/dist/{chunk-J73TDY7J.cjs → chunk-WTS34S5E.cjs} +25 -173
  62. package/dist/chunk-WTS34S5E.cjs.map +1 -0
  63. package/dist/{chunk-R7DI5UMU.js → chunk-XIBOFTOL.js} +3 -3
  64. package/dist/{chunk-R7DI5UMU.js.map → chunk-XIBOFTOL.js.map} +1 -1
  65. package/dist/{chunk-W4MXRZAB.js → chunk-XUEIUEPI.js} +6 -5
  66. package/dist/chunk-XUEIUEPI.js.map +1 -0
  67. package/dist/{chunk-UU2NV6SU.cjs → chunk-YEYKJFEC.cjs} +14 -13
  68. package/dist/chunk-YEYKJFEC.cjs.map +1 -0
  69. package/dist/{chunk-PY53ABWC.js → chunk-YMBBM5K7.js} +23 -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 -10
  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 -9
  77. package/dist/components/ActivitySequence.cjs +16 -11
  78. package/dist/components/ActivitySequence.d.cts +108 -4
  79. package/dist/components/ActivitySequence.d.ts +108 -4
  80. package/dist/components/ActivitySequence.js +15 -10
  81. package/dist/components/Dictation.cjs +7 -5
  82. package/dist/components/Dictation.d.cts +2 -1
  83. package/dist/components/Dictation.d.ts +2 -1
  84. package/dist/components/Dictation.js +6 -4
  85. package/dist/components/FillInTheBlanks.cjs +6 -5
  86. package/dist/components/FillInTheBlanks.d.cts +2 -1
  87. package/dist/components/FillInTheBlanks.d.ts +2 -1
  88. package/dist/components/FillInTheBlanks.js +5 -4
  89. package/dist/components/GapSelect.cjs +6 -5
  90. package/dist/components/GapSelect.d.cts +2 -1
  91. package/dist/components/GapSelect.d.ts +2 -1
  92. package/dist/components/GapSelect.js +5 -4
  93. package/dist/components/MultipleChoice.cjs +6 -5
  94. package/dist/components/MultipleChoice.d.cts +2 -1
  95. package/dist/components/MultipleChoice.d.ts +2 -1
  96. package/dist/components/MultipleChoice.js +5 -4
  97. package/dist/components/PronunciationFeedback.cjs +16 -0
  98. package/dist/components/PronunciationFeedback.cjs.map +1 -0
  99. package/dist/components/PronunciationFeedback.d.cts +64 -0
  100. package/dist/components/PronunciationFeedback.d.ts +64 -0
  101. package/dist/components/PronunciationFeedback.js +7 -0
  102. package/dist/components/PronunciationFeedback.js.map +1 -0
  103. package/dist/components/ReadAloud.cjs +21 -0
  104. package/dist/components/ReadAloud.cjs.map +1 -0
  105. package/dist/components/ReadAloud.d.cts +15 -0
  106. package/dist/components/ReadAloud.d.ts +15 -0
  107. package/dist/components/ReadAloud.js +12 -0
  108. package/dist/components/ReadAloud.js.map +1 -0
  109. package/dist/components/StimulusPanel.cjs +4 -4
  110. package/dist/components/StimulusPanel.d.cts +2 -1
  111. package/dist/components/StimulusPanel.d.ts +2 -1
  112. package/dist/components/StimulusPanel.js +3 -3
  113. package/dist/components/WrittenResponse.cjs +6 -5
  114. package/dist/components/WrittenResponse.d.cts +4 -3
  115. package/dist/components/WrittenResponse.d.ts +4 -3
  116. package/dist/components/WrittenResponse.js +5 -4
  117. package/dist/hooks/useSpeechRecorder.cjs +17 -0
  118. package/dist/hooks/useSpeechRecorder.cjs.map +1 -0
  119. package/dist/hooks/useSpeechRecorder.d.cts +127 -0
  120. package/dist/hooks/useSpeechRecorder.d.ts +127 -0
  121. package/dist/hooks/useSpeechRecorder.js +4 -0
  122. package/dist/hooks/useSpeechRecorder.js.map +1 -0
  123. package/dist/i18n/LkIntlProvider.cjs +7 -7
  124. package/dist/i18n/LkIntlProvider.d.cts +3 -2
  125. package/dist/i18n/LkIntlProvider.d.ts +3 -2
  126. package/dist/i18n/LkIntlProvider.js +1 -1
  127. package/dist/index.cjs +53 -32
  128. package/dist/index.cjs.map +1 -1
  129. package/dist/index.d.cts +6 -2
  130. package/dist/index.d.ts +6 -2
  131. package/dist/index.js +17 -12
  132. package/dist/index.js.map +1 -1
  133. package/dist/machine-CuwKqWXC.d.cts +26 -0
  134. package/dist/machine-CuwKqWXC.d.ts +26 -0
  135. package/dist/{strings-DPTyLpHt.d.ts → strings--ADnHcaA.d.cts} +214 -4
  136. package/dist/{strings-DPTyLpHt.d.cts → strings-CtdSf2St.d.ts} +214 -4
  137. package/dist/theme/ThemeProvider.cjs +6 -6
  138. package/dist/theme/ThemeProvider.d.cts +4 -4
  139. package/dist/theme/ThemeProvider.d.ts +4 -4
  140. package/dist/theme/ThemeProvider.js +1 -1
  141. package/dist/theme/skin.css +596 -3
  142. package/package.json +33 -3
  143. package/dist/chunk-2GXSA322.cjs.map +0 -1
  144. package/dist/chunk-3Q4I4KT6.js.map +0 -1
  145. package/dist/chunk-5N7OLFHB.js.map +0 -1
  146. package/dist/chunk-AMGK7DDM.cjs.map +0 -1
  147. package/dist/chunk-DQERNAWI.js.map +0 -1
  148. package/dist/chunk-DQNVAXG6.js.map +0 -1
  149. package/dist/chunk-HT4FFZZQ.cjs.map +0 -1
  150. package/dist/chunk-J73TDY7J.cjs.map +0 -1
  151. package/dist/chunk-K3EA547J.cjs.map +0 -1
  152. package/dist/chunk-K7NMNOPI.cjs.map +0 -1
  153. package/dist/chunk-MA7GXUWC.cjs.map +0 -1
  154. package/dist/chunk-NVUFEMPZ.cjs.map +0 -1
  155. package/dist/chunk-PSHNTDKG.js.map +0 -1
  156. package/dist/chunk-PY53ABWC.js.map +0 -1
  157. package/dist/chunk-UU2NV6SU.cjs.map +0 -1
  158. package/dist/chunk-W4MXRZAB.js.map +0 -1
  159. package/dist/chunk-XAGWBVK3.js.map +0 -1
  160. package/dist/chunk-YILH4ETT.js.map +0 -1
  161. package/dist/chunk-YZWXVZB3.js.map +0 -1
  162. package/dist/chunk-Z63JRG4M.cjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
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
+
3
47
  ## 13.0.0
4
48
 
5
49
  ### Major 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, Gap Select, Dictation, 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
@@ -69,8 +69,10 @@ export function Demo() {
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
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
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.
72
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.
73
- - **`<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).
74
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`.
75
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.
76
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.
@@ -85,9 +87,10 @@ export function Demo() {
85
87
  - **Activity-level overall feedback** — `{ correct, incorrect }` shown after submit (h5p "Overall Feedback" parity).
86
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.
87
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.
88
91
  - **`<ThemeProvider>` + `defaults.css`** — `--lk-*` design-token system; automatic dark mode via `prefers-color-scheme` (SSR-safe with `useSyncExternalStore`).
89
92
  - **`createTailwindTheme(theme)`** — optional Tailwind interop; consume the SDK palette from your own utilities.
90
- - **`<LkIntlProvider>`** — all 74 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).
91
94
  - **WCAG 2.2 AA** — axe-clean unit + Playwright e2e tests; full keyboard operability; numerically-verified contrast.
92
95
  - **RSC-compatible** — every component carries `'use client'` and hydrates inside a React Server Component tree.
93
96
 
@@ -100,11 +103,14 @@ export function Demo() {
100
103
  | `@intellectif/lk-react/components/FillInTheBlanks` | `<FillInTheBlanks>` (boundary-wrapped) |
101
104
  | `@intellectif/lk-react/components/GapSelect` | `<GapSelect>` dropdown cloze (boundary-wrapped) |
102
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) |
103
108
  | `@intellectif/lk-react/components/WrittenResponse` | `<WrittenResponse>` (boundary-wrapped) |
104
109
  | `@intellectif/lk-react/components/ActivityPreview` | `<ActivityPreview>` draft preview for editors |
105
110
  | `@intellectif/lk-react/components/ActivitySequence` | `<ActivitySequence>` question-set pager |
106
111
  | `@intellectif/lk-react/components/StimulusPanel` | `<StimulusPanel>` shared-stimulus region |
107
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 |
108
114
  | `@intellectif/lk-react/hooks/useXAPI` | LRS delivery (retry, never-throws) |
109
115
  | `@intellectif/lk-react/i18n/LkIntlProvider` | `<LkIntlProvider>`, `useLkStrings`, `useLkDirection`, `DEFAULT_STRINGS`, `mergeStrings`, `directionForLocale` |
110
116
  | `@intellectif/lk-react/theme/ThemeProvider` | `<ThemeProvider>`, `darkTheme`, `useTheme`, `createTailwindTheme` |
@@ -124,6 +130,7 @@ subpath of their own.
124
130
  |---|---|
125
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. |
126
132
  | **Grading on the client** | `practice` only. `exam` and `review` never score and never reveal correctness — pass a `redact()` projection and grade server-side. |
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). |
127
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. |
128
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. |
129
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). |
@@ -152,6 +159,7 @@ before assuming a migration is needed.
152
159
  - [Authoring & content storage](https://github.com/intellectif/learning-kit/blob/main/docs/authoring.md)
153
160
  - [Styling](https://github.com/intellectif/learning-kit/blob/main/docs/styling.md) — tokens, the skin, overrides, dark mode, Tailwind.
154
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.
155
163
  - [Project README](https://github.com/intellectif/learning-kit#readme) — full picture & monorepo layout.
156
164
 
157
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-DPTyLpHt.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-DPTyLpHt.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